Skip to content

test: rebuild the testing architecture — Vitest everywhere, self-provisioning integration DB, five-layer contract - #258

Open
leon0399 wants to merge 20 commits into
masterfrom
chore/test-architecture
Open

test: rebuild the testing architecture — Vitest everywhere, self-provisioning integration DB, five-layer contract#258
leon0399 wants to merge 20 commits into
masterfrom
chore/test-architecture

Conversation

@leon0399

@leon0399 leon0399 commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Summary

Rebuilds the testing architecture around one explicit contract (docs/testing.md), replacing four runners and three meanings of .spec.ts with a five-layer pyramid built from stock tools. Design was validated by a three-model review panel against twelve placement scenarios before implementation, then simplified a second time toward standard solutions.

What changed

  • Jest → Vitest for apps/api (80 files): unplugin-swc supplies the decorator metadata Nest DI silently loses under esbuild; ts-jest, the ESM transformIgnorePatterns hacks, jest-e2e.json, forceExit, and six Jest devDependencies are gone.
  • One naming rule: *.test.ts(x) is Vitest everywhere (.integration infix = needs real Postgres); root e2e/ is Playwright's island with its .spec.ts convention. nest g no longer scaffolds spec files.
  • Self-provisioning integration suite: a Testcontainers globalSetup starts a throwaway Postgres and reproduces the worst-case self-hosted topology (non-superuser app role owns the schema and runs migrations, app_rls BYPASSRLS function owner) — so pnpm --filter api test:integration is the whole story locally and in CI, and rls-test.sh (150 lines of container bash) is deleted. TEST_DATABASE_URL overrides for docker-less machines. Nothing silently skips anymore: the old DB-backed suites green-skipped to zero tests without env.
  • The api HTTP-e2e layer is dissolved into integration: supertest suites are integration tests of the HTTP boundary, moved into src/ feature modules (src/testing/ holds shared helpers). One DB-backed project, one command.
  • Root e2e/ per product surface: e2e/web/ suites, e2e/support/ boot infra; a future non-browser surface adds e2e/<surface>/.
  • Turbo correctness: test:integration / test:evals are cache: false — their outcome depends on live state turbo's hash cannot see.
  • AGENTS.md files across all workspaces updated to the new contract; CHANGELOG entry included.

Real bugs found by the migration

  • worker-harness.ts let .env.local's dev-database URL (leaked into process.env by ConfigModule.forRoot) win over TEST_DATABASE_URL — worker suites would have run against the dev database. Now an unconditional override.
  • rls-test.sh ran active-runs.integration twice (redundant since feat(web): background run-completion notifications, surviving a page reload #132).
  • worker.module.spec.ts required a real Postgres while silently self-skipping inside the unit suite — reclassified.

Verification

  • Unit: 507 tests green repo-wide (turbo run test).
  • Integration: 30 files / 256 tests green including the Testcontainers self-provisioning path (also green via the TEST_DATABASE_URL override path).
  • Browser e2e: full local run boots and executes end-to-end; remaining local failures carry the pre-existing locale-RangeError environment signature present on master — this job's CI run is authoritative.
  • Lint, typecheck (tsgo), format, build: green across the workspace.

Deliberate follow-ups (docs/testing.md § Follow-ups)

Story-migration of eligible apps/web component tests per the ≤2-mocks rubric; replacing the source-regex hydration test; removing dead describeIfDb guards; re-enabling four vitest/* oxlint rules.


Summary by cubic

Rebuilds the testing architecture with vitest across the repo and a self‑provisioned Postgres via Testcontainers, merging API HTTP e2e into the integration layer. Also migrates eligible apps/web tests to Storybook play tests, fixes the double‑resume race in chat, and further stabilizes browser e2e by raising the API rate‑limit ceiling for the harness.

  • Refactors

    • apps/api: Jest → vitest via unplugin-swc; removed Jest configs/deps; tsconfig types vitest/globals; .oxlintrc switches env to vitest and adds the vitest plugin.
    • One naming rule: *.test.ts(x) for unit; .integration.test.ts needs a real DB; root e2e/*.test.ts stays @playwright/test; nest-cli.json stops scaffolding specs.
    • Integration DB auto‑provisioned by Testcontainers (owner app + app_rls init SQL); TEST_DATABASE_URL override still works; removed apps/api/scripts/rls-test.sh.
    • HTTP-boundary tests folded into src/**/.integration.test.ts with shared helpers in src/testing/; one DB-backed project, one command.
    • Evals: qa-evals lives in the integration project, stays opt‑in behind RUN_MODEL_EVALS; docs state the self‑provisioned DB contract.
    • Isolation: per‑suite PGBOSS_SCHEMA randomized at module eval to prevent cross‑file job stealing.
    • CI mirrors local; turbo marks test:integration/test:evals as cache:false; Node pinned to 22.23.1.
    • apps/web: migrate low‑friction component tests to Storybook play tests; reduce jsdom tests where stories cover behavior; add sb.mock seams for chat/runs, chat/fork, and models/queries.
  • Bug Fixes

    • worker-harness.ts: force TEST_DATABASE_URL to override .env.local’s POSTGRES_URL.
    • Removed a duplicate active-runs.integration invocation; DB-backed suites now fail loudly when misconfigured.
    • Browser e2e: serve the runs queue at real concurrency (8) and remove dead RUN_EXECUTION_MODE; update selectors and chip text for Base UI/AI Elements.
    • Browser e2e: add API_RATE_LIMIT_PER_MINUTE (env-tunable, default 300) and raise it for the harness to avoid shared-IP 429s; assertions now report received status codes.
    • Browser e2e: wait for the “Reply ready” toast to dismiss before the second send to avoid click interception.
    • E2e config: fixed $schema path after the e2e/support/ move.
    • Web a11y: chat activity indicator is now a native <output> with role=status; model selector trigger and dialog are labelled.
    • Web chat resume race: run is resumed once per mount (passes resume: false and drives resumeStream() behind a guard), fixing duplicated answers and a TypeError on reload; e2e page fixture now fails tests on uncaught client exceptions.
    • Browser e2e: key cached auth state by account id (not worker slot) to prevent mismatched cookies on retries that caused 404s on GET /chats/:id/messages.

Written for commit 1d285c0. Summary will update on new commits.

Review in cubic

Update: browser-e2e investigated and mostly revived in this PR

The job had been red on master for 11 consecutive days (since Jul 20). CI-history bisection + failure-artifact analysis found four independent causes stacked behind the already-red gate:

  1. Stale Radix selector — org-units scoped dialogs by [data-state="open"]; Base UI (Migrate packages/ui from Radix to Base UI (base-nova) + canonical re-surface #238) uses data-open, so the selector matched nothing. Fixed here.
  2. Stale badge string — tool-loop asserted the pre-Replace the chat UI with Vercel AI Elements #239 chip text "done"; AI Elements says "Completed". The failure snapshot shows the whole tool flow rendering correctly. Fixed here.
  3. Dead concurrency config — the e2e api served the runs queue at concurrency 1 (RUN_EXECUTION_MODE env was removed by the durable-workers refactor and silently did nothing); measured throughput decayed 8→24s/turn during runs. Now concurrency 8 via llame.config.e2e.json. (Necessary, but experimentally NOT the cause of the persistent failures.)
  4. Two real product regressions the red gate hid, tracked separately: Resumed/streamed assistant message renders duplicated partial+final paragraphs (strict-mode violations across chat e2e) #259 (assistant answers render duplicated partial+final paragraphs on stream/resume — strict-mode violations) and Browser crash during chat runs: TypeError reading 'state' in the AI Elements chat UI #260 (browser TypeError: reading 'state' during tool runs).

Result on this branch: browser-e2e went from 6–7 failures to 16 passed / 2 failed / 1 flaky in 2.5m — the remaining red is exactly the #259 family. All other jobs green (rls = the Testcontainers path, 2m).

Also queued in docs/testing.md follow-ups: consolidating model test doubles onto ai/test's official MockLanguageModelV3 pattern (already used by two integration tests) instead of the 16 hand-forged ReturnType<typeof streamText> casts.

leon0399 and others added 9 commits July 31, 2026 17:33
One runner across the workspace, three explicit projects (unit /
integration / e2e) instead of two Jest configs with pattern games:

- unplugin-swc emits the decorator metadata NestJS DI needs (vitest's
  esbuild default silently resolves every provider as undefined)
- integration and e2e projects FAIL LOUDLY without a database instead of
  green-skipping to zero tests
- jest-e2e's forceExit is gone: suites run sequentially in one worker
  thread (the --runInBand equivalent) and exit cleanly
- rls-test.sh drives the new pnpm scripts; the duplicate
  active-runs.integration invocation (already covered by the
  .integration glob since #132) is dropped
- worker-harness now overrides POSTGRES_URL unconditionally from
  TEST_DATABASE_URL: ConfigModule leaks a developer's .env.local dev-DB
  URL into process.env, and the old !POSTGRES_URL guard silently pointed
  harness suites at the dev database
- test:integration/test:e2e/test:evals turbo tasks are cache: false —
  their pass/fail depends on live DB and model state turbo cannot hash

Verified: 507 unit, 192 integration (8 formerly-skipped worker tests now
run), 62 e2e — all green against a provisioned worst-case-owner Postgres.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BCPobQSCvqFqbgHk2aZzo
- src/**/*.spec.ts -> *.test.ts; *.integration.spec.ts -> *.integration.test.ts
- worker.module.spec.ts reclassified as *.integration.test.ts — it needs a
  real Postgres and was silently self-skipping inside the unit suite
- test/*.e2e-spec.ts -> e2e/*.test.ts (support.ts and the pg-boss
  schema-isolation setup move with it)
- qa-evals extracted to evals/ as its own opt-in vitest project
  (test:evals), out of the e2e include

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BCPobQSCvqFqbgHk2aZzo
Root e2e/ is product-level tests through a user-facing surface, one
subdirectory per surface: e2e/web/ holds today's browser suites, a future
surface (bot, CLI) adds its own e2e/<surface>/ instead of competing for
the root. Shared boot infra (db/model servers, fixtures, auth helpers)
moves to e2e/support/. App-scoped suites live with their app
(apps/api/e2e) — they never belonged here.

Verified: playwright test --list collects all 19 tests in 6 files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BCPobQSCvqFqbgHk2aZzo
scripts/check-test-naming.mjs scans git ls-files for the three violations
the runners' include globs would silently ignore: ".spec" outside root
e2e/, ".test" inside it, and the retired ".e2e-spec". Wired as a plain
step in the checks job and a lefthook pre-commit job — deliberately not a
turbo task, so per-package input hashing can never replay a stale pass
for a repo-wide invariant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BCPobQSCvqFqbgHk2aZzo
…ce sweep

The pyramid, the suffix-is-runner naming contract, placement rules
(including what a new app or surface inherits), the no-silent-skip and
no-cached-live-state rules, CI mapping, and the deliberate follow-up
list. AGENTS.md files updated off the Jest wording; CHANGELOG records
the rebuild.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BCPobQSCvqFqbgHk2aZzo
…gration; drop the naming cop

Three simplifications, one theme — replace bespoke governance with stock
tools and fewer categories:

- Testcontainers globalSetup provisions the throwaway worst-case-owner
  Postgres (non-superuser `app` owns + migrates, `app_rls` BYPASSRLS
  function owner) — `pnpm --filter api test:integration` is now the whole
  story, locally and in CI. rls-test.sh (150 lines of container bash) is
  deleted; TEST_DATABASE_URL still overrides for docker-less machines.
- The api HTTP e2e layer is gone as a category: supertest suites are
  integration tests of the HTTP boundary, moved into src/ feature dirs as
  *.integration.test.ts (support helpers in src/testing/). One DB-backed
  project, one command; the per-suite PGBOSS_SCHEMA isolation now covers
  every integration file.
- check-test-naming.mjs, its CI step, and its lefthook job are deleted:
  with one Vitest suffix and Playwright confined to root e2e/, directory
  does all the work — and nest-cli generateOptions.spec=false stops the
  scaffolder emitting .spec.ts files at the source.

Also: maxWorkers=2 (a WSL2-local constraint) no longer hardcoded.

Verified: merged integration project 30 files / 256 tests green via the
TEST_DATABASE_URL path (container path exercises in CI); unit 507 green;
typecheck/lint/build green; Playwright collects 19/6.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BCPobQSCvqFqbgHk2aZzo
Root conventions gain the one-line naming/placement rule; api commands
surface test:integration as its own line and Structure lists src/testing/
and evals/; web gains the stories-vs-jsdom placement gotcha (its biggest
guidance gap); storybook's test/ is named the sanctioned tooling-guard
home; packages/ui states that play-function stories ARE the
component-test layer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BCPobQSCvqFqbgHk2aZzo
…evel deeper

Caught by a local full-suite run: the rls-function-owner.sql path resolved
to e2e/docker/... after the e2e/support/ move, failing the db webServer
at startup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BCPobQSCvqFqbgHk2aZzo
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
llame-storybook Ready Ready Preview Aug 1, 2026 12:43am

@mesa-dot-dev

mesa-dot-dev Bot commented Jul 31, 2026

Copy link
Copy Markdown

You do not have enough credits to review this pull request. Please purchase more credits to continue.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Too many files!

This PR contains 152 files, which is 52 over the limit of 100.

To get a review, narrow the scope:
• coderabbit review --committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ba6c569c-7875-41d1-baf1-d48f1f947645

📥 Commits

Reviewing files that changed from the base of the PR and between 870b682 and 1d285c0.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (152)
  • .github/workflows/ci.yml
  • .node-version
  • AGENTS.md
  • CHANGELOG.md
  • apps/api/.oxlintrc.json
  • apps/api/AGENTS.md
  • apps/api/evals/qa-evals.test.ts
  • apps/api/nest-cli.json
  • apps/api/package.json
  • apps/api/scripts/rls-test.sh
  • apps/api/src/app.controller.test.ts
  • apps/api/src/app.integration.test.ts
  • apps/api/src/app.module.ts
  • apps/api/src/app.setup.test.ts
  • apps/api/src/auth/auth.controller.test.ts
  • apps/api/src/auth/auth.integration.test.ts
  • apps/api/src/auth/auth.service.test.ts
  • apps/api/src/auth/session-auth.guard.test.ts
  • apps/api/src/auth/session-token.service.test.ts
  • apps/api/src/chats/active-runs.integration.test.ts
  • apps/api/src/chats/chat-loop.integration.test.ts
  • apps/api/src/chats/chat-loop.service.test.ts
  • apps/api/src/chats/chat-sharing.integration.test.ts
  • apps/api/src/chats/chats-delete.integration.test.ts
  • apps/api/src/chats/chats-messages.integration.test.ts
  • apps/api/src/chats/chats-repository.test.ts
  • apps/api/src/chats/chats-rls.integration.test.ts
  • apps/api/src/chats/chats-search.integration.test.ts
  • apps/api/src/chats/chats.controller.test.ts
  • apps/api/src/chats/compaction-surfacing.integration.test.ts
  • apps/api/src/chats/context-builder.test.ts
  • apps/api/src/chats/dto/chats.dto.test.ts
  • apps/api/src/chats/fork-chat.integration.test.ts
  • apps/api/src/chats/reasoning-loop.integration.test.ts
  • apps/api/src/chats/reasoning-parts.test.ts
  • apps/api/src/chats/run-execution-tools.integration.test.ts
  • apps/api/src/chats/shared-chats.integration.test.ts
  • apps/api/src/chats/turn-telemetry.test.ts
  • apps/api/src/compaction/compaction-context.integration.test.ts
  • apps/api/src/compaction/compaction.test.ts
  • apps/api/src/compaction/compactions.integration.test.ts
  • apps/api/src/db/migration-journal.test.ts
  • apps/api/src/db/schema/chats.ts
  • apps/api/src/db/schema/model-context.test.ts
  • apps/api/src/db/tenant-db.service.test.ts
  • apps/api/src/identity/dto/identity.dto.test.ts
  • apps/api/src/identity/identity-admin.integration.test.ts
  • apps/api/src/identity/identity-invariants.integration.test.ts
  • apps/api/src/identity/identity-rls.integration.test.ts
  • apps/api/src/identity/org-path.test.ts
  • apps/api/src/identity/org-units.integration.test.ts
  • apps/api/src/identity/role-resolution.test.ts
  • apps/api/src/instance-config/config-loader.test.ts
  • apps/api/src/instance-config/interpolation.test.ts
  • apps/api/src/instance-config/prompt-loader.test.ts
  • apps/api/src/instance-config/schema.test.ts
  • apps/api/src/instance-config/schema.ts
  • apps/api/src/instance-config/worker-profile.service.test.ts
  • apps/api/src/models/keyless-provider.test.ts
  • apps/api/src/models/model-client-factory.test.ts
  • apps/api/src/models/model-client.test.ts
  • apps/api/src/models/models.controller.test.ts
  • apps/api/src/models/models.service.test.ts
  • apps/api/src/models/openai-model-client.tools.test.ts
  • apps/api/src/pins/pins-rls.integration.test.ts
  • apps/api/src/pins/pins.test.ts
  • apps/api/src/projects/projects-rls.integration.test.ts
  • apps/api/src/queue/queue.integration.test.ts
  • apps/api/src/queue/queue.module.ts
  • apps/api/src/queue/queue.type.test.ts
  • apps/api/src/runs/delta-buffer.test.ts
  • apps/api/src/runs/effective-context-resolver.test.ts
  • apps/api/src/runs/model-context-snapshot.test-fixture.test.ts
  • apps/api/src/runs/model-context-snapshots.integration.test.ts
  • apps/api/src/runs/model-context-snapshots.repository.test.ts
  • apps/api/src/runs/run-execution.service.test.ts
  • apps/api/src/runs/run-stream-bridge.test.ts
  • apps/api/src/runs/runs-worker.service.test.ts
  • apps/api/src/runs/runs.controller.test.ts
  • apps/api/src/runs/worker-concurrency.integration.test.ts
  • apps/api/src/runs/worker-harness.ts
  • apps/api/src/runs/worker-liveness.integration.test.ts
  • apps/api/src/search/chat/conversation-chunker.test.ts
  • apps/api/src/search/chat/eval/BASELINE.md
  • apps/api/src/search/chat/eval/search-eval.integration.test.ts
  • apps/api/src/search/core/core.test.ts
  • apps/api/src/search/search-index.integration.test.ts
  • apps/api/src/search/search-reindex.worker.test.ts
  • apps/api/src/testing/support.ts
  • apps/api/src/titles/title.test.ts
  • apps/api/src/tools/registry.test.ts
  • apps/api/src/tools/registry.ts
  • apps/api/src/tools/runner.test.ts
  • apps/api/src/tools/search-conversations.test.ts
  • apps/api/src/users/users.service.test.ts
  • apps/api/src/worker-mode.integration.test.ts
  • apps/api/src/worker.module.integration.test.ts
  • apps/api/test/jest-e2e.json
  • apps/api/test/jest-e2e.setup.ts
  • apps/api/tsconfig.build.json
  • apps/api/tsconfig.json
  • apps/api/turbo.json
  • apps/api/vitest.config.mts
  • apps/api/vitest.integration.global-setup.mts
  • apps/api/vitest.integration.setup.ts
  • apps/storybook/.storybook/preview.tsx
  • apps/storybook/AGENTS.md
  • apps/web/AGENTS.md
  • apps/web/app/(admin)/admin/components/admin-section-nav.stories.tsx
  • apps/web/app/(admin)/admin/components/admin-section-nav.test.tsx
  • apps/web/app/(chat)/components/chat-list-sidebar/chat-activity-indicator.stories.tsx
  • apps/web/app/(chat)/components/chat-list-sidebar/chat-activity-indicator.test.tsx
  • apps/web/app/(chat)/components/chat-list-sidebar/chat-activity-indicator.tsx
  • apps/web/app/(chat)/components/chat-page.compaction.test.tsx
  • apps/web/app/(chat)/components/chat-page.models.test.tsx
  • apps/web/app/(chat)/components/chat-page.tsx
  • apps/web/app/(chat)/components/compaction-boundary.stories.tsx
  • apps/web/app/(chat)/components/compaction-boundary.test.tsx
  • apps/web/app/(chat)/components/effective-context-inspector.stories.tsx
  • apps/web/app/(chat)/components/effective-context-inspector.test.tsx
  • apps/web/app/(chat)/components/message-fork-button.stories.tsx
  • apps/web/app/(chat)/components/message-fork-button.test.tsx
  • apps/web/app/(chat)/components/model-selector.stories.tsx
  • apps/web/app/(chat)/components/model-selector.test.tsx
  • apps/web/app/(chat)/components/model-selector.tsx
  • apps/web/app/(chat)/components/tool-cap-notice-part.stories.tsx
  • apps/web/app/(chat)/components/tool-cap-notice-part.test.tsx
  • apps/web/app/shell/app-sidebar/app-sidebar-admin-entry.stories.tsx
  • apps/web/app/shell/app-sidebar/app-sidebar-admin-entry.test.tsx
  • apps/web/app/shell/app-sidebar/app-sidebar-nav.stories.tsx
  • apps/web/app/shell/app-sidebar/app-sidebar-nav.test.tsx
  • apps/web/lib/services/chat/__mocks__/fork.ts
  • apps/web/lib/services/chat/__mocks__/runs.ts
  • apps/web/lib/services/models/__mocks__/queries.ts
  • docs/testing.md
  • e2e/support/auth-helpers.ts
  • e2e/support/db-server.ts
  • e2e/support/fixtures.ts
  • e2e/support/fixtures/llame.config.e2e.json
  • e2e/support/fixtures/prompts/context-target.md
  • e2e/support/model-server.ts
  • e2e/support/run-after-ready.ts
  • e2e/web/auth/auth.spec.ts
  • e2e/web/chat/chat-flow.spec.ts
  • e2e/web/chat/compaction-checkpoint.spec.ts
  • e2e/web/chat/model-context-transparency.spec.ts
  • e2e/web/chat/seed-compaction.ts
  • e2e/web/chat/tool-loop.spec.ts
  • e2e/web/org-units/org-units.spec.ts
  • package.json
  • packages/ui/AGENTS.md
  • playwright.config.ts

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@mesa-dot-dev

mesa-dot-dev Bot commented Jul 31, 2026

Copy link
Copy Markdown

Mesa Description

TL;DR

Rebuilds the monorepo testing architecture, migrating apps/api from Jest to Vitest and introducing self-provisioning PostgreSQL instances via Testcontainers for database integration tests. Consolidates the end-to-end and integration testing layers, shifts the frontend component-testing strategy to Storybook play-function tests with custom service mocks, and addresses critical bugs including chat-resumption race conditions in React Strict Mode and Playwright e2e stability issues.

What changed?

Testing Infrastructure & Tooling Config

  • .github/workflows/ci.yml: Replaced custom rls-test.sh script execution with unified Testcontainers-backed pnpm --filter api test:integration and separated unit tests from database-backed workflows.
  • .node-version & package.json: Bumped required Node.js version to 22.23.1 (minimum engine >=22.19).
  • apps/api/.oxlintrc.json: Configured Oxlint to support Vitest, adding vitest, unicorn, typescript, and oxc plugins and deprecating legacy Jest rules.
  • apps/api/nest-cli.json: Disabled default automatic spec file generation during Nest CLI scaffold commands.
  • apps/api/package.json: Swapped Jest dependencies with Vitest, unplugin-swc (to retain NestJS decorator metadata under esbuild), and @testcontainers/postgresql.
  • apps/api/tsconfig.json & tsconfig.build.json: Updated TypeScript configurations to use vitest/globals types and refined build exclusions.
  • apps/api/turbo.json: Disabled build-caching for test:integration and test:evals since their outputs depend on dynamic external states (e.g., databases and live models).
  • apps/api/vitest.config.mts: Added a new configuration defining distinct unit and integration testing projects with tailored timeouts.
  • apps/api/vitest.integration.global-setup.mts: Created a global setup script to launch throwaway Postgres containers, configure database roles, and apply Drizzle migrations.
  • apps/api/vitest.integration.setup.ts: Automated generation of unique, randomized PGBOSS_SCHEMA variables per test worker to avoid concurrent execution conflict and job-stealing.
  • playwright.config.ts: Shifted fixtures and e2e support files into e2e/support/, raised execution concurrency to 8, and integrated tunable API rate-limiting environment overrides.

Core API & Service Tests (Jest to Vitest Port)

  • Deleted Legacy Scripts: Removed apps/api/scripts/rls-test.sh, apps/api/test/jest-e2e.json, and apps/api/test/jest-e2e.setup.ts.
  • File Renaming & Relocating: Renamed and moved over 40 *.spec.ts files to *.test.ts (unit) or *.integration.test.ts (integration) and ported spy/mock utilities from jest.fn() to vi.fn().
  • apps/api/src/runs/worker-harness.ts: Updated to unconditionally override POSTGRES_URL with TEST_DATABASE_URL when present, preventing development database pollution during worker execution.
  • apps/api/src/app.module.ts: Configured the rate-limiting middleware (ThrottlerModule) to support custom API_RATE_LIMIT_PER_MINUTE limits to prevent shared-IP 429s during parallel CI test runs.

Browser E2E Tests (e2e/)

  • e2e/support/fixtures.ts: Extended Playwright page fixture with a listener that immediately throws an error and fails tests on uncaught client-side browser exceptions.
  • e2e/web/chat/compaction-checkpoint.spec.ts: Added logic waiting for the "Reply ready" toast message to dismiss before executing a subsequent message entry to avoid click interception.
  • e2e/web/chat/tool-loop.spec.ts: Updated the verification assertion text from 'done' to 'Completed' to match the new AI Elements Tool chip component states.
  • e2e/web/org-units/org-units.spec.ts: Updated dialog selectors from Radix's [data-state="open"] to Base UI's native [data-open].

Web Application & Storybook Testing (Storybook Migration)

  • apps/web/AGENTS.md & packages/ui/AGENTS.md: Established new architectural standards defining Storybook stories with play functions as the primary layer for DOM interaction and visual verification, reserving .test.tsx files for headless logic and pure unit tests.
  • Deleted Unit Tests & Migrated to Storybook Stories: Replaced rendering-based DOM unit tests with visual play-test stories (*.stories.tsx) and custom component assertions for:
    • AdminSectionNav
    • CompactionBoundary
    • EffectiveContextInspector
    • MessageForkButton
    • ModelSelector
    • AppSidebarAdminEntry
    • AppSidebarNav
  • Manual Storybook Mocks: Introduced manual mocking modules in apps/web under lib/services/chat/__mocks__ and lib/services/models/__mocks__ to stub network-dependent state queries and actions (e.g. run receipts, forks, and model listings).
  • apps/web/app/(chat)/components/chat-list-sidebar/chat-activity-indicator.tsx: Replaced non-semantic span nodes with an <output> element to natively satisfy accessibility status role specifications.
  • apps/web/app/(chat)/components/model-selector.tsx: Added explicit accessibility aria-label tags to the combobox dialog trigger and popover container.
  • apps/web/app/(chat)/components/chat-page.tsx: Fixed duplicate-response and TypeError crash issues caused by double-mounting streams in React Strict Mode. Disabled automatic resumption in the useChat hook, wrapping resumeStream() in a custom useEffect call guarded by a persistent resumedRef tracker.

Documentation

  • docs/testing.md: Authoritative guide detailing the monorepo's five-layer test hierarchy (unit, integration, component story tests, browser end-to-end, and evaluation tests), runtime requirements, execution commands, and future modernizations.
  • AGENTS.md (Root & Workspaces): Updated to reference the new testing standards, node versions, and folder layouts.

Description generated by Mesa. Update settings

…uires it)

.node-version pins 22.23.1 — the version the nix flake's nodejs_22
already provides and this branch was verified against.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BCPobQSCvqFqbgHk2aZzo

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3e45a6082a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/api/package.json
"scripts": {
"build": "nest build && node dist/instance-config/prompt-built-runtime.contract.js && node dist/generate-openapi.js && prettier --write --log-level warn openapi.json",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"format": "prettier --write \"src/**/*.ts\" \"e2e/**/*.ts\" \"evals/**/*.ts\"",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove the nonexistent API e2e formatting glob

When pnpm --filter api format runs, its working directory is apps/api, which has no e2e/ directory after the browser suites were moved to the repository-root e2e/. Prettier exits with status 2 and No files matching the pattern were found: "e2e/**/*.ts", so this workspace formatting command now always fails even when the matched files are correctly formatted; remove this glob or point it at an existing directory.

AGENTS.md reference: apps/api/AGENTS.md:L206-L206

Useful? React with 👍 / 👎.

Root cause of the 11-day-red browser-e2e job (measured, not guessed): the
e2e api process fell back to the default worker profile — `runs` at
concurrency 1 — while fullyParallel Playwright workers piled chat turns
onto it. Turn-completion telemetry in the failing CI run shows the single
slot's throughput degrading from ~8s/turn to ~24s/turn (for turns whose
model latency is milliseconds), so streamed answers blew through 15–25s
assertion timeouts while still queued. The failures grew over 11 days
precisely because the gate was already red and nobody read the list.

- llame.config.e2e.json now serves `runs` at concurrency 8 with
  poolSize 12 (>= concurrency, per docs/scaling.md's invariant)
- the RUN_EXECUTION_MODE env in playwright.config.ts was dead — removed
  by the durable-run-workers refactor, silently doing nothing — deleted
- docs/testing.md follow-ups gain the ai/test consolidation item: 16
  sites forge streamText results via double-casts while two integration
  tests already show the official MockLanguageModelV3 pattern

Remaining after this fix is expected to be real product regressions the
red gate hid (org-units dialog from the #238 Base UI migration is the
prime suspect) — tracked separately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BCPobQSCvqFqbgHk2aZzo
… gate red

Two vocabulary changes shipped while the browser-e2e gate was already red,
so their stale assertions were never noticed:

- org-units: dialogs are scoped by `[data-state="open"]` — Radix
  vocabulary. Base UI (#238) marks open state with `data-open`, so the
  selector matched nothing and every dialog interaction timed out.
  Reproduced solo on an idle machine; the dialog itself opens fine.
- tool-loop: the tool chip's resolved badge read "done" in the pre-#239
  ToolCallPart; the AI Elements Tool chip says "Completed". The failure
  snapshot shows the whole flow working — chip rendered, follow-up answer
  streamed — with only the stale string unmatched.

The remaining browser-e2e failures are real product bugs the red gate
hid, tracked separately: #259 (duplicated partial+final answer
paragraphs, strict-mode violations) and #260 (browser TypeError reading
'state' during tool runs).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BCPobQSCvqFqbgHk2aZzo
- fold the one-file evals vitest project into integration (qa-evals
  self-gates on RUN_MODEL_EVALS, so plain test:integration skips it —
  and it now inherits the self-provisioned database it previously lacked)
- provision app_rls by executing docker/postgres/initdb/02-app-rls-role.sql
  instead of restating its CREATE ROLE inline (same pattern as the
  rls-function-owner.sql step beside it)
- PGBOSS_SCHEMA: random per-suite name at module eval (setup files
  re-evaluate per file), replacing the expect.getState().testPath
  derivation and its hook-ordering caveat — same pattern as the
  worker harness
- drop workers.all entries that restate BUILT_IN_DEFAULTS (config-loader
  merges per-group; asserted by its own test)
- delete qa-evals' vi.setConfig (project testTimeout already 120s),
  collapse tsconfig.build's three vitest excludes into "vitest.*",
  trim over-explaining comments in playwright.config and the e2e config

Verified: unit 507, integration 30 files/256 tests (+ qa-evals
self-skip), typecheck, lint, build, playwright --list all green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BCPobQSCvqFqbgHk2aZzo
…ema path

- The docs told test:evals users to "bring POSTGRES_URL", but the
  integration globalSetup (deliberately) only honors TEST_DATABASE_URL as
  the provisioning opt-out and overwrites POSTGRES_URL — honoring ambient
  POSTGRES_URL instead would let an exported dev-DB URL silently receive
  integration-test writes, the accident class this branch eliminated. The
  docs now state the real contract: bring model credentials; the DB
  self-provisions, TEST_DATABASE_URL overrides. Stale project list in
  apps/api/AGENTS.md fixed alongside.
- llame.config.e2e.json's $schema pointer gains the directory level the
  e2e/support/ move added, restoring editor validation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BCPobQSCvqFqbgHk2aZzo
Per docs/testing.md rule 5's rubric (leaf component, <=2 module mocks):
compaction-boundary, effective-context-inspector, admin-section-nav,
app-sidebar-nav, app-sidebar-admin-entry (desktop), message-fork-button,
model-selector, chat-activity-indicator, and tool-cap-notice-part now
carry stories with play functions; their jsdom tests are deleted or
reduced to the part a story can't express (pure resolvers, a byte-for-byte
live-vs-history HTML parity check, and the mobile useIsMobile branch —
browser mode runs a fixed desktop viewport).

Three new sb.mock seams (chat/runs, chat/fork, models/queries) follow the
existing pins/active-runs pattern; the models mock re-implements its two
pure helpers rather than re-exporting them, since sb.mock redirects that
specifier back into the mock itself.

Running these in a real browser with the a11y addon immediately caught
three shipped defects jsdom could not see:
- chat-activity-indicator: aria-label on a bare span (aria-prohibited-attr)
  — now a native <output>, which carries role=status
- model-selector trigger: role=combobox is not a name-from-content role,
  so the picker was nameless to screen readers — now aria-labelled
- model-selector popover: Base UI's role=dialog needs its own name
  (aria-dialog-name) — now aria-labelled

Verified: web unit 46 files/291 tests, storybook 58 files/265 tests,
web+storybook lint/typecheck green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BCPobQSCvqFqbgHk2aZzo
Both bugs are one race. `useChat`'s built-in `resume` effect
(@ai-sdk/react dist/index.mjs:221-225) has neither a cleanup function nor
a re-entrancy guard, so React Strict Mode's double-invoked mount effect
calls `resumeStream()` twice on the same Chat instance. The api replays
the run for both requests, so two concurrent `makeRequest()` calls run
against one shared `activeResponse`:

- the first clears it (`this.activeResponse = void 0`) in its finally,
  and the second's finally dereferences it — `ai` reads
  `this.activeResponse.state.message` unguarded on the very line above
  the null-safe `this.activeResponse?.state.finishReason` (#260)
- each call accumulates its own streaming message state, so the answer
  lands twice: a partial paragraph beside the complete one (#259)

ChatPage now passes `resume: false` and drives `resumeStream()` from its
own effect behind a ref set synchronously before the call, so the second
Strict Mode invocation is a no-op. The component already remounts per
chat id (key={chatId}), so each session gets a fresh ref and Chat.

The e2e `page` fixture now fails any test that produced an uncaught
client exception: #260 threw on every mid-run reload for weeks while CI
only logged it, because nothing asserted on page errors.

The underlying SDK fragility (no re-entrancy guard; the unguarded
`activeResponse` read in the finally block) is worth reporting upstream —
this guard is the app-layer fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BCPobQSCvqFqbgHk2aZzo
The remaining browser-e2e failures were `GET :id/messages` returning
non-OK late in the run, worsening as more tests passed — the signature of
the global ThrottlerModule ceiling (300 req/min per IP), which every
parallel Playwright worker shares from localhost: page loads, history
fetches and 4s run polling all draw on one bucket. AUTH_RATE_LIMIT_PER_MINUTE
only ever covered /auth/v1.

The limit becomes env-tunable exactly like its auth counterpart (read once
at boot; production keeps the strict default), and the harness raises it.
The two assertions now report the status they got, so the next failure of
this class names 429 instead of "expected true, received false".

Refs #259, #260 (this is the last of the four stacked causes behind the
11-day-red gate — stale selectors, stale badge text, runs-queue
starvation, and now the request ceiling).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BCPobQSCvqFqbgHk2aZzo
The last browser-e2e failure was the first turn's "Reply ready" toast
rendering over the composer and intercepting the send click until it
auto-dismissed (Playwright named the interception outright). The toast is
incidental to what this spec asserts, so it waits for it to clear —
`toBeHidden` passes immediately when no toast is showing.

That a toast blocks the Send button at all is a real (if minor) UX
defect, filed as #262 rather than silently worked around here: toast
placement is a design decision.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BCPobQSCvqFqbgHk2aZzo

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/web/lib/services/chat/__mocks__/runs.ts">

<violation number="1" location="apps/web/lib/services/chat/__mocks__/runs.ts:14">
P3: Add the missing `detail` property to the mock's `runQueryKeys` and align the `contextReceipt` computed key with the real module. The mock omits `runQueryKeys.detail` (a function on the real export), and the `contextReceipt` key uses `"context"` instead of `"context-receipt"`. Any story or component that accesses `runQueryKeys.detail` gets `undefined` at runtime.</violation>
</file>

<file name="apps/api/package.json">

<violation number="1" location="apps/api/package.json:23">
P2: The `test` script lost the `--maxWorkers=2` CPU constraint that limited Jest to 2 parallel workers. Without a Vitest equivalent (e.g. `--poolOptions.threads.singleThread` or `maxConcurrency` in the vitest config), the unit suite will use all CPU cores, which can cause resource contention on constrained CI runners or developer machines.</violation>
</file>

<file name="apps/web/app/(chat)/components/tool-cap-notice-part.stories.tsx">

<violation number="1" location="apps/web/app/(chat)/components/tool-cap-notice-part.stories.tsx:27">
P3: The play-function assertion `/8\/8/` uses a loose substring regex that would pass even if the component rendered `(18/8)` or `(8/80)` instead of `(8/8)`. Anchor the pattern, for example with `/\\(8\/8\\)/` or `getByText('8/8')` with `exact: true`, so a rendering regression on the displayed values is caught.</violation>
</file>

<file name="apps/api/vitest.integration.global-setup.mts">

<violation number="1" location="apps/api/vitest.integration.global-setup.mts:34">
P2: Integration runs use a mutable Postgres image tag while the repository pins its Postgres image elsewhere, so a tag refresh can change test behavior without a code change; using the pinned digest would keep the self-provisioned topology reproducible.</violation>

<violation number="2" location="apps/api/vitest.integration.global-setup.mts:34">
P2: A provisioning failure can leak the throwaway container and open postgres.js clients because cleanup is registered only after every setup step succeeds; wrapping provisioning in `try/finally` would close clients and stop the container on both success and failure.</violation>
</file>

<file name="apps/api/vitest.integration.setup.ts">

<violation number="1" location="apps/api/vitest.integration.setup.ts:9">
P3: Runs using `TEST_DATABASE_URL` permanently accumulate one pg-boss schema per integration file. Because the override path intentionally reuses an already-provisioned database, repeated integration runs leave queue tables and metadata behind and can eventually pollute or exhaust the shared test database. A per-file teardown or run-level cleanup for the generated schemas would keep the override path isolated.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread apps/api/package.json
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"test:evals": "RUN_MODEL_EVALS=1 jest --config ./test/jest-e2e.json --testPathPattern qa-evals --silent=false",
"test": "vitest run --project unit",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The test script lost the --maxWorkers=2 CPU constraint that limited Jest to 2 parallel workers. Without a Vitest equivalent (e.g. --poolOptions.threads.singleThread or maxConcurrency in the vitest config), the unit suite will use all CPU cores, which can cause resource contention on constrained CI runners or developer machines.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/package.json, line 23:

<comment>The `test` script lost the `--maxWorkers=2` CPU constraint that limited Jest to 2 parallel workers. Without a Vitest equivalent (e.g. `--poolOptions.threads.singleThread` or `maxConcurrency` in the vitest config), the unit suite will use all CPU cores, which can cause resource contention on constrained CI runners or developer machines.</comment>

<file context>
@@ -20,12 +20,10 @@
-    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
-    "test:e2e": "jest --config ./test/jest-e2e.json",
-    "test:evals": "RUN_MODEL_EVALS=1 jest --config ./test/jest-e2e.json --testPathPattern qa-evals --silent=false",
+    "test": "vitest run --project unit",
+    "test:watch": "vitest --project unit",
+    "test:integration": "vitest run --project integration",
</file context>

return;
}

const container = await new PostgreSqlContainer('postgres:17-alpine').start();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Integration runs use a mutable Postgres image tag while the repository pins its Postgres image elsewhere, so a tag refresh can change test behavior without a code change; using the pinned digest would keep the self-provisioned topology reproducible.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/vitest.integration.global-setup.mts, line 34:

<comment>Integration runs use a mutable Postgres image tag while the repository pins its Postgres image elsewhere, so a tag refresh can change test behavior without a code change; using the pinned digest would keep the self-provisioned topology reproducible.</comment>

<file context>
@@ -0,0 +1,84 @@
+    return;
+  }
+
+  const container = await new PostgreSqlContainer('postgres:17-alpine').start();
+
+  const asSuperuser = postgres(container.getConnectionUri(), { max: 1 });
</file context>
Suggested change
const container = await new PostgreSqlContainer('postgres:17-alpine').start();
const container = await new PostgreSqlContainer('postgres:17-alpine@sha256:dc17045ccfd343b49600570ea734b9c4991cf1c3f3302e67df51e3b402dd55c4').start();

return;
}

const container = await new PostgreSqlContainer('postgres:17-alpine').start();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: A provisioning failure can leak the throwaway container and open postgres.js clients because cleanup is registered only after every setup step succeeds; wrapping provisioning in try/finally would close clients and stop the container on both success and failure.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/vitest.integration.global-setup.mts, line 34:

<comment>A provisioning failure can leak the throwaway container and open postgres.js clients because cleanup is registered only after every setup step succeeds; wrapping provisioning in `try/finally` would close clients and stop the container on both success and failure.</comment>

<file context>
@@ -0,0 +1,84 @@
+    return;
+  }
+
+  const container = await new PostgreSqlContainer('postgres:17-alpine').start();
+
+  const asSuperuser = postgres(container.getConnectionUri(), { max: 1 });
</file context>

// vi.mock seam); the remaining exports are inert stand-ins so any other
// storied component importing this module stays off the network.

export const runQueryKeys = {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Add the missing detail property to the mock's runQueryKeys and align the contextReceipt computed key with the real module. The mock omits runQueryKeys.detail (a function on the real export), and the contextReceipt key uses "context" instead of "context-receipt". Any story or component that accesses runQueryKeys.detail gets undefined at runtime.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/lib/services/chat/__mocks__/runs.ts, line 14:

<comment>Add the missing `detail` property to the mock's `runQueryKeys` and align the `contextReceipt` computed key with the real module. The mock omits `runQueryKeys.detail` (a function on the real export), and the `contextReceipt` key uses `"context"` instead of `"context-receipt"`. Any story or component that accesses `runQueryKeys.detail` gets `undefined` at runtime.</comment>

<file context>
@@ -0,0 +1,38 @@
+// vi.mock seam); the remaining exports are inert stand-ins so any other
+// storied component importing this module stays off the network.
+
+export const runQueryKeys = {
+  all: ["runs"] as const,
+  contextReceipt: (runId: string) => ["runs", runId, "context"] as const,
</file context>

args: { stepsUsed: 8, maxSteps: 8 },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText(/8\/8/)).toBeVisible();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The play-function assertion /8\/8/ uses a loose substring regex that would pass even if the component rendered (18/8) or (8/80) instead of (8/8). Anchor the pattern, for example with /\\(8\/8\\)/ or getByText('8/8') with exact: true, so a rendering regression on the displayed values is caught.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/app/(chat)/components/tool-cap-notice-part.stories.tsx, line 27:

<comment>The play-function assertion `/8\/8/` uses a loose substring regex that would pass even if the component rendered `(18/8)` or `(8/80)` instead of `(8/8)`. Anchor the pattern, for example with `/\\(8\/8\\)/` or `getByText('8/8')` with `exact: true`, so a rendering regression on the displayed values is caught.</comment>

<file context>
@@ -0,0 +1,30 @@
+  args: { stepsUsed: 8, maxSteps: 8 },
+  play: async ({ canvasElement }) => {
+    const canvas = within(canvasElement);
+    await expect(canvas.getByText(/8\/8/)).toBeVisible();
+    await expect(canvas.getByText(/Tool step limit reached/)).toBeVisible();
+  },
</file context>
Suggested change
await expect(canvas.getByText(/8\/8/)).toBeVisible();
await expect(canvas.getByText(/\(8\/8\)/)).toBeVisible();

* name here is unique per suite — same pattern as worker-harness.ts. Suites
* that provision their own schema simply override it.
*/
process.env.PGBOSS_SCHEMA = `pgboss_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Runs using TEST_DATABASE_URL permanently accumulate one pg-boss schema per integration file. Because the override path intentionally reuses an already-provisioned database, repeated integration runs leave queue tables and metadata behind and can eventually pollute or exhaust the shared test database. A per-file teardown or run-level cleanup for the generated schemas would keep the override path isolated.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/vitest.integration.setup.ts, line 9:

<comment>Runs using `TEST_DATABASE_URL` permanently accumulate one pg-boss schema per integration file. Because the override path intentionally reuses an already-provisioned database, repeated integration runs leave queue tables and metadata behind and can eventually pollute or exhaust the shared test database. A per-file teardown or run-level cleanup for the generated schemas would keep the override path isolated.</comment>

<file context>
@@ -0,0 +1,9 @@
+ * name here is unique per suite — same pattern as worker-harness.ts. Suites
+ * that provision their own schema simply override it.
+ */
+process.env.PGBOSS_SCHEMA = `pgboss_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
</file context>

A retry always restarts its worker, and Playwright reuses the worker SLOT
index (`parallelIndex`) when it does — but `workerAccount` registers a
fresh user per worker process. The storageState cache was keyed by slot
and short-circuits on existence, so a retry's new account inherited the
previous account's cookies: the browser acted as user A while the spec's
Bearer requests authenticated as user B, and the owner-scoped
`GET /chats/:id/messages` correctly 404'd on a chat the page had just
created. Keying by `workerAccount.id` ties the cached session to the
account it belongs to.

This is the "Bearer GET /messages returns 404" signature that had been
written off as a local-environment quirk; it was a harness bug all along,
only visible once retries were the common path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BCPobQSCvqFqbgHk2aZzo

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="e2e/support/fixtures.ts">

<violation number="1" location="e2e/support/fixtures.ts:100">
P3: Keying the cached-auth file by the per-worker account id fixes the retry cookie bug (good), but it also makes the `.auth/` file names non-deterministic per run, so the `.auth/` directory under `project.outputDir` grows by one file per worker on every CI/local run and is never reused or cleaned up — previously `parallelIndex` names overwrote the same bounded set of files between runs. Consider removing the per-worker auth files at worker teardown (or during global setup) to avoid unbounded accumulation.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread e2e/support/fixtures.ts
const fileName = path.resolve(
test.info().project.outputDir,
`.auth/${test.info().parallelIndex}.json`,
`.auth/${workerAccount.id}.json`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Keying the cached-auth file by the per-worker account id fixes the retry cookie bug (good), but it also makes the .auth/ file names non-deterministic per run, so the .auth/ directory under project.outputDir grows by one file per worker on every CI/local run and is never reused or cleaned up — previously parallelIndex names overwrote the same bounded set of files between runs. Consider removing the per-worker auth files at worker teardown (or during global setup) to avoid unbounded accumulation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At e2e/support/fixtures.ts, line 100:

<comment>Keying the cached-auth file by the per-worker account id fixes the retry cookie bug (good), but it also makes the `.auth/` file names non-deterministic per run, so the `.auth/` directory under `project.outputDir` grows by one file per worker on every CI/local run and is never reused or cleaned up — previously `parallelIndex` names overwrote the same bounded set of files between runs. Consider removing the per-worker auth files at worker teardown (or during global setup) to avoid unbounded accumulation.</comment>

<file context>
@@ -88,9 +88,16 @@ export const test = baseTest.extend<Fixtures, WorkerFixtures>({
       const fileName = path.resolve(
         test.info().project.outputDir,
-        `.auth/${test.info().parallelIndex}.json`,
+        `.auth/${workerAccount.id}.json`,
       );
 
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant