From 81ffdf535dbdc0b8f0d055f7eb12319674289d2b Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 17 Sep 2026 18:14:06 -0700 Subject: [PATCH 1/9] test(web): colocate unit tests beside their modules, delete apps/web/test (CL-8370) --- apps/web/bunfig.toml | 2 +- apps/web/package.json | 2 +- .../action-command-dispatch-race.test.tsx | 4 +- apps/web/{test => src}/agents-api.test.ts | 2 +- apps/web/{test => src}/api.test.tsx | 2 +- .../{test => src}/app-error-boundary.test.tsx | 2 +- .../app-shell-setup-note-removed.test.tsx | 8 +- apps/web/{test => src}/auth.test.tsx | 8 +- .../auth}/dither-background.test.tsx | 6 +- .../{test => src}/bench/membership.test.ts | 2 +- .../{test => src}/chat-header-toggle.test.tsx | 0 .../chat/agent-display-names.test.ts | 2 +- .../chat/agent-typing-indicator.test.tsx | 2 +- .../{test => src}/chat/artifact-chip.test.tsx | 4 +- .../chat/avatar-composition.test.tsx | 2 +- .../chat/blocks}/approve-block.test.tsx | 6 +- .../blocks}/connect-service-block.test.tsx | 4 +- .../chat/chat-bubble-flat.test.ts | 0 .../chat/chat-workspace.test.tsx | 2 +- .../{test => src}/chat/components.test.tsx | 12 +- .../composer-synchronous-double-send.test.tsx | 4 +- apps/web/src/chat/composer.test.tsx | 893 ++++++++++++++++-- .../chat/connect-service-flow.test.tsx | 4 +- .../chat/failed-turn-strip.test.tsx | 4 +- apps/web/{test => src}/chat/fake-clock.ts | 2 +- .../chat/inference-preamble-drift.test.ts | 2 +- .../{test => src}/chat/loading-state.test.tsx | 4 +- apps/web/{test => src}/chat/mentions.test.ts | 2 +- ...essage-grouping-and-failed-polish.test.tsx | 6 +- .../chat/message-hover-toolbar.test.tsx | 4 +- .../chat/no-usable-model-banner.test.tsx | 2 +- .../chat/own-message-alignment.test.tsx | 6 +- .../chat/pending-send-lifecycle.test.tsx | 4 +- .../chat/pr-thread-view.test.tsx | 4 +- .../chat/presence-stack.test.tsx | 2 +- .../chat/refresh-and-send.test.ts | 12 +- .../chat/resume-failed-banner.test.tsx | 2 +- ...ingle-message-list-reconciliation.test.tsx | 6 +- .../{test => src}/chat/stream-apply.test.ts | 4 +- .../chat/system-join-edge.test.tsx | 6 +- .../chat/system-row-chrome.test.tsx | 4 +- .../chat/team-avatar-stack.test.tsx | 2 +- .../chat/timeline-jump-to-latest.test.tsx | 4 +- .../chat/timeline-scroll-pin.test.tsx | 4 +- .../chat/tool-activity-view.test.tsx | 6 +- .../chat/use-streaming-reply.test.tsx | 4 +- .../chat/use-turn-activity.test.tsx | 2 +- .../chat/use-typing-indicator.test.tsx | 4 +- ...se-workbench-feed-refresh-scoping.test.tsx | 2 +- .../chat/use-workbench-stream.test.tsx | 2 +- .../chat => src/chat/wire}/blocks.test.tsx | 4 +- .../chat/workbench-loading-tip.test.tsx | 4 +- .../chat/workbench-presence.test.ts | 2 +- .../chat/workbench-settings-retry.test.tsx | 2 +- .../agents-section.test.tsx | 2 +- .../members-section.test.tsx | 2 +- .../chat/workbenches-mutated-event.test.ts | 2 +- .../bare-prefix-entity-scope.test.ts | 6 +- .../command-palette/command-groups.test.ts | 2 +- .../command-palette/entity-search.test.ts | 2 +- .../command-palette/recents.test.ts | 2 +- .../command-palette/scope.test.ts | 2 +- .../command-palette/static-commands.test.ts | 2 +- .../use-entity-search.test.tsx | 6 +- apps/web/{test => src}/dom-environment.ts | 0 apps/web/{test => src}/fetch-dedupe.test.tsx | 6 +- .../global-command-palette.test.tsx | 10 +- apps/web/{test => src}/initials.test.ts | 2 +- .../web/{test => src}/insights-route.test.tsx | 6 +- .../insights-row-activation.test.tsx | 4 +- .../library-artifact-deep-link.test.tsx | 6 +- .../library-page-selection.test.tsx | 4 +- .../library-upload-confirmation.test.tsx | 6 +- .../library/artifact-card.test.tsx | 4 +- .../library/artifact-renderer.test.tsx | 2 +- .../{test => src}/library/artifacts.test.ts | 2 +- apps/web/{test => src}/login-routing.test.tsx | 6 +- apps/web/{test => src}/page-type-drift.ts | 2 +- apps/web/{test => src}/pages.test.tsx | 8 +- .../pages}/agent-detail-page.test.tsx | 6 +- .../pages}/agent-skills-picker.test.tsx | 2 +- .../{test => src/pages}/agents-page.test.tsx | 6 +- .../pages}/create-agent-panel.test.tsx | 8 +- .../{test => src/pages}/home-page.test.tsx | 12 +- .../pages}/legacy-settings-redirects.test.tsx | 2 +- .../pages}/mission-control-page.test.tsx | 10 +- .../pages}/provisioning-error-page.test.tsx | 2 +- .../pages}/routine-detail-page.test.tsx | 14 +- .../pages}/routines-page.test.tsx | 8 +- .../pages}/settings-page.test.tsx | 10 +- .../pages}/skill-detail-page.test.tsx | 2 +- .../{test => src/pages}/skills-page.test.tsx | 6 +- .../pages}/workflow-detail-page.test.tsx | 4 +- .../{test => src}/pending-approvals.test.tsx | 10 +- apps/web/src/query-client.test.ts | 184 +--- apps/web/{test => src}/react-ui-toast-mock.ts | 0 apps/web/{test => src}/routes.test.tsx | 6 +- .../{test => src}/settings-access.test.tsx | 2 +- .../web/{test => src}/settings/access.test.ts | 2 +- .../settings/account-section.test.tsx | 2 +- .../settings/api-request.test.ts | 2 +- apps/web/{test => src}/settings/api.test.ts | 2 +- .../settings/credentials-api.test.ts | 2 +- .../settings/grant-preview.test.ts | 2 +- .../settings/grants-section.test.tsx | 4 +- .../{test => src}/settings/identity.test.ts | 2 +- .../settings/people-section.test.tsx | 2 +- .../settings/query-retry.test.tsx | 2 +- .../settings/raw-id-sweep.test.tsx | 8 +- .../settings/resource-vocabulary.test.ts | 2 +- .../settings/roles-section.test.tsx | 2 +- .../settings/section-registry.test.tsx | 6 +- .../web/{test => src}/settings/shell.test.tsx | 4 +- .../{test => src}/settings/strings.test.ts | 2 +- .../settings/tenancy-api.test.ts | 2 +- .../shell-chrome-wiring.test.tsx | 12 +- .../shell}/bench-activity.test.tsx | 6 +- .../shell}/canvas-column.test.tsx | 12 +- .../web/src/shell/context-menu/items.test.tsx | 2 +- .../lib}/context-menu-view.test.tsx | 6 +- .../context-menu/lib}/dialog-guard.test.ts | 2 +- .../context-menu/lib}/target-resolver.test.ts | 4 +- .../lib}/use-context-menu-state.test.tsx | 6 +- ...use-document-context-menu-trigger.test.tsx | 6 +- .../shell/layout}/shell-layout-hook.test.tsx | 4 +- .../shell/layout}/use-scroll-reset.test.tsx | 2 +- apps/web/src/shell/library-artifacts.test.ts | 85 +- .../shell}/routine-activity.test.ts | 2 +- apps/web/{test => src/shell}/sidebar.test.tsx | 10 +- .../shell}/stage-top-bar.test.tsx | 14 +- .../shell}/workbench-list.test.tsx | 6 +- .../stage-chrome-consistency.test.tsx | 6 +- .../stage-search-filter.test.tsx | 6 +- apps/web/{test => src}/stage-top-nav.test.tsx | 10 +- apps/web/{test => src}/tailwind-build.test.ts | 0 .../web/{test => src}/test-query-provider.tsx | 0 apps/web/{test => src}/theme-default.test.tsx | 0 .../toast-single-system.test.tsx | 6 +- apps/web/test/chat/composer.test.tsx | 848 ----------------- apps/web/test/library-artifacts.test.ts | 67 -- apps/web/test/query-client.test.ts | 41 - apps/web/tsconfig.json | 2 +- 142 files changed, 1176 insertions(+), 1518 deletions(-) rename apps/web/{test => src}/action-command-dispatch-race.test.tsx (95%) rename apps/web/{test => src}/agents-api.test.ts (99%) rename apps/web/{test => src}/api.test.tsx (98%) rename apps/web/{test => src}/app-error-boundary.test.tsx (97%) rename apps/web/{test => src}/app-shell-setup-note-removed.test.tsx (93%) rename apps/web/{test => src}/auth.test.tsx (98%) rename apps/web/{test => src/auth}/dither-background.test.tsx (93%) rename apps/web/{test => src}/bench/membership.test.ts (91%) rename apps/web/{test => src}/chat-header-toggle.test.tsx (100%) rename apps/web/{test => src}/chat/agent-display-names.test.ts (97%) rename apps/web/{test => src}/chat/agent-typing-indicator.test.tsx (96%) rename apps/web/{test => src}/chat/artifact-chip.test.tsx (96%) rename apps/web/{test => src}/chat/avatar-composition.test.tsx (99%) rename apps/web/{test/chat => src/chat/blocks}/approve-block.test.tsx (98%) rename apps/web/{test/chat => src/chat/blocks}/connect-service-block.test.tsx (96%) rename apps/web/{test => src}/chat/chat-bubble-flat.test.ts (100%) rename apps/web/{test => src}/chat/chat-workspace.test.tsx (98%) rename apps/web/{test => src}/chat/components.test.tsx (98%) rename apps/web/{test => src}/chat/composer-synchronous-double-send.test.tsx (95%) rename apps/web/{test => src}/chat/connect-service-flow.test.tsx (95%) rename apps/web/{test => src}/chat/failed-turn-strip.test.tsx (99%) rename apps/web/{test => src}/chat/fake-clock.ts (97%) rename apps/web/{test => src}/chat/inference-preamble-drift.test.ts (93%) rename apps/web/{test => src}/chat/loading-state.test.tsx (95%) rename apps/web/{test => src}/chat/mentions.test.ts (99%) rename apps/web/{test => src}/chat/message-grouping-and-failed-polish.test.tsx (98%) rename apps/web/{test => src}/chat/message-hover-toolbar.test.tsx (98%) rename apps/web/{test => src}/chat/no-usable-model-banner.test.tsx (94%) rename apps/web/{test => src}/chat/own-message-alignment.test.tsx (97%) rename apps/web/{test => src}/chat/pending-send-lifecycle.test.tsx (96%) rename apps/web/{test => src}/chat/pr-thread-view.test.tsx (99%) rename apps/web/{test => src}/chat/presence-stack.test.tsx (99%) rename apps/web/{test => src}/chat/refresh-and-send.test.ts (98%) rename apps/web/{test => src}/chat/resume-failed-banner.test.tsx (95%) rename apps/web/{test => src}/chat/single-message-list-reconciliation.test.tsx (96%) rename apps/web/{test => src}/chat/stream-apply.test.ts (93%) rename apps/web/{test => src}/chat/system-join-edge.test.tsx (96%) rename apps/web/{test => src}/chat/system-row-chrome.test.tsx (98%) rename apps/web/{test => src}/chat/team-avatar-stack.test.tsx (99%) rename apps/web/{test => src}/chat/timeline-jump-to-latest.test.tsx (97%) rename apps/web/{test => src}/chat/timeline-scroll-pin.test.tsx (97%) rename apps/web/{test => src}/chat/tool-activity-view.test.tsx (98%) rename apps/web/{test => src}/chat/use-streaming-reply.test.tsx (98%) rename apps/web/{test => src}/chat/use-turn-activity.test.tsx (98%) rename apps/web/{test => src}/chat/use-typing-indicator.test.tsx (97%) rename apps/web/{test => src}/chat/use-workbench-feed-refresh-scoping.test.tsx (97%) rename apps/web/{test => src}/chat/use-workbench-stream.test.tsx (98%) rename apps/web/{test/chat => src/chat/wire}/blocks.test.tsx (97%) rename apps/web/{test => src}/chat/workbench-loading-tip.test.tsx (96%) rename apps/web/{test => src}/chat/workbench-presence.test.ts (98%) rename apps/web/{test => src}/chat/workbench-settings-retry.test.tsx (97%) rename apps/web/{test/chat => src/chat/workbench-settings}/agents-section.test.tsx (99%) rename apps/web/{test/chat => src/chat/workbench-settings}/members-section.test.tsx (98%) rename apps/web/{test => src}/chat/workbenches-mutated-event.test.ts (99%) rename apps/web/{test => src}/command-palette/bare-prefix-entity-scope.test.ts (91%) rename apps/web/{test => src}/command-palette/command-groups.test.ts (98%) rename apps/web/{test => src}/command-palette/entity-search.test.ts (97%) rename apps/web/{test => src}/command-palette/recents.test.ts (99%) rename apps/web/{test => src}/command-palette/scope.test.ts (96%) rename apps/web/{test => src}/command-palette/static-commands.test.ts (92%) rename apps/web/{test => src}/command-palette/use-entity-search.test.tsx (96%) rename apps/web/{test => src}/dom-environment.ts (100%) rename apps/web/{test => src}/fetch-dedupe.test.tsx (97%) rename apps/web/{test => src}/global-command-palette.test.tsx (95%) rename apps/web/{test => src}/initials.test.ts (93%) rename apps/web/{test => src}/insights-route.test.tsx (97%) rename apps/web/{test => src}/insights-row-activation.test.tsx (97%) rename apps/web/{test => src}/library-artifact-deep-link.test.tsx (95%) rename apps/web/{test => src}/library-page-selection.test.tsx (98%) rename apps/web/{test => src}/library-upload-confirmation.test.tsx (97%) rename apps/web/{test => src}/library/artifact-card.test.tsx (89%) rename apps/web/{test => src}/library/artifact-renderer.test.tsx (98%) rename apps/web/{test => src}/library/artifacts.test.ts (98%) rename apps/web/{test => src}/login-routing.test.tsx (97%) rename apps/web/{test => src}/page-type-drift.ts (95%) rename apps/web/{test => src}/pages.test.tsx (94%) rename apps/web/{test => src/pages}/agent-detail-page.test.tsx (99%) rename apps/web/{test => src/pages}/agent-skills-picker.test.tsx (98%) rename apps/web/{test => src/pages}/agents-page.test.tsx (99%) rename apps/web/{test => src/pages}/create-agent-panel.test.tsx (98%) rename apps/web/{test => src/pages}/home-page.test.tsx (97%) rename apps/web/{test => src/pages}/legacy-settings-redirects.test.tsx (98%) rename apps/web/{test => src/pages}/mission-control-page.test.tsx (95%) rename apps/web/{test => src/pages}/provisioning-error-page.test.tsx (97%) rename apps/web/{test => src/pages}/routine-detail-page.test.tsx (94%) rename apps/web/{test => src/pages}/routines-page.test.tsx (95%) rename apps/web/{test => src/pages}/settings-page.test.tsx (92%) rename apps/web/{test => src/pages}/skill-detail-page.test.tsx (98%) rename apps/web/{test => src/pages}/skills-page.test.tsx (98%) rename apps/web/{test => src/pages}/workflow-detail-page.test.tsx (93%) rename apps/web/{test => src}/pending-approvals.test.tsx (95%) rename apps/web/{test => src}/react-ui-toast-mock.ts (100%) rename apps/web/{test => src}/routes.test.tsx (99%) rename apps/web/{test => src}/settings-access.test.tsx (97%) rename apps/web/{test => src}/settings/access.test.ts (96%) rename apps/web/{test => src}/settings/account-section.test.tsx (98%) rename apps/web/{test => src}/settings/api-request.test.ts (97%) rename apps/web/{test => src}/settings/api.test.ts (96%) rename apps/web/{test => src}/settings/credentials-api.test.ts (98%) rename apps/web/{test => src}/settings/grant-preview.test.ts (97%) rename apps/web/{test => src}/settings/grants-section.test.tsx (96%) rename apps/web/{test => src}/settings/identity.test.ts (98%) rename apps/web/{test => src}/settings/people-section.test.tsx (99%) rename apps/web/{test => src}/settings/query-retry.test.tsx (98%) rename apps/web/{test => src}/settings/raw-id-sweep.test.tsx (92%) rename apps/web/{test => src}/settings/resource-vocabulary.test.ts (94%) rename apps/web/{test => src}/settings/roles-section.test.tsx (98%) rename apps/web/{test => src}/settings/section-registry.test.tsx (97%) rename apps/web/{test => src}/settings/shell.test.tsx (95%) rename apps/web/{test => src}/settings/strings.test.ts (93%) rename apps/web/{test => src}/settings/tenancy-api.test.ts (99%) rename apps/web/{test => src}/shell-chrome-wiring.test.tsx (96%) rename apps/web/{test => src/shell}/bench-activity.test.tsx (97%) rename apps/web/{test => src/shell}/canvas-column.test.tsx (96%) rename apps/web/{test/context-menu => src/shell/context-menu/lib}/context-menu-view.test.tsx (96%) rename apps/web/{test/context-menu => src/shell/context-menu/lib}/dialog-guard.test.ts (96%) rename apps/web/{test/context-menu => src/shell/context-menu/lib}/target-resolver.test.ts (93%) rename apps/web/{test/context-menu => src/shell/context-menu/lib}/use-context-menu-state.test.tsx (90%) rename apps/web/{test/context-menu => src/shell/context-menu/lib}/use-document-context-menu-trigger.test.tsx (92%) rename apps/web/{test/shell-layout => src/shell/layout}/shell-layout-hook.test.tsx (97%) rename apps/web/{test/shell-layout => src/shell/layout}/use-scroll-reset.test.tsx (95%) rename apps/web/{test => src/shell}/routine-activity.test.ts (94%) rename apps/web/{test => src/shell}/sidebar.test.tsx (98%) rename apps/web/{test => src/shell}/stage-top-bar.test.tsx (91%) rename apps/web/{test => src/shell}/workbench-list.test.tsx (97%) rename apps/web/{test => src}/stage-chrome-consistency.test.tsx (95%) rename apps/web/{test => src}/stage-search-filter.test.tsx (95%) rename apps/web/{test => src}/stage-top-nav.test.tsx (96%) rename apps/web/{test => src}/tailwind-build.test.ts (100%) rename apps/web/{test => src}/test-query-provider.tsx (100%) rename apps/web/{test => src}/theme-default.test.tsx (100%) rename apps/web/{test => src}/toast-single-system.test.tsx (98%) delete mode 100644 apps/web/test/chat/composer.test.tsx delete mode 100644 apps/web/test/library-artifacts.test.ts delete mode 100644 apps/web/test/query-client.test.ts diff --git a/apps/web/bunfig.toml b/apps/web/bunfig.toml index 564f98dac7..3471b992ac 100644 --- a/apps/web/bunfig.toml +++ b/apps/web/bunfig.toml @@ -1,2 +1,2 @@ [test] -preload = ["./test/dom-environment.ts"] +preload = ["./src/dom-environment.ts"] diff --git a/apps/web/package.json b/apps/web/package.json index 25b09d158d..a8ca2c97e1 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -14,7 +14,7 @@ "typecheck": "tsc --noEmit", "build": "vite build", "watch": "vite build --watch", - "test": "bun run build && bun test ./src ./test" + "test": "bun run build && bun test ./src" }, "dependencies": { "@corbits/agent-directory": "workspace:*", diff --git a/apps/web/test/action-command-dispatch-race.test.tsx b/apps/web/src/action-command-dispatch-race.test.tsx similarity index 95% rename from apps/web/test/action-command-dispatch-race.test.tsx rename to apps/web/src/action-command-dispatch-race.test.tsx index 13f8f4a93b..55a7f45d70 100644 --- a/apps/web/test/action-command-dispatch-race.test.tsx +++ b/apps/web/src/action-command-dispatch-race.test.tsx @@ -14,8 +14,8 @@ import { afterEach, describe, expect, test } from "bun:test"; import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; -import { resetPendingDialogRequests, runActionCommand } from "../src/command-palette-actions"; -import { SkillsPage } from "../src/pages/skills-page"; +import { resetPendingDialogRequests, runActionCommand } from "./command-palette-actions"; +import { SkillsPage } from "./pages/skills-page"; import { TestQueryProvider } from "./test-query-provider"; let container: HTMLDivElement | null = null; diff --git a/apps/web/test/agents-api.test.ts b/apps/web/src/agents-api.test.ts similarity index 99% rename from apps/web/test/agents-api.test.ts rename to apps/web/src/agents-api.test.ts index 28d0e7b07a..ff71284ec5 100644 --- a/apps/web/test/agents-api.test.ts +++ b/apps/web/src/agents-api.test.ts @@ -12,7 +12,7 @@ import { listCatalogModels, loadAgentDirectory, updateAgentSkills, -} from "../src/agents-api"; +} from "./agents-api"; const realFetch = globalThis.fetch; diff --git a/apps/web/test/api.test.tsx b/apps/web/src/api.test.tsx similarity index 98% rename from apps/web/test/api.test.tsx rename to apps/web/src/api.test.tsx index b67e3710ef..515ac8755c 100644 --- a/apps/web/test/api.test.tsx +++ b/apps/web/src/api.test.tsx @@ -14,7 +14,7 @@ import { type } from "arktype"; import type { APIQuery } from "@/lib/api-query"; import { ApiQueryError } from "@/lib/api-query"; -import { approveApproval, rejectApproval, useAPIQuery } from "../src/api"; +import { approveApproval, rejectApproval, useAPIQuery } from "./api"; import { TestQueryProvider } from "./test-query-provider"; const realFetch = globalThis.fetch; diff --git a/apps/web/test/app-error-boundary.test.tsx b/apps/web/src/app-error-boundary.test.tsx similarity index 97% rename from apps/web/test/app-error-boundary.test.tsx rename to apps/web/src/app-error-boundary.test.tsx index 5d386c9b3d..e8a670198c 100644 --- a/apps/web/test/app-error-boundary.test.tsx +++ b/apps/web/src/app-error-boundary.test.tsx @@ -10,7 +10,7 @@ import type { ReactNode } from "react"; import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; -import { AppErrorBoundary } from "../src/app-error-boundary"; +import { AppErrorBoundary } from "./app-error-boundary"; let container: HTMLDivElement | null = null; let root: Root | null = null; diff --git a/apps/web/test/app-shell-setup-note-removed.test.tsx b/apps/web/src/app-shell-setup-note-removed.test.tsx similarity index 93% rename from apps/web/test/app-shell-setup-note-removed.test.tsx rename to apps/web/src/app-shell-setup-note-removed.test.tsx index 1b449aab4e..21762a7578 100644 --- a/apps/web/test/app-shell-setup-note-removed.test.tsx +++ b/apps/web/src/app-shell-setup-note-removed.test.tsx @@ -9,10 +9,10 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; -import { AppShell } from "../src/shell/app-shell"; -import { BenchProvider } from "../src/bench-context"; -import { NavigationProvider } from "../src/navigation"; -import { ShellChromeProvider } from "../src/shell/shell-chrome-provider"; +import { AppShell } from "./shell/app-shell"; +import { BenchProvider } from "./bench-context"; +import { NavigationProvider } from "./navigation"; +import { ShellChromeProvider } from "./shell/shell-chrome-provider"; import { TestQueryProvider } from "./test-query-provider"; const noop = () => undefined; diff --git a/apps/web/test/auth.test.tsx b/apps/web/src/auth.test.tsx similarity index 98% rename from apps/web/test/auth.test.tsx rename to apps/web/src/auth.test.tsx index 16c0d938ed..7efa28ffef 100644 --- a/apps/web/test/auth.test.tsx +++ b/apps/web/src/auth.test.tsx @@ -11,10 +11,10 @@ import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; import { renderToStaticMarkup } from "react-dom/server"; -import { App } from "../src/app"; -import { AuthScreen } from "../src/auth-screen"; -import { SOCIAL_SIGN_IN_PROVIDERS, fetchSession, signIn, signOut, signUp } from "../src/session"; -import type { SessionState } from "../src/session"; +import { App } from "./app"; +import { AuthScreen } from "./auth-screen"; +import { SOCIAL_SIGN_IN_PROVIDERS, fetchSession, signIn, signOut, signUp } from "./session"; +import type { SessionState } from "./session"; const realFetch = globalThis.fetch; diff --git a/apps/web/test/dither-background.test.tsx b/apps/web/src/auth/dither-background.test.tsx similarity index 93% rename from apps/web/test/dither-background.test.tsx rename to apps/web/src/auth/dither-background.test.tsx index aac2fa4040..8797003d6e 100644 --- a/apps/web/test/dither-background.test.tsx +++ b/apps/web/src/auth/dither-background.test.tsx @@ -9,9 +9,9 @@ import { describe, expect, test } from "bun:test"; import { renderToStaticMarkup } from "react-dom/server"; -import { AuthLayout } from "../src/auth/auth-layout"; -import { DitherBackground } from "../src/auth/dither-background"; -import { QuoteCard } from "../src/auth/quote-card"; +import { AuthLayout } from "./auth-layout"; +import { DitherBackground } from "./dither-background"; +import { QuoteCard } from "./quote-card"; describe("DitherBackground", () => { test("renders a decorative canvas, hidden from assistive tech", () => { diff --git a/apps/web/test/bench/membership.test.ts b/apps/web/src/bench/membership.test.ts similarity index 91% rename from apps/web/test/bench/membership.test.ts rename to apps/web/src/bench/membership.test.ts index 5c13b385cb..2e9dbb352e 100644 --- a/apps/web/test/bench/membership.test.ts +++ b/apps/web/src/bench/membership.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; -import { isRawIdentifier } from "../../src/bench/membership"; +import { isRawIdentifier } from "./membership"; describe("isRawIdentifier", () => { test("recognizes every platform id prefix this surface must never render", () => { diff --git a/apps/web/test/chat-header-toggle.test.tsx b/apps/web/src/chat-header-toggle.test.tsx similarity index 100% rename from apps/web/test/chat-header-toggle.test.tsx rename to apps/web/src/chat-header-toggle.test.tsx diff --git a/apps/web/test/chat/agent-display-names.test.ts b/apps/web/src/chat/agent-display-names.test.ts similarity index 97% rename from apps/web/test/chat/agent-display-names.test.ts rename to apps/web/src/chat/agent-display-names.test.ts index ccffac90ed..4a9f66b59f 100644 --- a/apps/web/test/chat/agent-display-names.test.ts +++ b/apps/web/src/chat/agent-display-names.test.ts @@ -3,7 +3,7 @@ import { describe, expect, test } from "bun:test"; import { agentDisplayNamesFromAgents, displayNameForAddress, -} from "../../src/chat/agent-display-names"; +} from "./agent-display-names"; describe("agentDisplayNamesFromAgents", () => { test("keys each agent's display name by its participant address", () => { diff --git a/apps/web/test/chat/agent-typing-indicator.test.tsx b/apps/web/src/chat/agent-typing-indicator.test.tsx similarity index 96% rename from apps/web/test/chat/agent-typing-indicator.test.tsx rename to apps/web/src/chat/agent-typing-indicator.test.tsx index 0f6513c3d6..c3334d3f7d 100644 --- a/apps/web/test/chat/agent-typing-indicator.test.tsx +++ b/apps/web/src/chat/agent-typing-indicator.test.tsx @@ -6,7 +6,7 @@ import { describe, expect, test } from "bun:test"; import { act, createElement } from "react"; import { createRoot } from "react-dom/client"; -import { AgentTypingIndicator } from "../../src/chat/typing-indicator"; +import { AgentTypingIndicator } from "./typing-indicator"; function render(names: readonly string[]) { const container = document.createElement("div"); diff --git a/apps/web/test/chat/artifact-chip.test.tsx b/apps/web/src/chat/artifact-chip.test.tsx similarity index 96% rename from apps/web/test/chat/artifact-chip.test.tsx rename to apps/web/src/chat/artifact-chip.test.tsx index 4b61ba1673..dd65a68c6e 100644 --- a/apps/web/test/chat/artifact-chip.test.tsx +++ b/apps/web/src/chat/artifact-chip.test.tsx @@ -4,8 +4,8 @@ import { describe, expect, test } from "bun:test"; import { renderToStaticMarkup } from "react-dom/server"; -import { ArtifactChip } from "../../src/chat/artifact-chip"; -import type { Part } from "../../src/chat/api"; +import { ArtifactChip } from "./artifact-chip"; +import type { Part } from "./api"; function filePart(overrides: Partial): Part & { kind: "file"; diff --git a/apps/web/test/chat/avatar-composition.test.tsx b/apps/web/src/chat/avatar-composition.test.tsx similarity index 99% rename from apps/web/test/chat/avatar-composition.test.tsx rename to apps/web/src/chat/avatar-composition.test.tsx index 56c903c15b..7d8e58b8d4 100644 --- a/apps/web/test/chat/avatar-composition.test.tsx +++ b/apps/web/src/chat/avatar-composition.test.tsx @@ -8,7 +8,7 @@ import { avatarClassForPrincipal, avatarColorForPrincipal, resolveAvatarFill, -} from "../../src/chat"; +} from "."; describe("avatar composition and identity resolution", () => { test("agent Corbit avatar is distinct from human initials avatar", () => { diff --git a/apps/web/test/chat/approve-block.test.tsx b/apps/web/src/chat/blocks/approve-block.test.tsx similarity index 98% rename from apps/web/test/chat/approve-block.test.tsx rename to apps/web/src/chat/blocks/approve-block.test.tsx index 270152404a..d8f54d3b32 100644 --- a/apps/web/test/chat/approve-block.test.tsx +++ b/apps/web/src/chat/blocks/approve-block.test.tsx @@ -18,9 +18,9 @@ import type { ApprovalLiveStatus, ApprovalStatusQuery, PlatformApprovalDetail, -} from "../../src/chat/blocks/approval-actions"; -import type { MessageItem } from "../../src/chat/api"; -import { WorkbenchTimeline } from "../../src/chat/timeline"; +} from "./approval-actions"; +import type { MessageItem } from "../api"; +import { WorkbenchTimeline } from "../timeline"; const PLATFORM_DETAIL: PlatformApprovalDetail = { agentName: "Payments Bot", diff --git a/apps/web/test/chat/connect-service-block.test.tsx b/apps/web/src/chat/blocks/connect-service-block.test.tsx similarity index 96% rename from apps/web/test/chat/connect-service-block.test.tsx rename to apps/web/src/chat/blocks/connect-service-block.test.tsx index f3a6c760e9..6322a5c384 100644 --- a/apps/web/test/chat/connect-service-block.test.tsx +++ b/apps/web/src/chat/blocks/connect-service-block.test.tsx @@ -12,8 +12,8 @@ import type { ReactElement } from "react"; import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; -import type { ConnectServiceCardProps } from "../../src/chat/blocks/connect-service-block"; -import { ConnectServiceBlockView } from "../../src/chat/blocks/connect-service-block"; +import type { ConnectServiceCardProps } from "./connect-service-block"; +import { ConnectServiceBlockView } from "./connect-service-block"; let container: HTMLDivElement | null = null; let root: Root | null = null; diff --git a/apps/web/test/chat/chat-bubble-flat.test.ts b/apps/web/src/chat/chat-bubble-flat.test.ts similarity index 100% rename from apps/web/test/chat/chat-bubble-flat.test.ts rename to apps/web/src/chat/chat-bubble-flat.test.ts diff --git a/apps/web/test/chat/chat-workspace.test.tsx b/apps/web/src/chat/chat-workspace.test.tsx similarity index 98% rename from apps/web/test/chat/chat-workspace.test.tsx rename to apps/web/src/chat/chat-workspace.test.tsx index 15167c756d..e3baac2c83 100644 --- a/apps/web/test/chat/chat-workspace.test.tsx +++ b/apps/web/src/chat/chat-workspace.test.tsx @@ -50,7 +50,7 @@ const WORKBENCH_WIRE = { participants: [] as { address: string; handle: string }[], }; -const { ChatWorkspace } = await import("../../src/chat/chat-workspace"); +const { ChatWorkspace } = await import("./chat-workspace"); const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/apps/web/test/chat/components.test.tsx b/apps/web/src/chat/components.test.tsx similarity index 98% rename from apps/web/test/chat/components.test.tsx rename to apps/web/src/chat/components.test.tsx index 26d31bf534..baba6a1c31 100644 --- a/apps/web/test/chat/components.test.tsx +++ b/apps/web/src/chat/components.test.tsx @@ -5,19 +5,19 @@ import { describe, expect, test } from "bun:test"; import { renderToStaticMarkup } from "react-dom/server"; -import type { MessageItem } from "../../src/chat/api"; -import { Composer } from "../../src/chat/composer"; -import { profileSubjectFromParticipant } from "../../src/chat/profile-subject"; -import { renamePayload, rowMenuLabels } from "../../src/chat/sidebar"; +import type { MessageItem } from "./api"; +import { Composer } from "./composer"; +import { profileSubjectFromParticipant } from "./profile-subject"; +import { renamePayload, rowMenuLabels } from "./sidebar"; import { isTypingStateExpired, nextTypingState, parseTypingEvent, typingLabel, TypingIndicator, -} from "../../src/chat/typing-indicator"; +} from "./typing-indicator"; -import { WorkbenchTimeline } from "../../src/chat/timeline"; +import { WorkbenchTimeline } from "./timeline"; /** The floor: no rendered text may ever contain a raw identifier. */ const RAW_ID_PATTERN = /\b(prn_|ins_|tnt_)[a-z0-9]/i; diff --git a/apps/web/test/chat/composer-synchronous-double-send.test.tsx b/apps/web/src/chat/composer-synchronous-double-send.test.tsx similarity index 95% rename from apps/web/test/chat/composer-synchronous-double-send.test.tsx rename to apps/web/src/chat/composer-synchronous-double-send.test.tsx index 0e36126cd6..9355d33f47 100644 --- a/apps/web/test/chat/composer-synchronous-double-send.test.tsx +++ b/apps/web/src/chat/composer-synchronous-double-send.test.tsx @@ -10,8 +10,8 @@ import { act, createElement, createRef } from "react"; import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; -import { Composer } from "../../src/chat/composer"; -import type { ComposerHandle, ComposerSendPayload } from "../../src/chat/composer"; +import { Composer } from "./composer"; +import type { ComposerHandle, ComposerSendPayload } from "./composer"; let container: HTMLDivElement | null = null; let root: Root | null = null; diff --git a/apps/web/src/chat/composer.test.tsx b/apps/web/src/chat/composer.test.tsx index 488b3735a5..5a487308f2 100644 --- a/apps/web/src/chat/composer.test.tsx +++ b/apps/web/src/chat/composer.test.tsx @@ -1,119 +1,848 @@ -import { describe, expect, test } from "bun:test"; +import { afterEach, describe, expect, spyOn, test } from "bun:test"; +import { act, createElement, createRef } from "react"; +import { createRoot } from "react-dom/client"; +import type { Root } from "react-dom/client"; +import * as errorSink from "@corbits/error-sink"; -import { - canStopComposer, - insertTextAtCaret, - spliceDictationTranscript, - speechRecognitionConstructor, - transcriptFromSpeechResults, -} from "./composer"; -import { isAwaitingReply } from "./streaming-reply"; +import { Composer } from "./composer"; +import type { ComposerHandle, ComposerSendPayload } from "./composer"; -describe("insertTextAtCaret", () => { - test("splices the insertion in at the caret", () => { - const result = insertTextAtCaret("hello world", 6, "@myra "); - expect(result.text).toBe("hello @myra world"); - expect(result.caret).toBe(12); +// The in-flight send contract survives the icon-only UI: its accessible name +// announces progress and the action remains unavailable until sending ends. +let container: HTMLDivElement | null = null; +let root: Root | null = null; + +afterEach(() => { + if (root !== null) { + act(() => root?.unmount()); + root = null; + } + if (container !== null) { + container.remove(); + container = null; + } +}); + +const settle = () => act(() => new Promise((resolve) => setTimeout(resolve, 0))); + +function mount(onSend: () => Promise) { + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); + const ref = createRef(); + act(() => { + root?.render( + createElement(Composer, { + ref, + agents: [], + onSend, + }), + ); }); + return container; +} + +function sendButton(): HTMLButtonElement { + const button = container?.querySelector('[aria-label^="Send"]'); + if (button === null || button === undefined) { + throw new Error("send button not found"); + } + return button; +} + +function keyboardHint(): Element | null { + return container?.querySelector(".chat-composer-keyboard-hint") ?? null; +} + +describe("Composer send button", () => { + test("updates its accessible label and stays disabled while sending", async () => { + let resolveSend: (value: boolean) => void = () => undefined; + const onSend = () => + new Promise((resolve) => { + resolveSend = resolve; + }); + mount(onSend); + + const textarea = container?.querySelector("textarea"); + if (textarea === null || textarea === undefined) { + throw new Error("composer textarea not found"); + } + act(() => { + const setter = Object.getOwnPropertyDescriptor( + globalThis.HTMLTextAreaElement.prototype, + "value", + )?.set; + setter?.call(textarea, "hello there"); + textarea.dispatchEvent(new Event("input", { bubbles: true })); + }); + await settle(); + + expect(sendButton().getAttribute("aria-label")).toBe("Send"); - test("appends at the end when the caret sits past the text", () => { - const result = insertTextAtCaret("hi", 2, "@myra "); - expect(result.text).toBe("hi@myra "); - expect(result.caret).toBe(8); + act(() => { + sendButton().click(); + }); + await settle(); + + expect(sendButton().getAttribute("aria-label")).toBe("Sending…"); + expect(sendButton().hasAttribute("disabled")).toBe(true); + + await act(async () => { + resolveSend(true); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(sendButton().getAttribute("aria-label")).toBe("Send"); }); +}); - test("inserts into an empty draft", () => { - const result = insertTextAtCaret("", 0, "@myra "); - expect(result.text).toBe("@myra "); - expect(result.caret).toBe(6); +test("labels the icon-only attachment action in the composer rail", () => { + mount(() => Promise.resolve(true)); + + expect(container?.querySelector('[aria-label="Attach files"]')?.textContent).toBe(""); +}); + +function textarea(): HTMLTextAreaElement { + const element = container?.querySelector("textarea"); + if (element === null || element === undefined) { + throw new Error("composer textarea not found"); + } + return element; +} + +function typeInto(element: HTMLTextAreaElement, text: string) { + const setter = Object.getOwnPropertyDescriptor( + globalThis.HTMLTextAreaElement.prototype, + "value", + )?.set; + act(() => { + setter?.call(element, text); + element.dispatchEvent(new Event("input", { bubbles: true })); + }); +} + +function mountWithMentions(onSend: (payload: ComposerSendPayload) => Promise) { + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); + const ref = createRef(); + act(() => { + root?.render( + createElement(Composer, { + ref, + agents: [{ id: "researcher@agents.example", handle: "researcher", label: "Researcher" }], + onSend, + }), + ); + }); + return container; +} + +describe("Composer keyboard hint", () => { + test("uses the existing action rail and appears only for a focused non-empty draft", async () => { + mount(() => Promise.resolve(true)); + expect(keyboardHint()).not.toBeNull(); + expect(keyboardHint()?.getAttribute("data-visible")).toBe("false"); + expect(keyboardHint()?.getAttribute("aria-hidden")).toBe("true"); + + act(() => { + textarea().focus(); + }); + typeInto(textarea(), "hello"); + await settle(); + + expect(keyboardHint()?.getAttribute("data-visible")).toBe("true"); + expect(keyboardHint()?.getAttribute("aria-hidden")).toBe("false"); + expect(keyboardHint()?.textContent).toBe("Enter to send"); + expect(container?.querySelectorAll(".chat-composer-actions button").length).toBe(2); + expect(container?.querySelector(".chat-composer-row > textarea")).toBe(textarea()); + }); + + test("hides when the focused draft is cleared", async () => { + mount(() => Promise.resolve(true)); + act(() => { + textarea().focus(); + }); + typeInto(textarea(), "hello"); + await settle(); + expect(keyboardHint()?.getAttribute("data-visible")).toBe("true"); + + typeInto(textarea(), ""); + await settle(); + expect(keyboardHint()?.getAttribute("data-visible")).toBe("false"); + expect(keyboardHint()?.getAttribute("aria-hidden")).toBe("true"); }); }); -describe("spliceDictationTranscript", () => { - test("inserts a space so two words do not glue together", () => { - const result = spliceDictationTranscript("hello", "", "world"); - expect(result.text).toBe("hello world"); - expect(result.caret).toBe(11); +describe("Composer growth containment", () => { + test("the textarea carries the max-height/overflow class and keeps applying its measured inline height", async () => { + mount(() => Promise.resolve(true)); + expect(textarea().className).toContain("chat-composer-input"); + + typeInto(textarea(), "line one\nline two\nline three"); + await settle(); + // The auto-grow effect still measures and writes an inline height on + // every change — the CSS transition only smooths that write, it does + // not replace it. + expect(textarea().style.height.endsWith("px")).toBe(true); }); +}); + +describe("Composer popover entrance", () => { + test("the mention popover carries the entrance class", async () => { + mountWithMentions(() => Promise.resolve(true)); + typeInto(textarea(), "@"); + await settle(); + const popover = container?.querySelector(".chat-mention-popover"); + expect(popover?.classList.contains("chat-popover-enter")).toBe(true); + }); +}); + +describe("Composer hit targets", () => { + test("attach and send buttons carry the extended-hit-area class", () => { + mount(() => Promise.resolve(true)); + const buttons = container?.querySelectorAll(".chat-composer-icon-button"); + expect(buttons?.length).toBe(2); + }); +}); + +describe("ComposerHandle.setText", () => { + test("replaces the existing draft and focuses with the caret at the end", async () => { + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); + const ref = createRef(); + act(() => { + root?.render( + createElement(Composer, { + ref, + agents: [], + onSend: () => Promise.resolve(true), + }), + ); + }); + + typeInto(textarea(), "unsent draft"); + await settle(); + expect(textarea().value).toBe("unsent draft"); + + await act(async () => { + ref.current?.setText("previous prompt"); + await new Promise((resolve) => { + requestAnimationFrame(() => resolve()); + }); + }); - test("does not add a second space when the prefix already ends with one", () => { - const result = spliceDictationTranscript("hello ", "", "world"); - expect(result.text).toBe("hello world"); - expect(result.caret).toBe(11); + expect(textarea().value).toBe("previous prompt"); + expect(document.activeElement).toBe(textarea()); + expect(textarea().selectionStart).toBe("previous prompt".length); + expect(textarea().selectionEnd).toBe("previous prompt".length); }); - test("keeps a following suffix on its own word boundary", () => { - const result = spliceDictationTranscript("hello", "there", "world"); - expect(result.text).toBe("hello world there"); - expect(result.caret).toBe(11); + test("Enter after setText of an @handle prompt sends instead of opening mention", async () => { + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); + const ref = createRef(); + const sent: ComposerSendPayload[] = []; + act(() => { + root?.render( + createElement(Composer, { + ref, + agents: [{ id: "researcher@agents.example", handle: "researcher", label: "Researcher" }], + onSend: (payload) => { + sent.push(payload); + return Promise.resolve(true); + }, + }), + ); + }); + + await act(async () => { + ref.current?.setText("@researcher"); + await new Promise((resolve) => { + requestAnimationFrame(() => resolve()); + }); + }); + await settle(); + + expect(container?.querySelector(".chat-mention-popover")).toBeNull(); + + act(() => { + textarea().dispatchEvent( + new KeyboardEvent("keydown", { + key: "Enter", + bubbles: true, + cancelable: true, + }), + ); + }); + await settle(); + + expect(sent).toEqual([{ text: "@researcher", attachments: [] }]); }); - test("drops an empty transcript without changing the draft", () => { - const result = spliceDictationTranscript("hello", " there", " "); - expect(result.text).toBe("hello there"); - expect(result.caret).toBe(5); + test("Send after setText does not carry leftover attachments", async () => { + const sent: ComposerSendPayload[] = []; + const ref = createRef(); + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); + act(() => { + root?.render( + createElement(Composer, { + ref, + agents: [], + onSend: (payload) => { + sent.push(payload); + return Promise.resolve(true); + }, + }), + ); + }); + + const fileInput = container.querySelector(".chat-composer-file-input"); + if (fileInput === null) throw new Error("file input not found"); + + const file = new File(["hello"], "notes.txt", { type: "text/plain" }); + Object.defineProperty(fileInput, "files", { + configurable: true, + value: { + 0: file, + length: 1, + item: (index: number) => (index === 0 ? file : null), + [Symbol.iterator]: function* () { + yield file; + }, + }, + }); + act(() => { + fileInput.dispatchEvent(new Event("change", { bubbles: true })); + }); + await settle(); + await settle(); + expect(container.querySelector(".chat-composer-attachments")).not.toBeNull(); + + await act(async () => { + ref.current?.setText("copied prompt"); + await new Promise((resolve) => { + requestAnimationFrame(() => resolve()); + }); + }); + await settle(); + + expect(container.querySelector(".chat-composer-attachments")).toBeNull(); + + act(() => { + sendButton().click(); + }); + await settle(); + + expect(sent).toEqual([{ text: "copied prompt", attachments: [] }]); }); }); -describe("transcriptFromSpeechResults", () => { - test("concatenates every alternative in order", () => { - expect( - transcriptFromSpeechResults([ - { isFinal: true, length: 1, 0: { transcript: "hello " } }, - { isFinal: false, length: 1, 0: { transcript: "world" } }, - ]), - ).toBe("hello world"); +// The composer's Stop affordance. `handleStop` guards against a +// double-click and re-enables itself once the host reports the turn is +// no longer running -- but a REJECTED stop request must also re-enable +// it, or a genuinely failed cancel (not a slow one) leaves the person +// with a permanently disabled button and no way to retry for the rest +// of that turn's life. +function stopButton(): HTMLButtonElement { + const button = container?.querySelector('[aria-label="Stop"]'); + if (button === null || button === undefined) { + throw new Error("stop button not found"); + } + return button; +} + +function mountStoppable( + running: boolean, + onStop: () => void | Promise, + onSend: (payload: ComposerSendPayload) => Promise = () => Promise.resolve(true), +) { + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); + act(() => { + root?.render( + createElement(Composer, { + agents: [], + onSend, + running, + onStop, + }), + ); + }); + return { + container, + rerender: (nextRunning: boolean) => { + act(() => { + root?.render( + createElement(Composer, { + agents: [], + onSend, + running: nextRunning, + onStop, + }), + ); + }); + }, + }; +} + +describe("Composer stop affordance", () => { + test("keeps Stop and Send together in the right-aligned action group", () => { + mountStoppable(true, () => undefined); + + const actions = container?.querySelector(".chat-composer-actions"); + const submitActions = stopButton().parentElement; + expect(actions?.children).toHaveLength(3); + expect(submitActions?.classList.contains("chat-composer-submit-actions")).toBe(true); + expect(submitActions?.children).toHaveLength(2); + expect(submitActions?.lastElementChild).toBe(sendButton()); + expect(actions?.lastElementChild).toBe(submitActions); + }); + + test("gives Stop and Send distinct accessible names while both are visible", () => { + mountStoppable(true, () => undefined); + + expect(stopButton().getAttribute("aria-label")).toBe("Stop"); + expect(sendButton().getAttribute("aria-label")).toBe("Send"); + }); + + test("keeps queued sends available while Stop is visible", async () => { + const sent: ComposerSendPayload[] = []; + mountStoppable( + true, + () => undefined, + (payload) => { + sent.push(payload); + return Promise.resolve(true); + }, + ); + + typeInto(textarea(), "follow-up"); + await settle(); + + expect(sendButton().hasAttribute("disabled")).toBe(false); + act(() => { + sendButton().click(); + }); + await settle(); + + expect(sent).toEqual([{ text: "follow-up", attachments: [] }]); + }); + + test("renders no Stop button when no turn is running", () => { + mountStoppable(false, () => undefined); + expect(container?.querySelector('[aria-label="Stop"]')).toBeNull(); + }); + + test("clicking Stop calls onStop and disables the button against a double-click", async () => { + let calls = 0; + mountStoppable(true, () => { + calls += 1; + }); + + expect(stopButton().hasAttribute("disabled")).toBe(false); + act(() => { + stopButton().click(); + }); + await settle(); + + expect(calls).toBe(1); + expect(stopButton().hasAttribute("disabled")).toBe(true); + + act(() => { + stopButton().click(); + }); + await settle(); + expect(calls).toBe(1); + }); + + test("the button re-enables once the host reports the turn is no longer running", async () => { + const { rerender } = mountStoppable(true, () => undefined); + act(() => { + stopButton().click(); + }); + await settle(); + expect(stopButton().hasAttribute("disabled")).toBe(true); + + rerender(false); + expect(container?.querySelector('[aria-label="Stop"]')).toBeNull(); + }); + + test("a stop request that REJECTS re-enables the button instead of leaving it stuck", async () => { + let reject: (err: unknown) => void = () => undefined; + mountStoppable( + true, + () => + new Promise((_resolve, rej) => { + reject = rej; + }), + ); + + act(() => { + stopButton().click(); + }); + await settle(); + expect(stopButton().hasAttribute("disabled")).toBe(true); + + await act(async () => { + reject(new Error("network error")); + await Promise.resolve(); + await Promise.resolve(); + }); + + // The turn is still running (the cancel request itself failed, not + // the turn) -- the button must come back so the user can try again. + expect(stopButton().hasAttribute("disabled")).toBe(false); }); }); -describe("speechRecognitionConstructor", () => { - test("returns null when the browser has no speech recognition", () => { - expect(speechRecognitionConstructor({})).toBeNull(); +describe("Composer dictate", () => { + const recognitions: FakeSpeechRecognition[] = []; + + class FakeSpeechRecognition { + continuous = false; + interimResults = false; + onresult: ((event: { results: FakeSpeechResult[] }) => void) | null = null; + onerror: ((event: unknown) => void) | null = null; + onend: (() => void) | null = null; + started = false; + resultOnStop: readonly { isFinal: boolean; transcript: string }[] | null = null; + + constructor() { + recognitions.push(this); + } + + start() { + this.started = true; + } + + stop() { + this.started = false; + if (this.resultOnStop !== null) { + this.emit(this.resultOnStop); + this.resultOnStop = null; + } + this.onend?.(); + } + + abort() { + this.started = false; + this.onend?.(); + } + + emit(segments: readonly { isFinal: boolean; transcript: string }[]) { + const results = segments.map((segment) => ({ + isFinal: segment.isFinal, + length: 1, + 0: { transcript: segment.transcript }, + })); + this.onresult?.({ results }); + } + } + + type FakeSpeechResult = { + readonly isFinal: boolean; + readonly length: number; + readonly 0: { readonly transcript: string }; + }; + + function installSpeechRecognition() { + Object.defineProperty(window, "SpeechRecognition", { + configurable: true, + writable: true, + value: FakeSpeechRecognition, + }); + } + + afterEach(() => { + recognitions.length = 0; + Reflect.deleteProperty(window, "SpeechRecognition"); }); - test("prefers SpeechRecognition over the webkit prefix", () => { - function SpeechRecognition() { - return undefined; + test("hides the dictate control when speech recognition is unavailable", () => { + mount(() => Promise.resolve(true)); + expect(container?.querySelector('[aria-label="Dictate"]')).toBeNull(); + }); + + test("starts recognition, inserts a transcript on a word boundary, and stops", async () => { + installSpeechRecognition(); + mount(() => Promise.resolve(true)); + typeInto(textarea(), "hello"); + textarea().setSelectionRange(5, 5); + await settle(); + + const dictate = container?.querySelector('[aria-label="Dictate"]'); + if (dictate === null || dictate === undefined) { + throw new Error("dictate button not found"); } - function webkitSpeechRecognition() { - return undefined; + + act(() => { + dictate.click(); + }); + await settle(); + + expect(recognitions.at(-1)?.started).toBe(true); + expect(recognitions.at(-1)?.continuous).toBe(true); + expect(recognitions.at(-1)?.interimResults).toBe(true); + expect(dictate.getAttribute("aria-label")).toBe("Stop dictating"); + expect(dictate.getAttribute("aria-pressed")).toBe("true"); + expect(dictate.getAttribute("data-listening")).toBe("true"); + + act(() => { + recognitions.at(-1)?.emit([{ isFinal: true, transcript: "world" }]); + }); + await settle(); + + expect(textarea().value).toBe("hello world"); + + act(() => { + dictate.click(); + }); + await settle(); + + expect(recognitions.at(-1)?.started).toBe(false); + const idle = container?.querySelector('[aria-label="Dictate"]'); + expect(idle?.getAttribute("aria-pressed")).toBe("false"); + expect(idle?.getAttribute("data-listening")).toBe("false"); + }); + + test("user Stop flushes a late final result into the draft", async () => { + installSpeechRecognition(); + mount(() => Promise.resolve(true)); + typeInto(textarea(), "hello"); + textarea().setSelectionRange(5, 5); + await settle(); + + const dictate = container?.querySelector('[aria-label="Dictate"]'); + if (dictate === null || dictate === undefined) { + throw new Error("dictate button not found"); } - const found = speechRecognitionConstructor({ - SpeechRecognition, - webkitSpeechRecognition, + + act(() => { + dictate.click(); + }); + await settle(); + + const rec = recognitions.at(-1); + if (rec === undefined) { + throw new Error("speech recognition was not constructed"); + } + rec.resultOnStop = [{ isFinal: true, transcript: "world" }]; + + act(() => { + dictate.click(); }); - expect(Object.is(found, SpeechRecognition)).toBe(true); + await settle(); + + expect(textarea().value).toBe("hello world"); + expect(rec.started).toBe(false); }); - test("falls back to webkitSpeechRecognition", () => { - function webkitSpeechRecognition() { - return undefined; + test("send while listening does not restore the draft from a late final result", async () => { + installSpeechRecognition(); + const sent: ComposerSendPayload[] = []; + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); + act(() => { + root?.render( + createElement(Composer, { + agents: [], + onSend: (payload) => { + sent.push(payload); + return Promise.resolve(true); + }, + }), + ); + }); + typeInto(textarea(), "hello"); + textarea().setSelectionRange(5, 5); + await settle(); + + const dictate = container?.querySelector('[aria-label="Dictate"]'); + if (dictate === null || dictate === undefined) { + throw new Error("dictate button not found"); + } + + act(() => { + dictate.click(); + }); + await settle(); + + const rec = recognitions.at(-1); + if (rec === undefined) { + throw new Error("speech recognition was not constructed"); } - const found = speechRecognitionConstructor({ webkitSpeechRecognition }); - expect(Object.is(found, webkitSpeechRecognition)).toBe(true); + rec.resultOnStop = [{ isFinal: true, transcript: "late words" }]; + + act(() => { + sendButton().click(); + }); + await settle(); + + act(() => { + rec.emit([{ isFinal: true, transcript: "late words" }]); + }); + await settle(); + + expect(textarea().value).toBe(""); + expect(sent).toEqual([{ text: "hello", attachments: [] }]); }); -}); -// The composer's stop affordance is a stand-in for "is there a -// turn to cancel" — offered whenever the host says a turn is running, -// independent of the composer's own `sending`/`preparing` state (queuing -// a follow-up message while a turn runs is still allowed, so the stop -// affordance and the send button coexist rather than one gating the -// other). -describe("canStopComposer", () => { - test("offers Stop while the host reports a turn running", () => { - expect(canStopComposer({ running: true })).toBe(true); + test("Enter while listening does not restore the draft from a late final result", async () => { + installSpeechRecognition(); + const sent: ComposerSendPayload[] = []; + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); + act(() => { + root?.render( + createElement(Composer, { + agents: [], + onSend: (payload) => { + sent.push(payload); + return Promise.resolve(true); + }, + }), + ); + }); + typeInto(textarea(), "hello"); + textarea().setSelectionRange(5, 5); + await settle(); + + const dictate = container?.querySelector('[aria-label="Dictate"]'); + if (dictate === null || dictate === undefined) { + throw new Error("dictate button not found"); + } + + act(() => { + dictate.click(); + }); + await settle(); + + const rec = recognitions.at(-1); + if (rec === undefined) { + throw new Error("speech recognition was not constructed"); + } + rec.resultOnStop = [{ isFinal: true, transcript: "late words" }]; + + act(() => { + textarea().dispatchEvent( + new KeyboardEvent("keydown", { + key: "Enter", + bubbles: true, + cancelable: true, + }), + ); + }); + await settle(); + + act(() => { + rec.emit([{ isFinal: true, transcript: "late words" }]); + }); + await settle(); + + expect(textarea().value).toBe(""); + expect(sent).toEqual([{ text: "hello", attachments: [] }]); }); - test("offers nothing when no turn is running", () => { - expect(canStopComposer({ running: false })).toBe(false); + test("stop then send does not restore the draft from a deferred final result", async () => { + installSpeechRecognition(); + const sent: ComposerSendPayload[] = []; + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); + act(() => { + root?.render( + createElement(Composer, { + agents: [], + onSend: (payload) => { + sent.push(payload); + return Promise.resolve(true); + }, + }), + ); + }); + typeInto(textarea(), "hello"); + textarea().setSelectionRange(5, 5); + await settle(); + + const dictate = container?.querySelector('[aria-label="Dictate"]'); + if (dictate === null || dictate === undefined) { + throw new Error("dictate button not found"); + } + + act(() => { + dictate.click(); + }); + await settle(); + + const rec = recognitions.at(-1); + if (rec === undefined) { + throw new Error("speech recognition was not constructed"); + } + + act(() => { + dictate.click(); + }); + await settle(); + + expect(textarea().value).toBe("hello"); + + act(() => { + sendButton().click(); + }); + await settle(); + + act(() => { + rec.emit([{ isFinal: true, transcript: "late words" }]); + }); + await settle(); + + expect(textarea().value).toBe(""); + expect(sent).toEqual([{ text: "hello", attachments: [] }]); }); - test("offers Stop while awaiting a turn that already has streamed text", () => { - expect( - canStopComposer({ - running: isAwaitingReply({ phase: "awaiting", text: "Hello" }), - }), - ).toBe(true); + test("does not report aborted or no-speech recognition errors", async () => { + installSpeechRecognition(); + const report = spyOn(errorSink, "reportError"); + mount(() => Promise.resolve(true)); + + const dictate = container?.querySelector('[aria-label="Dictate"]'); + if (dictate === null || dictate === undefined) { + throw new Error("dictate button not found"); + } + + act(() => { + dictate.click(); + }); + await settle(); + + const rec = recognitions.at(-1); + if (rec === undefined) { + throw new Error("speech recognition was not constructed"); + } + + act(() => { + rec.onerror?.({ error: "no-speech" }); + rec.onerror?.({ error: "aborted" }); + }); + await settle(); + + expect(report).not.toHaveBeenCalled(); + + act(() => { + rec.onerror?.({ error: "network" }); + }); + await settle(); + + expect(report).toHaveBeenCalled(); + report.mockRestore(); }); }); diff --git a/apps/web/test/chat/connect-service-flow.test.tsx b/apps/web/src/chat/connect-service-flow.test.tsx similarity index 95% rename from apps/web/test/chat/connect-service-flow.test.tsx rename to apps/web/src/chat/connect-service-flow.test.tsx index 7c401134e0..10d921fb37 100644 --- a/apps/web/test/chat/connect-service-flow.test.tsx +++ b/apps/web/src/chat/connect-service-flow.test.tsx @@ -14,8 +14,8 @@ import type { Root } from "react-dom/client"; import type { ConnectServiceActions, ConnectServiceQuery, -} from "../../src/chat/blocks/connect-service-actions"; -import { ConnectServiceBlockContainer } from "../../src/chat/blocks/connect-service-block-container"; +} from "./blocks/connect-service-actions"; +import { ConnectServiceBlockContainer } from "./blocks/connect-service-block-container"; let container: HTMLDivElement | null = null; let root: Root | null = null; diff --git a/apps/web/test/chat/failed-turn-strip.test.tsx b/apps/web/src/chat/failed-turn-strip.test.tsx similarity index 99% rename from apps/web/test/chat/failed-turn-strip.test.tsx rename to apps/web/src/chat/failed-turn-strip.test.tsx index 086ad3e039..4f5e7bba1c 100644 --- a/apps/web/test/chat/failed-turn-strip.test.tsx +++ b/apps/web/src/chat/failed-turn-strip.test.tsx @@ -9,8 +9,8 @@ import { act } from "react"; import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; -import type { MessageItem } from "../../src/chat/api"; -import { WorkbenchTimeline } from "../../src/chat/timeline"; +import type { MessageItem } from "./api"; +import { WorkbenchTimeline } from "./timeline"; if (typeof document === "undefined") { GlobalRegistrator.register(); diff --git a/apps/web/test/chat/fake-clock.ts b/apps/web/src/chat/fake-clock.ts similarity index 97% rename from apps/web/test/chat/fake-clock.ts rename to apps/web/src/chat/fake-clock.ts index 8f6b9244c6..d58db86d3c 100644 --- a/apps/web/test/chat/fake-clock.ts +++ b/apps/web/src/chat/fake-clock.ts @@ -2,7 +2,7 @@ // fires every due timer against a virtual clock with no real sleep, so a // test asserting a 30ms backstop actually fired takes 0ms of wall time // instead of racing CI's CPU-contended shards for those 30ms. -import type { Clock } from "../../src/chat/clock"; +import type { Clock } from "./clock"; export type FakeClock = { readonly clock: Clock; diff --git a/apps/web/test/chat/inference-preamble-drift.test.ts b/apps/web/src/chat/inference-preamble-drift.test.ts similarity index 93% rename from apps/web/test/chat/inference-preamble-drift.test.ts rename to apps/web/src/chat/inference-preamble-drift.test.ts index bea91aa0a2..421abe5ce0 100644 --- a/apps/web/test/chat/inference-preamble-drift.test.ts +++ b/apps/web/src/chat/inference-preamble-drift.test.ts @@ -14,7 +14,7 @@ import { readFileSync } from "node:fs"; import { dirname, extname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { CLASSIFIED_INFERENCE_FAILURE_PREAMBLES } from "../../src/chat/inference-failure"; +import { CLASSIFIED_INFERENCE_FAILURE_PREAMBLES } from "./inference-failure"; test("chat-ui's classified-failure preambles match the published director's exact strings", () => { const entry = fileURLToPath(import.meta.resolve("@intx/inference")); diff --git a/apps/web/test/chat/loading-state.test.tsx b/apps/web/src/chat/loading-state.test.tsx similarity index 95% rename from apps/web/test/chat/loading-state.test.tsx rename to apps/web/src/chat/loading-state.test.tsx index 367877ff21..ec4bb1edac 100644 --- a/apps/web/test/chat/loading-state.test.tsx +++ b/apps/web/src/chat/loading-state.test.tsx @@ -9,8 +9,8 @@ import type { ReactElement } from "react"; import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; -import { WorkbenchLoadingState } from "../../src/chat/loading-state"; -import { CHAT_STRINGS } from "../../src/chat/strings"; +import { WorkbenchLoadingState } from "./loading-state"; +import { CHAT_STRINGS } from "./strings"; let container: HTMLDivElement | null = null; let root: Root | null = null; diff --git a/apps/web/test/chat/mentions.test.ts b/apps/web/src/chat/mentions.test.ts similarity index 99% rename from apps/web/test/chat/mentions.test.ts rename to apps/web/src/chat/mentions.test.ts index 11ae93ba57..dd89358a09 100644 --- a/apps/web/test/chat/mentions.test.ts +++ b/apps/web/src/chat/mentions.test.ts @@ -5,7 +5,7 @@ import { filterMentionCandidates, insertMention, mentionCandidatesFromParticipants, -} from "../../src/chat/mentions"; +} from "./mentions"; describe("activeMentionQuery", () => { test("detects an open mention right after the @", () => { diff --git a/apps/web/test/chat/message-grouping-and-failed-polish.test.tsx b/apps/web/src/chat/message-grouping-and-failed-polish.test.tsx similarity index 98% rename from apps/web/test/chat/message-grouping-and-failed-polish.test.tsx rename to apps/web/src/chat/message-grouping-and-failed-polish.test.tsx index a7d133220a..7aa162ec2c 100644 --- a/apps/web/test/chat/message-grouping-and-failed-polish.test.tsx +++ b/apps/web/src/chat/message-grouping-and-failed-polish.test.tsx @@ -8,9 +8,9 @@ import { act } from "react"; import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; -import type { MessageItem } from "../../src/chat/api"; -import type { PendingActions, TimelineMessageItem } from "../../src/chat/timeline"; -import { WorkbenchTimeline } from "../../src/chat/timeline"; +import type { MessageItem } from "./api"; +import type { PendingActions, TimelineMessageItem } from "./timeline"; +import { WorkbenchTimeline } from "./timeline"; let container: HTMLDivElement | null = null; let root: Root | null = null; diff --git a/apps/web/test/chat/message-hover-toolbar.test.tsx b/apps/web/src/chat/message-hover-toolbar.test.tsx similarity index 98% rename from apps/web/test/chat/message-hover-toolbar.test.tsx rename to apps/web/src/chat/message-hover-toolbar.test.tsx index ccade71598..d1c11ffcac 100644 --- a/apps/web/test/chat/message-hover-toolbar.test.tsx +++ b/apps/web/src/chat/message-hover-toolbar.test.tsx @@ -9,8 +9,8 @@ import { act } from "react"; import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; -import { WorkbenchTimeline } from "../../src/chat/timeline"; -import type { CurrentUser, TimelineMessageItem } from "../../src/chat/timeline"; +import { WorkbenchTimeline } from "./timeline"; +import type { CurrentUser, TimelineMessageItem } from "./timeline"; const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/apps/web/test/chat/no-usable-model-banner.test.tsx b/apps/web/src/chat/no-usable-model-banner.test.tsx similarity index 94% rename from apps/web/test/chat/no-usable-model-banner.test.tsx rename to apps/web/src/chat/no-usable-model-banner.test.tsx index b9c004f59b..d1ea68740a 100644 --- a/apps/web/test/chat/no-usable-model-banner.test.tsx +++ b/apps/web/src/chat/no-usable-model-banner.test.tsx @@ -6,7 +6,7 @@ import { act } from "react"; import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; -import { NoUsableModelBanner } from "../../src/chat/no-usable-model-banner"; +import { NoUsableModelBanner } from "./no-usable-model-banner"; let container: HTMLDivElement | null = null; let root: Root | null = null; diff --git a/apps/web/test/chat/own-message-alignment.test.tsx b/apps/web/src/chat/own-message-alignment.test.tsx similarity index 97% rename from apps/web/test/chat/own-message-alignment.test.tsx rename to apps/web/src/chat/own-message-alignment.test.tsx index 43d0c15e7e..707d3c6f9b 100644 --- a/apps/web/test/chat/own-message-alignment.test.tsx +++ b/apps/web/src/chat/own-message-alignment.test.tsx @@ -11,9 +11,9 @@ import { act } from "react"; import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; -import type { MessageItem } from "../../src/chat/api"; -import type { CurrentUser } from "../../src/chat/timeline"; -import { WorkbenchTimeline } from "../../src/chat/timeline"; +import type { MessageItem } from "./api"; +import type { CurrentUser } from "./timeline"; +import { WorkbenchTimeline } from "./timeline"; let container: HTMLDivElement | null = null; let root: Root | null = null; diff --git a/apps/web/test/chat/pending-send-lifecycle.test.tsx b/apps/web/src/chat/pending-send-lifecycle.test.tsx similarity index 96% rename from apps/web/test/chat/pending-send-lifecycle.test.tsx rename to apps/web/src/chat/pending-send-lifecycle.test.tsx index 35cbe8dafb..42410b6bd5 100644 --- a/apps/web/test/chat/pending-send-lifecycle.test.tsx +++ b/apps/web/src/chat/pending-send-lifecycle.test.tsx @@ -7,8 +7,8 @@ import { act } from "react"; import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; -import type { PendingActions, TimelineMessageItem } from "../../src/chat/timeline"; -import { WorkbenchTimeline } from "../../src/chat/timeline"; +import type { PendingActions, TimelineMessageItem } from "./timeline"; +import { WorkbenchTimeline } from "./timeline"; let container: HTMLDivElement | null = null; let root: Root | null = null; diff --git a/apps/web/test/chat/pr-thread-view.test.tsx b/apps/web/src/chat/pr-thread-view.test.tsx similarity index 99% rename from apps/web/test/chat/pr-thread-view.test.tsx rename to apps/web/src/chat/pr-thread-view.test.tsx index 52309076e5..2eac1d005b 100644 --- a/apps/web/test/chat/pr-thread-view.test.tsx +++ b/apps/web/src/chat/pr-thread-view.test.tsx @@ -17,8 +17,8 @@ import type { PrThreadFailedTurn, PrThreadReply, PrThreadViewProps, -} from "../../src/chat/pr-thread-view"; -import { PrQueuedStrip, PrThreadView } from "../../src/chat/pr-thread-view"; +} from "./pr-thread-view"; +import { PrQueuedStrip, PrThreadView } from "./pr-thread-view"; let container: HTMLDivElement | null = null; let root: Root | null = null; diff --git a/apps/web/test/chat/presence-stack.test.tsx b/apps/web/src/chat/presence-stack.test.tsx similarity index 99% rename from apps/web/test/chat/presence-stack.test.tsx rename to apps/web/src/chat/presence-stack.test.tsx index d95753007b..0265c38ffd 100644 --- a/apps/web/test/chat/presence-stack.test.tsx +++ b/apps/web/src/chat/presence-stack.test.tsx @@ -79,7 +79,7 @@ function stubFetch() { }) as typeof fetch; } -const { ChatWorkspace, TEAM_AVATAR_STACK_LIMIT } = await import("../../src/chat/chat-workspace"); +const { ChatWorkspace, TEAM_AVATAR_STACK_LIMIT } = await import("./chat-workspace"); const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/apps/web/test/chat/refresh-and-send.test.ts b/apps/web/src/chat/refresh-and-send.test.ts similarity index 98% rename from apps/web/test/chat/refresh-and-send.test.ts rename to apps/web/src/chat/refresh-and-send.test.ts index fa02b1b76c..418b923b79 100644 --- a/apps/web/test/chat/refresh-and-send.test.ts +++ b/apps/web/src/chat/refresh-and-send.test.ts @@ -12,8 +12,8 @@ import { mergePendingSends, mergeStreamingReply, withScrollSnapshot, -} from "../../src/chat/chat-workspace"; -import type { PendingSend } from "../../src/chat/chat-workspace"; +} from "./chat-workspace"; +import type { PendingSend } from "./chat-workspace"; import { attachmentsAfterSend, attachmentBytesOnComposer, @@ -27,10 +27,10 @@ import { draftAfterSend, partsForSend, validateAttachmentPick, -} from "../../src/chat/composer"; -import type { ComposerAttachment } from "../../src/chat/composer"; -import { CHAT_STRINGS } from "../../src/chat/strings"; -import { backoffDelayMs, shouldConnect } from "../../src/chat/use-workbench-stream"; +} from "./composer"; +import type { ComposerAttachment } from "./composer"; +import { CHAT_STRINGS } from "./strings"; +import { backoffDelayMs, shouldConnect } from "./use-workbench-stream"; describe("draftAfterSend (B2: a failed send keeps the draft)", () => { test("clears the draft once the send succeeds", () => { diff --git a/apps/web/test/chat/resume-failed-banner.test.tsx b/apps/web/src/chat/resume-failed-banner.test.tsx similarity index 95% rename from apps/web/test/chat/resume-failed-banner.test.tsx rename to apps/web/src/chat/resume-failed-banner.test.tsx index 2927a15a51..1850093d2f 100644 --- a/apps/web/test/chat/resume-failed-banner.test.tsx +++ b/apps/web/src/chat/resume-failed-banner.test.tsx @@ -6,7 +6,7 @@ import { act } from "react"; import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; -import { ResumeFailedBanner } from "../../src/chat/resume-failed-banner"; +import { ResumeFailedBanner } from "./resume-failed-banner"; let container: HTMLDivElement | null = null; let root: Root | null = null; diff --git a/apps/web/test/chat/single-message-list-reconciliation.test.tsx b/apps/web/src/chat/single-message-list-reconciliation.test.tsx similarity index 96% rename from apps/web/test/chat/single-message-list-reconciliation.test.tsx rename to apps/web/src/chat/single-message-list-reconciliation.test.tsx index ad746f03a4..23550b408c 100644 --- a/apps/web/test/chat/single-message-list-reconciliation.test.tsx +++ b/apps/web/src/chat/single-message-list-reconciliation.test.tsx @@ -11,9 +11,9 @@ import { act } from "react"; import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; -import type { MessageItem } from "../../src/chat/api"; -import type { TimelineMessageItem } from "../../src/chat/timeline"; -import { WorkbenchTimeline } from "../../src/chat/timeline"; +import type { MessageItem } from "./api"; +import type { TimelineMessageItem } from "./timeline"; +import { WorkbenchTimeline } from "./timeline"; let container: HTMLDivElement | null = null; let root: Root | null = null; diff --git a/apps/web/test/chat/stream-apply.test.ts b/apps/web/src/chat/stream-apply.test.ts similarity index 93% rename from apps/web/test/chat/stream-apply.test.ts rename to apps/web/src/chat/stream-apply.test.ts index 6b5998633b..9bf1391a6e 100644 --- a/apps/web/test/chat/stream-apply.test.ts +++ b/apps/web/src/chat/stream-apply.test.ts @@ -4,8 +4,8 @@ import { describe, expect, test } from "bun:test"; -import { ensureReplyThreadRow } from "../../src/chat/use-workbench-feed"; -import type { WorkbenchThreadRow } from "../../src/chat/api"; +import { ensureReplyThreadRow } from "./use-workbench-feed"; +import type { WorkbenchThreadRow } from "./api"; describe("ensureReplyThreadRow", () => { const empty: { diff --git a/apps/web/test/chat/system-join-edge.test.tsx b/apps/web/src/chat/system-join-edge.test.tsx similarity index 96% rename from apps/web/test/chat/system-join-edge.test.tsx rename to apps/web/src/chat/system-join-edge.test.tsx index e0e48b097f..794d21bb24 100644 --- a/apps/web/test/chat/system-join-edge.test.tsx +++ b/apps/web/src/chat/system-join-edge.test.tsx @@ -8,9 +8,9 @@ import { act } from "react"; import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; -import type { MessageItem } from "../../src/chat/api"; -import type { CurrentUser } from "../../src/chat/timeline"; -import { WorkbenchTimeline } from "../../src/chat/timeline"; +import type { MessageItem } from "./api"; +import type { CurrentUser } from "./timeline"; +import { WorkbenchTimeline } from "./timeline"; let container: HTMLDivElement | null = null; let root: Root | null = null; diff --git a/apps/web/test/chat/system-row-chrome.test.tsx b/apps/web/src/chat/system-row-chrome.test.tsx similarity index 98% rename from apps/web/test/chat/system-row-chrome.test.tsx rename to apps/web/src/chat/system-row-chrome.test.tsx index fb71204869..66366846d1 100644 --- a/apps/web/test/chat/system-row-chrome.test.tsx +++ b/apps/web/src/chat/system-row-chrome.test.tsx @@ -6,8 +6,8 @@ import { act } from "react"; import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; -import type { MessageItem } from "../../src/chat/api"; -import { WorkbenchTimeline } from "../../src/chat/timeline"; +import type { MessageItem } from "./api"; +import { WorkbenchTimeline } from "./timeline"; let container: HTMLDivElement | null = null; let root: Root | null = null; diff --git a/apps/web/test/chat/team-avatar-stack.test.tsx b/apps/web/src/chat/team-avatar-stack.test.tsx similarity index 99% rename from apps/web/test/chat/team-avatar-stack.test.tsx rename to apps/web/src/chat/team-avatar-stack.test.tsx index a9ed6bb779..5740b8f802 100644 --- a/apps/web/test/chat/team-avatar-stack.test.tsx +++ b/apps/web/src/chat/team-avatar-stack.test.tsx @@ -95,7 +95,7 @@ function stubFetch(workbenchWire: { }) as typeof fetch; } -const { ChatWorkspace } = await import("../../src/chat/chat-workspace"); +const { ChatWorkspace } = await import("./chat-workspace"); const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/apps/web/test/chat/timeline-jump-to-latest.test.tsx b/apps/web/src/chat/timeline-jump-to-latest.test.tsx similarity index 97% rename from apps/web/test/chat/timeline-jump-to-latest.test.tsx rename to apps/web/src/chat/timeline-jump-to-latest.test.tsx index 27f571c992..fe4879f539 100644 --- a/apps/web/test/chat/timeline-jump-to-latest.test.tsx +++ b/apps/web/src/chat/timeline-jump-to-latest.test.tsx @@ -8,8 +8,8 @@ import { act } from "react"; import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; -import type { MessageItem } from "../../src/chat/api"; -import { WorkbenchTimeline } from "../../src/chat/timeline"; +import type { MessageItem } from "./api"; +import { WorkbenchTimeline } from "./timeline"; const realResizeObserver = globalThis.ResizeObserver; diff --git a/apps/web/test/chat/timeline-scroll-pin.test.tsx b/apps/web/src/chat/timeline-scroll-pin.test.tsx similarity index 97% rename from apps/web/test/chat/timeline-scroll-pin.test.tsx rename to apps/web/src/chat/timeline-scroll-pin.test.tsx index abcbec7357..c9588b9912 100644 --- a/apps/web/test/chat/timeline-scroll-pin.test.tsx +++ b/apps/web/src/chat/timeline-scroll-pin.test.tsx @@ -16,8 +16,8 @@ import { act } from "react"; import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; -import type { MessageItem } from "../../src/chat/api"; -import { WorkbenchTimeline } from "../../src/chat/timeline"; +import type { MessageItem } from "./api"; +import { WorkbenchTimeline } from "./timeline"; const realResizeObserver = globalThis.ResizeObserver; diff --git a/apps/web/test/chat/tool-activity-view.test.tsx b/apps/web/src/chat/tool-activity-view.test.tsx similarity index 98% rename from apps/web/test/chat/tool-activity-view.test.tsx rename to apps/web/src/chat/tool-activity-view.test.tsx index ceb345dcff..947ba8d1e7 100644 --- a/apps/web/test/chat/tool-activity-view.test.tsx +++ b/apps/web/src/chat/tool-activity-view.test.tsx @@ -8,9 +8,9 @@ import { fileURLToPath } from "node:url"; import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; -import type { ToolTracePart } from "../../src/chat/wire/parts"; -import { toToolActivityRow, type ToolActivityRow } from "../../src/chat/tool-activity"; -import { LiveToolActivity, ToolActivityGroup } from "../../src/chat/tool-activity-view"; +import type { ToolTracePart } from "./wire/parts"; +import { toToolActivityRow, type ToolActivityRow } from "./tool-activity"; +import { LiveToolActivity, ToolActivityGroup } from "./tool-activity-view"; function trace(part: Partial): ToolTracePart { return { diff --git a/apps/web/test/chat/use-streaming-reply.test.tsx b/apps/web/src/chat/use-streaming-reply.test.tsx similarity index 98% rename from apps/web/test/chat/use-streaming-reply.test.tsx rename to apps/web/src/chat/use-streaming-reply.test.tsx index 4ffec23e07..65c67f7a66 100644 --- a/apps/web/test/chat/use-streaming-reply.test.tsx +++ b/apps/web/src/chat/use-streaming-reply.test.tsx @@ -8,8 +8,8 @@ import { describe, expect, test } from "bun:test"; import { act, createElement, useState } from "react"; import { createRoot } from "react-dom/client"; -import { useStreamingReply } from "../../src/chat/streaming-reply"; -import type { StreamingReplyState } from "../../src/chat/streaming-reply"; +import { useStreamingReply } from "./streaming-reply"; +import type { StreamingReplyState } from "./streaming-reply"; import { createFakeClock } from "./fake-clock"; function mount(initialWorkbenchId: string | null, clearMs?: number, minVisibleMs = 0) { diff --git a/apps/web/test/chat/use-turn-activity.test.tsx b/apps/web/src/chat/use-turn-activity.test.tsx similarity index 98% rename from apps/web/test/chat/use-turn-activity.test.tsx rename to apps/web/src/chat/use-turn-activity.test.tsx index 1714d62111..dc851aec0a 100644 --- a/apps/web/test/chat/use-turn-activity.test.tsx +++ b/apps/web/src/chat/use-turn-activity.test.tsx @@ -9,7 +9,7 @@ import { describe, expect, test } from "bun:test"; import { act, createElement, useState } from "react"; import { createRoot } from "react-dom/client"; -import { useTurnActivity, TurnActivityStrip } from "../../src/chat/turn-activity"; +import { useTurnActivity, TurnActivityStrip } from "./turn-activity"; import { createFakeClock } from "./fake-clock"; function mount(initialWorkbenchId: string | null, staleMs?: number) { diff --git a/apps/web/test/chat/use-typing-indicator.test.tsx b/apps/web/src/chat/use-typing-indicator.test.tsx similarity index 97% rename from apps/web/test/chat/use-typing-indicator.test.tsx rename to apps/web/src/chat/use-typing-indicator.test.tsx index d695d3cfbb..932dacbc3f 100644 --- a/apps/web/test/chat/use-typing-indicator.test.tsx +++ b/apps/web/src/chat/use-typing-indicator.test.tsx @@ -7,8 +7,8 @@ import { describe, expect, test } from "bun:test"; import { act, createElement, useState } from "react"; import { createRoot } from "react-dom/client"; -import { useTypingIndicator } from "../../src/chat/typing-indicator"; -import type { TypingState } from "../../src/chat/typing-indicator"; +import { useTypingIndicator } from "./typing-indicator"; +import type { TypingState } from "./typing-indicator"; const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/apps/web/test/chat/use-workbench-feed-refresh-scoping.test.tsx b/apps/web/src/chat/use-workbench-feed-refresh-scoping.test.tsx similarity index 97% rename from apps/web/test/chat/use-workbench-feed-refresh-scoping.test.tsx rename to apps/web/src/chat/use-workbench-feed-refresh-scoping.test.tsx index bd78ed9c23..d28a3248ad 100644 --- a/apps/web/test/chat/use-workbench-feed-refresh-scoping.test.tsx +++ b/apps/web/src/chat/use-workbench-feed-refresh-scoping.test.tsx @@ -10,7 +10,7 @@ import { act, createElement, useState } from "react"; import { createRoot } from "react-dom/client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { chatFeedQueryKeyPrefix, useWorkbenchFeed } from "../../src/chat/use-workbench-feed"; +import { chatFeedQueryKeyPrefix, useWorkbenchFeed } from "./use-workbench-feed"; const realFetch = globalThis.fetch; diff --git a/apps/web/test/chat/use-workbench-stream.test.tsx b/apps/web/src/chat/use-workbench-stream.test.tsx similarity index 98% rename from apps/web/test/chat/use-workbench-stream.test.tsx rename to apps/web/src/chat/use-workbench-stream.test.tsx index 41fa0d5f2f..c35bf4cc97 100644 --- a/apps/web/test/chat/use-workbench-stream.test.tsx +++ b/apps/web/src/chat/use-workbench-stream.test.tsx @@ -11,7 +11,7 @@ import { afterEach, describe, expect, test } from "bun:test"; import { act, createElement } from "react"; import { createRoot } from "react-dom/client"; -import { parseStreamEventJson, useWorkbenchStream } from "../../src/chat/use-workbench-stream"; +import { parseStreamEventJson, useWorkbenchStream } from "./use-workbench-stream"; const realEventSource = globalThis.EventSource; const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/apps/web/test/chat/blocks.test.tsx b/apps/web/src/chat/wire/blocks.test.tsx similarity index 97% rename from apps/web/test/chat/blocks.test.tsx rename to apps/web/src/chat/wire/blocks.test.tsx index d51ac88a74..acc9f6789c 100644 --- a/apps/web/test/chat/blocks.test.tsx +++ b/apps/web/src/chat/wire/blocks.test.tsx @@ -5,8 +5,8 @@ import { describe, expect, test } from "bun:test"; import { renderToStaticMarkup } from "react-dom/server"; -import type { MessageItem } from "../../src/chat/api"; -import { WorkbenchTimeline } from "../../src/chat/timeline"; +import type { MessageItem } from "../api"; +import { WorkbenchTimeline } from "../timeline"; function messageWithBlock(type: string, data: unknown): MessageItem[] { return [ diff --git a/apps/web/test/chat/workbench-loading-tip.test.tsx b/apps/web/src/chat/workbench-loading-tip.test.tsx similarity index 96% rename from apps/web/test/chat/workbench-loading-tip.test.tsx rename to apps/web/src/chat/workbench-loading-tip.test.tsx index 260c334b8d..5fef81b17b 100644 --- a/apps/web/test/chat/workbench-loading-tip.test.tsx +++ b/apps/web/src/chat/workbench-loading-tip.test.tsx @@ -9,8 +9,8 @@ import { act } from "react"; import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; -import { WorkbenchTimeline } from "../../src/chat/timeline"; -import { CHAT_STRINGS } from "../../src/chat/strings"; +import { WorkbenchTimeline } from "./timeline"; +import { CHAT_STRINGS } from "./strings"; let container: HTMLDivElement | null = null; let root: Root | null = null; diff --git a/apps/web/test/chat/workbench-presence.test.ts b/apps/web/src/chat/workbench-presence.test.ts similarity index 98% rename from apps/web/test/chat/workbench-presence.test.ts rename to apps/web/src/chat/workbench-presence.test.ts index 5164f19b61..c4469a197b 100644 --- a/apps/web/test/chat/workbench-presence.test.ts +++ b/apps/web/src/chat/workbench-presence.test.ts @@ -8,7 +8,7 @@ import { nextPresenceRoster, parsePresenceEvent, parsePresenceSnapshotEvent, -} from "../../src/chat/workbench-presence"; +} from "./workbench-presence"; describe("parsePresenceEvent", () => { test("parses a well-formed chat.presence payload", () => { diff --git a/apps/web/test/chat/workbench-settings-retry.test.tsx b/apps/web/src/chat/workbench-settings-retry.test.tsx similarity index 97% rename from apps/web/test/chat/workbench-settings-retry.test.tsx rename to apps/web/src/chat/workbench-settings-retry.test.tsx index 64babe0807..7fe9dde03d 100644 --- a/apps/web/test/chat/workbench-settings-retry.test.tsx +++ b/apps/web/src/chat/workbench-settings-retry.test.tsx @@ -10,7 +10,7 @@ import { act, createElement } from "react"; import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; -import { WorkbenchSettingsSurface } from "../../src/chat/workbench-settings"; +import { WorkbenchSettingsSurface } from "./workbench-settings"; const realFetch = globalThis.fetch; afterEach(() => { diff --git a/apps/web/test/chat/agents-section.test.tsx b/apps/web/src/chat/workbench-settings/agents-section.test.tsx similarity index 99% rename from apps/web/test/chat/agents-section.test.tsx rename to apps/web/src/chat/workbench-settings/agents-section.test.tsx index e234bd64bc..dae07fd0d5 100644 --- a/apps/web/test/chat/agents-section.test.tsx +++ b/apps/web/src/chat/workbench-settings/agents-section.test.tsx @@ -14,7 +14,7 @@ import { act, createElement } from "react"; import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; -import { WorkbenchSettingsSurface } from "../../src/chat/workbench-settings"; +import { WorkbenchSettingsSurface } from "."; const realFetch = globalThis.fetch; diff --git a/apps/web/test/chat/members-section.test.tsx b/apps/web/src/chat/workbench-settings/members-section.test.tsx similarity index 98% rename from apps/web/test/chat/members-section.test.tsx rename to apps/web/src/chat/workbench-settings/members-section.test.tsx index 6cac8b28dd..1ab086a4b7 100644 --- a/apps/web/test/chat/members-section.test.tsx +++ b/apps/web/src/chat/workbench-settings/members-section.test.tsx @@ -11,7 +11,7 @@ import { act, createElement } from "react"; import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; -import { WorkbenchSettingsSurface } from "../../src/chat/workbench-settings"; +import { WorkbenchSettingsSurface } from "."; const realFetch = globalThis.fetch; diff --git a/apps/web/test/chat/workbenches-mutated-event.test.ts b/apps/web/src/chat/workbenches-mutated-event.test.ts similarity index 99% rename from apps/web/test/chat/workbenches-mutated-event.test.ts rename to apps/web/src/chat/workbenches-mutated-event.test.ts index d36c6e76ff..252c2771f7 100644 --- a/apps/web/test/chat/workbenches-mutated-event.test.ts +++ b/apps/web/src/chat/workbenches-mutated-event.test.ts @@ -5,7 +5,7 @@ import { WORKBENCHES_MUTATED_EVENT, applyStreamWorkbenchesMutated, createWorkbench, -} from "../../src/chat/api"; +} from "./api"; if (typeof document === "undefined") { GlobalRegistrator.register(); diff --git a/apps/web/test/command-palette/bare-prefix-entity-scope.test.ts b/apps/web/src/command-palette/bare-prefix-entity-scope.test.ts similarity index 91% rename from apps/web/test/command-palette/bare-prefix-entity-scope.test.ts rename to apps/web/src/command-palette/bare-prefix-entity-scope.test.ts index 9552956273..0c8ab31055 100644 --- a/apps/web/test/command-palette/bare-prefix-entity-scope.test.ts +++ b/apps/web/src/command-palette/bare-prefix-entity-scope.test.ts @@ -1,8 +1,8 @@ import { describe, expect, test } from "bun:test"; -import { isBareScopeQuery, parsePaletteQuery } from "../../src/command-palette/scope"; -import { searchEntities } from "../../src/command-palette/entity-search"; -import { buildCommandPaletteGroups } from "../../src/command-palette/command-groups"; +import { isBareScopeQuery, parsePaletteQuery } from "./scope"; +import { searchEntities } from "./entity-search"; +import { buildCommandPaletteGroups } from "./command-groups"; describe("bare `#` scope against entity-search-backed sources", () => { test("a bare # prefix strips to an empty query, and searchEntities returns nothing for an empty query", () => { diff --git a/apps/web/test/command-palette/command-groups.test.ts b/apps/web/src/command-palette/command-groups.test.ts similarity index 98% rename from apps/web/test/command-palette/command-groups.test.ts rename to apps/web/src/command-palette/command-groups.test.ts index d89cd33623..702b6f99a4 100644 --- a/apps/web/test/command-palette/command-groups.test.ts +++ b/apps/web/src/command-palette/command-groups.test.ts @@ -3,7 +3,7 @@ import { describe, expect, test } from "bun:test"; import { buildCommandPaletteGroups, type PaletteSource, -} from "../../src/command-palette/command-groups"; +} from "./command-groups"; const actions: PaletteSource = { id: "actions", diff --git a/apps/web/test/command-palette/entity-search.test.ts b/apps/web/src/command-palette/entity-search.test.ts similarity index 97% rename from apps/web/test/command-palette/entity-search.test.ts rename to apps/web/src/command-palette/entity-search.test.ts index d1927aa578..56ca447074 100644 --- a/apps/web/test/command-palette/entity-search.test.ts +++ b/apps/web/src/command-palette/entity-search.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; -import { searchEntities } from "../../src/command-palette/entity-search"; +import { searchEntities } from "./entity-search"; describe("searchEntities", () => { const sources = [ diff --git a/apps/web/test/command-palette/recents.test.ts b/apps/web/src/command-palette/recents.test.ts similarity index 99% rename from apps/web/test/command-palette/recents.test.ts rename to apps/web/src/command-palette/recents.test.ts index 33c92e8c9a..7d7b222aec 100644 --- a/apps/web/test/command-palette/recents.test.ts +++ b/apps/web/src/command-palette/recents.test.ts @@ -6,7 +6,7 @@ import { removeRecentEntry, type RecentEntry, type RecentsStorage, -} from "../../src/command-palette/recents"; +} from "./recents"; function entry(id: string, title = id): RecentEntry { return { kind: "workbench", id, title }; diff --git a/apps/web/test/command-palette/scope.test.ts b/apps/web/src/command-palette/scope.test.ts similarity index 96% rename from apps/web/test/command-palette/scope.test.ts rename to apps/web/src/command-palette/scope.test.ts index 3db2455b46..57eda6a26f 100644 --- a/apps/web/test/command-palette/scope.test.ts +++ b/apps/web/src/command-palette/scope.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; -import { isBareScopeQuery, parsePaletteQuery } from "../../src/command-palette/scope"; +import { isBareScopeQuery, parsePaletteQuery } from "./scope"; describe("parsePaletteQuery", () => { test("no prefix leaves the whole string unscoped", () => { diff --git a/apps/web/test/command-palette/static-commands.test.ts b/apps/web/src/command-palette/static-commands.test.ts similarity index 92% rename from apps/web/test/command-palette/static-commands.test.ts rename to apps/web/src/command-palette/static-commands.test.ts index 124633a25b..52db4e1717 100644 --- a/apps/web/test/command-palette/static-commands.test.ts +++ b/apps/web/src/command-palette/static-commands.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; -import { buildStaticCommands, matchesQuery } from "../../src/command-palette/static-commands"; +import { buildStaticCommands, matchesQuery } from "./static-commands"; describe("buildStaticCommands", () => { test("maps real routes to commands, one per route, in order", () => { diff --git a/apps/web/test/command-palette/use-entity-search.test.tsx b/apps/web/src/command-palette/use-entity-search.test.tsx similarity index 96% rename from apps/web/test/command-palette/use-entity-search.test.tsx rename to apps/web/src/command-palette/use-entity-search.test.tsx index e9f48a8fd1..6f0c168fd6 100644 --- a/apps/web/test/command-palette/use-entity-search.test.tsx +++ b/apps/web/src/command-palette/use-entity-search.test.tsx @@ -2,9 +2,9 @@ import { describe, expect, test } from "bun:test"; import { act, createElement, useState } from "react"; import { createRoot } from "react-dom/client"; -import { useEntitySearch } from "../../src/command-palette/use-entity-search"; -import type { UseEntitySearchResult } from "../../src/command-palette/use-entity-search"; -import type { SearchableEntity } from "../../src/command-palette/entity-search"; +import { useEntitySearch } from "./use-entity-search"; +import type { UseEntitySearchResult } from "./use-entity-search"; +import type { SearchableEntity } from "./entity-search"; const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/apps/web/test/dom-environment.ts b/apps/web/src/dom-environment.ts similarity index 100% rename from apps/web/test/dom-environment.ts rename to apps/web/src/dom-environment.ts diff --git a/apps/web/test/fetch-dedupe.test.tsx b/apps/web/src/fetch-dedupe.test.tsx similarity index 97% rename from apps/web/test/fetch-dedupe.test.tsx rename to apps/web/src/fetch-dedupe.test.tsx index ee6fcebd92..3af04812e0 100644 --- a/apps/web/test/fetch-dedupe.test.tsx +++ b/apps/web/src/fetch-dedupe.test.tsx @@ -10,9 +10,9 @@ import { act } from "react"; import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; -import { BenchProvider } from "../src/bench-context"; -import { requestWorkbenchRename } from "../src/workbench-rename-events"; -import { WorkbenchList } from "../src/shell/workbench-list"; +import { BenchProvider } from "./bench-context"; +import { requestWorkbenchRename } from "./workbench-rename-events"; +import { WorkbenchList } from "./shell/workbench-list"; import { createTestQueryClient, TestQueryProvider } from "./test-query-provider"; const realFetch = globalThis.fetch; diff --git a/apps/web/test/global-command-palette.test.tsx b/apps/web/src/global-command-palette.test.tsx similarity index 95% rename from apps/web/test/global-command-palette.test.tsx rename to apps/web/src/global-command-palette.test.tsx index e51aa03c3f..2741804d85 100644 --- a/apps/web/test/global-command-palette.test.tsx +++ b/apps/web/src/global-command-palette.test.tsx @@ -12,11 +12,11 @@ import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { ThemeProvider } from "@corbits/react-ui"; -import { BenchProvider } from "../src/bench-context"; -import { CommandPaletteProvider } from "../src/command-palette-provider"; -import { setCommandPaletteOpen } from "../src/command-palette-open-store"; -import { NavigationProvider } from "../src/navigation"; -import { StageTopBar } from "../src/shell/stage-top-bar"; +import { BenchProvider } from "./bench-context"; +import { CommandPaletteProvider } from "./command-palette-provider"; +import { setCommandPaletteOpen } from "./command-palette-open-store"; +import { NavigationProvider } from "./navigation"; +import { StageTopBar } from "./shell/stage-top-bar"; import { TestQueryProvider } from "./test-query-provider"; const noop = () => undefined; diff --git a/apps/web/test/initials.test.ts b/apps/web/src/initials.test.ts similarity index 93% rename from apps/web/test/initials.test.ts rename to apps/web/src/initials.test.ts index 88935870fd..de8201f920 100644 --- a/apps/web/test/initials.test.ts +++ b/apps/web/src/initials.test.ts @@ -4,7 +4,7 @@ import { describe, expect, test } from "bun:test"; -import { initialsOf } from "../src/shell/docks"; +import { initialsOf } from "./shell/docks"; describe("initialsOf", () => { test("takes the first letters of the account name", () => { diff --git a/apps/web/test/insights-route.test.tsx b/apps/web/src/insights-route.test.tsx similarity index 97% rename from apps/web/test/insights-route.test.tsx rename to apps/web/src/insights-route.test.tsx index f59d90c788..7a0535653a 100644 --- a/apps/web/test/insights-route.test.tsx +++ b/apps/web/src/insights-route.test.tsx @@ -10,9 +10,9 @@ import { act } from "react"; import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; -import { BenchProvider } from "../src/bench-context"; -import { NavigationProvider } from "../src/navigation"; -import { InsightsRoute } from "../src/pages/insights-page"; +import { BenchProvider } from "./bench-context"; +import { NavigationProvider } from "./navigation"; +import { InsightsRoute } from "./pages/insights-page"; import { TestQueryProvider } from "./test-query-provider"; const realFetch = globalThis.fetch; diff --git a/apps/web/test/insights-row-activation.test.tsx b/apps/web/src/insights-row-activation.test.tsx similarity index 97% rename from apps/web/test/insights-row-activation.test.tsx rename to apps/web/src/insights-row-activation.test.tsx index 9070444f05..473542ff7b 100644 --- a/apps/web/test/insights-row-activation.test.tsx +++ b/apps/web/src/insights-row-activation.test.tsx @@ -3,8 +3,8 @@ import { act } from "react"; import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; -import type { InsightsRun } from "../src/insights-api"; -import { InsightsRunsHistory } from "../src/pages/insights-page"; +import type { InsightsRun } from "./insights-api"; +import { InsightsRunsHistory } from "./pages/insights-page"; import { TestQueryProvider } from "./test-query-provider"; globalThis.fetch = ((_input: RequestInfo | URL, _init?: RequestInit) => diff --git a/apps/web/test/library-artifact-deep-link.test.tsx b/apps/web/src/library-artifact-deep-link.test.tsx similarity index 95% rename from apps/web/test/library-artifact-deep-link.test.tsx rename to apps/web/src/library-artifact-deep-link.test.tsx index 11406f5f0f..f0ec0a4abc 100644 --- a/apps/web/test/library-artifact-deep-link.test.tsx +++ b/apps/web/src/library-artifact-deep-link.test.tsx @@ -7,9 +7,9 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; -import { BenchProvider } from "../src/bench-context"; -import { NavigationProvider } from "../src/navigation"; -import { LibraryRoute } from "../src/pages/library-page"; +import { BenchProvider } from "./bench-context"; +import { NavigationProvider } from "./navigation"; +import { LibraryRoute } from "./pages/library-page"; import { TestQueryProvider } from "./test-query-provider"; const noop = () => undefined; diff --git a/apps/web/test/library-page-selection.test.tsx b/apps/web/src/library-page-selection.test.tsx similarity index 98% rename from apps/web/test/library-page-selection.test.tsx rename to apps/web/src/library-page-selection.test.tsx index 2beb44bf57..47de341f4f 100644 --- a/apps/web/test/library-page-selection.test.tsx +++ b/apps/web/src/library-page-selection.test.tsx @@ -9,8 +9,8 @@ import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { spyOnReactUiToast } from "./react-ui-toast-mock"; -import { LibraryPage } from "../src/pages/library-page"; -import { LIBRARY_BULK_OPERATION_IDS } from "../src/shell/library-artifacts"; +import { LibraryPage } from "./pages/library-page"; +import { LIBRARY_BULK_OPERATION_IDS } from "./shell/library-artifacts"; const toastMock = spyOnReactUiToast(); diff --git a/apps/web/test/library-upload-confirmation.test.tsx b/apps/web/src/library-upload-confirmation.test.tsx similarity index 97% rename from apps/web/test/library-upload-confirmation.test.tsx rename to apps/web/src/library-upload-confirmation.test.tsx index 87a0b4f70f..2c1350b1fd 100644 --- a/apps/web/test/library-upload-confirmation.test.tsx +++ b/apps/web/src/library-upload-confirmation.test.tsx @@ -10,9 +10,9 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; -import { BenchProvider } from "../src/bench-context"; -import { NavigationProvider } from "../src/navigation"; -import { LibraryRoute } from "../src/pages/library-page"; +import { BenchProvider } from "./bench-context"; +import { NavigationProvider } from "./navigation"; +import { LibraryRoute } from "./pages/library-page"; import { spyOnReactUiToast } from "./react-ui-toast-mock"; import { TestQueryProvider } from "./test-query-provider"; diff --git a/apps/web/test/library/artifact-card.test.tsx b/apps/web/src/library/artifact-card.test.tsx similarity index 89% rename from apps/web/test/library/artifact-card.test.tsx rename to apps/web/src/library/artifact-card.test.tsx index 6d2ab33fb7..6c6e079351 100644 --- a/apps/web/test/library/artifact-card.test.tsx +++ b/apps/web/src/library/artifact-card.test.tsx @@ -5,8 +5,8 @@ import { describe, expect, test } from "bun:test"; import { renderToStaticMarkup } from "react-dom/server"; -import { ArtifactCard } from "../../src/library/artifact-card"; -import type { ArtifactSummary } from "../../src/library/artifact-summary"; +import { ArtifactCard } from "./artifact-card"; +import type { ArtifactSummary } from "@corbits/artifact-ui/types"; function artifact(overrides: Partial & { readonly id: string }): ArtifactSummary { return { diff --git a/apps/web/test/library/artifact-renderer.test.tsx b/apps/web/src/library/artifact-renderer.test.tsx similarity index 98% rename from apps/web/test/library/artifact-renderer.test.tsx rename to apps/web/src/library/artifact-renderer.test.tsx index b8fed649d1..f96c21ec5d 100644 --- a/apps/web/test/library/artifact-renderer.test.tsx +++ b/apps/web/src/library/artifact-renderer.test.tsx @@ -8,7 +8,7 @@ import { describe, expect, test } from "bun:test"; import { renderToStaticMarkup } from "react-dom/server"; import { CSV_ROW_CAP } from "@corbits/react-ui"; -import { ArtifactRenderer } from "../../src/library/artifact-renderer"; +import { ArtifactRenderer } from "./artifact-renderer"; function oversizedCsv(rowCount: number): string { const header = "id,name\n"; diff --git a/apps/web/test/library/artifacts.test.ts b/apps/web/src/library/artifacts.test.ts similarity index 98% rename from apps/web/test/library/artifacts.test.ts rename to apps/web/src/library/artifacts.test.ts index e1466d663c..9090bcc02d 100644 --- a/apps/web/test/library/artifacts.test.ts +++ b/apps/web/src/library/artifacts.test.ts @@ -4,7 +4,7 @@ import { describe, expect, test } from "bun:test"; -import { ArtifactSummary, filterArtifacts, sortArtifacts } from "../../src/library/index"; +import { ArtifactSummary, filterArtifacts, sortArtifacts } from "./index"; function artifact(overrides: Partial & { readonly id: string }): ArtifactSummary { return { diff --git a/apps/web/test/login-routing.test.tsx b/apps/web/src/login-routing.test.tsx similarity index 97% rename from apps/web/test/login-routing.test.tsx rename to apps/web/src/login-routing.test.tsx index 7e0ecdb2d0..4b187a28d4 100644 --- a/apps/web/test/login-routing.test.tsx +++ b/apps/web/src/login-routing.test.tsx @@ -9,9 +9,9 @@ import { afterEach, describe, expect, test } from "bun:test"; import { act, useCallback, useEffect, useState } from "react"; import { createRoot, type Root } from "react-dom/client"; -import { App } from "../src/app"; -import { validatedNextPath } from "../src/login-next"; -import type { SessionState, SessionUser } from "../src/session"; +import { App } from "./app"; +import { validatedNextPath } from "./login-next"; +import type { SessionState, SessionUser } from "./session"; const realFetch = globalThis.fetch; diff --git a/apps/web/test/page-type-drift.ts b/apps/web/src/page-type-drift.ts similarity index 95% rename from apps/web/test/page-type-drift.ts rename to apps/web/src/page-type-drift.ts index e45f68e96d..9aa1bf5aff 100644 --- a/apps/web/test/page-type-drift.ts +++ b/apps/web/src/page-type-drift.ts @@ -5,7 +5,7 @@ import { PrincipalSummary, paginatedSchema } from "@intx/types"; import type { APIQuery } from "@/lib/api-query"; -import type { PrincipalsPage, Principal } from "../src/api"; +import type { PrincipalsPage, Principal } from "./api"; const _schema = paginatedSchema(PrincipalSummary); type Inferred = typeof _schema.infer; diff --git a/apps/web/test/pages.test.tsx b/apps/web/src/pages.test.tsx similarity index 94% rename from apps/web/test/pages.test.tsx rename to apps/web/src/pages.test.tsx index 1a8c4e4fc2..8b2efd2363 100644 --- a/apps/web/test/pages.test.tsx +++ b/apps/web/src/pages.test.tsx @@ -6,10 +6,10 @@ import { renderToStaticMarkup } from "react-dom/server"; import type { ArtifactSummary } from "@/library"; -import { BenchProvider } from "../src/bench-context"; -import { NavigationProvider } from "../src/navigation"; -import { LibraryPage } from "../src/pages/library-page"; -import { SettingsRoute } from "../src/pages/settings-page"; +import { BenchProvider } from "./bench-context"; +import { NavigationProvider } from "./navigation"; +import { LibraryPage } from "./pages/library-page"; +import { SettingsRoute } from "./pages/settings-page"; import { TestQueryProvider } from "./test-query-provider"; describe("empty states", () => { diff --git a/apps/web/test/agent-detail-page.test.tsx b/apps/web/src/pages/agent-detail-page.test.tsx similarity index 99% rename from apps/web/test/agent-detail-page.test.tsx rename to apps/web/src/pages/agent-detail-page.test.tsx index 81a3e76ed3..7b39aa37c2 100644 --- a/apps/web/test/agent-detail-page.test.tsx +++ b/apps/web/src/pages/agent-detail-page.test.tsx @@ -17,9 +17,9 @@ import { duplicateHandle, recentRunsForDefinition, type SaveReport, -} from "../src/pages/agent-detail-page"; -import type { AgentDefinitionWithDisplayName } from "../src/agents-directory"; -import type { AgentDefinitionDetail, AgentInstance, CatalogModel } from "../src/agents-api"; +} from "./agent-detail-page"; +import type { AgentDefinitionWithDisplayName } from "../agents-directory"; +import type { AgentDefinitionDetail, AgentInstance, CatalogModel } from "../agents-api"; const definition: AgentDefinitionWithDisplayName = { id: "wfd_1", diff --git a/apps/web/test/agent-skills-picker.test.tsx b/apps/web/src/pages/agent-skills-picker.test.tsx similarity index 98% rename from apps/web/test/agent-skills-picker.test.tsx rename to apps/web/src/pages/agent-skills-picker.test.tsx index 1dd1f6f0d6..84e3e5dad6 100644 --- a/apps/web/test/agent-skills-picker.test.tsx +++ b/apps/web/src/pages/agent-skills-picker.test.tsx @@ -7,7 +7,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; -import { AgentSkillsPicker } from "../src/pages/agent-skills-picker"; +import { AgentSkillsPicker } from "./agent-skills-picker"; const TENANT = "tnt_1"; diff --git a/apps/web/test/agents-page.test.tsx b/apps/web/src/pages/agents-page.test.tsx similarity index 99% rename from apps/web/test/agents-page.test.tsx rename to apps/web/src/pages/agents-page.test.tsx index a277f98f68..8f664a7340 100644 --- a/apps/web/test/agents-page.test.tsx +++ b/apps/web/src/pages/agents-page.test.tsx @@ -16,9 +16,9 @@ import { archiveDefinitions, archiveResultToast, runsInLast7Days, -} from "../src/pages/agents-page"; -import type { AgentDefinitionWithDisplayName } from "../src/agents-directory"; -import type { AgentInstance } from "../src/agents-api"; +} from "./agents-page"; +import type { AgentDefinitionWithDisplayName } from "../agents-directory"; +import type { AgentInstance } from "../agents-api"; // `name` is the immutable kebab identifier (the URL slug); `displayName` is // what `withDisplayNames` derives from the definition's diff --git a/apps/web/test/create-agent-panel.test.tsx b/apps/web/src/pages/create-agent-panel.test.tsx similarity index 98% rename from apps/web/test/create-agent-panel.test.tsx rename to apps/web/src/pages/create-agent-panel.test.tsx index c98380ba6d..eeb2238a33 100644 --- a/apps/web/test/create-agent-panel.test.tsx +++ b/apps/web/src/pages/create-agent-panel.test.tsx @@ -14,7 +14,7 @@ import { readFileSync } from "node:fs"; import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; -import { CreateAgentPanel } from "../src/pages/create-agent-panel"; +import { CreateAgentPanel } from "./create-agent-panel"; const realFetch = globalThis.fetch; @@ -379,7 +379,7 @@ describe("CreateAgentPanel drafting failure — fails closed", () => { describe("agent creation entry points", () => { test("the chat page never imports this form — agent creation lives outside it entirely", () => { const chatPageSource = readFileSync( - new URL("../src/pages/chat-page.tsx", import.meta.url), + new URL("./chat-page.tsx", import.meta.url), "utf8", ); expect(chatPageSource).not.toContain('from "./create-agent-panel"'); @@ -388,7 +388,7 @@ describe("agent creation entry points", () => { test("the command palette's new-workbench opens the template picker, not an instant mint", () => { const commandPaletteActionsSource = readFileSync( - new URL("../src/command-palette-actions.ts", import.meta.url), + new URL("../command-palette-actions.ts", import.meta.url), "utf8", ); expect(commandPaletteActionsSource).not.toContain("instant-agent-create"); @@ -397,7 +397,7 @@ describe("agent creation entry points", () => { test("the new-workbench picker mints through instant-agent-create.ts's createWorkbench", () => { const pickerSource = readFileSync( - new URL("../src/pages/new-workbench-picker.tsx", import.meta.url), + new URL("./new-workbench-picker.tsx", import.meta.url), "utf8", ); expect(pickerSource).toContain('from "../instant-agent-create"'); diff --git a/apps/web/test/home-page.test.tsx b/apps/web/src/pages/home-page.test.tsx similarity index 97% rename from apps/web/test/home-page.test.tsx rename to apps/web/src/pages/home-page.test.tsx index 8017df005e..2c1ebb9474 100644 --- a/apps/web/test/home-page.test.tsx +++ b/apps/web/src/pages/home-page.test.tsx @@ -20,10 +20,10 @@ import { createRoot } from "react-dom/client"; import { readFileSync } from "node:fs"; import type { Root } from "react-dom/client"; -import { BenchProvider } from "../src/bench-context"; -import { NavigationProvider } from "../src/navigation"; -import { HomeRoute } from "../src/pages/home-page"; -import { TestQueryProvider } from "./test-query-provider"; +import { BenchProvider } from "../bench-context"; +import { NavigationProvider } from "../navigation"; +import { HomeRoute } from "./home-page"; +import { TestQueryProvider } from "../test-query-provider"; const realFetch = globalThis.fetch; @@ -472,7 +472,7 @@ describe("the other two entries land on the same `/` hop", () => { // through `/login?next=...` returns to that path instead, so the // literal `navigate("/")` call this test used to pin no longer // applies unconditionally. - const source = readFileSync(new URL("../src/main.tsx", import.meta.url), "utf8"); + const source = readFileSync(new URL("../main.tsx", import.meta.url), "utf8"); expect(source).toMatch( /handleSignedIn[\s\S]*?navigate\(validatedNextPath\(window\.location\.search\)\)/, ); @@ -480,7 +480,7 @@ describe("the other two entries land on the same `/` hop", () => { test("onboarding hands off to `/` once a working credential is confirmed", () => { const source = readFileSync( - new URL("../src/pages/onboarding-page.tsx", import.meta.url), + new URL("./onboarding-page.tsx", import.meta.url), "utf8", ); expect(source).toContain('navigate("/")'); diff --git a/apps/web/test/legacy-settings-redirects.test.tsx b/apps/web/src/pages/legacy-settings-redirects.test.tsx similarity index 98% rename from apps/web/test/legacy-settings-redirects.test.tsx rename to apps/web/src/pages/legacy-settings-redirects.test.tsx index 3c9a157516..ea08dbacec 100644 --- a/apps/web/test/legacy-settings-redirects.test.tsx +++ b/apps/web/src/pages/legacy-settings-redirects.test.tsx @@ -10,7 +10,7 @@ import { LegacyLibraryRedirect, LegacySettingsAgentsRedirect, LegacySettingsSkillsRedirect, -} from "../src/pages/legacy-settings-redirects"; +} from "./legacy-settings-redirects"; let container: HTMLDivElement | null = null; let root: Root | null = null; diff --git a/apps/web/test/mission-control-page.test.tsx b/apps/web/src/pages/mission-control-page.test.tsx similarity index 95% rename from apps/web/test/mission-control-page.test.tsx rename to apps/web/src/pages/mission-control-page.test.tsx index 43d2c826a3..b85d0c8a29 100644 --- a/apps/web/test/mission-control-page.test.tsx +++ b/apps/web/src/pages/mission-control-page.test.tsx @@ -6,15 +6,15 @@ import type { Root } from "react-dom/client"; import type { Workbench } from "@/chat"; import { FIRE_RUNNING_WINDOW_MS } from "@corbits/workflows/client"; -import { BenchContext, type BenchState } from "../src/bench-context"; -import { NavigationProvider } from "../src/navigation"; +import { BenchContext, type BenchState } from "../bench-context"; +import { NavigationProvider } from "../navigation"; import { computeInFlightRows, computeJumpBackRows, MissionControlRoute, -} from "../src/pages/mission-control-page"; -import type { RoutineActivityItem } from "../src/shell/routine-activity"; -import { TestQueryProvider } from "./test-query-provider"; +} from "./mission-control-page"; +import type { RoutineActivityItem } from "../shell/routine-activity"; +import { TestQueryProvider } from "../test-query-provider"; const realFetch = globalThis.fetch; diff --git a/apps/web/test/provisioning-error-page.test.tsx b/apps/web/src/pages/provisioning-error-page.test.tsx similarity index 97% rename from apps/web/test/provisioning-error-page.test.tsx rename to apps/web/src/pages/provisioning-error-page.test.tsx index 0f4b06c3c9..c7e856b4fd 100644 --- a/apps/web/test/provisioning-error-page.test.tsx +++ b/apps/web/src/pages/provisioning-error-page.test.tsx @@ -11,7 +11,7 @@ import { act } from "react"; import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; -import { ProvisioningErrorPage } from "../src/pages/provisioning-error-page"; +import { ProvisioningErrorPage } from "./provisioning-error-page"; let container: HTMLDivElement | null = null; let root: Root | null = null; diff --git a/apps/web/test/routine-detail-page.test.tsx b/apps/web/src/pages/routine-detail-page.test.tsx similarity index 94% rename from apps/web/test/routine-detail-page.test.tsx rename to apps/web/src/pages/routine-detail-page.test.tsx index 18f14aa626..e4a073834d 100644 --- a/apps/web/test/routine-detail-page.test.tsx +++ b/apps/web/src/pages/routine-detail-page.test.tsx @@ -7,10 +7,10 @@ import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; import { renderToStaticMarkup } from "react-dom/server"; -import { NavigationProvider } from "../src/navigation"; -import { resolveRoutineSegment, RoutineDetailPage } from "../src/pages/routine-detail-page"; -import type { GlobalRoutineRow } from "../src/global-routines"; -import type { ScheduledWorkflowDefinition } from "../src/routines-api"; +import { NavigationProvider } from "../navigation"; +import { resolveRoutineSegment, RoutineDetailPage } from "./routine-detail-page"; +import type { GlobalRoutineRow } from "../global-routines"; +import type { ScheduledWorkflowDefinition } from "../routines-api"; const noop = () => undefined; @@ -247,9 +247,9 @@ describe("RoutineDetailRoute", () => { segment: string, itemsByTenant: Record[]>, ): Promise<{ container: HTMLDivElement; root: Root }> { - const { BenchProvider } = await import("../src/bench-context"); - const { RoutineDetailRoute } = await import("../src/pages/routine-detail-page"); - const { TestQueryProvider } = await import("./test-query-provider"); + const { BenchProvider } = await import("../bench-context"); + const { RoutineDetailRoute } = await import("./routine-detail-page"); + const { TestQueryProvider } = await import("../test-query-provider"); globalThis.fetch = mockFetch(itemsByTenant); const container = document.createElement("div"); diff --git a/apps/web/test/routines-page.test.tsx b/apps/web/src/pages/routines-page.test.tsx similarity index 95% rename from apps/web/test/routines-page.test.tsx rename to apps/web/src/pages/routines-page.test.tsx index 1e39e4b764..aefabb1dcd 100644 --- a/apps/web/test/routines-page.test.tsx +++ b/apps/web/src/pages/routines-page.test.tsx @@ -9,10 +9,10 @@ import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; import { renderToStaticMarkup } from "react-dom/server"; -import { GlobalRoutinesList, scheduleSentence } from "../src/pages/routines-page"; -import type { GlobalRoutineRow } from "../src/pages/routines-page"; -import { NavigationProvider } from "../src/navigation"; -import type { ScheduledWorkflowDefinition } from "../src/routines-api"; +import { GlobalRoutinesList, scheduleSentence } from "./routines-page"; +import type { GlobalRoutineRow } from "./routines-page"; +import { NavigationProvider } from "../navigation"; +import type { ScheduledWorkflowDefinition } from "../routines-api"; const noop = () => undefined; diff --git a/apps/web/test/settings-page.test.tsx b/apps/web/src/pages/settings-page.test.tsx similarity index 92% rename from apps/web/test/settings-page.test.tsx rename to apps/web/src/pages/settings-page.test.tsx index 4fe78f6d3b..88c9f710ce 100644 --- a/apps/web/test/settings-page.test.tsx +++ b/apps/web/src/pages/settings-page.test.tsx @@ -9,11 +9,11 @@ import { createRoot, type Root } from "react-dom/client"; import { QueryClient } from "@tanstack/react-query"; import { SETTINGS_STRINGS, type TenancyAccess } from "@/settings"; -import type { PrincipalsPage } from "../src/api"; -import { BenchProvider } from "../src/bench-context"; -import { SettingsRoute } from "../src/pages/settings-page"; -import { meKeys, tenantKeys } from "../src/query-client"; -import { TestQueryProvider } from "./test-query-provider"; +import type { PrincipalsPage } from "../api"; +import { BenchProvider } from "../bench-context"; +import { SettingsRoute } from "./settings-page"; +import { meKeys, tenantKeys } from "../query-client"; +import { TestQueryProvider } from "../test-query-provider"; const principalsPage: PrincipalsPage = { data: [ diff --git a/apps/web/test/skill-detail-page.test.tsx b/apps/web/src/pages/skill-detail-page.test.tsx similarity index 98% rename from apps/web/test/skill-detail-page.test.tsx rename to apps/web/src/pages/skill-detail-page.test.tsx index aab4e98e40..d263b64726 100644 --- a/apps/web/test/skill-detail-page.test.tsx +++ b/apps/web/src/pages/skill-detail-page.test.tsx @@ -10,7 +10,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; -import { SkillDetailPage } from "../src/pages/skill-detail-page"; +import { SkillDetailPage } from "./skill-detail-page"; const TENANT = "tnt_1"; const NAME = "triage"; diff --git a/apps/web/test/skills-page.test.tsx b/apps/web/src/pages/skills-page.test.tsx similarity index 98% rename from apps/web/test/skills-page.test.tsx rename to apps/web/src/pages/skills-page.test.tsx index e691ec3220..5700d6812f 100644 --- a/apps/web/test/skills-page.test.tsx +++ b/apps/web/src/pages/skills-page.test.tsx @@ -7,9 +7,9 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; -import { validationIssues } from "../src/pages/create-skill-dialog"; -import { SkillsPage } from "../src/pages/skills-page"; -import { TestQueryProvider } from "./test-query-provider"; +import { validationIssues } from "./create-skill-dialog"; +import { SkillsPage } from "./skills-page"; +import { TestQueryProvider } from "../test-query-provider"; const TENANT = "tnt_1"; diff --git a/apps/web/test/workflow-detail-page.test.tsx b/apps/web/src/pages/workflow-detail-page.test.tsx similarity index 93% rename from apps/web/test/workflow-detail-page.test.tsx rename to apps/web/src/pages/workflow-detail-page.test.tsx index bf598fc8d5..5efaa7fa8a 100644 --- a/apps/web/test/workflow-detail-page.test.tsx +++ b/apps/web/src/pages/workflow-detail-page.test.tsx @@ -7,8 +7,8 @@ import { describe, expect, test } from "bun:test"; import { renderToStaticMarkup } from "react-dom/server"; -import { WorkflowDetailPage } from "../src/pages/workflow-detail-page"; -import type { WorkflowDefinitionDetailT } from "../src/workflow-detail-api"; +import { WorkflowDetailPage } from "./workflow-detail-page"; +import type { WorkflowDefinitionDetailT } from "../workflow-detail-api"; const baseDetail: WorkflowDefinitionDetailT = { definitionId: "wfd_1", diff --git a/apps/web/test/pending-approvals.test.tsx b/apps/web/src/pending-approvals.test.tsx similarity index 95% rename from apps/web/test/pending-approvals.test.tsx rename to apps/web/src/pending-approvals.test.tsx index 21864045d7..8dbb1e0279 100644 --- a/apps/web/test/pending-approvals.test.tsx +++ b/apps/web/src/pending-approvals.test.tsx @@ -10,11 +10,11 @@ import { QueryClient } from "@tanstack/react-query"; import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; -import { createChatApprovalActions } from "../src/approval-actions"; -import { BenchContext, type BenchState } from "../src/bench-context"; -import { NavigationProvider } from "../src/navigation"; -import { MissionControlRoute } from "../src/pages/mission-control-page"; -import { usePendingApprovals } from "../src/pending-approvals"; +import { createChatApprovalActions } from "./approval-actions"; +import { BenchContext, type BenchState } from "./bench-context"; +import { NavigationProvider } from "./navigation"; +import { MissionControlRoute } from "./pages/mission-control-page"; +import { usePendingApprovals } from "./pending-approvals"; import { TestQueryProvider } from "./test-query-provider"; const realFetch = globalThis.fetch; diff --git a/apps/web/src/query-client.test.ts b/apps/web/src/query-client.test.ts index d8c8bb4a02..b4bc6d32b0 100644 --- a/apps/web/src/query-client.test.ts +++ b/apps/web/src/query-client.test.ts @@ -1,173 +1,41 @@ -import { describe, expect, mock, test } from "bun:test"; -import { QueryClient } from "@tanstack/react-query"; +// Query-key helpers — pure unit coverage so the TanStack cutover does not +// depend only on page-level smoke. The APIQuery adapter itself +// (`toAPIQuery`) is covered in `@/lib/api-query`. -import { ApiQueryError, UnauthenticatedError } from "@/lib/api-query"; +import { describe, expect, test } from "bun:test"; -import { - createAppQueryClient, - isAuthInvalidError, - pathToQueryKey, - shouldRetryQuery, - tenantKeys, -} from "./query-client"; +import { meKeys, pathToQueryKey, tenantKeys } from "./query-client"; -const TENANT = "tenant_1"; - -describe("shouldRetryQuery", () => { - test("never retries once unauthenticated", () => { - expect(shouldRetryQuery(0, new UnauthenticatedError())).toBe(false); - }); - - test("never retries a definitive 404", () => { - expect(shouldRetryQuery(0, new ApiQueryError("not found", 404))).toBe(false); - }); - - test("retries other statuses up to 3 attempts", () => { - const error = new ApiQueryError("server exploded", 500); - expect(shouldRetryQuery(0, error)).toBe(true); - expect(shouldRetryQuery(2, error)).toBe(true); - expect(shouldRetryQuery(3, error)).toBe(false); - }); - - test("retries a network failure with no status up to 3 attempts", () => { - const error = new ApiQueryError("network down"); - expect(shouldRetryQuery(0, error)).toBe(true); - expect(shouldRetryQuery(3, error)).toBe(false); - }); -}); - -describe("isAuthInvalidError", () => { - test("an UnauthenticatedError is auth-invalid", () => { - expect(isAuthInvalidError(new UnauthenticatedError())).toBe(true); - }); - - test("an ApiQueryError with status 401 is auth-invalid", () => { - expect(isAuthInvalidError(new ApiQueryError("nope", 401))).toBe(true); - }); - - test("an ApiQueryError with a different status is not", () => { - expect(isAuthInvalidError(new ApiQueryError("nope", 403))).toBe(false); - expect(isAuthInvalidError(new ApiQueryError("boom", 500))).toBe(false); - }); - - test("a plain error is not", () => { - expect(isAuthInvalidError(new Error("boom"))).toBe(false); - }); -}); - -// a hub restarted on an empty DB mid-session (or a cookie for a -// deleted user, or a session that simply expired) shows up as a 401 on -// whatever query or mutation happens to run next — never a dedicated -// "session ended" event. Every consumer of this client must be routed -// through the same `onAuthInvalid` callback, not left to render its own -// local "sign in required" box while the rest of the shell renders on. -describe("createAppQueryClient's auth-invalid wiring", () => { - test("a query throwing UnauthenticatedError calls onAuthInvalid", async () => { - const onAuthInvalid = mock(() => undefined); - const client = createAppQueryClient(onAuthInvalid); - await client - .fetchQuery({ - queryKey: ["probe"], - queryFn: () => { - throw new UnauthenticatedError(); - }, - retry: false, - }) - .catch(() => undefined); - expect(onAuthInvalid).toHaveBeenCalledTimes(1); - }); - - test("a query throwing a 401 ApiQueryError calls onAuthInvalid", async () => { - const onAuthInvalid = mock(() => undefined); - const client = createAppQueryClient(onAuthInvalid); - await client - .fetchQuery({ - queryKey: ["probe-401"], - queryFn: () => { - throw new ApiQueryError("nope", 401); - }, - retry: false, - }) - .catch(() => undefined); - expect(onAuthInvalid).toHaveBeenCalledTimes(1); +describe("pathToQueryKey", () => { + test("maps identity-scoped hub paths onto meKeys", () => { + expect(pathToQueryKey("/api/me")).toEqual(meKeys.profile); + expect(pathToQueryKey("/api/me/principals")).toEqual(meKeys.principals); }); - test("a query failing for an unrelated reason never calls onAuthInvalid", async () => { - const onAuthInvalid = mock(() => undefined); - const client = createAppQueryClient(onAuthInvalid); - await client - .fetchQuery({ - queryKey: ["probe-500"], - queryFn: () => { - throw new ApiQueryError("server exploded", 500); - }, - retry: false, - }) - .catch(() => undefined); - expect(onAuthInvalid).not.toHaveBeenCalled(); + test("maps the pending-approvals list onto a tenant-scoped key", () => { + expect(pathToQueryKey("/api/tenants/tnt_1/approvals")).toEqual( + tenantKeys.pendingApprovals("tnt_1"), + ); }); - test("a mutation that 401s also calls onAuthInvalid", async () => { - const onAuthInvalid = mock(() => undefined); - const client = createAppQueryClient(onAuthInvalid); - const mutation = client.getMutationCache().build(client, { - mutationFn: () => { - throw new UnauthenticatedError(); - }, - }); - await mutation.execute(undefined).catch(() => undefined); - expect(onAuthInvalid).toHaveBeenCalledTimes(1); + test("maps tenant assets onto a tenant-scoped key", () => { + expect(pathToQueryKey("/api/tenants/tnt_1/assets")).toEqual(tenantKeys.assets("tnt_1")); }); - test("defaults to a no-op when no callback is given", async () => { - const client = createAppQueryClient(); - await expect( - client.fetchQuery({ - queryKey: ["probe-default"], - queryFn: () => { - throw new UnauthenticatedError(); - }, - retry: false, - }), - ).rejects.toBeInstanceOf(UnauthenticatedError); + test("falls back to a path key for unknown routes", () => { + expect(pathToQueryKey("/api/mystery")).toEqual(["path", "/api/mystery"]); }); }); -describe("pathToQueryKey", () => { - test("maps the artifacts list under the artifacts key family", () => { - expect(pathToQueryKey(`/api/tenants/${TENANT}/artifacts`)).toEqual([ - ...tenantKeys.artifacts(TENANT), - "", - ]); - }); - - test("maps artifacts/counts under the artifacts key family, not the path fallback", () => { - expect(pathToQueryKey(`/api/tenants/${TENANT}/artifacts/counts`)).toEqual( - tenantKeys.artifactCounts(TENANT), +describe("tenantKeys.routineActivity", () => { + // the sidebar routine-activity seam keeps a shared cache key that + // names the seam — not the deleted `/top-level-runs` route — so both + // mounts subscribe to one entry and a future native fires equivalent has + // a key to rewire. + test("keys routine activity per tenant without the deleted route name", () => { + expect(tenantKeys.routineActivity("tnt_1")).toEqual(["tenant", "tnt_1", "routine-activity"]); + expect((tenantKeys.routineActivity("tnt_1") as readonly unknown[]).join("/")).not.toContain( + "top-level-runs", ); }); }); - -describe("invalidating tenantKeys.artifacts after an upload", () => { - test("also invalidates the counts query, not just the list", async () => { - const queryClient = new QueryClient(); - const listKey = pathToQueryKey(`/api/tenants/${TENANT}/artifacts`); - const countsKey = pathToQueryKey(`/api/tenants/${TENANT}/artifacts/counts`); - - queryClient.setQueryData(listKey, { data: [], nextCursor: null }); - queryClient.setQueryData(countsKey, { - all: 0, - document: 0, - sheet: 0, - pdf: 0, - routine: 0, - }); - - await queryClient.invalidateQueries({ - queryKey: tenantKeys.artifacts(TENANT), - }); - - expect(queryClient.getQueryState(listKey)?.isInvalidated).toBe(true); - expect(queryClient.getQueryState(countsKey)?.isInvalidated).toBe(true); - }); -}); diff --git a/apps/web/test/react-ui-toast-mock.ts b/apps/web/src/react-ui-toast-mock.ts similarity index 100% rename from apps/web/test/react-ui-toast-mock.ts rename to apps/web/src/react-ui-toast-mock.ts diff --git a/apps/web/test/routes.test.tsx b/apps/web/src/routes.test.tsx similarity index 99% rename from apps/web/test/routes.test.tsx rename to apps/web/src/routes.test.tsx index c5b6ceb776..d5348a3b53 100644 --- a/apps/web/test/routes.test.tsx +++ b/apps/web/src/routes.test.tsx @@ -10,7 +10,7 @@ import { afterEach, describe, expect, test } from "bun:test"; import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; -import { App } from "../src/app"; +import { App } from "./app"; import { AGENT_DETAIL_PATH, APP_ROUTES, @@ -19,8 +19,8 @@ import { ROUTINE_DETAIL_PATH, SKILL_DETAIL_PATH, WORKFLOW_DETAIL_PATH, -} from "../src/routes"; -import type { SessionState } from "../src/session"; +} from "./routes"; +import type { SessionState } from "./session"; /** Slug-addressed detail routes. The generic render loop below renders * each `route.path` verbatim, which for these is the pattern diff --git a/apps/web/test/settings-access.test.tsx b/apps/web/src/settings-access.test.tsx similarity index 97% rename from apps/web/test/settings-access.test.tsx rename to apps/web/src/settings-access.test.tsx index 21c5781714..04dd7437cd 100644 --- a/apps/web/test/settings-access.test.tsx +++ b/apps/web/src/settings-access.test.tsx @@ -7,7 +7,7 @@ import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import type { TenancyAccess } from "@/settings"; -import { useSettingsAccess } from "../src/settings-access"; +import { useSettingsAccess } from "./settings-access"; import { createTestQueryClient, TestQueryProvider } from "./test-query-provider"; const realFetch = globalThis.fetch; diff --git a/apps/web/test/settings/access.test.ts b/apps/web/src/settings/access.test.ts similarity index 96% rename from apps/web/test/settings/access.test.ts rename to apps/web/src/settings/access.test.ts index e382e43846..30f7c1a29e 100644 --- a/apps/web/test/settings/access.test.ts +++ b/apps/web/src/settings/access.test.ts @@ -5,7 +5,7 @@ import { afterEach, describe, expect, test } from "bun:test"; -import { coalesceSectionAccess, probeSectionAccess } from "../../src/settings/access"; +import { coalesceSectionAccess, probeSectionAccess } from "./access"; const realFetch = globalThis.fetch; diff --git a/apps/web/test/settings/account-section.test.tsx b/apps/web/src/settings/account-section.test.tsx similarity index 98% rename from apps/web/test/settings/account-section.test.tsx rename to apps/web/src/settings/account-section.test.tsx index ce3d3a9e57..b1bfbe644b 100644 --- a/apps/web/test/settings/account-section.test.tsx +++ b/apps/web/src/settings/account-section.test.tsx @@ -9,7 +9,7 @@ import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; import { ThemeProvider } from "@corbits/react-ui"; -import { AccountSectionView, AppearanceSection } from "../../src/settings/account-section"; +import { AccountSectionView, AppearanceSection } from "./account-section"; let container: HTMLDivElement | null = null; let root: Root | null = null; diff --git a/apps/web/test/settings/api-request.test.ts b/apps/web/src/settings/api-request.test.ts similarity index 97% rename from apps/web/test/settings/api-request.test.ts rename to apps/web/src/settings/api-request.test.ts index 30323b0cd4..e83603e17e 100644 --- a/apps/web/test/settings/api-request.test.ts +++ b/apps/web/src/settings/api-request.test.ts @@ -4,7 +4,7 @@ import { describe, expect, test } from "bun:test"; -import { apiRequest, readErrorEnvelope } from "../../src/settings/api-request"; +import { apiRequest, readErrorEnvelope } from "./api-request"; class FakeApiError extends Error { constructor( diff --git a/apps/web/test/settings/api.test.ts b/apps/web/src/settings/api.test.ts similarity index 96% rename from apps/web/test/settings/api.test.ts rename to apps/web/src/settings/api.test.ts index 92cc236f82..26793fefb5 100644 --- a/apps/web/test/settings/api.test.ts +++ b/apps/web/src/settings/api.test.ts @@ -5,7 +5,7 @@ import { afterEach, describe, expect, test } from "bun:test"; import { UnauthenticatedError } from "@/lib/api-query"; -import { SettingsApiError, getAccount, renameBench } from "../../src/settings/api"; +import { SettingsApiError, getAccount, renameBench } from "./api"; const realFetch = globalThis.fetch; diff --git a/apps/web/test/settings/credentials-api.test.ts b/apps/web/src/settings/credentials-api.test.ts similarity index 98% rename from apps/web/test/settings/credentials-api.test.ts rename to apps/web/src/settings/credentials-api.test.ts index e1c711f736..3e237b7c1c 100644 --- a/apps/web/test/settings/credentials-api.test.ts +++ b/apps/web/src/settings/credentials-api.test.ts @@ -8,7 +8,7 @@ import { deleteCredential, listCredentials, listProviders, -} from "../../src/settings/credentials-api"; +} from "./credentials-api"; const realFetch = globalThis.fetch; diff --git a/apps/web/test/settings/grant-preview.test.ts b/apps/web/src/settings/grant-preview.test.ts similarity index 97% rename from apps/web/test/settings/grant-preview.test.ts rename to apps/web/src/settings/grant-preview.test.ts index 2b2d6aa663..22135c729e 100644 --- a/apps/web/test/settings/grant-preview.test.ts +++ b/apps/web/src/settings/grant-preview.test.ts @@ -4,7 +4,7 @@ import { expiryIsoFromPreset, expiryLabelFromPreset, grantPreviewSentence, -} from "../../src/settings/grant-preview"; +} from "./grant-preview"; describe("grantPreviewSentence", () => { test("defaults the subject when no target is chosen", () => { diff --git a/apps/web/test/settings/grants-section.test.tsx b/apps/web/src/settings/grants-section.test.tsx similarity index 96% rename from apps/web/test/settings/grants-section.test.tsx rename to apps/web/src/settings/grants-section.test.tsx index 3299527cd9..1636ce61eb 100644 --- a/apps/web/test/settings/grants-section.test.tsx +++ b/apps/web/src/settings/grants-section.test.tsx @@ -15,8 +15,8 @@ import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; import { renderToStaticMarkup } from "react-dom/server"; -import { CreateGrantDialog, GrantsTable } from "../../src/settings/grants-section"; -import { PRINCIPAL_KIND_LABEL, PRINCIPAL_KIND_ORDER } from "../../src/settings/identity"; +import { CreateGrantDialog, GrantsTable } from "./grants-section"; +import { PRINCIPAL_KIND_LABEL, PRINCIPAL_KIND_ORDER } from "./identity"; const timestamps = { createdAt: "2026-01-01T00:00:00.000Z", diff --git a/apps/web/test/settings/identity.test.ts b/apps/web/src/settings/identity.test.ts similarity index 98% rename from apps/web/test/settings/identity.test.ts rename to apps/web/src/settings/identity.test.ts index ed027db57b..21994cf14b 100644 --- a/apps/web/test/settings/identity.test.ts +++ b/apps/web/src/settings/identity.test.ts @@ -7,7 +7,7 @@ import { PRINCIPAL_KIND_LABEL, PRINCIPAL_KIND_ORDER, principalLabel, -} from "../../src/settings/identity"; +} from "./identity"; describe("principalLabel", () => { test("passes through an already-humane display name unchanged", () => { diff --git a/apps/web/test/settings/people-section.test.tsx b/apps/web/src/settings/people-section.test.tsx similarity index 99% rename from apps/web/test/settings/people-section.test.tsx rename to apps/web/src/settings/people-section.test.tsx index 6c034b0487..5ff2fe5021 100644 --- a/apps/web/test/settings/people-section.test.tsx +++ b/apps/web/src/settings/people-section.test.tsx @@ -33,7 +33,7 @@ const reportErrorSpy = spyOn(errorSink, "reportError").mockImplementation( }, ); -const { PeopleSection, PeopleTable } = await import("../../src/settings/people-section"); +const { PeopleSection, PeopleTable } = await import("./people-section"); afterAll(() => { reportErrorSpy.mockRestore(); diff --git a/apps/web/test/settings/query-retry.test.tsx b/apps/web/src/settings/query-retry.test.tsx similarity index 98% rename from apps/web/test/settings/query-retry.test.tsx rename to apps/web/src/settings/query-retry.test.tsx index 54d140df8a..7070a96dc1 100644 --- a/apps/web/test/settings/query-retry.test.tsx +++ b/apps/web/src/settings/query-retry.test.tsx @@ -10,7 +10,7 @@ import { act } from "react"; import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; -import { PeopleSection } from "../../src/settings/people-section"; +import { PeopleSection } from "./people-section"; const realFetch = globalThis.fetch; afterEach(() => { diff --git a/apps/web/test/settings/raw-id-sweep.test.tsx b/apps/web/src/settings/raw-id-sweep.test.tsx similarity index 92% rename from apps/web/test/settings/raw-id-sweep.test.tsx rename to apps/web/src/settings/raw-id-sweep.test.tsx index b790244e38..789515dbc0 100644 --- a/apps/web/test/settings/raw-id-sweep.test.tsx +++ b/apps/web/src/settings/raw-id-sweep.test.tsx @@ -9,10 +9,10 @@ import { describe, expect, test } from "bun:test"; import { renderToStaticMarkup } from "react-dom/server"; -import { AccountSectionView } from "../../src/settings/account-section"; -import { GrantsTable } from "../../src/settings/grants-section"; -import { PeopleTable } from "../../src/settings/people-section"; -import { RolesTable } from "../../src/settings/roles-section"; +import { AccountSectionView } from "./account-section"; +import { GrantsTable } from "./grants-section"; +import { PeopleTable } from "./people-section"; +import { RolesTable } from "./roles-section"; const AGENT_REF_ID = "agt_8f14e45fceea167a5a36dedd4bea2543"; const UUID_PATTERN = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i; diff --git a/apps/web/test/settings/resource-vocabulary.test.ts b/apps/web/src/settings/resource-vocabulary.test.ts similarity index 94% rename from apps/web/test/settings/resource-vocabulary.test.ts rename to apps/web/src/settings/resource-vocabulary.test.ts index f43d2df19a..0dc1f6c35a 100644 --- a/apps/web/test/settings/resource-vocabulary.test.ts +++ b/apps/web/src/settings/resource-vocabulary.test.ts @@ -5,7 +5,7 @@ import { describe, expect, test } from "bun:test"; -import { GRANT_RESOURCES, GRANT_RESOURCE_LABEL } from "../../src/settings/resource-vocabulary"; +import { GRANT_RESOURCES, GRANT_RESOURCE_LABEL } from "./resource-vocabulary"; describe("GRANT_RESOURCE_LABEL", () => { test("covers every resource in GRANT_RESOURCES with a distinct, plain-language label", () => { diff --git a/apps/web/test/settings/roles-section.test.tsx b/apps/web/src/settings/roles-section.test.tsx similarity index 98% rename from apps/web/test/settings/roles-section.test.tsx rename to apps/web/src/settings/roles-section.test.tsx index 487cecfeeb..8e2710f0d5 100644 --- a/apps/web/test/settings/roles-section.test.tsx +++ b/apps/web/src/settings/roles-section.test.tsx @@ -11,7 +11,7 @@ import { describe, expect, test } from "bun:test"; import { readFileSync } from "node:fs"; import { renderToStaticMarkup } from "react-dom/server"; -import { RoleAssignments, RolesTable } from "../../src/settings/roles-section"; +import { RoleAssignments, RolesTable } from "./roles-section"; const timestamps = { createdAt: "2026-01-01T00:00:00.000Z", diff --git a/apps/web/test/settings/section-registry.test.tsx b/apps/web/src/settings/section-registry.test.tsx similarity index 97% rename from apps/web/test/settings/section-registry.test.tsx rename to apps/web/src/settings/section-registry.test.tsx index 305028fc42..6c804613e4 100644 --- a/apps/web/test/settings/section-registry.test.tsx +++ b/apps/web/src/settings/section-registry.test.tsx @@ -14,9 +14,9 @@ import { Robot } from "@/lib/icons"; import { insertEveryoneSections, resolveSettingsSectionGroups, -} from "../../src/settings/section-registry"; -import type { TenancyAccess } from "../../src/settings/access"; -import type { SettingsSection } from "../../src/settings/shell"; +} from "./section-registry"; +import type { TenancyAccess } from "./access"; +import type { SettingsSection } from "./shell"; const denied: TenancyAccess = { people: "denied", diff --git a/apps/web/test/settings/shell.test.tsx b/apps/web/src/settings/shell.test.tsx similarity index 95% rename from apps/web/test/settings/shell.test.tsx rename to apps/web/src/settings/shell.test.tsx index e0392157d8..91f5ee9a38 100644 --- a/apps/web/test/settings/shell.test.tsx +++ b/apps/web/src/settings/shell.test.tsx @@ -5,8 +5,8 @@ import { describe, expect, test } from "bun:test"; import { Bell } from "@/lib/icons"; -import { flattenSettingsSections, resolveActiveSection } from "../../src/settings/shell"; -import type { SettingsSection, SettingsSectionGroup } from "../../src/settings/shell"; +import { flattenSettingsSections, resolveActiveSection } from "./shell"; +import type { SettingsSection, SettingsSectionGroup } from "./shell"; function section(id: string): SettingsSection { return { id, title: id, icon: Bell, render: () =>
{id}
}; diff --git a/apps/web/test/settings/strings.test.ts b/apps/web/src/settings/strings.test.ts similarity index 93% rename from apps/web/test/settings/strings.test.ts rename to apps/web/src/settings/strings.test.ts index 6d3c3ad5dd..82cdea7a80 100644 --- a/apps/web/test/settings/strings.test.ts +++ b/apps/web/src/settings/strings.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; -import { SETTINGS_STRINGS } from "../../src/settings/strings"; +import { SETTINGS_STRINGS } from "./strings"; function stringValues(value: unknown): string[] { if (typeof value === "string") return [value]; diff --git a/apps/web/test/settings/tenancy-api.test.ts b/apps/web/src/settings/tenancy-api.test.ts similarity index 99% rename from apps/web/test/settings/tenancy-api.test.ts rename to apps/web/src/settings/tenancy-api.test.ts index 65a55c9f5e..4218b080bf 100644 --- a/apps/web/test/settings/tenancy-api.test.ts +++ b/apps/web/src/settings/tenancy-api.test.ts @@ -18,7 +18,7 @@ import { revokeGrant, unassignRole, updatePrincipalStatus, -} from "../../src/settings/tenancy-api"; +} from "./tenancy-api"; const realFetch = globalThis.fetch; diff --git a/apps/web/test/shell-chrome-wiring.test.tsx b/apps/web/src/shell-chrome-wiring.test.tsx similarity index 96% rename from apps/web/test/shell-chrome-wiring.test.tsx rename to apps/web/src/shell-chrome-wiring.test.tsx index c0b72869c3..7ee51da28f 100644 --- a/apps/web/test/shell-chrome-wiring.test.tsx +++ b/apps/web/src/shell-chrome-wiring.test.tsx @@ -20,10 +20,10 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; -import { AppShell } from "../src/shell/app-shell"; -import { BenchProvider } from "../src/bench-context"; -import { runActionCommand } from "../src/command-palette-actions"; -import { NavigationProvider } from "../src/navigation"; +import { AppShell } from "./shell/app-shell"; +import { BenchProvider } from "./bench-context"; +import { runActionCommand } from "./command-palette-actions"; +import { NavigationProvider } from "./navigation"; import { useCanvasColumnArtifact, useCanvasColumnProfile, @@ -32,8 +32,8 @@ import { useOpenArtifactInCanvas, useOpenProfileInCanvas, useOpenRoutineInCanvas, -} from "../src/shell/canvas-availability"; -import { ShellChromeProvider } from "../src/shell/shell-chrome-provider"; +} from "./shell/canvas-availability"; +import { ShellChromeProvider } from "./shell/shell-chrome-provider"; import { TestQueryProvider } from "./test-query-provider"; const noop = () => undefined; diff --git a/apps/web/test/bench-activity.test.tsx b/apps/web/src/shell/bench-activity.test.tsx similarity index 97% rename from apps/web/test/bench-activity.test.tsx rename to apps/web/src/shell/bench-activity.test.tsx index 51f21d00cf..d3c452498a 100644 --- a/apps/web/test/bench-activity.test.tsx +++ b/apps/web/src/shell/bench-activity.test.tsx @@ -11,9 +11,9 @@ import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { WORKBENCHES_MUTATED_EVENT } from "@/chat"; -import { useBenchActivity } from "../src/shell/bench-activity"; -import type { BenchActivityQuery } from "../src/shell/bench-activity"; -import { TestQueryProvider } from "./test-query-provider"; +import { useBenchActivity } from "./bench-activity"; +import type { BenchActivityQuery } from "./bench-activity"; +import { TestQueryProvider } from "../test-query-provider"; const realFetch = globalThis.fetch; diff --git a/apps/web/test/canvas-column.test.tsx b/apps/web/src/shell/canvas-column.test.tsx similarity index 96% rename from apps/web/test/canvas-column.test.tsx rename to apps/web/src/shell/canvas-column.test.tsx index 61e78b2331..9308f48cd9 100644 --- a/apps/web/test/canvas-column.test.tsx +++ b/apps/web/src/shell/canvas-column.test.tsx @@ -5,7 +5,7 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; -import { spyOnReactUiToast } from "./react-ui-toast-mock"; +import { spyOnReactUiToast } from "../react-ui-toast-mock"; import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; @@ -15,15 +15,15 @@ let ensureProfileDmResult: Promise< { kind: "ready"; workbenchId: string } | { kind: "error"; message: string } > = Promise.resolve({ kind: "ready", workbenchId: "chn_dm" }); -mock.module("../src/profile-relations", () => ({ +mock.module("../profile-relations", () => ({ ensureProfileDm: mock(() => ensureProfileDmResult), loadSharedWorkbenches: mock(() => Promise.resolve([])), })); -const { BenchProvider } = await import("../src/bench-context"); -const { NavigationProvider } = await import("../src/navigation"); -const { CanvasColumn } = await import("../src/shell/canvas-column"); -const { TestQueryProvider } = await import("./test-query-provider"); +const { BenchProvider } = await import("../bench-context"); +const { NavigationProvider } = await import("../navigation"); +const { CanvasColumn } = await import("./canvas-column"); +const { TestQueryProvider } = await import("../test-query-provider"); const noop = () => undefined; const realFetch = globalThis.fetch; diff --git a/apps/web/src/shell/context-menu/items.test.tsx b/apps/web/src/shell/context-menu/items.test.tsx index de053cb1ee..a6d996c30f 100644 --- a/apps/web/src/shell/context-menu/items.test.tsx +++ b/apps/web/src/shell/context-menu/items.test.tsx @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, mock, spyOn, test } from "bun:test"; -import { spyOnReactUiToast } from "../../../test/react-ui-toast-mock"; +import { spyOnReactUiToast } from "../../react-ui-toast-mock"; import type { ContextMenuEntry } from "@/shell/context-menu/lib"; import { WORKBENCHES_MUTATED_EVENT } from "@/chat"; import * as errorSink from "@corbits/error-sink"; diff --git a/apps/web/test/context-menu/context-menu-view.test.tsx b/apps/web/src/shell/context-menu/lib/context-menu-view.test.tsx similarity index 96% rename from apps/web/test/context-menu/context-menu-view.test.tsx rename to apps/web/src/shell/context-menu/lib/context-menu-view.test.tsx index 1cd4cdd401..12d15b4fcc 100644 --- a/apps/web/test/context-menu/context-menu-view.test.tsx +++ b/apps/web/src/shell/context-menu/lib/context-menu-view.test.tsx @@ -3,9 +3,9 @@ import { act, createElement, useState } from "react"; import { createRoot } from "react-dom/client"; import { Dialog, DialogContent, DialogTitle } from "@corbits/react-ui"; -import { contextMenuItem } from "../../src/shell/context-menu/lib/menu"; -import { ContextMenuView } from "../../src/shell/context-menu/lib/context-menu-view"; -import type { ContextMenu } from "../../src/shell/context-menu/lib/menu"; +import { contextMenuItem } from "./menu"; +import { ContextMenuView } from "./context-menu-view"; +import type { ContextMenu } from "./menu"; const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/apps/web/test/context-menu/dialog-guard.test.ts b/apps/web/src/shell/context-menu/lib/dialog-guard.test.ts similarity index 96% rename from apps/web/test/context-menu/dialog-guard.test.ts rename to apps/web/src/shell/context-menu/lib/dialog-guard.test.ts index 236f50fb21..ba9e15cc30 100644 --- a/apps/web/test/context-menu/dialog-guard.test.ts +++ b/apps/web/src/shell/context-menu/lib/dialog-guard.test.ts @@ -3,7 +3,7 @@ import { afterEach, describe, expect, test } from "bun:test"; import { isBlockingOverlayOpen, isInsideInteractiveInput, -} from "../../src/shell/context-menu/lib/dialog-guard"; +} from "./dialog-guard"; afterEach(() => { document.body.innerHTML = ""; diff --git a/apps/web/test/context-menu/target-resolver.test.ts b/apps/web/src/shell/context-menu/lib/target-resolver.test.ts similarity index 93% rename from apps/web/test/context-menu/target-resolver.test.ts rename to apps/web/src/shell/context-menu/lib/target-resolver.test.ts index 7744775b33..ae09a9b9ac 100644 --- a/apps/web/test/context-menu/target-resolver.test.ts +++ b/apps/web/src/shell/context-menu/lib/target-resolver.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test"; -import { resolveTarget } from "../../src/shell/context-menu/lib/target-resolver"; -import type { TargetDefinition } from "../../src/shell/context-menu/lib/target-resolver"; +import { resolveTarget } from "./target-resolver"; +import type { TargetDefinition } from "./target-resolver"; type Target = | { readonly type: "channel"; readonly id: string } diff --git a/apps/web/test/context-menu/use-context-menu-state.test.tsx b/apps/web/src/shell/context-menu/lib/use-context-menu-state.test.tsx similarity index 90% rename from apps/web/test/context-menu/use-context-menu-state.test.tsx rename to apps/web/src/shell/context-menu/lib/use-context-menu-state.test.tsx index 9fcb8b5275..4bc99c5914 100644 --- a/apps/web/test/context-menu/use-context-menu-state.test.tsx +++ b/apps/web/src/shell/context-menu/lib/use-context-menu-state.test.tsx @@ -2,9 +2,9 @@ import { describe, expect, test } from "bun:test"; import { act, createElement } from "react"; import { createRoot } from "react-dom/client"; -import { contextMenuItem } from "../../src/shell/context-menu/lib/menu"; -import { useContextMenuState } from "../../src/shell/context-menu/lib/use-context-menu-state"; -import type { ContextMenuState } from "../../src/shell/context-menu/lib/use-context-menu-state"; +import { contextMenuItem } from "./menu"; +import { useContextMenuState } from "./use-context-menu-state"; +import type { ContextMenuState } from "./use-context-menu-state"; function mount() { const container = document.createElement("div"); diff --git a/apps/web/test/context-menu/use-document-context-menu-trigger.test.tsx b/apps/web/src/shell/context-menu/lib/use-document-context-menu-trigger.test.tsx similarity index 92% rename from apps/web/test/context-menu/use-document-context-menu-trigger.test.tsx rename to apps/web/src/shell/context-menu/lib/use-document-context-menu-trigger.test.tsx index 4089a255fe..b032ef4c07 100644 --- a/apps/web/test/context-menu/use-document-context-menu-trigger.test.tsx +++ b/apps/web/src/shell/context-menu/lib/use-document-context-menu-trigger.test.tsx @@ -2,9 +2,9 @@ import { afterEach, describe, expect, test } from "bun:test"; import { act, createElement } from "react"; import { createRoot } from "react-dom/client"; -import { contextMenuItem } from "../../src/shell/context-menu/lib/menu"; -import { useDocumentContextMenuTrigger } from "../../src/shell/context-menu/lib/use-document-context-menu-trigger"; -import type { ContextMenu } from "../../src/shell/context-menu/lib/menu"; +import { contextMenuItem } from "./menu"; +import { useDocumentContextMenuTrigger } from "./use-document-context-menu-trigger"; +import type { ContextMenu } from "./menu"; const MENU: ContextMenu = { entries: [contextMenuItem({ id: "open", label: "Open", onSelect: () => undefined })], diff --git a/apps/web/test/shell-layout/shell-layout-hook.test.tsx b/apps/web/src/shell/layout/shell-layout-hook.test.tsx similarity index 97% rename from apps/web/test/shell-layout/shell-layout-hook.test.tsx rename to apps/web/src/shell/layout/shell-layout-hook.test.tsx index 83c01e891d..bef5dc83b0 100644 --- a/apps/web/test/shell-layout/shell-layout-hook.test.tsx +++ b/apps/web/src/shell/layout/shell-layout-hook.test.tsx @@ -14,8 +14,8 @@ import { COMPACT_MAX_WIDTH, NARROW_MAX_WIDTH, type ShellLayoutMode, -} from "../../src/shell/layout/breakpoints"; -import { useShellLayoutMode } from "../../src/shell/layout/use-shell-layout"; +} from "./breakpoints"; +import { useShellLayoutMode } from "./use-shell-layout"; type StubQuery = { matches: boolean; diff --git a/apps/web/test/shell-layout/use-scroll-reset.test.tsx b/apps/web/src/shell/layout/use-scroll-reset.test.tsx similarity index 95% rename from apps/web/test/shell-layout/use-scroll-reset.test.tsx rename to apps/web/src/shell/layout/use-scroll-reset.test.tsx index 17fe4ed76d..da27b5cbbb 100644 --- a/apps/web/test/shell-layout/use-scroll-reset.test.tsx +++ b/apps/web/src/shell/layout/use-scroll-reset.test.tsx @@ -3,7 +3,7 @@ import { act, createElement, useRef, useState } from "react"; import { createRoot } from "react-dom/client"; import type { RefObject } from "react"; -import { useScrollReset } from "../../src/shell/layout/use-scroll-reset"; +import { useScrollReset } from "./use-scroll-reset"; function mount(initialPath: string) { const container = document.createElement("div"); diff --git a/apps/web/src/shell/library-artifacts.test.ts b/apps/web/src/shell/library-artifacts.test.ts index 8acaacfe17..1f25ef983c 100644 --- a/apps/web/src/shell/library-artifacts.test.ts +++ b/apps/web/src/shell/library-artifacts.test.ts @@ -1,50 +1,67 @@ -import { describe, expect, test } from "bun:test"; +import { afterEach, describe, expect, test } from "bun:test"; +import { UnauthenticatedError } from "@/lib/api-query"; import { - artifactUploadToast, - copyArtifactLinksActionLabel, - copyArtifactLinksToastLabel, - uploadMimeTypeFromSource, + artifactListRowToSummary, + isArtifactsUnavailableStatus, + mapArtifactListToSummaries, + uploadArtifactFiles, + type ArtifactListRow, } from "./library-artifacts"; -describe("copy-link labels", () => { - test("action label is count-aware", () => { - expect(copyArtifactLinksActionLabel(1)).toBe("Copy link"); - expect(copyArtifactLinksActionLabel(3)).toBe("Copy 3 links"); - }); +const realFetch = globalThis.fetch; - test("toast label is count-aware", () => { - expect(copyArtifactLinksToastLabel(1)).toBe("Link copied"); - expect(copyArtifactLinksToastLabel(3)).toBe("3 links copied"); - }); +afterEach(() => { + globalThis.fetch = realFetch; }); -describe("artifactUploadToast", () => { - test("a single file is confirmed by name", () => { - expect(artifactUploadToast(["q3-report.pdf"])).toBe("Uploaded · q3-report.pdf"); - }); +const sample: ArtifactListRow = { + id: "art_1", + kind: "file", + title: "Quarterly report.pdf", + ownerName: "Ada", + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-02T00:00:00.000Z", +}; - test("several files are confirmed by count", () => { - expect(artifactUploadToast(["a.png", "b.png", "c.png", "d.png"])).toBe("Uploaded 4 files"); +describe("library-artifacts", () => { + test("maps list rows onto ArtifactSummary without inventing fields", () => { + expect(artifactListRowToSummary(sample)).toEqual({ + id: "art_1", + title: "Quarterly report.pdf", + kind: "file", + ownerName: "Ada", + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-02T00:00:00.000Z", + }); }); -}); -describe("uploadMimeTypeFromSource", () => { - test("reads the mime type off a real upload's source", () => { - expect( - uploadMimeTypeFromSource({ - origin: "library-upload", - upload: { id: "u1", mimeType: "text/markdown", filename: "a.md" }, - }), - ).toBe("text/markdown"); + test("maps a list without inventing or dropping rows", () => { + const second: ArtifactListRow = { + ...sample, + id: "art_2", + kind: "document", + title: "notes.txt", + ownerName: null, + }; + const mapped = mapArtifactListToSummaries([sample, second]); + expect(mapped).toHaveLength(2); + expect(mapped[0]?.id).toBe("art_1"); + expect(mapped[1]?.kind).toBe("document"); + expect(mapped[1]?.ownerName).toBeNull(); }); - test("is null for an artifact with no upload backing", () => { - expect(uploadMimeTypeFromSource({ origin: "chat" })).toBeNull(); + test("detects the unconfigured-plane status", () => { + expect(isArtifactsUnavailableStatus(503)).toBe(true); + expect(isArtifactsUnavailableStatus(500)).toBe(false); + expect(isArtifactsUnavailableStatus(undefined)).toBe(false); }); - test("is null when the upload field is malformed", () => { - expect(uploadMimeTypeFromSource({ upload: "not-an-object" })).toBeNull(); - expect(uploadMimeTypeFromSource({ upload: { id: "u1" } })).toBeNull(); + test("uploadArtifactFiles throws an UnauthenticatedError on 401", async () => { + globalThis.fetch = ((_input: RequestInfo | URL, _init?: RequestInit) => + Promise.resolve(new Response(null, { status: 401 }))) as typeof fetch; + await expect(uploadArtifactFiles("tnt_1", [new File(["x"], "x.txt")])).rejects.toBeInstanceOf( + UnauthenticatedError, + ); }); }); diff --git a/apps/web/test/routine-activity.test.ts b/apps/web/src/shell/routine-activity.test.ts similarity index 94% rename from apps/web/test/routine-activity.test.ts rename to apps/web/src/shell/routine-activity.test.ts index 42afd5ca69..3579ac9d32 100644 --- a/apps/web/test/routine-activity.test.ts +++ b/apps/web/src/shell/routine-activity.test.ts @@ -10,7 +10,7 @@ import { afterEach, describe, expect, test } from "bun:test"; -import { listRoutineActivity } from "../src/shell/routine-activity"; +import { listRoutineActivity } from "./routine-activity"; const realFetch = globalThis.fetch; diff --git a/apps/web/test/sidebar.test.tsx b/apps/web/src/shell/sidebar.test.tsx similarity index 98% rename from apps/web/test/sidebar.test.tsx rename to apps/web/src/shell/sidebar.test.tsx index 2df93c5078..ea6e1bbeba 100644 --- a/apps/web/test/sidebar.test.tsx +++ b/apps/web/src/shell/sidebar.test.tsx @@ -8,11 +8,11 @@ import { act } from "react"; import { createRoot } from "react-dom/client"; import { renderToStaticMarkup } from "react-dom/server"; -import { BenchProvider } from "../src/bench-context"; -import { APP_ROUTES, NAV_ROUTES } from "../src/routes"; -import { Sidebar } from "../src/shell/sidebar"; -import { SIDEBAR_EMPTY_COPY } from "../src/shell/workbench-list"; -import { TestQueryProvider } from "./test-query-provider"; +import { BenchProvider } from "../bench-context"; +import { APP_ROUTES, NAV_ROUTES } from "../routes"; +import { Sidebar } from "./sidebar"; +import { SIDEBAR_EMPTY_COPY } from "./workbench-list"; +import { TestQueryProvider } from "../test-query-provider"; const noop = () => undefined; const realFetch = globalThis.fetch; diff --git a/apps/web/test/stage-top-bar.test.tsx b/apps/web/src/shell/stage-top-bar.test.tsx similarity index 91% rename from apps/web/test/stage-top-bar.test.tsx rename to apps/web/src/shell/stage-top-bar.test.tsx index 5ba471d68e..21869f24cb 100644 --- a/apps/web/test/stage-top-bar.test.tsx +++ b/apps/web/src/shell/stage-top-bar.test.tsx @@ -10,12 +10,12 @@ import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { renderToStaticMarkup } from "react-dom/server"; -import { AppShell } from "../src/shell/app-shell"; -import { ShellChromeProvider } from "../src/shell/shell-chrome-provider"; -import { StageTopBar } from "../src/shell/stage-top-bar"; -import { BenchProvider } from "../src/bench-context"; -import { NavigationProvider } from "../src/navigation"; -import { TestQueryProvider } from "./test-query-provider"; +import { AppShell } from "./app-shell"; +import { ShellChromeProvider } from "./shell-chrome-provider"; +import { StageTopBar } from "./stage-top-bar"; +import { BenchProvider } from "../bench-context"; +import { NavigationProvider } from "../navigation"; +import { TestQueryProvider } from "../test-query-provider"; const noop = () => undefined; const realFetch = globalThis.fetch; @@ -54,7 +54,7 @@ describe("StageTopBar", () => { }); test("an interactive subtitle is not clipped by overflow hidden", () => { - const css = readFileSync(new URL("../src/app.css", import.meta.url), "utf8"); + const css = readFileSync(new URL("../app.css", import.meta.url), "utf8"); const interactive = css .split("}") .find((candidate) => candidate.includes(".stage-top-bar-sub:has(button)")); diff --git a/apps/web/test/workbench-list.test.tsx b/apps/web/src/shell/workbench-list.test.tsx similarity index 97% rename from apps/web/test/workbench-list.test.tsx rename to apps/web/src/shell/workbench-list.test.tsx index c006a7756c..728f00545f 100644 --- a/apps/web/test/workbench-list.test.tsx +++ b/apps/web/src/shell/workbench-list.test.tsx @@ -3,9 +3,9 @@ import { act } from "react"; import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; -import { BenchProvider } from "../src/bench-context"; -import { WorkbenchList, SIDEBAR_EMPTY_COPY } from "../src/shell/workbench-list"; -import { TestQueryProvider } from "./test-query-provider"; +import { BenchProvider } from "../bench-context"; +import { WorkbenchList, SIDEBAR_EMPTY_COPY } from "./workbench-list"; +import { TestQueryProvider } from "../test-query-provider"; const realFetch = globalThis.fetch; diff --git a/apps/web/test/stage-chrome-consistency.test.tsx b/apps/web/src/stage-chrome-consistency.test.tsx similarity index 95% rename from apps/web/test/stage-chrome-consistency.test.tsx rename to apps/web/src/stage-chrome-consistency.test.tsx index ded964eb6b..0f1cf4dada 100644 --- a/apps/web/test/stage-chrome-consistency.test.tsx +++ b/apps/web/src/stage-chrome-consistency.test.tsx @@ -9,9 +9,9 @@ import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { renderToStaticMarkup } from "react-dom/server"; -import { AgentsPage } from "../src/pages/agents-page"; -import { LibraryPage } from "../src/pages/library-page"; -import { SkillsPage } from "../src/pages/skills-page"; +import { AgentsPage } from "./pages/agents-page"; +import { LibraryPage } from "./pages/library-page"; +import { SkillsPage } from "./pages/skills-page"; import { TestQueryProvider } from "./test-query-provider"; const noop = () => undefined; diff --git a/apps/web/test/stage-search-filter.test.tsx b/apps/web/src/stage-search-filter.test.tsx similarity index 95% rename from apps/web/test/stage-search-filter.test.tsx rename to apps/web/src/stage-search-filter.test.tsx index b8de4b8da2..91d1927fa7 100644 --- a/apps/web/test/stage-search-filter.test.tsx +++ b/apps/web/src/stage-search-filter.test.tsx @@ -11,10 +11,10 @@ import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { useState } from "react"; -import { setCommandPaletteOpen, useCommandPaletteOpen } from "../src/command-palette-open-store"; -import { StageTopBar } from "../src/shell/stage-top-bar"; +import { setCommandPaletteOpen, useCommandPaletteOpen } from "./command-palette-open-store"; +import { StageTopBar } from "./shell/stage-top-bar"; -const appCss = readFileSync(new URL("../src/app.css", import.meta.url), "utf8"); +const appCss = readFileSync(new URL("./app.css", import.meta.url), "utf8"); function ruleFor(css: string, className: string): string { const selector = new RegExp(`\\.${className}\\s*[,{]`); diff --git a/apps/web/test/stage-top-nav.test.tsx b/apps/web/src/stage-top-nav.test.tsx similarity index 96% rename from apps/web/test/stage-top-nav.test.tsx rename to apps/web/src/stage-top-nav.test.tsx index 3eabc8a1d9..213ea1aef2 100644 --- a/apps/web/test/stage-top-nav.test.tsx +++ b/apps/web/src/stage-top-nav.test.tsx @@ -11,10 +11,10 @@ import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { renderToStaticMarkup } from "react-dom/server"; -import { NavigationProvider } from "../src/navigation"; -import { SkillDetailPage } from "../src/pages/skill-detail-page"; -import { SkillsPage } from "../src/pages/skills-page"; -import { StageTopBar } from "../src/shell/stage-top-bar"; +import { NavigationProvider } from "./navigation"; +import { SkillDetailPage } from "./pages/skill-detail-page"; +import { SkillsPage } from "./pages/skills-page"; +import { StageTopBar } from "./shell/stage-top-bar"; import { TestQueryProvider } from "./test-query-provider"; const noop = () => undefined; @@ -101,7 +101,7 @@ describe("stage breadcrumbs", () => { , ); - const css = readFileSync(new URL("../src/app.css", import.meta.url), "utf8"); + const css = readFileSync(new URL("./app.css", import.meta.url), "utf8"); for (const className of ["stage-crumb-link", "stage-crumb-current"]) { expect(markup).toContain(`class="${className}"`); const rule = ruleFor(css, className); diff --git a/apps/web/test/tailwind-build.test.ts b/apps/web/src/tailwind-build.test.ts similarity index 100% rename from apps/web/test/tailwind-build.test.ts rename to apps/web/src/tailwind-build.test.ts diff --git a/apps/web/test/test-query-provider.tsx b/apps/web/src/test-query-provider.tsx similarity index 100% rename from apps/web/test/test-query-provider.tsx rename to apps/web/src/test-query-provider.tsx diff --git a/apps/web/test/theme-default.test.tsx b/apps/web/src/theme-default.test.tsx similarity index 100% rename from apps/web/test/theme-default.test.tsx rename to apps/web/src/theme-default.test.tsx diff --git a/apps/web/test/toast-single-system.test.tsx b/apps/web/src/toast-single-system.test.tsx similarity index 98% rename from apps/web/test/toast-single-system.test.tsx rename to apps/web/src/toast-single-system.test.tsx index 3d57bb0117..f35e2ea163 100644 --- a/apps/web/test/toast-single-system.test.tsx +++ b/apps/web/src/toast-single-system.test.tsx @@ -13,9 +13,9 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; -import { BenchProvider } from "../src/bench-context"; -import { NavigationProvider } from "../src/navigation"; -import { NewWorkbenchPickerRoute } from "../src/pages/new-workbench-picker"; +import { BenchProvider } from "./bench-context"; +import { NavigationProvider } from "./navigation"; +import { NewWorkbenchPickerRoute } from "./pages/new-workbench-picker"; import { clearToasts } from "./react-ui-toast-mock"; import { TestQueryProvider } from "./test-query-provider"; diff --git a/apps/web/test/chat/composer.test.tsx b/apps/web/test/chat/composer.test.tsx deleted file mode 100644 index b84651a148..0000000000 --- a/apps/web/test/chat/composer.test.tsx +++ /dev/null @@ -1,848 +0,0 @@ -import { afterEach, describe, expect, spyOn, test } from "bun:test"; -import { act, createElement, createRef } from "react"; -import { createRoot } from "react-dom/client"; -import type { Root } from "react-dom/client"; -import * as errorSink from "@corbits/error-sink"; - -import { Composer } from "../../src/chat/composer"; -import type { ComposerHandle, ComposerSendPayload } from "../../src/chat/composer"; - -// The in-flight send contract survives the icon-only UI: its accessible name -// announces progress and the action remains unavailable until sending ends. -let container: HTMLDivElement | null = null; -let root: Root | null = null; - -afterEach(() => { - if (root !== null) { - act(() => root?.unmount()); - root = null; - } - if (container !== null) { - container.remove(); - container = null; - } -}); - -const settle = () => act(() => new Promise((resolve) => setTimeout(resolve, 0))); - -function mount(onSend: () => Promise) { - container = document.createElement("div"); - document.body.appendChild(container); - root = createRoot(container); - const ref = createRef(); - act(() => { - root?.render( - createElement(Composer, { - ref, - agents: [], - onSend, - }), - ); - }); - return container; -} - -function sendButton(): HTMLButtonElement { - const button = container?.querySelector('[aria-label^="Send"]'); - if (button === null || button === undefined) { - throw new Error("send button not found"); - } - return button; -} - -function keyboardHint(): Element | null { - return container?.querySelector(".chat-composer-keyboard-hint") ?? null; -} - -describe("Composer send button", () => { - test("updates its accessible label and stays disabled while sending", async () => { - let resolveSend: (value: boolean) => void = () => undefined; - const onSend = () => - new Promise((resolve) => { - resolveSend = resolve; - }); - mount(onSend); - - const textarea = container?.querySelector("textarea"); - if (textarea === null || textarea === undefined) { - throw new Error("composer textarea not found"); - } - act(() => { - const setter = Object.getOwnPropertyDescriptor( - globalThis.HTMLTextAreaElement.prototype, - "value", - )?.set; - setter?.call(textarea, "hello there"); - textarea.dispatchEvent(new Event("input", { bubbles: true })); - }); - await settle(); - - expect(sendButton().getAttribute("aria-label")).toBe("Send"); - - act(() => { - sendButton().click(); - }); - await settle(); - - expect(sendButton().getAttribute("aria-label")).toBe("Sending…"); - expect(sendButton().hasAttribute("disabled")).toBe(true); - - await act(async () => { - resolveSend(true); - await Promise.resolve(); - await Promise.resolve(); - }); - - expect(sendButton().getAttribute("aria-label")).toBe("Send"); - }); -}); - -test("labels the icon-only attachment action in the composer rail", () => { - mount(() => Promise.resolve(true)); - - expect(container?.querySelector('[aria-label="Attach files"]')?.textContent).toBe(""); -}); - -function textarea(): HTMLTextAreaElement { - const element = container?.querySelector("textarea"); - if (element === null || element === undefined) { - throw new Error("composer textarea not found"); - } - return element; -} - -function typeInto(element: HTMLTextAreaElement, text: string) { - const setter = Object.getOwnPropertyDescriptor( - globalThis.HTMLTextAreaElement.prototype, - "value", - )?.set; - act(() => { - setter?.call(element, text); - element.dispatchEvent(new Event("input", { bubbles: true })); - }); -} - -function mountWithMentions(onSend: (payload: ComposerSendPayload) => Promise) { - container = document.createElement("div"); - document.body.appendChild(container); - root = createRoot(container); - const ref = createRef(); - act(() => { - root?.render( - createElement(Composer, { - ref, - agents: [{ id: "researcher@agents.example", handle: "researcher", label: "Researcher" }], - onSend, - }), - ); - }); - return container; -} - -describe("Composer keyboard hint", () => { - test("uses the existing action rail and appears only for a focused non-empty draft", async () => { - mount(() => Promise.resolve(true)); - expect(keyboardHint()).not.toBeNull(); - expect(keyboardHint()?.getAttribute("data-visible")).toBe("false"); - expect(keyboardHint()?.getAttribute("aria-hidden")).toBe("true"); - - act(() => { - textarea().focus(); - }); - typeInto(textarea(), "hello"); - await settle(); - - expect(keyboardHint()?.getAttribute("data-visible")).toBe("true"); - expect(keyboardHint()?.getAttribute("aria-hidden")).toBe("false"); - expect(keyboardHint()?.textContent).toBe("Enter to send"); - expect(container?.querySelectorAll(".chat-composer-actions button").length).toBe(2); - expect(container?.querySelector(".chat-composer-row > textarea")).toBe(textarea()); - }); - - test("hides when the focused draft is cleared", async () => { - mount(() => Promise.resolve(true)); - act(() => { - textarea().focus(); - }); - typeInto(textarea(), "hello"); - await settle(); - expect(keyboardHint()?.getAttribute("data-visible")).toBe("true"); - - typeInto(textarea(), ""); - await settle(); - expect(keyboardHint()?.getAttribute("data-visible")).toBe("false"); - expect(keyboardHint()?.getAttribute("aria-hidden")).toBe("true"); - }); -}); - -describe("Composer growth containment", () => { - test("the textarea carries the max-height/overflow class and keeps applying its measured inline height", async () => { - mount(() => Promise.resolve(true)); - expect(textarea().className).toContain("chat-composer-input"); - - typeInto(textarea(), "line one\nline two\nline three"); - await settle(); - // The auto-grow effect still measures and writes an inline height on - // every change — the CSS transition only smooths that write, it does - // not replace it. - expect(textarea().style.height.endsWith("px")).toBe(true); - }); -}); - -describe("Composer popover entrance", () => { - test("the mention popover carries the entrance class", async () => { - mountWithMentions(() => Promise.resolve(true)); - typeInto(textarea(), "@"); - await settle(); - const popover = container?.querySelector(".chat-mention-popover"); - expect(popover?.classList.contains("chat-popover-enter")).toBe(true); - }); -}); - -describe("Composer hit targets", () => { - test("attach and send buttons carry the extended-hit-area class", () => { - mount(() => Promise.resolve(true)); - const buttons = container?.querySelectorAll(".chat-composer-icon-button"); - expect(buttons?.length).toBe(2); - }); -}); - -describe("ComposerHandle.setText", () => { - test("replaces the existing draft and focuses with the caret at the end", async () => { - container = document.createElement("div"); - document.body.appendChild(container); - root = createRoot(container); - const ref = createRef(); - act(() => { - root?.render( - createElement(Composer, { - ref, - agents: [], - onSend: () => Promise.resolve(true), - }), - ); - }); - - typeInto(textarea(), "unsent draft"); - await settle(); - expect(textarea().value).toBe("unsent draft"); - - await act(async () => { - ref.current?.setText("previous prompt"); - await new Promise((resolve) => { - requestAnimationFrame(() => resolve()); - }); - }); - - expect(textarea().value).toBe("previous prompt"); - expect(document.activeElement).toBe(textarea()); - expect(textarea().selectionStart).toBe("previous prompt".length); - expect(textarea().selectionEnd).toBe("previous prompt".length); - }); - - test("Enter after setText of an @handle prompt sends instead of opening mention", async () => { - container = document.createElement("div"); - document.body.appendChild(container); - root = createRoot(container); - const ref = createRef(); - const sent: ComposerSendPayload[] = []; - act(() => { - root?.render( - createElement(Composer, { - ref, - agents: [{ id: "researcher@agents.example", handle: "researcher", label: "Researcher" }], - onSend: (payload) => { - sent.push(payload); - return Promise.resolve(true); - }, - }), - ); - }); - - await act(async () => { - ref.current?.setText("@researcher"); - await new Promise((resolve) => { - requestAnimationFrame(() => resolve()); - }); - }); - await settle(); - - expect(container?.querySelector(".chat-mention-popover")).toBeNull(); - - act(() => { - textarea().dispatchEvent( - new KeyboardEvent("keydown", { - key: "Enter", - bubbles: true, - cancelable: true, - }), - ); - }); - await settle(); - - expect(sent).toEqual([{ text: "@researcher", attachments: [] }]); - }); - - test("Send after setText does not carry leftover attachments", async () => { - const sent: ComposerSendPayload[] = []; - const ref = createRef(); - container = document.createElement("div"); - document.body.appendChild(container); - root = createRoot(container); - act(() => { - root?.render( - createElement(Composer, { - ref, - agents: [], - onSend: (payload) => { - sent.push(payload); - return Promise.resolve(true); - }, - }), - ); - }); - - const fileInput = container.querySelector(".chat-composer-file-input"); - if (fileInput === null) throw new Error("file input not found"); - - const file = new File(["hello"], "notes.txt", { type: "text/plain" }); - Object.defineProperty(fileInput, "files", { - configurable: true, - value: { - 0: file, - length: 1, - item: (index: number) => (index === 0 ? file : null), - [Symbol.iterator]: function* () { - yield file; - }, - }, - }); - act(() => { - fileInput.dispatchEvent(new Event("change", { bubbles: true })); - }); - await settle(); - await settle(); - expect(container.querySelector(".chat-composer-attachments")).not.toBeNull(); - - await act(async () => { - ref.current?.setText("copied prompt"); - await new Promise((resolve) => { - requestAnimationFrame(() => resolve()); - }); - }); - await settle(); - - expect(container.querySelector(".chat-composer-attachments")).toBeNull(); - - act(() => { - sendButton().click(); - }); - await settle(); - - expect(sent).toEqual([{ text: "copied prompt", attachments: [] }]); - }); -}); - -// The composer's Stop affordance. `handleStop` guards against a -// double-click and re-enables itself once the host reports the turn is -// no longer running -- but a REJECTED stop request must also re-enable -// it, or a genuinely failed cancel (not a slow one) leaves the person -// with a permanently disabled button and no way to retry for the rest -// of that turn's life. -function stopButton(): HTMLButtonElement { - const button = container?.querySelector('[aria-label="Stop"]'); - if (button === null || button === undefined) { - throw new Error("stop button not found"); - } - return button; -} - -function mountStoppable( - running: boolean, - onStop: () => void | Promise, - onSend: (payload: ComposerSendPayload) => Promise = () => Promise.resolve(true), -) { - container = document.createElement("div"); - document.body.appendChild(container); - root = createRoot(container); - act(() => { - root?.render( - createElement(Composer, { - agents: [], - onSend, - running, - onStop, - }), - ); - }); - return { - container, - rerender: (nextRunning: boolean) => { - act(() => { - root?.render( - createElement(Composer, { - agents: [], - onSend, - running: nextRunning, - onStop, - }), - ); - }); - }, - }; -} - -describe("Composer stop affordance", () => { - test("keeps Stop and Send together in the right-aligned action group", () => { - mountStoppable(true, () => undefined); - - const actions = container?.querySelector(".chat-composer-actions"); - const submitActions = stopButton().parentElement; - expect(actions?.children).toHaveLength(3); - expect(submitActions?.classList.contains("chat-composer-submit-actions")).toBe(true); - expect(submitActions?.children).toHaveLength(2); - expect(submitActions?.lastElementChild).toBe(sendButton()); - expect(actions?.lastElementChild).toBe(submitActions); - }); - - test("gives Stop and Send distinct accessible names while both are visible", () => { - mountStoppable(true, () => undefined); - - expect(stopButton().getAttribute("aria-label")).toBe("Stop"); - expect(sendButton().getAttribute("aria-label")).toBe("Send"); - }); - - test("keeps queued sends available while Stop is visible", async () => { - const sent: ComposerSendPayload[] = []; - mountStoppable( - true, - () => undefined, - (payload) => { - sent.push(payload); - return Promise.resolve(true); - }, - ); - - typeInto(textarea(), "follow-up"); - await settle(); - - expect(sendButton().hasAttribute("disabled")).toBe(false); - act(() => { - sendButton().click(); - }); - await settle(); - - expect(sent).toEqual([{ text: "follow-up", attachments: [] }]); - }); - - test("renders no Stop button when no turn is running", () => { - mountStoppable(false, () => undefined); - expect(container?.querySelector('[aria-label="Stop"]')).toBeNull(); - }); - - test("clicking Stop calls onStop and disables the button against a double-click", async () => { - let calls = 0; - mountStoppable(true, () => { - calls += 1; - }); - - expect(stopButton().hasAttribute("disabled")).toBe(false); - act(() => { - stopButton().click(); - }); - await settle(); - - expect(calls).toBe(1); - expect(stopButton().hasAttribute("disabled")).toBe(true); - - act(() => { - stopButton().click(); - }); - await settle(); - expect(calls).toBe(1); - }); - - test("the button re-enables once the host reports the turn is no longer running", async () => { - const { rerender } = mountStoppable(true, () => undefined); - act(() => { - stopButton().click(); - }); - await settle(); - expect(stopButton().hasAttribute("disabled")).toBe(true); - - rerender(false); - expect(container?.querySelector('[aria-label="Stop"]')).toBeNull(); - }); - - test("a stop request that REJECTS re-enables the button instead of leaving it stuck", async () => { - let reject: (err: unknown) => void = () => undefined; - mountStoppable( - true, - () => - new Promise((_resolve, rej) => { - reject = rej; - }), - ); - - act(() => { - stopButton().click(); - }); - await settle(); - expect(stopButton().hasAttribute("disabled")).toBe(true); - - await act(async () => { - reject(new Error("network error")); - await Promise.resolve(); - await Promise.resolve(); - }); - - // The turn is still running (the cancel request itself failed, not - // the turn) -- the button must come back so the user can try again. - expect(stopButton().hasAttribute("disabled")).toBe(false); - }); -}); - -describe("Composer dictate", () => { - const recognitions: FakeSpeechRecognition[] = []; - - class FakeSpeechRecognition { - continuous = false; - interimResults = false; - onresult: ((event: { results: FakeSpeechResult[] }) => void) | null = null; - onerror: ((event: unknown) => void) | null = null; - onend: (() => void) | null = null; - started = false; - resultOnStop: readonly { isFinal: boolean; transcript: string }[] | null = null; - - constructor() { - recognitions.push(this); - } - - start() { - this.started = true; - } - - stop() { - this.started = false; - if (this.resultOnStop !== null) { - this.emit(this.resultOnStop); - this.resultOnStop = null; - } - this.onend?.(); - } - - abort() { - this.started = false; - this.onend?.(); - } - - emit(segments: readonly { isFinal: boolean; transcript: string }[]) { - const results = segments.map((segment) => ({ - isFinal: segment.isFinal, - length: 1, - 0: { transcript: segment.transcript }, - })); - this.onresult?.({ results }); - } - } - - type FakeSpeechResult = { - readonly isFinal: boolean; - readonly length: number; - readonly 0: { readonly transcript: string }; - }; - - function installSpeechRecognition() { - Object.defineProperty(window, "SpeechRecognition", { - configurable: true, - writable: true, - value: FakeSpeechRecognition, - }); - } - - afterEach(() => { - recognitions.length = 0; - Reflect.deleteProperty(window, "SpeechRecognition"); - }); - - test("hides the dictate control when speech recognition is unavailable", () => { - mount(() => Promise.resolve(true)); - expect(container?.querySelector('[aria-label="Dictate"]')).toBeNull(); - }); - - test("starts recognition, inserts a transcript on a word boundary, and stops", async () => { - installSpeechRecognition(); - mount(() => Promise.resolve(true)); - typeInto(textarea(), "hello"); - textarea().setSelectionRange(5, 5); - await settle(); - - const dictate = container?.querySelector('[aria-label="Dictate"]'); - if (dictate === null || dictate === undefined) { - throw new Error("dictate button not found"); - } - - act(() => { - dictate.click(); - }); - await settle(); - - expect(recognitions.at(-1)?.started).toBe(true); - expect(recognitions.at(-1)?.continuous).toBe(true); - expect(recognitions.at(-1)?.interimResults).toBe(true); - expect(dictate.getAttribute("aria-label")).toBe("Stop dictating"); - expect(dictate.getAttribute("aria-pressed")).toBe("true"); - expect(dictate.getAttribute("data-listening")).toBe("true"); - - act(() => { - recognitions.at(-1)?.emit([{ isFinal: true, transcript: "world" }]); - }); - await settle(); - - expect(textarea().value).toBe("hello world"); - - act(() => { - dictate.click(); - }); - await settle(); - - expect(recognitions.at(-1)?.started).toBe(false); - const idle = container?.querySelector('[aria-label="Dictate"]'); - expect(idle?.getAttribute("aria-pressed")).toBe("false"); - expect(idle?.getAttribute("data-listening")).toBe("false"); - }); - - test("user Stop flushes a late final result into the draft", async () => { - installSpeechRecognition(); - mount(() => Promise.resolve(true)); - typeInto(textarea(), "hello"); - textarea().setSelectionRange(5, 5); - await settle(); - - const dictate = container?.querySelector('[aria-label="Dictate"]'); - if (dictate === null || dictate === undefined) { - throw new Error("dictate button not found"); - } - - act(() => { - dictate.click(); - }); - await settle(); - - const rec = recognitions.at(-1); - if (rec === undefined) { - throw new Error("speech recognition was not constructed"); - } - rec.resultOnStop = [{ isFinal: true, transcript: "world" }]; - - act(() => { - dictate.click(); - }); - await settle(); - - expect(textarea().value).toBe("hello world"); - expect(rec.started).toBe(false); - }); - - test("send while listening does not restore the draft from a late final result", async () => { - installSpeechRecognition(); - const sent: ComposerSendPayload[] = []; - container = document.createElement("div"); - document.body.appendChild(container); - root = createRoot(container); - act(() => { - root?.render( - createElement(Composer, { - agents: [], - onSend: (payload) => { - sent.push(payload); - return Promise.resolve(true); - }, - }), - ); - }); - typeInto(textarea(), "hello"); - textarea().setSelectionRange(5, 5); - await settle(); - - const dictate = container?.querySelector('[aria-label="Dictate"]'); - if (dictate === null || dictate === undefined) { - throw new Error("dictate button not found"); - } - - act(() => { - dictate.click(); - }); - await settle(); - - const rec = recognitions.at(-1); - if (rec === undefined) { - throw new Error("speech recognition was not constructed"); - } - rec.resultOnStop = [{ isFinal: true, transcript: "late words" }]; - - act(() => { - sendButton().click(); - }); - await settle(); - - act(() => { - rec.emit([{ isFinal: true, transcript: "late words" }]); - }); - await settle(); - - expect(textarea().value).toBe(""); - expect(sent).toEqual([{ text: "hello", attachments: [] }]); - }); - - test("Enter while listening does not restore the draft from a late final result", async () => { - installSpeechRecognition(); - const sent: ComposerSendPayload[] = []; - container = document.createElement("div"); - document.body.appendChild(container); - root = createRoot(container); - act(() => { - root?.render( - createElement(Composer, { - agents: [], - onSend: (payload) => { - sent.push(payload); - return Promise.resolve(true); - }, - }), - ); - }); - typeInto(textarea(), "hello"); - textarea().setSelectionRange(5, 5); - await settle(); - - const dictate = container?.querySelector('[aria-label="Dictate"]'); - if (dictate === null || dictate === undefined) { - throw new Error("dictate button not found"); - } - - act(() => { - dictate.click(); - }); - await settle(); - - const rec = recognitions.at(-1); - if (rec === undefined) { - throw new Error("speech recognition was not constructed"); - } - rec.resultOnStop = [{ isFinal: true, transcript: "late words" }]; - - act(() => { - textarea().dispatchEvent( - new KeyboardEvent("keydown", { - key: "Enter", - bubbles: true, - cancelable: true, - }), - ); - }); - await settle(); - - act(() => { - rec.emit([{ isFinal: true, transcript: "late words" }]); - }); - await settle(); - - expect(textarea().value).toBe(""); - expect(sent).toEqual([{ text: "hello", attachments: [] }]); - }); - - test("stop then send does not restore the draft from a deferred final result", async () => { - installSpeechRecognition(); - const sent: ComposerSendPayload[] = []; - container = document.createElement("div"); - document.body.appendChild(container); - root = createRoot(container); - act(() => { - root?.render( - createElement(Composer, { - agents: [], - onSend: (payload) => { - sent.push(payload); - return Promise.resolve(true); - }, - }), - ); - }); - typeInto(textarea(), "hello"); - textarea().setSelectionRange(5, 5); - await settle(); - - const dictate = container?.querySelector('[aria-label="Dictate"]'); - if (dictate === null || dictate === undefined) { - throw new Error("dictate button not found"); - } - - act(() => { - dictate.click(); - }); - await settle(); - - const rec = recognitions.at(-1); - if (rec === undefined) { - throw new Error("speech recognition was not constructed"); - } - - act(() => { - dictate.click(); - }); - await settle(); - - expect(textarea().value).toBe("hello"); - - act(() => { - sendButton().click(); - }); - await settle(); - - act(() => { - rec.emit([{ isFinal: true, transcript: "late words" }]); - }); - await settle(); - - expect(textarea().value).toBe(""); - expect(sent).toEqual([{ text: "hello", attachments: [] }]); - }); - - test("does not report aborted or no-speech recognition errors", async () => { - installSpeechRecognition(); - const report = spyOn(errorSink, "reportError"); - mount(() => Promise.resolve(true)); - - const dictate = container?.querySelector('[aria-label="Dictate"]'); - if (dictate === null || dictate === undefined) { - throw new Error("dictate button not found"); - } - - act(() => { - dictate.click(); - }); - await settle(); - - const rec = recognitions.at(-1); - if (rec === undefined) { - throw new Error("speech recognition was not constructed"); - } - - act(() => { - rec.onerror?.({ error: "no-speech" }); - rec.onerror?.({ error: "aborted" }); - }); - await settle(); - - expect(report).not.toHaveBeenCalled(); - - act(() => { - rec.onerror?.({ error: "network" }); - }); - await settle(); - - expect(report).toHaveBeenCalled(); - report.mockRestore(); - }); -}); diff --git a/apps/web/test/library-artifacts.test.ts b/apps/web/test/library-artifacts.test.ts deleted file mode 100644 index 6dfe20171a..0000000000 --- a/apps/web/test/library-artifacts.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { afterEach, describe, expect, test } from "bun:test"; - -import { UnauthenticatedError } from "@/lib/api-query"; -import { - artifactListRowToSummary, - isArtifactsUnavailableStatus, - mapArtifactListToSummaries, - uploadArtifactFiles, - type ArtifactListRow, -} from "../src/shell/library-artifacts"; - -const realFetch = globalThis.fetch; - -afterEach(() => { - globalThis.fetch = realFetch; -}); - -const sample: ArtifactListRow = { - id: "art_1", - kind: "file", - title: "Quarterly report.pdf", - ownerName: "Ada", - createdAt: "2026-01-01T00:00:00.000Z", - updatedAt: "2026-01-02T00:00:00.000Z", -}; - -describe("library-artifacts", () => { - test("maps list rows onto ArtifactSummary without inventing fields", () => { - expect(artifactListRowToSummary(sample)).toEqual({ - id: "art_1", - title: "Quarterly report.pdf", - kind: "file", - ownerName: "Ada", - createdAt: "2026-01-01T00:00:00.000Z", - updatedAt: "2026-01-02T00:00:00.000Z", - }); - }); - - test("maps a list without inventing or dropping rows", () => { - const second: ArtifactListRow = { - ...sample, - id: "art_2", - kind: "document", - title: "notes.txt", - ownerName: null, - }; - const mapped = mapArtifactListToSummaries([sample, second]); - expect(mapped).toHaveLength(2); - expect(mapped[0]?.id).toBe("art_1"); - expect(mapped[1]?.kind).toBe("document"); - expect(mapped[1]?.ownerName).toBeNull(); - }); - - test("detects the unconfigured-plane status", () => { - expect(isArtifactsUnavailableStatus(503)).toBe(true); - expect(isArtifactsUnavailableStatus(500)).toBe(false); - expect(isArtifactsUnavailableStatus(undefined)).toBe(false); - }); - - test("uploadArtifactFiles throws an UnauthenticatedError on 401", async () => { - globalThis.fetch = ((_input: RequestInfo | URL, _init?: RequestInit) => - Promise.resolve(new Response(null, { status: 401 }))) as typeof fetch; - await expect(uploadArtifactFiles("tnt_1", [new File(["x"], "x.txt")])).rejects.toBeInstanceOf( - UnauthenticatedError, - ); - }); -}); diff --git a/apps/web/test/query-client.test.ts b/apps/web/test/query-client.test.ts deleted file mode 100644 index 89540cd3cf..0000000000 --- a/apps/web/test/query-client.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -// Query-key helpers — pure unit coverage so the TanStack cutover does not -// depend only on page-level smoke. The APIQuery adapter itself -// (`toAPIQuery`) is covered in `@/lib/api-query`. - -import { describe, expect, test } from "bun:test"; - -import { meKeys, pathToQueryKey, tenantKeys } from "../src/query-client"; - -describe("pathToQueryKey", () => { - test("maps identity-scoped hub paths onto meKeys", () => { - expect(pathToQueryKey("/api/me")).toEqual(meKeys.profile); - expect(pathToQueryKey("/api/me/principals")).toEqual(meKeys.principals); - }); - - test("maps the pending-approvals list onto a tenant-scoped key", () => { - expect(pathToQueryKey("/api/tenants/tnt_1/approvals")).toEqual( - tenantKeys.pendingApprovals("tnt_1"), - ); - }); - - test("maps tenant assets onto a tenant-scoped key", () => { - expect(pathToQueryKey("/api/tenants/tnt_1/assets")).toEqual(tenantKeys.assets("tnt_1")); - }); - - test("falls back to a path key for unknown routes", () => { - expect(pathToQueryKey("/api/mystery")).toEqual(["path", "/api/mystery"]); - }); -}); - -describe("tenantKeys.routineActivity", () => { - // the sidebar routine-activity seam keeps a shared cache key that - // names the seam — not the deleted `/top-level-runs` route — so both - // mounts subscribe to one entry and a future native fires equivalent has - // a key to rewire. - test("keys routine activity per tenant without the deleted route name", () => { - expect(tenantKeys.routineActivity("tnt_1")).toEqual(["tenant", "tnt_1", "routine-activity"]); - expect((tenantKeys.routineActivity("tnt_1") as readonly unknown[]).join("/")).not.toContain( - "top-level-runs", - ); - }); -}); diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index c6fb970cbd..2f570287f1 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "../../tsconfig.base.json", - "include": ["src", "vite.config.ts", "test"], + "include": ["src", "vite.config.ts"], "compilerOptions": { "jsx": "react-jsx", "lib": ["ESNext", "DOM", "DOM.Iterable"], From 9eed38cbf6300d73ba11b87c8bc90e07b1aa7abe Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 17 Sep 2026 18:19:02 -0700 Subject: [PATCH 2/9] test: colocate myra unit tests, move root/hub helpers and DB suites under e2e/ (CL-8370) --- agents/myra/package.json | 2 ++ agents/myra/{test => src}/boundary.test.ts | 2 +- agents/myra/{test => src}/definition.test.ts | 2 +- agents/myra/tsconfig.json | 4 +++- apps/hub/tsconfig.json | 12 +++++++--- bun.lock | 22 ++----------------- .../hub/test => e2e/hub}/composition.test.ts | 6 ++--- .../test => e2e/hub}/mailbox-persist.test.ts | 4 ++-- {test => e2e/lib}/database-url.ts | 0 {test => e2e/lib}/db-gate.ts | 0 {test => e2e/lib}/disposable-hub-data-dir.ts | 0 {test => e2e}/tsconfig.json | 2 +- package.json | 4 +++- packages/agent-directory/tsconfig.json | 8 +++++-- packages/cron/tsconfig.json | 4 +++- packages/inference-catalog/package.json | 2 ++ .../{test => src}/fixtures.ts | 0 packages/inference-catalog/src/price.test.ts | 2 +- .../src/resolve-chain.test.ts | 2 +- packages/inference-catalog/tsconfig.json | 2 +- packages/workflows/tsconfig.json | 4 +++- scripts/typecheck.ts | 6 ++--- 22 files changed, 47 insertions(+), 43 deletions(-) rename agents/myra/{test => src}/boundary.test.ts (95%) rename agents/myra/{test => src}/definition.test.ts (99%) rename {apps/hub/test => e2e/hub}/composition.test.ts (92%) rename {apps/hub/test => e2e/hub}/mailbox-persist.test.ts (98%) rename {test => e2e/lib}/database-url.ts (100%) rename {test => e2e/lib}/db-gate.ts (100%) rename {test => e2e/lib}/disposable-hub-data-dir.ts (100%) rename {test => e2e}/tsconfig.json (81%) rename packages/inference-catalog/{test => src}/fixtures.ts (100%) diff --git a/agents/myra/package.json b/agents/myra/package.json index 4d817c83e5..fa1509fcca 100644 --- a/agents/myra/package.json +++ b/agents/myra/package.json @@ -10,6 +10,8 @@ }, "files": [ "src", + "!src/**/*.test.ts", + "!src/**/*.test.tsx", "LICENSE", "README.md" ], diff --git a/agents/myra/test/boundary.test.ts b/agents/myra/src/boundary.test.ts similarity index 95% rename from agents/myra/test/boundary.test.ts rename to agents/myra/src/boundary.test.ts index 7cd3621b2f..cb7c5bcb48 100644 --- a/agents/myra/test/boundary.test.ts +++ b/agents/myra/src/boundary.test.ts @@ -35,7 +35,7 @@ test("shipped sources import only published platform packages", async () => { const importPattern = /from\s+"([^"]+)"/g; const violations: string[] = []; for (const file of await shippedFiles()) { - if (!file.endsWith(".ts")) continue; + if (!file.endsWith(".ts") || file.endsWith(".test.ts")) continue; const content = await readFile(file, "utf8"); for (const match of content.matchAll(importPattern)) { const specifier = match[1] ?? ""; diff --git a/agents/myra/test/definition.test.ts b/agents/myra/src/definition.test.ts similarity index 99% rename from agents/myra/test/definition.test.ts rename to agents/myra/src/definition.test.ts index ffb44ec86d..164d72aa68 100644 --- a/agents/myra/test/definition.test.ts +++ b/agents/myra/src/definition.test.ts @@ -12,7 +12,7 @@ import { ASSISTANT_WORKFLOW_ID, buildAssistantWorkflow, serializeAssistantWorkflow, -} from "../src/index"; +} from "./index"; const INPUT = { triggerAddress: "ins_dep000000000000@example.test", diff --git a/agents/myra/tsconfig.json b/agents/myra/tsconfig.json index a8c2b9966d..5e0fa777c0 100644 --- a/agents/myra/tsconfig.json +++ b/agents/myra/tsconfig.json @@ -10,6 +10,8 @@ "tsBuildInfoFile": "tsconfig.tsbuildinfo", "rootDir": "../.." }, - "include": ["src", "test"], + "include": [ + "src" + ], "exclude": [] } diff --git a/apps/hub/tsconfig.json b/apps/hub/tsconfig.json index cd2c17eb40..0938e8e6d6 100644 --- a/apps/hub/tsconfig.json +++ b/apps/hub/tsconfig.json @@ -1,9 +1,15 @@ { "extends": "../../tsconfig.base.json", - "include": ["src", "test"], + "include": [ + "src" + ], "compilerOptions": { - "types": ["bun"], - "customConditions": ["bun"], + "types": [ + "bun" + ], + "customConditions": [ + "bun" + ], "noEmit": true } } diff --git a/bun.lock b/bun.lock index 4255bca669..9d519d85ce 100644 --- a/bun.lock +++ b/bun.lock @@ -15,11 +15,13 @@ "@corbits/xai-provider": "github:corbitsdev/corbits-xai-provider#9f2d4bac40ea075df092fef807404a13c638cf14", "@intx/crypto": "workspace:*", "@intx/db": "workspace:*", + "@intx/hub-sessions": "workspace:*", "@intx/log": "workspace:*", "@intx/mime": "0.3.0", "@intx/types": "0.3.0", "@types/bun": "1.3.14", "@typescript/native-preview": "7.0.0-dev.20260707.2", + "drizzle-orm": "catalog:", "jiti": "^2.7.0", "oxfmt": "0.68.0", "oxlint": "1.83.0", @@ -1980,8 +1982,6 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@corbits/inference-catalog/@corbits/ollama-adapter": ["@corbits/ollama-adapter@github:corbitsdev/corbits-ollama-adapter#0387eb1", { "dependencies": { "arktype": "2.2.3" }, "peerDependencies": { "@intx/inference": ">=0.3.0", "@intx/types": ">=0.3.0" } }, "corbitsdev-corbits-ollama-adapter-0387eb1", "sha512-O1dl5lSwWzNGJibQLlpYDFKJZWpstgHX519bl8oe1ALL/KHszT/tBDlpsD9jN3hmQD3jV2L1jNwvOwLOBLtpyw=="], - "@corbits/mailbox/hono-openapi": ["hono-openapi@1.3.1", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.11.2", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-NLVeVkhKZ3drmQNEIPac8HX8Y54uf1hJAgIM/7MfDsaeVVmB+QILWQxx5x3R3NvRHgedcbEbOCGY2uR7WQYyMw=="], "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], @@ -2000,16 +2000,8 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@workbench/hub/@corbits/artifacts": ["@corbits/artifacts@github:corbitsdev/corbits-artifacts#5309a15", { "dependencies": { "@hono/standard-validator": "^0.2.3" }, "peerDependencies": { "@intx/hub-api": "^0.3.0", "@intx/types": "^0.3.0", "arktype": "^2.1.29", "drizzle-orm": "^0.45.2", "hono": "^4.12.32", "hono-openapi": "^1.2.0", "postgres": "^3.4.9" } }, "corbitsdev-corbits-artifacts-5309a15", "sha512-zGHkq++juclUrOd0O8/Z5ziTldmJZ6gqyH/c9cs5S10UPU4gqn8CHwq174SiJXY0ujJ3cNpJXnrr+0fcqlfQ1w=="], - - "@workbench/hub/@corbits/codex-provider": ["@corbits/codex-provider@github:corbitsdev/corbits-codex-provider#a1f475f", { "dependencies": { "@corbits/oauth-core": "github:corbitsdev/corbits-oauth-core", "@corbits/openai-responses": "github:corbitsdev/corbits-openai-responses", "arktype": "2.2.3" }, "peerDependencies": { "@intx/inference": ">=0.3.0", "@intx/types": ">=0.3.0" } }, "corbitsdev-corbits-codex-provider-a1f475f", "sha512-D6d0lblWx/4KudfLwQoQGrLB8GKRLP/HVp1cx08L2/hOGZAgH6txLPpeEtBqjl+R520qXSSPfM0kDC+3XNaAfw=="], - - "@workbench/hub/@corbits/mailbox": ["@corbits/mailbox@github:corbitsdev/corbits-mailbox#e85b81e", { "dependencies": { "@hono/standard-validator": "0.2.3", "@intx/crypto": "0.3.0", "@standard-community/standard-json": "0.3.5", "@standard-community/standard-openapi": "0.2.9", "arktype": "2.1.29", "hono-openapi": "1.3.1" }, "peerDependencies": { "@intx/log": "^0.2.2", "drizzle-orm": "^0.45.2", "hono": "^4.12.0", "postgres": "^3.4.0" } }, "corbitsdev-corbits-mailbox-e85b81e", "sha512-ATPP/pll1sYG1DLKV1HvtboK8q1tFkqayQqRXdUmRUIsFJx5wqzd8YA1XELjXzLu1P2f2uCMe2CjsJHGv6E90Q=="], - "@workbench/hub/@corbits/memory": ["@corbits/memory@github:corbitsdev/corbits-memory#e74da20", { "dependencies": { "@intx/agent": "0.2.2", "@intx/authz": "0.2.2", "@intx/hub-api": "0.2.2", "@intx/log": "0.2.2", "@intx/workflow": "0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.1", "hono": "^4.9.0", "hono-openapi": "^1.3.1", "postgres": "^3.4.7" } }, "corbitsdev-corbits-memory-e74da20", "sha512-/uA9xTcF7XwMXgnR8OZamierzkLG4g4mhw3u9Fl3Uj6ait8pH3wcoQ6V32on1nMdaqtPRdYVFnxof5O+4ZacGA=="], - "@workbench/hub/@corbits/xai-provider": ["@corbits/xai-provider@github:corbitsdev/corbits-xai-provider#9f2d4ba", { "dependencies": { "@corbits/oauth-core": "github:corbitsdev/corbits-oauth-core", "@corbits/openai-responses": "github:corbitsdev/corbits-openai-responses", "arktype": "2.2.3" }, "peerDependencies": { "@intx/inference": ">=0.3.0", "@intx/types": ">=0.3.0" } }, "corbitsdev-corbits-xai-provider-9f2d4ba", "sha512-ZjvKxGcp+QzCh8zeXHg7kNbtkpR/itZkmQAcdG1aOAOjHaNwfGVpkPc/c/XtwPNsQyaDjF8W9CHb4rH1xI2uCA=="], - "better-call/@better-auth/utils": ["@better-auth/utils@0.5.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA=="], "body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], @@ -2100,16 +2092,6 @@ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], - "@workbench/hub/@corbits/codex-provider/@corbits/oauth-core": ["@corbits/oauth-core@github:corbitsdev/corbits-oauth-core#1b7f9fb", { "dependencies": { "arktype": "2.2.3" } }, "corbitsdev-corbits-oauth-core-1b7f9fb", "sha512-PscyiDAl46umTM44NQPN1q5+PsGUV53Rxr4Wz/Ofz+k9LOOz6FBKamRFtWR8YWdKJt9I8sPOLOyEcMNNN2i+YQ=="], - - "@workbench/hub/@corbits/codex-provider/@corbits/openai-responses": ["@corbits/openai-responses@github:corbitsdev/corbits-openai-responses#7efe000", { "dependencies": { "arktype": "2.2.3" }, "peerDependencies": { "@intx/inference": ">=0.3.0", "@intx/types": ">=0.3.0" } }, "corbitsdev-corbits-openai-responses-7efe000", "sha512-pH6C8T1WDJjqM+YbnQpofd7HgP5krEj48JCCQK16KI98a6kaz+gffpgdOdQpU3aNVZnFlyADXod1Z4tqUG/3ww=="], - - "@workbench/hub/@corbits/mailbox/hono-openapi": ["hono-openapi@1.3.1", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.11.2", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-NLVeVkhKZ3drmQNEIPac8HX8Y54uf1hJAgIM/7MfDsaeVVmB+QILWQxx5x3R3NvRHgedcbEbOCGY2uR7WQYyMw=="], - - "@workbench/hub/@corbits/xai-provider/@corbits/oauth-core": ["@corbits/oauth-core@github:corbitsdev/corbits-oauth-core#1b7f9fb", { "dependencies": { "arktype": "2.2.3" } }, "corbitsdev-corbits-oauth-core-1b7f9fb", "sha512-PscyiDAl46umTM44NQPN1q5+PsGUV53Rxr4Wz/Ofz+k9LOOz6FBKamRFtWR8YWdKJt9I8sPOLOyEcMNNN2i+YQ=="], - - "@workbench/hub/@corbits/xai-provider/@corbits/openai-responses": ["@corbits/openai-responses@github:corbitsdev/corbits-openai-responses#7efe000", { "dependencies": { "arktype": "2.2.3" }, "peerDependencies": { "@intx/inference": ">=0.3.0", "@intx/types": ">=0.3.0" } }, "corbitsdev-corbits-openai-responses-7efe000", "sha512-pH6C8T1WDJjqM+YbnQpofd7HgP5krEj48JCCQK16KI98a6kaz+gffpgdOdQpU3aNVZnFlyADXod1Z4tqUG/3ww=="], - "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], diff --git a/apps/hub/test/composition.test.ts b/e2e/hub/composition.test.ts similarity index 92% rename from apps/hub/test/composition.test.ts rename to e2e/hub/composition.test.ts index d332f6c74b..a4dc7b2dba 100644 --- a/apps/hub/test/composition.test.ts +++ b/e2e/hub/composition.test.ts @@ -9,7 +9,7 @@ import { afterAll, expect, test } from "bun:test"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; -import { dbGate } from "../../../test/db-gate"; +import { dbGate } from "../lib/db-gate"; const databaseUrl = process.env["DATABASE_URL"] ?? ""; const describeIfDb = dbGate(databaseUrl, import.meta.path); @@ -41,7 +41,7 @@ afterAll(async () => { describeIfDb("boot", () => { test("serves stock platform routes", async () => { - const { createHubServer } = await import("../src/server"); + const { createHubServer } = await import("../../apps/hub/src/server"); const opts = await createHubServer(); const status = await opts.fetch(new Request("http://localhost/status")); @@ -52,7 +52,7 @@ describeIfDb("boot", () => { }); test("a Corbits mount (mailbox) sits inside the native tenant middleware", async () => { - const { createHubServer } = await import("../src/server"); + const { createHubServer } = await import("../../apps/hub/src/server"); const opts = await createHubServer(); // Anonymous request to a Corbits-mounted route: the platform's tenant diff --git a/apps/hub/test/mailbox-persist.test.ts b/e2e/hub/mailbox-persist.test.ts similarity index 98% rename from apps/hub/test/mailbox-persist.test.ts rename to e2e/hub/mailbox-persist.test.ts index 036b7a31ba..dd296f5a05 100644 --- a/apps/hub/test/mailbox-persist.test.ts +++ b/e2e/hub/mailbox-persist.test.ts @@ -20,8 +20,8 @@ import { type MailboxDb, } from "@corbits/mailbox"; import { createHubSessionLookups, type AgentRepoStore } from "@intx/hub-sessions"; -import { createHubMailboxAuthorizeSender } from "../src/mailbox-persist"; -import { dbGate } from "../../../test/db-gate"; +import { createHubMailboxAuthorizeSender } from "../../apps/hub/src/mailbox-persist"; +import { dbGate } from "../lib/db-gate"; const databaseUrl = process.env["DATABASE_URL"] ?? ""; const describeIfDb = dbGate(databaseUrl, import.meta.path); diff --git a/test/database-url.ts b/e2e/lib/database-url.ts similarity index 100% rename from test/database-url.ts rename to e2e/lib/database-url.ts diff --git a/test/db-gate.ts b/e2e/lib/db-gate.ts similarity index 100% rename from test/db-gate.ts rename to e2e/lib/db-gate.ts diff --git a/test/disposable-hub-data-dir.ts b/e2e/lib/disposable-hub-data-dir.ts similarity index 100% rename from test/disposable-hub-data-dir.ts rename to e2e/lib/disposable-hub-data-dir.ts diff --git a/test/tsconfig.json b/e2e/tsconfig.json similarity index 81% rename from test/tsconfig.json rename to e2e/tsconfig.json index 3ec7530c9f..a9d2d8b3bb 100644 --- a/test/tsconfig.json +++ b/e2e/tsconfig.json @@ -4,5 +4,5 @@ "types": ["bun"], "noEmit": true }, - "include": ["*.ts"] + "include": ["**/*.ts"] } diff --git a/package.json b/package.json index c393dcdfb6..c02e21b0aa 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,9 @@ "oxlint": "1.83.0", "oxfmt": "0.68.0", "puppeteer-core": "^25.7.0", - "typescript": "6.0.3" + "typescript": "6.0.3", + "@intx/hub-sessions": "workspace:*", + "drizzle-orm": "catalog:" }, "engines": { "bun": ">=1.2.0" diff --git a/packages/agent-directory/tsconfig.json b/packages/agent-directory/tsconfig.json index d7611c1229..eb6192f1fb 100644 --- a/packages/agent-directory/tsconfig.json +++ b/packages/agent-directory/tsconfig.json @@ -1,8 +1,12 @@ { "extends": "../../tsconfig.base.json", - "include": ["src", "test"], + "include": [ + "src" + ], "compilerOptions": { - "types": ["bun"], + "types": [ + "bun" + ], "noEmit": true } } diff --git a/packages/cron/tsconfig.json b/packages/cron/tsconfig.json index a8c2b9966d..5e0fa777c0 100644 --- a/packages/cron/tsconfig.json +++ b/packages/cron/tsconfig.json @@ -10,6 +10,8 @@ "tsBuildInfoFile": "tsconfig.tsbuildinfo", "rootDir": "../.." }, - "include": ["src", "test"], + "include": [ + "src" + ], "exclude": [] } diff --git a/packages/inference-catalog/package.json b/packages/inference-catalog/package.json index 1281fe920d..93904a0f7d 100644 --- a/packages/inference-catalog/package.json +++ b/packages/inference-catalog/package.json @@ -10,6 +10,8 @@ }, "files": [ "src", + "!src/**/*.test.ts", + "!src/**/*.test.tsx", "LICENSE", "README.md" ], diff --git a/packages/inference-catalog/test/fixtures.ts b/packages/inference-catalog/src/fixtures.ts similarity index 100% rename from packages/inference-catalog/test/fixtures.ts rename to packages/inference-catalog/src/fixtures.ts diff --git a/packages/inference-catalog/src/price.test.ts b/packages/inference-catalog/src/price.test.ts index 010b5ba3d6..0c09c7b4b5 100644 --- a/packages/inference-catalog/src/price.test.ts +++ b/packages/inference-catalog/src/price.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; -import { pricing } from "../test/fixtures"; +import { pricing } from "./fixtures"; import { groupPricingByOffering, perMTok, priceForOffering, referenceCostUsd } from "./price"; describe("perMTok", () => { diff --git a/packages/inference-catalog/src/resolve-chain.test.ts b/packages/inference-catalog/src/resolve-chain.test.ts index 728bffc349..4223a1a0af 100644 --- a/packages/inference-catalog/src/resolve-chain.test.ts +++ b/packages/inference-catalog/src/resolve-chain.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test"; import type { ProviderPreference } from "@intx/types"; -import { offering, pricing } from "../test/fixtures"; +import { offering, pricing } from "./fixtures"; import { EMPTY_POLICY, type BenchModelPolicy } from "./policy"; import { chainToModelRequirements, diff --git a/packages/inference-catalog/tsconfig.json b/packages/inference-catalog/tsconfig.json index a8c2b9966d..e125e90614 100644 --- a/packages/inference-catalog/tsconfig.json +++ b/packages/inference-catalog/tsconfig.json @@ -10,6 +10,6 @@ "tsBuildInfoFile": "tsconfig.tsbuildinfo", "rootDir": "../.." }, - "include": ["src", "test"], + "include": ["src"], "exclude": [] } diff --git a/packages/workflows/tsconfig.json b/packages/workflows/tsconfig.json index 0896db0dac..509234b1d5 100644 --- a/packages/workflows/tsconfig.json +++ b/packages/workflows/tsconfig.json @@ -10,7 +10,9 @@ "tsBuildInfoFile": "tsconfig.tsbuildinfo", "rootDir": "../.." }, - "include": ["src", "test"], + "include": [ + "src" + ], "exclude": [], "references": [ { diff --git a/scripts/typecheck.ts b/scripts/typecheck.ts index 9d57710cdd..e8875b2f13 100644 --- a/scripts/typecheck.ts +++ b/scripts/typecheck.ts @@ -79,10 +79,10 @@ const projectCommands = projectPaths.map((path) => ["bunx", "tsgo", "-p", path, const projectsOk = await runInParallel(projectCommands); -const [rootCode, hubDataDirCode] = await Promise.all([ +const [rootCode, e2eCode] = await Promise.all([ run(["bunx", "tsgo", "-p", "tsconfig.json", "--noEmit"]), - run(["bunx", "tsgo", "-p", "test/tsconfig.json", "--noEmit"]), + run(["bunx", "tsgo", "-p", "e2e/tsconfig.json", "--noEmit"]), ]); -if (!projectsOk || rootCode !== 0 || hubDataDirCode !== 0) { +if (!projectsOk || rootCode !== 0 || e2eCode !== 0) { process.exit(1); } From 2a075e4340f83b7c9bb55dd86a18ccba25d939c0 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 17 Sep 2026 18:20:42 -0700 Subject: [PATCH 3/9] test: move cron/granola/linear DB suites under e2e/, colocate linear regression test (CL-8370) --- bun.lock | 3 +++ .../cron/test => e2e/cron}/ticker.drizzle.test.ts | 10 +++++----- package.json | 2 ++ 3 files changed, 10 insertions(+), 5 deletions(-) rename {packages/cron/test => e2e/cron}/ticker.drizzle.test.ts (93%) diff --git a/bun.lock b/bun.lock index 9d519d85ce..2ad555eb70 100644 --- a/bun.lock +++ b/bun.lock @@ -10,11 +10,14 @@ "devDependencies": { "@corbits/artifacts": "github:corbitsdev/corbits-artifacts#5309a15d4aa504f132c54b4da08bb672b4903873", "@corbits/codex-provider": "github:corbitsdev/corbits-codex-provider#a1f475f341261c05fa8dade0c82f1246c175b5b3", + "@corbits/credential-providers": "workspace:*", "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#e85b81e4a2e0cc318dd33a4f0f92337b6fac7f38", "@corbits/ollama-adapter": "github:corbitsdev/corbits-ollama-adapter#0387eb17bee5ac87530e9c48bb3bffd9a6998ded", "@corbits/xai-provider": "github:corbitsdev/corbits-xai-provider#9f2d4bac40ea075df092fef807404a13c638cf14", + "@intx/authz": "0.3.0", "@intx/crypto": "workspace:*", "@intx/db": "workspace:*", + "@intx/harness": "workspace:*", "@intx/hub-sessions": "workspace:*", "@intx/log": "workspace:*", "@intx/mime": "0.3.0", diff --git a/packages/cron/test/ticker.drizzle.test.ts b/e2e/cron/ticker.drizzle.test.ts similarity index 93% rename from packages/cron/test/ticker.drizzle.test.ts rename to e2e/cron/ticker.drizzle.test.ts index ca49ad917c..b21cfb56a1 100644 --- a/packages/cron/test/ticker.drizzle.test.ts +++ b/e2e/cron/ticker.drizzle.test.ts @@ -8,11 +8,11 @@ import { randomUUID } from "node:crypto"; import { createDB, dropSchema, runMigrations, schema } from "@intx/db"; import { eq } from "drizzle-orm"; -import { dbTargetFromUrl } from "../../../scripts/db-setup"; -import { e2eDatabaseUrl } from "../../../test/database-url"; -import { dbGate } from "../../../test/db-gate"; -import { applyCronMigrations, cronScheduleTable } from "../src/schema"; -import { createCronTicker } from "../src/ticker"; +import { dbTargetFromUrl } from "../../scripts/db-setup"; +import { e2eDatabaseUrl } from "../lib/database-url"; +import { dbGate } from "../lib/db-gate"; +import { applyCronMigrations, cronScheduleTable } from "../../packages/cron/src/schema"; +import { createCronTicker } from "../../packages/cron/src/ticker"; const databaseUrl = e2eDatabaseUrl(); const describeIfDb = dbGate(databaseUrl, import.meta.path); diff --git a/package.json b/package.json index c02e21b0aa..54c0409a52 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,8 @@ "puppeteer-core": "^25.7.0", "typescript": "6.0.3", "@intx/hub-sessions": "workspace:*", + "@intx/authz": "0.3.0", + "@intx/harness": "workspace:*", "drizzle-orm": "catalog:" }, "engines": { From 58d87729008adf4d18f51b85fc5ee3ca6203025b Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 17 Sep 2026 18:22:00 -0700 Subject: [PATCH 4/9] test(webhook-triggers): colocate unit tests, move DB suites under e2e/ (CL-8370) --- bun.lock | 1 + package.json | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/bun.lock b/bun.lock index 2ad555eb70..bf2e54e0f4 100644 --- a/bun.lock +++ b/bun.lock @@ -28,6 +28,7 @@ "jiti": "^2.7.0", "oxfmt": "0.68.0", "oxlint": "1.83.0", + "postgres": "catalog:", "puppeteer-core": "^25.7.0", "typescript": "6.0.3", }, diff --git a/package.json b/package.json index 54c0409a52..2826b09551 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,8 @@ "@intx/hub-sessions": "workspace:*", "@intx/authz": "0.3.0", "@intx/harness": "workspace:*", - "drizzle-orm": "catalog:" + "drizzle-orm": "catalog:", + "postgres": "catalog:" }, "engines": { "bun": ">=1.2.0" From a7b3e9d96fd49c39a4132c6a2405dc9aa79ea1b9 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 17 Sep 2026 18:25:26 -0700 Subject: [PATCH 5/9] test(agent-directory,workflows): colocate unit tests, move DB/integration suites under e2e/ (CL-8370) --- bun.lock | 42 ++++++++++++++++++- .../routes.integration.test.ts | 12 +++--- .../tool-package-version.drizzle.test.ts | 10 ++--- .../visible-definitions.drizzle.test.ts | 8 ++-- .../workflows}/agent-session.test.ts | 7 +++- package.json | 3 ++ .../agent-definition-draft-routes.test.ts | 6 +-- .../agent-workflow-definition.test.ts} | 2 +- .../{test => src}/routes.test.ts | 14 +++---- .../{test => src}/source-tree.ts | 4 +- .../{test => src}/validation.test.ts | 2 +- .../workflow-capability-routes.test.ts | 12 +++--- .../workflow-create-routes.test.ts | 10 ++--- .../workflow-skill-pin-routes.test.ts | 8 ++-- 14 files changed, 92 insertions(+), 48 deletions(-) rename {packages/agent-directory/test => e2e/agent-directory}/routes.integration.test.ts (95%) rename {packages/agent-directory/test => e2e/agent-directory}/tool-package-version.drizzle.test.ts (94%) rename {packages/agent-directory/test => e2e/agent-directory}/visible-definitions.drizzle.test.ts (96%) rename {packages/workflows/test => e2e/workflows}/agent-session.test.ts (98%) rename packages/agent-directory/{test => src}/agent-definition-draft-routes.test.ts (95%) rename packages/agent-directory/{test/agent-workflow.test.ts => src/agent-workflow-definition.test.ts} (99%) rename packages/agent-directory/{test => src}/routes.test.ts (99%) rename packages/agent-directory/{test => src}/source-tree.ts (96%) rename packages/agent-directory/{test => src}/validation.test.ts (99%) rename packages/agent-directory/{test => src}/workflow-capability-routes.test.ts (97%) rename packages/agent-directory/{test => src}/workflow-create-routes.test.ts (98%) rename packages/agent-directory/{test => src}/workflow-skill-pin-routes.test.ts (98%) diff --git a/bun.lock b/bun.lock index bf2e54e0f4..bafe94379a 100644 --- a/bun.lock +++ b/bun.lock @@ -13,11 +13,14 @@ "@corbits/credential-providers": "workspace:*", "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#e85b81e4a2e0cc318dd33a4f0f92337b6fac7f38", "@corbits/ollama-adapter": "github:corbitsdev/corbits-ollama-adapter#0387eb17bee5ac87530e9c48bb3bffd9a6998ded", + "@corbits/tool-registry-publish": "workspace:*", + "@corbits/webhook-triggers": "workspace:*", "@corbits/xai-provider": "github:corbitsdev/corbits-xai-provider#9f2d4bac40ea075df092fef807404a13c638cf14", "@intx/authz": "0.3.0", "@intx/crypto": "workspace:*", "@intx/db": "workspace:*", "@intx/harness": "workspace:*", + "@intx/hub-common": "0.3.0", "@intx/hub-sessions": "workspace:*", "@intx/log": "workspace:*", "@intx/mime": "0.3.0", @@ -25,6 +28,7 @@ "@types/bun": "1.3.14", "@typescript/native-preview": "7.0.0-dev.20260707.2", "drizzle-orm": "catalog:", + "hono": "catalog:", "jiti": "^2.7.0", "oxfmt": "0.68.0", "oxlint": "1.83.0", @@ -67,7 +71,7 @@ "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#e85b81e4a2e0cc318dd33a4f0f92337b6fac7f38", "@corbits/memory": "github:corbitsdev/corbits-memory#e74da20f148a302dff5400915fe504ee2395e913", "@corbits/url-path": "workspace:*", - "@corbits/webhooks": "github:corbitsdev/webhooks#570bd52688920c0ce018a8fbcd046199fa4125dd", + "@corbits/webhook-triggers": "workspace:*", "@corbits/workflows": "workspace:*", "@corbits/xai-provider": "github:corbitsdev/corbits-xai-provider#9f2d4bac40ea075df092fef807404a13c638cf14", "@intx/authz": "0.3.0", @@ -213,6 +217,14 @@ "typescript": "catalog:", }, }, + "packages/collections": { + "name": "@corbits/collections", + "version": "0.0.1", + "devDependencies": { + "@types/bun": "catalog:", + "typescript": "catalog:", + }, + }, "packages/cron": { "name": "@corbits/cron", "version": "0.0.1", @@ -267,6 +279,30 @@ "typescript": "catalog:", }, }, + "packages/webhook-triggers": { + "name": "@corbits/webhook-triggers", + "version": "0.0.1", + "dependencies": { + "@corbits/collections": "workspace:*", + "@corbits/error-sink": "workspace:*", + "@corbits/workflows": "workspace:*", + "@intx/crypto": "0.3.0", + "@intx/db": "workspace:*", + "@intx/hub-api": "workspace:*", + "@intx/hub-common": "0.3.0", + "@intx/hub-sessions": "workspace:*", + "@intx/log": "0.3.0", + "@intx/types": "workspace:*", + "arktype": "catalog:", + "drizzle-orm": "catalog:", + "hono": "catalog:", + "postgres": "catalog:", + }, + "devDependencies": { + "@types/bun": "catalog:", + "typescript": "catalog:", + }, + }, "packages/workflows": { "name": "@corbits/workflows", "version": "0.0.1", @@ -790,6 +826,8 @@ "@corbits/codex-provider": ["@corbits/codex-provider@github:corbitsdev/corbits-codex-provider#a1f475f", { "dependencies": { "@corbits/oauth-core": "github:corbitsdev/corbits-oauth-core", "@corbits/openai-responses": "github:corbitsdev/corbits-openai-responses", "arktype": "2.2.3" }, "peerDependencies": { "@intx/inference": ">=0.3.0", "@intx/types": ">=0.3.0" } }, "corbitsdev-corbits-codex-provider-a1f475f", "sha512-D6d0lblWx/4KudfLwQoQGrLB8GKRLP/HVp1cx08L2/hOGZAgH6txLPpeEtBqjl+R520qXSSPfM0kDC+3XNaAfw=="], + "@corbits/collections": ["@corbits/collections@workspace:packages/collections"], + "@corbits/credential-header": ["@corbits/credential-header@github:corbitsdev/corbits-credential-header#eee7b7e", { "peerDependencies": { "@intx/harness": ">=0.3.0", "@intx/types": ">=0.3.0" } }, "corbitsdev-corbits-credential-header-eee7b7e", "sha512-jCgdLZDxHSqL7PJo/xwp/xTCXTBdBgTQb4ZMBwRYCS6x29j8EPtuQiX0mvzZPRXVi02cmz45Shbc2/xQsjZ1oQ=="], "@corbits/credential-mcp": ["@corbits/credential-mcp@github:corbitsdev/corbits-credential-mcp#fb9ec3a", { "peerDependencies": { "@intx/harness": ">=0.3.0", "@intx/types": ">=0.3.0" } }, "corbitsdev-corbits-credential-mcp-fb9ec3a", "sha512-Co4jR+L3pfBbVYY7I6jRftoHeSb5+LCxzEEaAhVLYA3aY2PrPHZV37+DWxj3fcK/KdzboqbT3XSnqU8DyHRU9Q=="], @@ -820,7 +858,7 @@ "@corbits/url-path": ["@corbits/url-path@workspace:packages/url-path"], - "@corbits/webhooks": ["@corbits/webhooks@github:corbitsdev/webhooks#570bd52", { "dependencies": { "@intx/crypto": "^0.3.0", "@intx/db": "^0.3.0", "@intx/hub-api": "^0.3.0", "@intx/mime": "^0.3.0", "@intx/types": "^0.3.0", "hono": "4.11.9" } }, "corbitsdev-webhooks-570bd52", "sha512-GWduublrOUDgiH20a5VHFcI/zcf4mcFlQL9IEesyXKXJLekqpLFbacs4V490h0jl0oVG9KQ5dVb+CjRfTcajZA=="], + "@corbits/webhook-triggers": ["@corbits/webhook-triggers@workspace:packages/webhook-triggers"], "@corbits/workflow-authoring-tools": ["@corbits/workflow-authoring-tools@workspace:tools/workflow-authoring"], diff --git a/packages/agent-directory/test/routes.integration.test.ts b/e2e/agent-directory/routes.integration.test.ts similarity index 95% rename from packages/agent-directory/test/routes.integration.test.ts rename to e2e/agent-directory/routes.integration.test.ts index 65851a704a..ffdd423d98 100644 --- a/packages/agent-directory/test/routes.integration.test.ts +++ b/e2e/agent-directory/routes.integration.test.ts @@ -33,12 +33,12 @@ import { createAgentRepoStore, createAssetService } from "@intx/hub-sessions"; import { generateKeyPair } from "@intx/crypto"; import type { RequireGrant, TenantEnv } from "@intx/hub-api"; -import { dbTargetFromUrl } from "../../../scripts/db-setup"; -import { createAgentDefinitionRoutes } from "../src/routes"; -import type { PinnedSkillIndexResolver } from "../src/routes"; -import type { DefinitionAssetHistory } from "../src/definition-history"; -import type { CapabilityInventoryProvider } from "../src/capability-inventory"; -import { dbGate } from "../../../test/db-gate"; +import { dbTargetFromUrl } from "../../scripts/db-setup"; +import { createAgentDefinitionRoutes } from "../../packages/agent-directory/src/routes"; +import type { PinnedSkillIndexResolver } from "../../packages/agent-directory/src/routes"; +import type { DefinitionAssetHistory } from "../../packages/agent-directory/src/definition-history"; +import type { CapabilityInventoryProvider } from "../../packages/agent-directory/src/capability-inventory"; +import { dbGate } from "../lib/db-gate"; const databaseUrl = process.env["DATABASE_URL"]; const describeIfDb = dbGate(databaseUrl, import.meta.path); diff --git a/packages/agent-directory/test/tool-package-version.drizzle.test.ts b/e2e/agent-directory/tool-package-version.drizzle.test.ts similarity index 94% rename from packages/agent-directory/test/tool-package-version.drizzle.test.ts rename to e2e/agent-directory/tool-package-version.drizzle.test.ts index 05e8864ac8..a4c99936d3 100644 --- a/packages/agent-directory/test/tool-package-version.drizzle.test.ts +++ b/e2e/agent-directory/tool-package-version.drizzle.test.ts @@ -17,11 +17,11 @@ import { createDB, dropSchema, runMigrations, schema } from "@intx/db"; import type { AssetService } from "@intx/hub-sessions"; import { CORBITS_TOOLS_REGISTRY } from "../src/tool-registry"; -import { dbTargetFromUrl } from "../../../scripts/db-setup"; -import { e2eDatabaseUrl } from "../../../test/database-url"; -import { dbGate } from "../../../test/db-gate"; -import { CapabilityOutOfInventoryError } from "../src/capability-inventory"; -import { resolvePinnedVersion } from "../src/tool-package-version"; +import { dbTargetFromUrl } from "../../scripts/db-setup"; +import { e2eDatabaseUrl } from "../lib/database-url"; +import { dbGate } from "../lib/db-gate"; +import { CapabilityOutOfInventoryError } from "../../packages/agent-directory/src/capability-inventory"; +import { resolvePinnedVersion } from "../../packages/agent-directory/src/tool-package-version"; const databaseUrl = e2eDatabaseUrl(); const describeIfDb = dbGate(databaseUrl, import.meta.path); diff --git a/packages/agent-directory/test/visible-definitions.drizzle.test.ts b/e2e/agent-directory/visible-definitions.drizzle.test.ts similarity index 96% rename from packages/agent-directory/test/visible-definitions.drizzle.test.ts rename to e2e/agent-directory/visible-definitions.drizzle.test.ts index 910a6c3252..b0bb909adb 100644 --- a/packages/agent-directory/test/visible-definitions.drizzle.test.ts +++ b/e2e/agent-directory/visible-definitions.drizzle.test.ts @@ -11,10 +11,10 @@ import { afterAll, beforeAll, expect, test } from "bun:test"; import { createDB, runMigrations, dropSchema, schema } from "@intx/db"; -import { dbTargetFromUrl } from "../../../scripts/db-setup"; -import { e2eDatabaseUrl } from "../../../test/database-url"; -import { listVisibleAgentDefinitions } from "../src/visible-definitions"; -import { dbGate } from "../../../test/db-gate"; +import { dbTargetFromUrl } from "../../scripts/db-setup"; +import { e2eDatabaseUrl } from "../lib/database-url"; +import { listVisibleAgentDefinitions } from "../../packages/agent-directory/src/visible-definitions"; +import { dbGate } from "../lib/db-gate"; const databaseUrl = e2eDatabaseUrl(); const describeIfDb = dbGate(databaseUrl, import.meta.path); diff --git a/packages/workflows/test/agent-session.test.ts b/e2e/workflows/agent-session.test.ts similarity index 98% rename from packages/workflows/test/agent-session.test.ts rename to e2e/workflows/agent-session.test.ts index eb0cdeaefa..2e9fec07b6 100644 --- a/packages/workflows/test/agent-session.test.ts +++ b/e2e/workflows/agent-session.test.ts @@ -21,9 +21,12 @@ import { eq } from "drizzle-orm"; import { createDB, schema, type DB } from "@intx/db"; import { generateId } from "@intx/hub-common"; import { resolveRunSessionId } from "@intx/hub-sessions"; -import { dbGate } from "../../../test/db-gate"; +import { dbGate } from "../lib/db-gate"; -import { ensureRunSession, recordAgentSessionAtProvision } from "../src/launch/agent-session"; +import { + ensureRunSession, + recordAgentSessionAtProvision, +} from "../../packages/workflows/src/launch/agent-session"; function dbConfigFromUrl(databaseUrl: string) { const url = new URL(databaseUrl); diff --git a/package.json b/package.json index 2826b09551..012ddf85b5 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,9 @@ "@intx/hub-sessions": "workspace:*", "@intx/authz": "0.3.0", "@intx/harness": "workspace:*", + "@intx/hub-common": "0.3.0", + "@corbits/tool-registry-publish": "workspace:*", + "hono": "catalog:", "drizzle-orm": "catalog:", "postgres": "catalog:" }, diff --git a/packages/agent-directory/test/agent-definition-draft-routes.test.ts b/packages/agent-directory/src/agent-definition-draft-routes.test.ts similarity index 95% rename from packages/agent-directory/test/agent-definition-draft-routes.test.ts rename to packages/agent-directory/src/agent-definition-draft-routes.test.ts index 077aea811d..506efe2642 100644 --- a/packages/agent-directory/test/agent-definition-draft-routes.test.ts +++ b/packages/agent-directory/src/agent-definition-draft-routes.test.ts @@ -11,9 +11,9 @@ import { configureSync, resetSync } from "@intx/log"; import type { RequireGrant, TenantEnv } from "@intx/hub-api"; import { makeErrorEnvelope, parseErrorEnvelope } from "@corbits/error-sink"; -import { MyraAgentDefinitionDraftingUnavailableError } from "../src/agent-definition-drafting"; -import { createAgentDefinitionDraftRoutes } from "../src/agent-definition-draft-routes"; -import { OneShotRunUnreachableError } from "../src/one-shot-prompt"; +import { MyraAgentDefinitionDraftingUnavailableError } from "./agent-definition-drafting"; +import { createAgentDefinitionDraftRoutes } from "./agent-definition-draft-routes"; +import { OneShotRunUnreachableError } from "./one-shot-prompt"; const TENANT = { id: "tnt_1", diff --git a/packages/agent-directory/test/agent-workflow.test.ts b/packages/agent-directory/src/agent-workflow-definition.test.ts similarity index 99% rename from packages/agent-directory/test/agent-workflow.test.ts rename to packages/agent-directory/src/agent-workflow-definition.test.ts index 10d54fad1a..d8826768c3 100644 --- a/packages/agent-directory/test/agent-workflow.test.ts +++ b/packages/agent-directory/src/agent-workflow-definition.test.ts @@ -14,7 +14,7 @@ import { serializeAgentDefinitionWorkflow, withAgentModel, withoutAgentModel, -} from "../src/agent-workflow"; +} from "./agent-workflow"; const INPUT = { handle: "research-buddy", diff --git a/packages/agent-directory/test/routes.test.ts b/packages/agent-directory/src/routes.test.ts similarity index 99% rename from packages/agent-directory/test/routes.test.ts rename to packages/agent-directory/src/routes.test.ts index 2178809fd2..7854f497bf 100644 --- a/packages/agent-directory/test/routes.test.ts +++ b/packages/agent-directory/src/routes.test.ts @@ -22,21 +22,21 @@ import { serializeAgentDefinitionWorkflow, withAgentToolPackagePin, readPinnedSkillNames, -} from "../src/agent-workflow"; +} from "./agent-workflow"; import { agentDefinitionSourceTree, AGENT_DEFINITION_ENTRY_PATH, readAgentDefinitionWorkflowJson, RetiredWorkflowEnvelopeError, -} from "../src/definition-asset"; -import { createAgentDefinitionRoutes, SkillIndexResolutionError } from "../src/routes"; -import type { PinnedSkillIndexResolver } from "../src/routes"; +} from "./definition-asset"; +import { createAgentDefinitionRoutes, SkillIndexResolutionError } from "./routes"; +import type { PinnedSkillIndexResolver } from "./routes"; import { createWorkflowSkillPinRoutes, type WorkflowRunAuthenticator, -} from "../src/workflow-skill-pin-routes"; -import type { DefinitionAssetHistory } from "../src/definition-history"; -import type { CapabilityInventoryProvider } from "../src/capability-inventory"; +} from "./workflow-skill-pin-routes"; +import type { DefinitionAssetHistory } from "./definition-history"; +import type { CapabilityInventoryProvider } from "./capability-inventory"; import { definitionFrom, SOURCE_TREE_PATHS, storedDefinitionBytesWithSkills } from "./source-tree"; /** A `readAssetBlob` that always answers the definition's entry module diff --git a/packages/agent-directory/test/source-tree.ts b/packages/agent-directory/src/source-tree.ts similarity index 96% rename from packages/agent-directory/test/source-tree.ts rename to packages/agent-directory/src/source-tree.ts index 0391ff1a85..be5984b705 100644 --- a/packages/agent-directory/test/source-tree.ts +++ b/packages/agent-directory/src/source-tree.ts @@ -7,12 +7,12 @@ import { AGENT_DEFINITION_ENTRY_PATH, agentDefinitionSourceTree, parseAgentDefinitionEntry, -} from "../src/definition-asset"; +} from "./definition-asset"; import { buildAgentDefinitionWorkflow, reindexPinnedSkills, serializeAgentDefinitionWorkflow, -} from "../src/agent-workflow"; +} from "./agent-workflow"; /** The two files a definition's asset tree carries, in render order. */ export const SOURCE_TREE_PATHS = ["package.json", AGENT_DEFINITION_ENTRY_PATH]; diff --git a/packages/agent-directory/test/validation.test.ts b/packages/agent-directory/src/validation.test.ts similarity index 99% rename from packages/agent-directory/test/validation.test.ts rename to packages/agent-directory/src/validation.test.ts index e575ceeca8..ae18f49486 100644 --- a/packages/agent-directory/test/validation.test.ts +++ b/packages/agent-directory/src/validation.test.ts @@ -5,7 +5,7 @@ import { CreateAgentDefinitionInput, UpdateAgentSkillsInput, UpdateDefinitionStatusInput, -} from "../src/validation"; +} from "./validation"; const VALID = { name: "Research Buddy", diff --git a/packages/agent-directory/test/workflow-capability-routes.test.ts b/packages/agent-directory/src/workflow-capability-routes.test.ts similarity index 97% rename from packages/agent-directory/test/workflow-capability-routes.test.ts rename to packages/agent-directory/src/workflow-capability-routes.test.ts index 3ad9be68e9..12ea6f18b8 100644 --- a/packages/agent-directory/test/workflow-capability-routes.test.ts +++ b/packages/agent-directory/src/workflow-capability-routes.test.ts @@ -15,17 +15,17 @@ import { buildAgentDefinitionWorkflow, serializeAgentDefinitionWorkflow, readPinnedSkillNames, -} from "../src/agent-workflow"; +} from "./agent-workflow"; import { createWorkflowCapabilityRoutes, type WorkflowCapabilityRunScope, type WorkflowRunAuthenticator, -} from "../src/workflow-capability-routes"; -import { agentDefinitionSourceTree, AGENT_DEFINITION_ENTRY_PATH } from "../src/definition-asset"; -import type { PinnedSkillIndexResolver } from "../src/routes"; +} from "./workflow-capability-routes"; +import { agentDefinitionSourceTree, AGENT_DEFINITION_ENTRY_PATH } from "./definition-asset"; +import type { PinnedSkillIndexResolver } from "./routes"; import { definitionFrom, SOURCE_TREE_PATHS } from "./source-tree"; -import type { CapabilityInventoryProvider } from "../src/capability-inventory"; -import { CORBITS_TOOLS_REGISTRY } from "../src/tool-registry"; +import type { CapabilityInventoryProvider } from "./capability-inventory"; +import { CORBITS_TOOLS_REGISTRY } from "./tool-registry"; const TENANT_ID = "tnt_1"; const RUN_ID = "run_1"; diff --git a/packages/agent-directory/test/workflow-create-routes.test.ts b/packages/agent-directory/src/workflow-create-routes.test.ts similarity index 98% rename from packages/agent-directory/test/workflow-create-routes.test.ts rename to packages/agent-directory/src/workflow-create-routes.test.ts index e555747b74..635d12b43f 100644 --- a/packages/agent-directory/test/workflow-create-routes.test.ts +++ b/packages/agent-directory/src/workflow-create-routes.test.ts @@ -13,14 +13,14 @@ import type { DB } from "@intx/db"; import { createWorkflowAgentCreateRoutes, type CreateWorkflowAgentCreateRoutesDeps, -} from "../src/workflow-create-routes"; +} from "./workflow-create-routes"; import type { WorkflowCapabilityRunScope, WorkflowRunAuthenticator, -} from "../src/workflow-capability-routes"; -import type { PinnedSkillIndexResolver } from "../src/routes"; -import type { CapabilityInventoryProvider } from "../src/capability-inventory"; -import { CORBITS_TOOLS_REGISTRY } from "../src/tool-registry"; +} from "./workflow-capability-routes"; +import type { PinnedSkillIndexResolver } from "./routes"; +import type { CapabilityInventoryProvider } from "./capability-inventory"; +import { CORBITS_TOOLS_REGISTRY } from "./tool-registry"; import { definitionFrom, SOURCE_TREE_PATHS } from "./source-tree"; const TENANT_ID = "tnt_1"; diff --git a/packages/agent-directory/test/workflow-skill-pin-routes.test.ts b/packages/agent-directory/src/workflow-skill-pin-routes.test.ts similarity index 98% rename from packages/agent-directory/test/workflow-skill-pin-routes.test.ts rename to packages/agent-directory/src/workflow-skill-pin-routes.test.ts index 92c9306d3b..d63eaf03e1 100644 --- a/packages/agent-directory/test/workflow-skill-pin-routes.test.ts +++ b/packages/agent-directory/src/workflow-skill-pin-routes.test.ts @@ -14,14 +14,14 @@ import { buildAgentDefinitionWorkflow, serializeAgentDefinitionWorkflow, readPinnedSkillNames, -} from "../src/agent-workflow"; +} from "./agent-workflow"; import { createWorkflowSkillPinRoutes, type WorkflowSkillPinRunScope, type WorkflowRunAuthenticator, -} from "../src/workflow-skill-pin-routes"; -import { agentDefinitionSourceTree, AGENT_DEFINITION_ENTRY_PATH } from "../src/definition-asset"; -import type { PinnedSkillIndexResolver } from "../src/routes"; +} from "./workflow-skill-pin-routes"; +import { agentDefinitionSourceTree, AGENT_DEFINITION_ENTRY_PATH } from "./definition-asset"; +import type { PinnedSkillIndexResolver } from "./routes"; import { definitionFrom, SOURCE_TREE_PATHS, storedDefinitionBytesWithSkills } from "./source-tree"; const TENANT_ID = "tnt_1"; From 36a220ba32cf6184b866e15b2c824a949c36789c Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 17 Sep 2026 18:27:05 -0700 Subject: [PATCH 6/9] docs: point test-path references at colocated/e2e locations, trim lint globs (CL-8370) --- AGENTS.md | 9 ++++++++- package.json | 6 +++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 69a0ae66b2..c0a157518b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,7 +60,7 @@ never seeds data on a client's behalf. init can then walk up to the enclosing `.git` and land a genesis commit on the working branch. Tests that boot the hub or touch seed/deploy git paths must call `installDisposableHubDataDir()` from - `test/disposable-hub-data-dir.ts`; never delete the `HUB_DATA_DIR` key. + `e2e/lib/disposable-hub-data-dir.ts`; never delete the `HUB_DATA_DIR` key. - Deployment mechanics are not settled enough to state here — see [IMPLEMENTATION.md](IMPLEMENTATION.md)'s Deployment section first. @@ -76,6 +76,13 @@ Env flags (unset behavior): ## Tests +Two homes only. A load-bearing unit test sits beside the module it covers +(`src/**/*.test.ts`); a load-bearing integration or DB-backed suite lives +under root `e2e//`, with shared helpers in `e2e/lib/`. No `test/` +directory exists anywhere — not in apps, packages, agents, or tools. A test +that isn't load-bearing (pins a literal, a shape, a re-export, or re-proves +Interchange's own behavior) is deleted, not moved. + Tests are meaningful red/green tests only — no coverage theater. An outdated test is deleted in the same PR that breaks it, not adapted to keep passing; rebuilding coverage for the area it covered happens under CL-8150. diff --git a/package.json b/package.json index 012ddf85b5..3dce0f331a 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,9 @@ "scripts": { "check": "bun run typecheck && bun run lint && bun run fmt:check && bun run test", "typecheck": "bun run scripts/typecheck.ts", - "lint": "oxlint apps packages tools agents workflows scripts test e2e", - "fmt": "oxfmt apps packages tools agents workflows scripts test e2e", - "fmt:check": "oxfmt --check apps packages tools agents workflows scripts test e2e", + "lint": "oxlint apps packages tools agents workflows scripts e2e", + "fmt": "oxfmt apps packages tools agents workflows scripts e2e", + "fmt:check": "oxfmt --check apps packages tools agents workflows scripts e2e", "build": "bun run scripts/run-all.ts build", "test": "bun run scripts/run-all.ts test", "dev": "bun run scripts/dev.ts", From 78127804cc891289726074824feac322126ab1b9 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 17 Sep 2026 18:31:03 -0700 Subject: [PATCH 7/9] chore: root deps for e2e DB suites, fmt fixes, fix migration-runner helper paths (CL-8370) --- agents/myra/tsconfig.json | 4 +--- apps/hub/tsconfig.json | 12 +++--------- apps/web/src/chat/agent-display-names.test.ts | 5 +---- apps/web/src/chat/connect-service-flow.test.tsx | 5 +---- apps/web/src/chat/pr-thread-view.test.tsx | 6 +----- apps/web/src/chat/workbenches-mutated-event.test.ts | 6 +----- apps/web/src/command-palette/command-groups.test.ts | 5 +---- apps/web/src/pages/create-agent-panel.test.tsx | 5 +---- apps/web/src/pages/home-page.test.tsx | 5 +---- apps/web/src/settings/grant-preview.test.ts | 6 +----- apps/web/src/settings/identity.test.ts | 6 +----- apps/web/src/settings/section-registry.test.tsx | 5 +---- .../src/shell/context-menu/lib/dialog-guard.test.ts | 5 +---- apps/web/src/shell/layout/shell-layout-hook.test.tsx | 6 +----- bun.lock | 1 + e2e/agent-directory/routes.integration.test.ts | 2 +- package.json | 1 + packages/agent-directory/tsconfig.json | 8 ++------ packages/cron/tsconfig.json | 4 +--- packages/workflows/tsconfig.json | 4 +--- 20 files changed, 23 insertions(+), 78 deletions(-) diff --git a/agents/myra/tsconfig.json b/agents/myra/tsconfig.json index 5e0fa777c0..e125e90614 100644 --- a/agents/myra/tsconfig.json +++ b/agents/myra/tsconfig.json @@ -10,8 +10,6 @@ "tsBuildInfoFile": "tsconfig.tsbuildinfo", "rootDir": "../.." }, - "include": [ - "src" - ], + "include": ["src"], "exclude": [] } diff --git a/apps/hub/tsconfig.json b/apps/hub/tsconfig.json index 0938e8e6d6..db14f93e63 100644 --- a/apps/hub/tsconfig.json +++ b/apps/hub/tsconfig.json @@ -1,15 +1,9 @@ { "extends": "../../tsconfig.base.json", - "include": [ - "src" - ], + "include": ["src"], "compilerOptions": { - "types": [ - "bun" - ], - "customConditions": [ - "bun" - ], + "types": ["bun"], + "customConditions": ["bun"], "noEmit": true } } diff --git a/apps/web/src/chat/agent-display-names.test.ts b/apps/web/src/chat/agent-display-names.test.ts index 4a9f66b59f..f9bb8c5bca 100644 --- a/apps/web/src/chat/agent-display-names.test.ts +++ b/apps/web/src/chat/agent-display-names.test.ts @@ -1,9 +1,6 @@ import { describe, expect, test } from "bun:test"; -import { - agentDisplayNamesFromAgents, - displayNameForAddress, -} from "./agent-display-names"; +import { agentDisplayNamesFromAgents, displayNameForAddress } from "./agent-display-names"; describe("agentDisplayNamesFromAgents", () => { test("keys each agent's display name by its participant address", () => { diff --git a/apps/web/src/chat/connect-service-flow.test.tsx b/apps/web/src/chat/connect-service-flow.test.tsx index 10d921fb37..b787b7491a 100644 --- a/apps/web/src/chat/connect-service-flow.test.tsx +++ b/apps/web/src/chat/connect-service-flow.test.tsx @@ -11,10 +11,7 @@ import type { ReactElement } from "react"; import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; -import type { - ConnectServiceActions, - ConnectServiceQuery, -} from "./blocks/connect-service-actions"; +import type { ConnectServiceActions, ConnectServiceQuery } from "./blocks/connect-service-actions"; import { ConnectServiceBlockContainer } from "./blocks/connect-service-block-container"; let container: HTMLDivElement | null = null; diff --git a/apps/web/src/chat/pr-thread-view.test.tsx b/apps/web/src/chat/pr-thread-view.test.tsx index 2eac1d005b..26e7037c8c 100644 --- a/apps/web/src/chat/pr-thread-view.test.tsx +++ b/apps/web/src/chat/pr-thread-view.test.tsx @@ -13,11 +13,7 @@ import type { ReactElement } from "react"; import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; -import type { - PrThreadFailedTurn, - PrThreadReply, - PrThreadViewProps, -} from "./pr-thread-view"; +import type { PrThreadFailedTurn, PrThreadReply, PrThreadViewProps } from "./pr-thread-view"; import { PrQueuedStrip, PrThreadView } from "./pr-thread-view"; let container: HTMLDivElement | null = null; diff --git a/apps/web/src/chat/workbenches-mutated-event.test.ts b/apps/web/src/chat/workbenches-mutated-event.test.ts index 252c2771f7..3f604d9f3d 100644 --- a/apps/web/src/chat/workbenches-mutated-event.test.ts +++ b/apps/web/src/chat/workbenches-mutated-event.test.ts @@ -1,11 +1,7 @@ import { afterEach, describe, expect, test } from "bun:test"; import { GlobalRegistrator } from "@happy-dom/global-registrator"; -import { - WORKBENCHES_MUTATED_EVENT, - applyStreamWorkbenchesMutated, - createWorkbench, -} from "./api"; +import { WORKBENCHES_MUTATED_EVENT, applyStreamWorkbenchesMutated, createWorkbench } from "./api"; if (typeof document === "undefined") { GlobalRegistrator.register(); diff --git a/apps/web/src/command-palette/command-groups.test.ts b/apps/web/src/command-palette/command-groups.test.ts index 702b6f99a4..8bef9fa96b 100644 --- a/apps/web/src/command-palette/command-groups.test.ts +++ b/apps/web/src/command-palette/command-groups.test.ts @@ -1,9 +1,6 @@ import { describe, expect, test } from "bun:test"; -import { - buildCommandPaletteGroups, - type PaletteSource, -} from "./command-groups"; +import { buildCommandPaletteGroups, type PaletteSource } from "./command-groups"; const actions: PaletteSource = { id: "actions", diff --git a/apps/web/src/pages/create-agent-panel.test.tsx b/apps/web/src/pages/create-agent-panel.test.tsx index eeb2238a33..c23e209263 100644 --- a/apps/web/src/pages/create-agent-panel.test.tsx +++ b/apps/web/src/pages/create-agent-panel.test.tsx @@ -378,10 +378,7 @@ describe("CreateAgentPanel drafting failure — fails closed", () => { describe("agent creation entry points", () => { test("the chat page never imports this form — agent creation lives outside it entirely", () => { - const chatPageSource = readFileSync( - new URL("./chat-page.tsx", import.meta.url), - "utf8", - ); + const chatPageSource = readFileSync(new URL("./chat-page.tsx", import.meta.url), "utf8"); expect(chatPageSource).not.toContain('from "./create-agent-panel"'); expect(chatPageSource).not.toContain("instant-agent-create"); }); diff --git a/apps/web/src/pages/home-page.test.tsx b/apps/web/src/pages/home-page.test.tsx index 2c1ebb9474..49176f2375 100644 --- a/apps/web/src/pages/home-page.test.tsx +++ b/apps/web/src/pages/home-page.test.tsx @@ -479,10 +479,7 @@ describe("the other two entries land on the same `/` hop", () => { }); test("onboarding hands off to `/` once a working credential is confirmed", () => { - const source = readFileSync( - new URL("./onboarding-page.tsx", import.meta.url), - "utf8", - ); + const source = readFileSync(new URL("./onboarding-page.tsx", import.meta.url), "utf8"); expect(source).toContain('navigate("/")'); }); }); diff --git a/apps/web/src/settings/grant-preview.test.ts b/apps/web/src/settings/grant-preview.test.ts index 22135c729e..5f46fe4578 100644 --- a/apps/web/src/settings/grant-preview.test.ts +++ b/apps/web/src/settings/grant-preview.test.ts @@ -1,10 +1,6 @@ import { describe, expect, test } from "bun:test"; -import { - expiryIsoFromPreset, - expiryLabelFromPreset, - grantPreviewSentence, -} from "./grant-preview"; +import { expiryIsoFromPreset, expiryLabelFromPreset, grantPreviewSentence } from "./grant-preview"; describe("grantPreviewSentence", () => { test("defaults the subject when no target is chosen", () => { diff --git a/apps/web/src/settings/identity.test.ts b/apps/web/src/settings/identity.test.ts index 21994cf14b..939fd5686d 100644 --- a/apps/web/src/settings/identity.test.ts +++ b/apps/web/src/settings/identity.test.ts @@ -3,11 +3,7 @@ import { describe, expect, test } from "bun:test"; -import { - PRINCIPAL_KIND_LABEL, - PRINCIPAL_KIND_ORDER, - principalLabel, -} from "./identity"; +import { PRINCIPAL_KIND_LABEL, PRINCIPAL_KIND_ORDER, principalLabel } from "./identity"; describe("principalLabel", () => { test("passes through an already-humane display name unchanged", () => { diff --git a/apps/web/src/settings/section-registry.test.tsx b/apps/web/src/settings/section-registry.test.tsx index 6c804613e4..a337443776 100644 --- a/apps/web/src/settings/section-registry.test.tsx +++ b/apps/web/src/settings/section-registry.test.tsx @@ -11,10 +11,7 @@ import { describe, expect, test } from "bun:test"; import { Robot } from "@/lib/icons"; -import { - insertEveryoneSections, - resolveSettingsSectionGroups, -} from "./section-registry"; +import { insertEveryoneSections, resolveSettingsSectionGroups } from "./section-registry"; import type { TenancyAccess } from "./access"; import type { SettingsSection } from "./shell"; diff --git a/apps/web/src/shell/context-menu/lib/dialog-guard.test.ts b/apps/web/src/shell/context-menu/lib/dialog-guard.test.ts index ba9e15cc30..5270a2901c 100644 --- a/apps/web/src/shell/context-menu/lib/dialog-guard.test.ts +++ b/apps/web/src/shell/context-menu/lib/dialog-guard.test.ts @@ -1,9 +1,6 @@ import { afterEach, describe, expect, test } from "bun:test"; -import { - isBlockingOverlayOpen, - isInsideInteractiveInput, -} from "./dialog-guard"; +import { isBlockingOverlayOpen, isInsideInteractiveInput } from "./dialog-guard"; afterEach(() => { document.body.innerHTML = ""; diff --git a/apps/web/src/shell/layout/shell-layout-hook.test.tsx b/apps/web/src/shell/layout/shell-layout-hook.test.tsx index bef5dc83b0..6351f17a12 100644 --- a/apps/web/src/shell/layout/shell-layout-hook.test.tsx +++ b/apps/web/src/shell/layout/shell-layout-hook.test.tsx @@ -10,11 +10,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; -import { - COMPACT_MAX_WIDTH, - NARROW_MAX_WIDTH, - type ShellLayoutMode, -} from "./breakpoints"; +import { COMPACT_MAX_WIDTH, NARROW_MAX_WIDTH, type ShellLayoutMode } from "./breakpoints"; import { useShellLayoutMode } from "./use-shell-layout"; type StubQuery = { diff --git a/bun.lock b/bun.lock index bafe94379a..2c2548e64a 100644 --- a/bun.lock +++ b/bun.lock @@ -20,6 +20,7 @@ "@intx/crypto": "workspace:*", "@intx/db": "workspace:*", "@intx/harness": "workspace:*", + "@intx/hub-api": "workspace:*", "@intx/hub-common": "0.3.0", "@intx/hub-sessions": "workspace:*", "@intx/log": "workspace:*", diff --git a/e2e/agent-directory/routes.integration.test.ts b/e2e/agent-directory/routes.integration.test.ts index ffdd423d98..e11e65a735 100644 --- a/e2e/agent-directory/routes.integration.test.ts +++ b/e2e/agent-directory/routes.integration.test.ts @@ -84,7 +84,7 @@ const fakeHistory: DefinitionAssetHistory = { readBlobAtCommit: () => Promise.resolve(null), }; -const allowAllRequireGrant: RequireGrant = () => async (_c, next) => { +const allowAllRequireGrant: RequireGrant = () => async (_c: unknown, next: () => Promise) => { await next(); }; diff --git a/package.json b/package.json index 3dce0f331a..9238582679 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@intx/harness": "workspace:*", "@intx/hub-common": "0.3.0", "@corbits/tool-registry-publish": "workspace:*", + "@intx/hub-api": "workspace:*", "hono": "catalog:", "drizzle-orm": "catalog:", "postgres": "catalog:" diff --git a/packages/agent-directory/tsconfig.json b/packages/agent-directory/tsconfig.json index eb6192f1fb..61587c6018 100644 --- a/packages/agent-directory/tsconfig.json +++ b/packages/agent-directory/tsconfig.json @@ -1,12 +1,8 @@ { "extends": "../../tsconfig.base.json", - "include": [ - "src" - ], + "include": ["src"], "compilerOptions": { - "types": [ - "bun" - ], + "types": ["bun"], "noEmit": true } } diff --git a/packages/cron/tsconfig.json b/packages/cron/tsconfig.json index 5e0fa777c0..e125e90614 100644 --- a/packages/cron/tsconfig.json +++ b/packages/cron/tsconfig.json @@ -10,8 +10,6 @@ "tsBuildInfoFile": "tsconfig.tsbuildinfo", "rootDir": "../.." }, - "include": [ - "src" - ], + "include": ["src"], "exclude": [] } diff --git a/packages/workflows/tsconfig.json b/packages/workflows/tsconfig.json index 509234b1d5..e620bfff2c 100644 --- a/packages/workflows/tsconfig.json +++ b/packages/workflows/tsconfig.json @@ -10,9 +10,7 @@ "tsBuildInfoFile": "tsconfig.tsbuildinfo", "rootDir": "../.." }, - "include": [ - "src" - ], + "include": ["src"], "exclude": [], "references": [ { From b463d22d1bb3caa188ff23e22e31fb3b72b4db21 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 17 Sep 2026 18:48:30 -0700 Subject: [PATCH 8/9] test: cut apps/web colocated tests to load-bearing logic; delete dead e2e/browser and doomed-package suites (CL-8370) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deletes 103 apps/web unit tests that only rendered a component, pinned copy/CSS/route literals, or re-tested composition/section-registry wiring; keeps 28 that cover wire parsing, grant/approval decisions, credential/secret shaping, and dedup/reconciliation logic a user would notice break. Deletes e2e/browser/walkthrough.ts (referenced a deleted connections module and a missing harness.ts) and the e2e suites for webhook-triggers/granola/linear/tool-package-version, which depend on @corbits/credential-providers and @corbits/tool-registry-publish — both being deleted. Wires e2e/ into `bun run test` via a new `test:e2e` script that runs when DATABASE_URL is set and skips (via each suite's own dbGate banner) otherwise. --- AGENTS.md | 1 - apps/web/src/agents-api.test.ts | 401 ----- apps/web/src/api.test.tsx | 140 -- apps/web/src/app-error-boundary.test.tsx | 74 - .../src/app-shell-setup-note-removed.test.tsx | 99 -- apps/web/src/auth.test.tsx | 294 ---- apps/web/src/auth/dither-background.test.tsx | 58 - apps/web/src/chat-header-toggle.test.tsx | 136 -- .../src/chat/agent-typing-indicator.test.tsx | 52 - apps/web/src/chat/artifact-chip.test.tsx | 76 - apps/web/src/chat/avatar-composition.test.tsx | 75 - .../blocks/connect-service-block.test.tsx | 168 --- apps/web/src/chat/chat-bubble-flat.test.ts | 40 - apps/web/src/chat/chat-workspace.test.tsx | 153 -- apps/web/src/chat/components.test.tsx | 786 ---------- .../src/chat/connect-service-flow.test.tsx | 102 -- apps/web/src/chat/failed-turn-strip.test.tsx | 483 ------ apps/web/src/chat/fake-clock.ts | 58 - .../src/chat/inference-preamble-drift.test.ts | 25 - apps/web/src/chat/loading-state.test.tsx | 84 -- ...essage-grouping-and-failed-polish.test.tsx | 285 ---- .../src/chat/message-hover-toolbar.test.tsx | 341 ----- .../src/chat/no-usable-model-banner.test.tsx | 39 - .../src/chat/own-message-alignment.test.tsx | 172 --- apps/web/src/chat/pr-thread-view.test.tsx | 583 -------- apps/web/src/chat/presence-stack.test.tsx | 214 --- .../src/chat/resume-failed-banner.test.tsx | 42 - apps/web/src/chat/system-join-edge.test.tsx | 145 -- apps/web/src/chat/system-row-chrome.test.tsx | 245 --- apps/web/src/chat/team-avatar-stack.test.tsx | 309 ---- .../src/chat/timeline-jump-to-latest.test.tsx | 145 -- .../web/src/chat/timeline-scroll-pin.test.tsx | 127 -- .../web/src/chat/use-streaming-reply.test.tsx | 369 ----- apps/web/src/chat/use-turn-activity.test.tsx | 182 --- .../src/chat/use-typing-indicator.test.tsx | 131 -- ...se-workbench-feed-refresh-scoping.test.tsx | 115 -- .../src/chat/workbench-loading-tip.test.tsx | 86 -- .../chat/workbench-settings-retry.test.tsx | 123 -- .../agents-section.test.tsx | 1048 ------------- .../members-section.test.tsx | 238 --- .../bare-prefix-entity-scope.test.ts | 83 -- .../command-palette/command-groups.test.ts | 144 -- apps/web/src/command-palette/recents.test.ts | 149 -- .../command-palette/static-commands.test.ts | 37 - .../use-entity-search.test.tsx | 197 --- apps/web/src/global-command-palette.test.tsx | 237 --- apps/web/src/initials.test.ts | 22 - apps/web/src/insights-route.test.tsx | 182 --- apps/web/src/insights-row-activation.test.tsx | 136 -- .../src/library-artifact-deep-link.test.tsx | 131 -- apps/web/src/library-page-selection.test.tsx | 355 ----- .../src/library-upload-confirmation.test.tsx | 165 -- apps/web/src/library/artifact-card.test.tsx | 37 - .../src/library/artifact-renderer.test.tsx | 109 -- apps/web/src/page-type-drift.ts | 27 - apps/web/src/pages.test.tsx | 97 -- apps/web/src/pages/agent-detail-page.test.tsx | 581 -------- .../src/pages/agent-skills-picker.test.tsx | 122 -- apps/web/src/pages/agents-page.test.tsx | 618 -------- .../web/src/pages/create-agent-panel.test.tsx | 403 ----- apps/web/src/pages/home-page.test.tsx | 485 ------ .../pages/legacy-settings-redirects.test.tsx | 100 -- .../src/pages/mission-control-page.test.tsx | 240 --- .../pages/provisioning-error-page.test.tsx | 74 - .../src/pages/routine-detail-page.test.tsx | 302 ---- apps/web/src/pages/routines-page.test.tsx | 197 --- apps/web/src/pages/settings-page.test.tsx | 120 -- apps/web/src/pages/skill-detail-page.test.tsx | 169 --- apps/web/src/pages/skills-page.test.tsx | 391 ----- .../src/pages/workflow-detail-page.test.tsx | 55 - apps/web/src/routes.test.tsx | 374 ----- .../web/src/settings/account-section.test.tsx | 156 -- apps/web/src/settings/api-request.test.ts | 94 -- apps/web/src/settings/api.test.ts | 82 - apps/web/src/settings/grants-section.test.tsx | 160 -- apps/web/src/settings/identity.test.ts | 57 - apps/web/src/settings/people-section.test.tsx | 464 ------ apps/web/src/settings/query-retry.test.tsx | 111 -- apps/web/src/settings/raw-id-sweep.test.tsx | 108 -- .../src/settings/resource-vocabulary.test.ts | 35 - apps/web/src/settings/roles-section.test.tsx | 168 --- .../src/settings/section-registry.test.tsx | 172 --- apps/web/src/settings/shell.test.tsx | 68 - apps/web/src/settings/strings.test.ts | 28 - apps/web/src/settings/tenancy-api.test.ts | 202 --- apps/web/src/shell-chrome-wiring.test.tsx | 336 ----- apps/web/src/shell/bench-activity.test.tsx | 252 ---- apps/web/src/shell/canvas-column.test.tsx | 324 ---- .../lib/context-menu-view.test.tsx | 201 --- .../context-menu/lib/dialog-guard.test.ts | 55 - .../context-menu/lib/target-resolver.test.ts | 81 - .../lib/use-context-menu-state.test.tsx | 95 -- ...use-document-context-menu-trigger.test.tsx | 121 -- .../shell/layout/shell-layout-hook.test.tsx | 130 -- .../shell/layout/use-scroll-reset.test.tsx | 59 - apps/web/src/shell/sidebar.test.tsx | 711 --------- apps/web/src/shell/stage-top-bar.test.tsx | 148 -- apps/web/src/shell/workbench-list.test.tsx | 254 ---- .../web/src/stage-chrome-consistency.test.tsx | 128 -- apps/web/src/stage-search-filter.test.tsx | 164 -- apps/web/src/stage-top-nav.test.tsx | 241 --- apps/web/src/tailwind-build.test.ts | 44 - apps/web/src/theme-default.test.tsx | 59 - apps/web/src/toast-single-system.test.tsx | 288 ---- bun.lock | 5 - .../tool-package-version.drizzle.test.ts | 172 --- e2e/browser/walkthrough.ts | 1327 ----------------- e2e/tsconfig.json | 1 + package.json | 7 +- scripts/run-all.ts | 24 + 110 files changed, 27 insertions(+), 21688 deletions(-) delete mode 100644 apps/web/src/agents-api.test.ts delete mode 100644 apps/web/src/api.test.tsx delete mode 100644 apps/web/src/app-error-boundary.test.tsx delete mode 100644 apps/web/src/app-shell-setup-note-removed.test.tsx delete mode 100644 apps/web/src/auth.test.tsx delete mode 100644 apps/web/src/auth/dither-background.test.tsx delete mode 100644 apps/web/src/chat-header-toggle.test.tsx delete mode 100644 apps/web/src/chat/agent-typing-indicator.test.tsx delete mode 100644 apps/web/src/chat/artifact-chip.test.tsx delete mode 100644 apps/web/src/chat/avatar-composition.test.tsx delete mode 100644 apps/web/src/chat/blocks/connect-service-block.test.tsx delete mode 100644 apps/web/src/chat/chat-bubble-flat.test.ts delete mode 100644 apps/web/src/chat/chat-workspace.test.tsx delete mode 100644 apps/web/src/chat/components.test.tsx delete mode 100644 apps/web/src/chat/connect-service-flow.test.tsx delete mode 100644 apps/web/src/chat/failed-turn-strip.test.tsx delete mode 100644 apps/web/src/chat/fake-clock.ts delete mode 100644 apps/web/src/chat/inference-preamble-drift.test.ts delete mode 100644 apps/web/src/chat/loading-state.test.tsx delete mode 100644 apps/web/src/chat/message-grouping-and-failed-polish.test.tsx delete mode 100644 apps/web/src/chat/message-hover-toolbar.test.tsx delete mode 100644 apps/web/src/chat/no-usable-model-banner.test.tsx delete mode 100644 apps/web/src/chat/own-message-alignment.test.tsx delete mode 100644 apps/web/src/chat/pr-thread-view.test.tsx delete mode 100644 apps/web/src/chat/presence-stack.test.tsx delete mode 100644 apps/web/src/chat/resume-failed-banner.test.tsx delete mode 100644 apps/web/src/chat/system-join-edge.test.tsx delete mode 100644 apps/web/src/chat/system-row-chrome.test.tsx delete mode 100644 apps/web/src/chat/team-avatar-stack.test.tsx delete mode 100644 apps/web/src/chat/timeline-jump-to-latest.test.tsx delete mode 100644 apps/web/src/chat/timeline-scroll-pin.test.tsx delete mode 100644 apps/web/src/chat/use-streaming-reply.test.tsx delete mode 100644 apps/web/src/chat/use-turn-activity.test.tsx delete mode 100644 apps/web/src/chat/use-typing-indicator.test.tsx delete mode 100644 apps/web/src/chat/use-workbench-feed-refresh-scoping.test.tsx delete mode 100644 apps/web/src/chat/workbench-loading-tip.test.tsx delete mode 100644 apps/web/src/chat/workbench-settings-retry.test.tsx delete mode 100644 apps/web/src/chat/workbench-settings/agents-section.test.tsx delete mode 100644 apps/web/src/chat/workbench-settings/members-section.test.tsx delete mode 100644 apps/web/src/command-palette/bare-prefix-entity-scope.test.ts delete mode 100644 apps/web/src/command-palette/command-groups.test.ts delete mode 100644 apps/web/src/command-palette/recents.test.ts delete mode 100644 apps/web/src/command-palette/static-commands.test.ts delete mode 100644 apps/web/src/command-palette/use-entity-search.test.tsx delete mode 100644 apps/web/src/global-command-palette.test.tsx delete mode 100644 apps/web/src/initials.test.ts delete mode 100644 apps/web/src/insights-route.test.tsx delete mode 100644 apps/web/src/insights-row-activation.test.tsx delete mode 100644 apps/web/src/library-artifact-deep-link.test.tsx delete mode 100644 apps/web/src/library-page-selection.test.tsx delete mode 100644 apps/web/src/library-upload-confirmation.test.tsx delete mode 100644 apps/web/src/library/artifact-card.test.tsx delete mode 100644 apps/web/src/library/artifact-renderer.test.tsx delete mode 100644 apps/web/src/page-type-drift.ts delete mode 100644 apps/web/src/pages.test.tsx delete mode 100644 apps/web/src/pages/agent-detail-page.test.tsx delete mode 100644 apps/web/src/pages/agent-skills-picker.test.tsx delete mode 100644 apps/web/src/pages/agents-page.test.tsx delete mode 100644 apps/web/src/pages/create-agent-panel.test.tsx delete mode 100644 apps/web/src/pages/home-page.test.tsx delete mode 100644 apps/web/src/pages/legacy-settings-redirects.test.tsx delete mode 100644 apps/web/src/pages/mission-control-page.test.tsx delete mode 100644 apps/web/src/pages/provisioning-error-page.test.tsx delete mode 100644 apps/web/src/pages/routine-detail-page.test.tsx delete mode 100644 apps/web/src/pages/routines-page.test.tsx delete mode 100644 apps/web/src/pages/settings-page.test.tsx delete mode 100644 apps/web/src/pages/skill-detail-page.test.tsx delete mode 100644 apps/web/src/pages/skills-page.test.tsx delete mode 100644 apps/web/src/pages/workflow-detail-page.test.tsx delete mode 100644 apps/web/src/routes.test.tsx delete mode 100644 apps/web/src/settings/account-section.test.tsx delete mode 100644 apps/web/src/settings/api-request.test.ts delete mode 100644 apps/web/src/settings/api.test.ts delete mode 100644 apps/web/src/settings/grants-section.test.tsx delete mode 100644 apps/web/src/settings/identity.test.ts delete mode 100644 apps/web/src/settings/people-section.test.tsx delete mode 100644 apps/web/src/settings/query-retry.test.tsx delete mode 100644 apps/web/src/settings/raw-id-sweep.test.tsx delete mode 100644 apps/web/src/settings/resource-vocabulary.test.ts delete mode 100644 apps/web/src/settings/roles-section.test.tsx delete mode 100644 apps/web/src/settings/section-registry.test.tsx delete mode 100644 apps/web/src/settings/shell.test.tsx delete mode 100644 apps/web/src/settings/strings.test.ts delete mode 100644 apps/web/src/settings/tenancy-api.test.ts delete mode 100644 apps/web/src/shell-chrome-wiring.test.tsx delete mode 100644 apps/web/src/shell/bench-activity.test.tsx delete mode 100644 apps/web/src/shell/canvas-column.test.tsx delete mode 100644 apps/web/src/shell/context-menu/lib/context-menu-view.test.tsx delete mode 100644 apps/web/src/shell/context-menu/lib/dialog-guard.test.ts delete mode 100644 apps/web/src/shell/context-menu/lib/target-resolver.test.ts delete mode 100644 apps/web/src/shell/context-menu/lib/use-context-menu-state.test.tsx delete mode 100644 apps/web/src/shell/context-menu/lib/use-document-context-menu-trigger.test.tsx delete mode 100644 apps/web/src/shell/layout/shell-layout-hook.test.tsx delete mode 100644 apps/web/src/shell/layout/use-scroll-reset.test.tsx delete mode 100644 apps/web/src/shell/sidebar.test.tsx delete mode 100644 apps/web/src/shell/stage-top-bar.test.tsx delete mode 100644 apps/web/src/shell/workbench-list.test.tsx delete mode 100644 apps/web/src/stage-chrome-consistency.test.tsx delete mode 100644 apps/web/src/stage-search-filter.test.tsx delete mode 100644 apps/web/src/stage-top-nav.test.tsx delete mode 100644 apps/web/src/tailwind-build.test.ts delete mode 100644 apps/web/src/theme-default.test.tsx delete mode 100644 apps/web/src/toast-single-system.test.tsx delete mode 100644 e2e/agent-directory/tool-package-version.drizzle.test.ts delete mode 100644 e2e/browser/walkthrough.ts diff --git a/AGENTS.md b/AGENTS.md index c0a157518b..31a480e739 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,7 +72,6 @@ Env flags (unset behavior): | `HUB_DATA_DIR` | hub boot fails — required runtime config | | `CI` | set by GitHub Actions; not a caller flag | | `E2E_PROVIDER` / `E2E_PROVIDER_API_KEY` / `OLLAMA_BASE_URL` | live-inference e2e stays on the noop/stub path | -| `CHROME_PATH` | browser walkthrough falls back to platform defaults, fails if none exist | ## Tests diff --git a/apps/web/src/agents-api.test.ts b/apps/web/src/agents-api.test.ts deleted file mode 100644 index ff71284ec5..0000000000 --- a/apps/web/src/agents-api.test.ts +++ /dev/null @@ -1,401 +0,0 @@ -// The Agents page directory client: model catalog path/shape and failure -// isolation so a broken picker never blanks definitions and instances. - -import { afterEach, describe, expect, test } from "bun:test"; - -import { ApiQueryError } from "@/lib/api-query"; - -import { - clearAgentModel, - draftAgentDefinition, - getAgentDefinitionBySlug, - listCatalogModels, - loadAgentDirectory, - updateAgentSkills, -} from "./agents-api"; - -const realFetch = globalThis.fetch; - -afterEach(() => { - globalThis.fetch = realFetch; -}); - -type RecordedCall = { readonly path: string; readonly method: string }; - -function stubFetch(respond: (path: string) => Response): RecordedCall[] { - const calls: RecordedCall[] = []; - globalThis.fetch = ((input: RequestInfo | URL, init?: RequestInit) => { - const full = - typeof input === "string" - ? input - : `${new URL(String(input)).pathname}${new URL(String(input)).search}`; - const path = typeof input === "string" ? input : full; - calls.push({ path, method: init?.method ?? "GET" }); - // Matchers key on the path-with-query the client builds. - return Promise.resolve(respond(path)); - }) as typeof fetch; - return calls; -} - -const json = (body: unknown, status = 200) => - new Response(JSON.stringify(body), { - status, - headers: { "content-type": "application/json" }, - }); - -const modelFixture = { - id: "mdl_1", - tenantId: "tnt_1", - canonicalName: "anthropic/claude-sonnet-4", - displayName: "Claude Sonnet 4", - description: null, - disabled: false, - createdAt: "2026-01-01T00:00:00.000Z", - updatedAt: "2026-01-01T00:00:00.000Z", -}; - -const disabledModel = { - ...modelFixture, - id: "mdl_2", - canonicalName: "disabled/model", - disabled: true, -}; - -const definitionFixture = { - id: "wfd_1", - tenantId: "tnt_1", - name: "Researcher", - description: "Answers research questions", - currentVersion: "1", - status: "deployed" as const, - createdAt: "2026-01-01T00:00:00.000Z", - updatedAt: "2026-01-01T00:00:00.000Z", -}; - -const instanceFixture = { - id: "ins_1", - definitionId: "wfd_1", - definitionName: "Researcher", - tenantId: "tnt_1", - address: "ins_1@acme.localhost", - status: "running" as const, - createdAt: "2026-01-01T00:00:00.000Z", - updatedAt: "2026-01-01T00:00:00.000Z", -}; - -describe("listCatalogModels", () => { - test("fetches the paginated catalog models endpoint", async () => { - const calls = stubFetch((path) => { - expect(path.startsWith("/api/tenants/tnt_1/catalog/models")).toBe(true); - return json({ data: [modelFixture, disabledModel], nextCursor: null }); - }); - - const models = await listCatalogModels("tnt_1"); - expect(calls[0]?.path).toContain("/api/tenants/tnt_1/catalog/models"); - expect(models).toEqual([modelFixture]); - }); - - test("drops embedding-named, hf.co, and .gguf catalog rows from the picker", async () => { - const embedding = { - ...modelFixture, - id: "mdl_embed", - canonicalName: "all-minilm", - displayName: "all-minilm", - }; - const hfPath = { - ...modelFixture, - id: "mdl_hf", - canonicalName: "hf.co/bartowski/Llama-3.2-1B-Instruct-GGUF:Q4_K_M", - displayName: "hf.co/bartowski/Llama-3.2-1B-Instruct-GGUF:Q4_K_M", - }; - const gguf = { - ...modelFixture, - id: "mdl_gguf", - canonicalName: "model.Q4_K_M.gguf", - displayName: "model.Q4_K_M.gguf", - }; - stubFetch(() => - json({ - data: [modelFixture, embedding, hfPath, gguf, disabledModel], - nextCursor: null, - }), - ); - - const models = await listCatalogModels("tnt_1"); - expect(models).toEqual([modelFixture]); - }); - - test("rejects the bare-array discovery shape the wrong endpoint returns", async () => { - stubFetch(() => - json([ - { - id: "mdl_1", - canonicalName: "anthropic/claude-sonnet-4", - offerings: [], - }, - ]), - ); - - await expect(listCatalogModels("tnt_1")).rejects.toThrow(/Unexpected response shape/); - }); -}); - -describe("loadAgentDirectory", () => { - test("loads definitions and instances even when the model catalog fails", async () => { - stubFetch((path) => { - if (path.includes("/workflows/definitions")) { - return json({ data: [definitionFixture], nextCursor: null }); - } - if (path.includes("/workflows/runs")) { - return json({ data: [instanceFixture], nextCursor: null }); - } - if (path.includes("/catalog/models")) { - return json({ error: { message: "catalog down" } }, 503); - } - return json({ error: { message: "unexpected" } }, 500); - }); - - const directory = await loadAgentDirectory("tnt_1"); - expect(directory.definitions).toEqual([definitionFixture]); - expect(directory.instances).toEqual([instanceFixture]); - expect(directory.models).toEqual([]); - expect(directory.modelsError).toMatch(/503|catalog/i); - }); - - test("surfaces a definitions failure as a hard error", async () => { - stubFetch((path) => { - if (path.includes("/workflows/definitions")) { - return json({ error: { message: "nope" } }, 500); - } - if (path.includes("/workflows/runs")) { - return json({ data: [instanceFixture], nextCursor: null }); - } - if (path.includes("/catalog/models")) { - return json({ data: [modelFixture], nextCursor: null }); - } - return json({ error: { message: "unexpected" } }, 500); - }); - - await expect(loadAgentDirectory("tnt_1")).rejects.toThrow(/500/); - }); - - test("returns ready models when the catalog succeeds", async () => { - stubFetch((path) => { - if (path.includes("/workflows/definitions")) { - return json({ data: [definitionFixture], nextCursor: null }); - } - if (path.includes("/workflows/runs")) { - return json({ data: [instanceFixture], nextCursor: null }); - } - if (path.includes("/catalog/models")) { - return json({ data: [modelFixture], nextCursor: null }); - } - return json({ error: { message: "unexpected" } }, 500); - }); - - const directory = await loadAgentDirectory("tnt_1"); - expect(directory.models).toEqual([modelFixture]); - expect(directory.modelsError).toBeUndefined(); - }); - - test("carries each definition's attached skills", async () => { - stubFetch((path) => { - if (path.includes("/workflows/definitions")) { - return json({ data: [definitionFixture], nextCursor: null }); - } - if (path.includes("/workflows/runs")) { - return json({ data: [instanceFixture], nextCursor: null }); - } - if (path.includes("/catalog/models")) { - return json({ data: [modelFixture], nextCursor: null }); - } - if (path.includes("/agent-definitions/skills")) { - return json({ skills: { wfd_1: ["web-research"] } }); - } - return json({ error: { message: "unexpected" } }, 500); - }); - - const directory = await loadAgentDirectory("tnt_1"); - expect(directory.definitionSkills).toEqual({ wfd_1: ["web-research"] }); - expect(directory.skillsError).toBeUndefined(); - }); - - test("a broken skills endpoint keeps the page and surfaces skillsError, never silent empty", async () => { - stubFetch((path) => { - if (path.includes("/workflows/definitions")) { - return json({ data: [definitionFixture], nextCursor: null }); - } - if (path.includes("/workflows/runs")) { - return json({ data: [instanceFixture], nextCursor: null }); - } - if (path.includes("/catalog/models")) { - return json({ data: [modelFixture], nextCursor: null }); - } - if (path.includes("/agent-definitions/skills")) { - return json({ error: { message: "down" } }, 500); - } - return json({ error: { message: "unexpected" } }, 500); - }); - - const directory = await loadAgentDirectory("tnt_1"); - expect(directory.definitions).toEqual([definitionFixture]); - expect(directory.instances).toEqual([instanceFixture]); - expect(directory.definitionSkills).toEqual({}); - expect(directory.skillsError).toMatch(/500|down/i); - }); - - // instances come from the native tenant-scoped - // `GET /workflows/runs` top-level listing — the deleted `/top-level-runs` - // route (and its `feed=fires` variant) no longer exists. - test("reads instances from the native top-level GET /workflows/runs listing", async () => { - const calls = stubFetch((path) => { - if (path.includes("/workflows/definitions")) { - return json({ data: [definitionFixture], nextCursor: null }); - } - if (path.includes("/workflows/runs")) { - return json({ data: [instanceFixture], nextCursor: null }); - } - if (path.includes("/catalog/models")) { - return json({ data: [modelFixture], nextCursor: null }); - } - return json({ error: { message: "unexpected" } }, 500); - }); - - const directory = await loadAgentDirectory("tnt_1"); - expect(directory.instances).toEqual([instanceFixture]); - expect(calls.some((c) => c.path.includes("/workflows/runs"))).toBe(true); - expect(calls.some((c) => c.path.includes("/top-level-runs"))).toBe(false); - }); -}); - -describe("updateAgentSkills", () => { - test("PUTs the full replacement skill set and returns it back", async () => { - const calls = stubFetch((path) => { - expect(path).toBe("/api/tenants/tnt_1/agent-definitions/wfd_1/skills"); - return json({ skills: ["web-research"] }); - }); - - const skills = await updateAgentSkills("tnt_1", "wfd_1", ["web-research"]); - expect(calls.length).toBe(1); - expect(calls[0]?.method).toBe("PUT"); - expect(skills).toEqual(["web-research"]); - }); -}); - -describe("getAgentDefinitionBySlug", () => { - // A slug-addressed page must not depend on the definition being inside - // the listing's first (and only) page — it asks the server by name. - test("resolves one definition by name, never through the paginated listing", async () => { - const calls = stubFetch((path) => { - expect(path).toBe("/api/tenants/tnt_1/agent-definitions/by-name/triage-bot"); - return json({ - id: "wfd_1", - tenantId: "tnt_1", - name: "triage-bot", - description: "Triage bot", - currentVersion: "1", - status: "deployed", - createdAt: "2026-08-01T00:00:00.000Z", - updatedAt: "2026-08-01T00:00:00.000Z", - }); - }); - - const definition = await getAgentDefinitionBySlug("tnt_1", "triage-bot"); - expect(definition.id).toBe("wfd_1"); - expect(calls.every((call) => !call.path.includes("limit="))).toBe(true); - }); -}); - -describe("clearAgentModel", () => { - test("DELETEs the model capability rather than posting an empty name", async () => { - const calls = stubFetch((path) => { - expect(path).toBe("/api/tenants/tnt_1/agent-definitions/wfd_1/capabilities/model"); - return json({ skills: [] }); - }); - - await clearAgentModel("tnt_1", "wfd_1"); - expect(calls[0]?.method).toBe("DELETE"); - }); -}); - -const DRAFT_FAILED_MESSAGE = - "Myra couldn't draft a starting prompt for that. Write one yourself, or try again."; - -describe("draftAgentDefinition errors", () => { - test("keeps the envelope userMessage and refId on a 422", async () => { - stubFetch(() => - json( - { - error: { - code: "drafting_failed", - userMessage: DRAFT_FAILED_MESSAGE, - refId: "ref_draft_1", - }, - }, - 422, - ), - ); - - try { - await draftAgentDefinition("tnt_1", { name: "Research Buddy" }); - throw new Error("expected draftAgentDefinition to reject"); - } catch (err) { - expect(err).toBeInstanceOf(ApiQueryError); - const error = err as ApiQueryError; - expect(error.message).toBe(DRAFT_FAILED_MESSAGE); - expect(error.refId).toBe("ref_draft_1"); - expect(error.status).toBe(422); - } - }); - - test("keeps the envelope userMessage and refId on a 500", async () => { - stubFetch(() => - json( - { - error: { - code: "internal_error", - userMessage: "Something went wrong. Please try again.", - refId: "ref_sink_1", - }, - }, - 500, - ), - ); - - try { - await draftAgentDefinition("tnt_1", { name: "Research Buddy" }); - throw new Error("expected draftAgentDefinition to reject"); - } catch (err) { - expect(err).toBeInstanceOf(ApiQueryError); - const error = err as ApiQueryError; - expect(error.message).toBe("Something went wrong. Please try again."); - expect(error.refId).toBe("ref_sink_1"); - expect(error.status).toBe(500); - } - }); - - test("does not treat a legacy {code, message} body as an envelope", async () => { - stubFetch(() => - json( - { - error: { - code: "drafting_failed", - message: "MyraAgentDefinitionDraftingUnavailableError: no myra\n at draft", - }, - }, - 422, - ), - ); - - try { - await draftAgentDefinition("tnt_1", { name: "Research Buddy" }); - throw new Error("expected draftAgentDefinition to reject"); - } catch (err) { - expect(err).toBeInstanceOf(ApiQueryError); - const error = err as ApiQueryError; - expect(error.message).toBe("The server answered 422."); - expect(error.refId).toBeUndefined(); - expect(error.message).not.toContain("no myra"); - } - }); -}); diff --git a/apps/web/src/api.test.tsx b/apps/web/src/api.test.tsx deleted file mode 100644 index 515ac8755c..0000000000 --- a/apps/web/src/api.test.tsx +++ /dev/null @@ -1,140 +0,0 @@ -// The approval mutations this app's approvals page now drives: approve is -// always scope "once" (the hub 400s on "always" — see -// vendor/intx/hub-api/src/routes/approvals.ts), and reject carries an -// optional message. Stub global fetch and assert both the request made and -// how the response is parsed, the same convention the settings-ui and -// bench-ui API clients are tested with. - -import { afterEach, describe, expect, test } from "bun:test"; -import { act } from "react"; -import { createRoot } from "react-dom/client"; -import type { Root } from "react-dom/client"; -import { type } from "arktype"; - -import type { APIQuery } from "@/lib/api-query"; -import { ApiQueryError } from "@/lib/api-query"; - -import { approveApproval, rejectApproval, useAPIQuery } from "./api"; -import { TestQueryProvider } from "./test-query-provider"; - -const realFetch = globalThis.fetch; - -afterEach(() => { - globalThis.fetch = realFetch; -}); - -type RecordedCall = { readonly path: string; readonly init?: RequestInit }; - -function stubFetch(respond: (path: string) => Response): RecordedCall[] { - const calls: RecordedCall[] = []; - globalThis.fetch = ((input: RequestInfo | URL, init?: RequestInit) => { - const path = typeof input === "string" ? input : new URL(String(input)).pathname; - calls.push(init === undefined ? { path } : { path, init }); - return Promise.resolve(respond(path)); - }) as typeof fetch; - return calls; -} - -const json = (body: unknown, status = 200) => - new Response(JSON.stringify(body), { - status, - headers: { "content-type": "application/json" }, - }); - -const approvalFixture = { - id: "apr_1", - tenantId: "tnt_1", - anchorRunId: "run_anchor1", - runId: "run_1", - agentAddress: "agent://tnt_1/billing-bot", - correlationId: "corr_1", - toolDefinition: { name: "send_email" }, - toolArguments: { to: "person@example.com" }, - scope: "once", - status: "approved", - timeoutAt: null, - resolvedAt: "2026-01-01T00:00:00.000Z", - createdAt: "2026-01-01T00:00:00.000Z", - updatedAt: "2026-01-01T00:00:00.000Z", -}; - -describe("approveApproval", () => { - test("always sends scope 'once'", async () => { - const calls = stubFetch(() => json(approvalFixture)); - await approveApproval("tnt_1", "apr_1"); - expect(calls[0]?.path).toBe("/api/tenants/tnt_1/approvals/apr_1/approve"); - expect(JSON.parse(String(calls[0]?.init?.body))).toEqual({ - scope: "once", - }); - }); - - test("throws an ApiQueryError on a non-2xx response", async () => { - stubFetch(() => json({ error: { code: "already_resolved" } }, 409)); - await expect(approveApproval("tnt_1", "apr_1")).rejects.toBeInstanceOf(ApiQueryError); - }); -}); - -describe("rejectApproval", () => { - test("omits the message body when none is given", async () => { - const calls = stubFetch(() => json({ ...approvalFixture, status: "rejected" })); - await rejectApproval("tnt_1", "apr_1"); - expect(JSON.parse(String(calls[0]?.init?.body))).toEqual({}); - }); - - test("sends the message when given", async () => { - const calls = stubFetch(() => json({ ...approvalFixture, status: "rejected" })); - await rejectApproval("tnt_1", "apr_1", "Not now"); - expect(JSON.parse(String(calls[0]?.init?.body))).toEqual({ - message: "Not now", - }); - }); -}); - -describe("useAPIQuery", () => { - const PingSchema = type({ ok: "boolean" }); - - async function mountHook(path: string): Promise<{ - readonly latest: () => APIQuery<{ ok: boolean }>; - readonly root: Root; - readonly container: HTMLDivElement; - }> { - let latest: APIQuery<{ ok: boolean }> = { kind: "loading" }; - function Probe() { - latest = useAPIQuery(path, PingSchema); - return null; - } - const container = document.createElement("div"); - document.body.appendChild(container); - const root = createRoot(container); - await act(async () => { - root.render( - - - , - ); - }); - return { latest: () => latest, root, container }; - } - - async function settle() { - for (let i = 0; i < 20; i++) { - await act(async () => { - await new Promise((resolve) => setTimeout(resolve, 0)); - }); - } - } - - test("a non-2xx response reports the error kind carrying the response status", async () => { - stubFetch(() => json({ error: { code: "not_found" } }, 404)); - const { latest, root, container } = await mountHook("/api/ping"); - await settle(); - expect(latest()).toEqual({ - kind: "error", - message: "Something went wrong. Try again.", - retry: expect.any(Function), - status: 404, - }); - root.unmount(); - container.remove(); - }); -}); diff --git a/apps/web/src/app-error-boundary.test.tsx b/apps/web/src/app-error-boundary.test.tsx deleted file mode 100644 index e8a670198c..0000000000 --- a/apps/web/src/app-error-boundary.test.tsx +++ /dev/null @@ -1,74 +0,0 @@ -// a render error anywhere in the tree used to leave the reader -// staring at a blank white page (no error boundary existed anywhere in -// apps/web). This pins the boundary's red/green behaviour — a throwing -// child renders the designed EmptyState, never a blank screen — and that -// a healthy tree passes straight through untouched. - -import { afterEach, describe, expect, test } from "bun:test"; -import { act } from "react"; -import type { ReactNode } from "react"; -import { createRoot } from "react-dom/client"; -import type { Root } from "react-dom/client"; - -import { AppErrorBoundary } from "./app-error-boundary"; - -let container: HTMLDivElement | null = null; -let root: Root | null = null; - -afterEach(() => { - if (root !== null) act(() => root?.unmount()); - if (container !== null) container.remove(); - root = null; - container = null; -}); - -function Bomb(): never { - throw new Error("kaboom"); -} - -function render(children: ReactNode) { - container = document.createElement("div"); - document.body.appendChild(container); - root = createRoot(container); - act(() => root?.render({children})); - return container; -} - -describe("AppErrorBoundary", () => { - test("a healthy tree passes straight through", () => { - const el = render(

Everything is fine

); - expect(el.textContent).toContain("Everything is fine"); - }); - - test("a throwing child renders the empty state, not a blank screen", () => { - // React logs the caught error to the real console during this render; - // that's expected noise for a deliberately-throwing test component. - const originalError = console.error; - console.error = () => undefined; - let el: HTMLDivElement; - try { - el = render(); - } finally { - console.error = originalError; - } - expect(el.textContent).toContain("This screen hit a snag"); - expect(el.textContent).toContain("Reload"); - expect(el.textContent).not.toContain("Everything is fine"); - }); - - // a render crash that never reaches a sink is undiagnosable in - // production — the boundary must report through `reportError` - // (`@corbits/error-sink`) and show the refId that call returns, so a - // person hitting this can quote it back to support. - test("a caught render error surfaces a refId a person can quote", () => { - const originalError = console.error; - console.error = () => undefined; - let el: HTMLDivElement; - try { - el = render(); - } finally { - console.error = originalError; - } - expect(el.textContent).toMatch(/Reference: [0-9a-z]+-[0-9a-z]+/); - }); -}); diff --git a/apps/web/src/app-shell-setup-note-removed.test.tsx b/apps/web/src/app-shell-setup-note-removed.test.tsx deleted file mode 100644 index 21762a7578..0000000000 --- a/apps/web/src/app-shell-setup-note-removed.test.tsx +++ /dev/null @@ -1,99 +0,0 @@ -// Regression test: the owner's first-run feedback was explicit — the -// "still setting up in the background" note is noise at the exact moment -// someone is forming a first impression, and it must never appear. This -// mounts the real shell with the legacy session flag set (as a stale -// browser tab from before the removal would have it) and asserts the -// note cannot render, proving removal rather than just a hidden default. - -import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { act } from "react"; -import { createRoot, type Root } from "react-dom/client"; - -import { AppShell } from "./shell/app-shell"; -import { BenchProvider } from "./bench-context"; -import { NavigationProvider } from "./navigation"; -import { ShellChromeProvider } from "./shell/shell-chrome-provider"; -import { TestQueryProvider } from "./test-query-provider"; - -const noop = () => undefined; -const realFetch = globalThis.fetch; -const realMatchMedia = window.matchMedia; - -const user = { id: "user_1", name: "Ada Lovelace", email: "ada@example.com" }; - -const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); -const settle = () => act(() => sleep(10)); - -function stubMatchMedia(matching: Record): void { - window.matchMedia = ((media: string) => - ({ - media, - matches: matching[media] ?? false, - addEventListener: noop, - removeEventListener: noop, - }) as unknown as MediaQueryList) as typeof window.matchMedia; -} - -const emptyMemberships = () => - new Response(JSON.stringify({ data: [], nextCursor: null }), { - status: 200, - headers: { "content-type": "application/json" }, - }); - -// A pending provisioning status is exactly the shape that used to flip the -// note visible once the legacy session flag was set. -const pendingProvisioningStatus = () => - new Response(JSON.stringify({ kind: "provisioning", setupAgentReady: true }), { - status: 200, - headers: { "content-type": "application/json" }, - }); - -describe("app shell no longer shows the background setup note", () => { - let container: HTMLDivElement; - let root: Root; - - beforeEach(() => { - stubMatchMedia({}); - sessionStorage.setItem("workbench.setup-in-progress", "1"); - globalThis.fetch = ((input: RequestInfo | URL) => { - const url = typeof input === "string" ? input : String(input); - return Promise.resolve( - url.includes("provisioning-status") ? pendingProvisioningStatus() : emptyMemberships(), - ); - }) as typeof fetch; - container = document.createElement("div"); - document.body.appendChild(container); - root = createRoot(container); - }); - - afterEach(() => { - act(() => root.unmount()); - container.remove(); - globalThis.fetch = realFetch; - window.matchMedia = realMatchMedia; - sessionStorage.clear(); - }); - - test("renders no setup note text or markup, even with the legacy flag set", async () => { - await act(async () => { - root.render( - - - - - - {"Inbox"} - - - - - , - ); - }); - await settle(); - await settle(); - - expect(container.textContent).not.toContain("still setting up"); - expect(container.querySelector(".setup-progress-note")).toBeNull(); - }); -}); diff --git a/apps/web/src/auth.test.tsx b/apps/web/src/auth.test.tsx deleted file mode 100644 index 7efa28ffef..0000000000 --- a/apps/web/src/auth.test.tsx +++ /dev/null @@ -1,294 +0,0 @@ -// The auth gate, tested at our wiring: the session probe and the -// email+password calls are the only fetches the signed-out app knows how to -// make, and the signed-out tree contains the auth screen instead of any -// screen that talks to the hub. - -import { ThemeProvider } from "@corbits/react-ui"; -import { afterEach, describe, expect, test } from "bun:test"; -import { useCallback, useEffect, useState } from "react"; -import { act } from "react"; -import { createRoot } from "react-dom/client"; -import type { Root } from "react-dom/client"; -import { renderToStaticMarkup } from "react-dom/server"; - -import { App } from "./app"; -import { AuthScreen } from "./auth-screen"; -import { SOCIAL_SIGN_IN_PROVIDERS, fetchSession, signIn, signOut, signUp } from "./session"; -import type { SessionState } from "./session"; - -const realFetch = globalThis.fetch; - -afterEach(() => { - globalThis.fetch = realFetch; -}); - -type RecordedCall = { readonly path: string; readonly init?: RequestInit }; - -function stubFetch( - respond: (path: string) => Response = () => { - throw new Error("unexpected fetch"); - }, -): RecordedCall[] { - const calls: RecordedCall[] = []; - globalThis.fetch = ((input: RequestInfo | URL, init?: RequestInit) => { - const path = typeof input === "string" ? input : new URL(String(input)).pathname; - calls.push(init === undefined ? { path } : { path, init }); - return Promise.resolve(respond(path)); - }) as typeof fetch; - return calls; -} - -const json = (body: unknown, status = 200) => - new Response(JSON.stringify(body), { - status, - headers: { "content-type": "application/json" }, - }); - -const user = { id: "user_1", name: "ada", email: "ada@example.com" }; - -const noop = () => undefined; - -function renderApp(session: SessionState, path = "/"): string { - return renderToStaticMarkup( - - - , - ); -} - -describe("session probe", () => { - test("a null body means signed out, via the one auth endpoint", async () => { - const calls = stubFetch(() => json(null)); - const state = await fetchSession(); - expect(state).toEqual({ kind: "signed-out" }); - expect(calls.map((call) => call.path)).toEqual(["/api/auth/get-session"]); - }); - - test("a session body means signed in", async () => { - stubFetch(() => json({ session: { id: "sess_1" }, user })); - const state = await fetchSession(); - expect(state).toEqual({ kind: "signed-in", user }); - }); - - test("a hub failure is an error state, not a sign-out", async () => { - stubFetch(() => json({ message: "boom" }, 500)); - const state = await fetchSession(); - expect(state.kind).toBe("error"); - }); - - // a session body that parses as JSON but carries no usable - // `user` (the shape a hub restarted against an empty DB, or a cookie - // for a since-deleted user, can answer with) must still land on login - // — never the "connection lost" error screen, which would strand the - // app in a broken half-state instead of the auth screen. - test("a session body with no user means signed out, not a connectivity error", async () => { - stubFetch(() => json({ session: { id: "sess_1" } })); - const state = await fetchSession(); - expect(state).toEqual({ kind: "signed-out" }); - }); -}); - -/** Mirrors `main.tsx`'s `Root`'s own probe-on-mount wiring, minus - * provisioning — the minimum needed to prove a real DOM mount, driven by - * the real `fetchSession`, never renders the shell for an invalid session. - * `/login` is a real route now: an invalid session redirects - * there rather than swapping in the auth screen at whatever path was - * requested, so this mounts a real `navigate` to follow that redirect. */ -function ProbedApp() { - const [path, setPath] = useState("/"); - const [session, setSession] = useState({ kind: "loading" }); - const navigate = useCallback((to: string) => { - setPath(new URL(to, "http://localhost").pathname); - }, []); - useEffect(() => { - void fetchSession().then(setSession); - }, []); - return ( - - - - ); -} - -describe("an invalid session always lands on login", () => { - let container: HTMLDivElement | null = null; - let root: Root | null = null; - - afterEach(() => { - globalThis.fetch = realFetch; - if (root !== null) { - act(() => root?.unmount()); - root = null; - } - if (container !== null) { - container.remove(); - container = null; - } - }); - - test("a null session probe renders the login screen, never the shell", async () => { - stubFetch(() => json(null)); - container = document.createElement("div"); - document.body.appendChild(container); - root = createRoot(container); - await act(async () => { - root?.render(); - }); - await act(async () => { - await new Promise((resolve) => setTimeout(resolve, 0)); - }); - - expect(container.querySelector('[data-testid="shell-sidebar"]')).toBeNull(); - expect(container.textContent).toContain("Welcome back"); - }); -}); - -describe("email and password calls", () => { - test("sign-in posts credentials to better-auth's endpoint", async () => { - const calls = stubFetch(() => json({ token: "t", user })); - const result = await signIn("ada@example.com", "hunter22"); - expect(result).toEqual({ ok: true, user }); - expect(calls).toHaveLength(1); - const call = calls[0]; - expect(call?.path).toBe("/api/auth/sign-in/email"); - expect(call?.init?.method).toBe("POST"); - expect(JSON.parse(String(call?.init?.body))).toEqual({ - email: "ada@example.com", - password: "hunter22", - }); - }); - - test("sign-up posts a name derived from the address", async () => { - const calls = stubFetch(() => json({ token: "t", user })); - const result = await signUp("ada@example.com", "hunter22"); - expect(result).toEqual({ ok: true, user }); - expect(calls[0]?.path).toBe("/api/auth/sign-up/email"); - expect(JSON.parse(String(calls[0]?.init?.body))).toEqual({ - name: "ada", - email: "ada@example.com", - password: "hunter22", - }); - }); - - test("a rejected sign-in surfaces the hub's message", async () => { - stubFetch(() => json({ message: "Invalid email or password" }, 401)); - const result = await signIn("ada@example.com", "wrong"); - expect(result).toEqual({ - ok: false, - message: "Invalid email or password", - }); - }); - - test("sign-out posts to better-auth's endpoint", async () => { - const calls = stubFetch(() => json({ success: true })); - await signOut(); - expect(calls.map((call) => call.path)).toEqual(["/api/auth/sign-out"]); - }); -}); - -describe("the gate", () => { - test("signed out at /login renders the auth screen and fires no authenticated fetch", () => { - const calls = stubFetch(); - const markup = renderApp({ kind: "signed-out" }, "/login"); - expect(calls).toHaveLength(0); - expect(markup).toContain("Email"); - expect(markup).toContain("Password"); - expect(markup).not.toContain("aria-current"); - expect(markup).not.toContain("/api/me"); - expect(markup).not.toContain("Sign out"); - }); - - test("signed in renders the shell with the sidebar and the avatar's account menu trigger", () => { - const markup = renderApp({ kind: "signed-in", user }); - // The one sidebar plus the account affordance — a menu (weekly usage, - // Settings, feedback, Log out), not a plain link straight to settings - // (grown to the reference shape in). - expect(markup).toContain('data-testid="shell-sidebar"'); - expect(markup).toContain("shell-sidebar-account-btn"); - expect(markup).toContain('aria-label="ada · Account menu"'); - expect(markup).not.toContain("user_1"); - }); - - test("loading and error are their own screens, not a broken shell", () => { - expect(renderApp({ kind: "loading" })).toContain("Getting your workbench ready"); - const markup = renderApp({ kind: "error", message: "socket hang up" }); - expect(markup).toContain("socket hang up"); - expect(markup).toContain("Try again"); - }); -}); - -// Stock Interchange exposes no sign-in discovery endpoint, so the buttons -// come from client config: the providers this client knows how to draw. -// The hub still decides which of them actually work (better-auth only -// wires the credential pairs it was given); an unconfigured click surfaces -// better-auth's own error on the form, never a new endpoint. -describe("social sign-in buttons", () => { - test("the client config lists the providers the sign-in screen offers", () => { - expect([...SOCIAL_SIGN_IN_PROVIDERS].sort()).toEqual(["github", "google"]); - }); - - test("the auth screen draws every client-configured provider button with no fetch", () => { - const calls = stubFetch(); - const markup = renderToStaticMarkup(); - expect(calls).toHaveLength(0); - expect(markup).toContain("Continue with Google"); - expect(markup).toContain("Continue with GitHub"); - }); - - // Keeper: stock Interchange exposes no sign-in discovery endpoint, so the - // buttons always render and the hub decides which providers work — an - // unconfigured click must surface better-auth's own message on the form. - test("a failed social sign-in surfaces better-auth's message on the form", async () => { - const calls = stubFetch(() => - json({ message: "Social sign-in is not configured for this provider" }, 400), - ); - const container = document.createElement("div"); - document.body.appendChild(container); - const root = createRoot(container); - try { - await act(async () => { - root.render(); - }); - const button = [...container.querySelectorAll("button")].find( - (element) => element.textContent === "Continue with Google", - ); - expect(button).toBeDefined(); - await act(async () => { - button?.click(); - await new Promise((resolve) => setTimeout(resolve, 0)); - }); - await act(async () => { - await new Promise((resolve) => setTimeout(resolve, 0)); - }); - expect(calls.map((call) => call.path)).toEqual(["/api/auth/sign-in/social"]); - expect(container.textContent).toContain("Social sign-in is not configured for this provider"); - } finally { - await act(async () => { - root.unmount(); - }); - container.remove(); - } - }); -}); - -describe("auth screen modes", () => { - test("sign-in leads, with creating an account as the toggle", () => { - const markup = renderToStaticMarkup(); - expect(markup).toContain("Welcome back"); - expect(markup).toContain("Create an account"); - }); -}); diff --git a/apps/web/src/auth/dither-background.test.tsx b/apps/web/src/auth/dither-background.test.tsx deleted file mode 100644 index 8797003d6e..0000000000 --- a/apps/web/src/auth/dither-background.test.tsx +++ /dev/null @@ -1,58 +0,0 @@ -// The dithered auth panel, tested at our wiring. This suite runs in bun's -// bare test environment (no DOM, no @testing-library), so it asserts the -// server-rendered markup: effects never run, which is exactly the no-JS -// contract the panel is built around — the canvas must be inert decoration -// and the hero must already be painted underneath it. The animation -// loop itself (rAF scheduling, offscreen pause, reduced-motion) lives inside -// a browser-only effect and is exercised by the browser, not here. - -import { describe, expect, test } from "bun:test"; -import { renderToStaticMarkup } from "react-dom/server"; - -import { AuthLayout } from "./auth-layout"; -import { DitherBackground } from "./dither-background"; -import { QuoteCard } from "./quote-card"; - -describe("DitherBackground", () => { - test("renders a decorative canvas, hidden from assistive tech", () => { - const markup = renderToStaticMarkup(); - expect(markup).toContain(" { - const markup = renderToStaticMarkup(); - expect(markup).toContain("image-rendering:pixelated"); - }); - - test("passes the caller's class through to the canvas", () => { - const markup = renderToStaticMarkup(); - expect(markup).toContain('class="auth-panel-dither"'); - }); -}); - -describe("the auth brand panel", () => { - test("paints the hero image under the canvas as the no-JS fallback", () => { - const markup = renderToStaticMarkup(form); - const imgAt = markup.indexOf("/images/hero-dither.png"); - const canvasAt = markup.indexOf(" { - const markup = renderToStaticMarkup(form); - expect(markup).toContain(" { - test("renders a quote without localStorage (server render has none)", () => { - const markup = renderToStaticMarkup(); - expect(markup).toContain(" { - const url = String(input); - if (url.includes("/chat/workbenches?kind=workbench")) { - return Promise.resolve(jsonResponse({ items: [workbenchWire] })); - } - if (url.includes("/chat/workbenches?kind=chat")) { - return Promise.resolve(jsonResponse({ items: [] })); - } - if (url.includes("/threads")) { - return Promise.resolve( - jsonResponse({ - rootThreadId: "th_1", - items: [ - { - id: "th_1", - kind: "root", - parentMessageId: null, - runRef: null, - title: null, - createdAt: "2026-01-15T12:00:00.000Z", - }, - ], - }), - ); - } - if (url.includes("/messages")) { - return Promise.resolve(jsonResponse({ items: [] })); - } - return Promise.resolve(jsonResponse({})); -} - -const leading =