CI flake: vitest browser-mode resolveManualMock RPC race fails CI after green tests #1080

Closed
opened 2026-05-10 17:23:18 +00:00 by claude-desktop · 1 comment
Collaborator

Symptom

Every test in apps/web passes, then the runner exits with code 1 on an unhandled rejection during teardown:

Error: [vitest] There was an error when mocking a module. If you are using "vi.mock" factory, make sure there are no top level variables inside, since this call is hoisted to top of the file.
 ❯ createHelpfulError @vitest+mocker/dist/chunk-registry.js:189:16
 ❯ ManualMockedModule.factory @vitest+browser/dist/index.js:3232:34
 ❯ ManualMockedModule.resolve @vitest+mocker/dist/chunk-registry.js:161:21
 ❯ RouteHandler._handleInternal playwright-core/lib/client/network.js:696
Caused by: Error: [birpc] rpc is closed, cannot call "resolveManualMock"

Failure path

  1. All tests in a file finish; vitest closes its birpc channel.
  2. Playwright's RouteHandler keeps intercepting requests for vi.mock'd modules — its lifecycle is the page, not the runner.
  3. A late module fetch (likely from React effect cleanup or HMR-style reload) hits the route, which calls resolveManualMock over the closed RPC.
  4. Rejection propagates as unhandled, runner exits 1.

Affected versions

  • vitest 4.1.5
  • @vitest/browser-playwright 4.1.5
  • playwright-core 1.59.1

Affected PRs / runs (already)

  • #1079 (PR) — runs #3396, #3399, #3400 (all server-only changes)
  • #3392, #3394, #3395, #3398 — unrelated work, same teardown rejection
  • Earlier fix attempts: 47b47f0a (fix(web): wait for drawer unmount in app-shell test), ccc3be9c (fix(ci): await unmount in nav-sections re-render test), 60760cca (fix(web): serialise vitest browser test files to dodge dep-optimizer race — adds fileParallelism: false). Each fixed a near-cause; none caught this race.

Workaround currently in place

apps/web/vitest.config.ts excludes the loudest leakers:

  • src/components/app-shell.test.tsx
  • src/components/nav-sections.test.tsx
  • src/components/sidebar-nav.test.tsx

This restores green CI but cuts coverage on those three components. Re-enable as part of the fix.

Fix attempts that did NOT work

  • optimizeDeps.include for react-dom/client + @base-ui-components/react/radio[-group] — kills the mid-suite "Vite unexpectedly reloaded a test" reload warning, but the RPC race fires regardless
  • Replacing the async vi.mock("@tanstack/react-router", async () => ({ ...await vi.importActual(...), ...routerStubs })) with a sync vi.mock("@tanstack/react-router", () => routerStubs) — only addresses one of ~10 async factories in the suite
  • globalSetup process.on("unhandledRejection", ...) swallow restricted to the exact RPC-closed message — handler attached but the rejection is not caught (the rejection appears to fire in a context where this handler is not active — possibly a child process or a different async hook chain)

Fix directions worth trying

  1. Pin / bisect vitest + @vitest/browser-playwright versions until a release without the race. 4.1.5 is the current pin; check 4.1.4 / 4.0.x or whatever the upstream issue tracker recommends.
  2. Migrate every vi.mock(..., async () => …) factory in the suite to a sync factory — eliminates the source of late resolveManualMock calls. ~10 files: app-shell.test.tsx, nav-sections.test.tsx, sidebar-nav.test.tsx, avatar-menu.test.tsx, watchdog-panel.test.tsx, settings.repos.test.tsx, settings.service.test.tsx, settings.voice-input.test.tsx, settings.agents.admin.test.tsx, settings.agent-config.test.tsx, agents.live.test.tsx, agents.facets.test.tsx, agents.roster.test.tsx, agents.sessions.test.tsx, settings.language.test.tsx, board/board.test.tsx, features/agent-config/history-tab.test.tsx (grep vi.mock.*async\\|importActual to confirm).
  3. Patch the upstream RouteHandler at startup to ignore RPC-closed errors — surgical but fragile.
  4. Switch off browser mode for the leaker files and migrate them to a happy-dom or unit harness.

Acceptance criteria

  • Vitest browser-mode CI run is reliably green across 5+ consecutive PRs
  • The three excluded files (app-shell.test.tsx, nav-sections.test.tsx, sidebar-nav.test.tsx) are re-enabled with no exclude entries beyond e2e/** + settings-manifest.test.ts
  • No vi.mock(..., async () => { … vi.importActual(...) … }) factories remain (or the fix proves they are safe)
  • CI duration unchanged or improved

References

  • Spec: this conversation, 2026-05-10
  • Audit: PR #1079 (server-only) was blocked by this flake; opened separately to keep the engine PR scope clean.
## Symptom Every test in `apps/web` passes, then the runner exits with code 1 on an unhandled rejection during teardown: ``` Error: [vitest] There was an error when mocking a module. If you are using "vi.mock" factory, make sure there are no top level variables inside, since this call is hoisted to top of the file. ❯ createHelpfulError @vitest+mocker/dist/chunk-registry.js:189:16 ❯ ManualMockedModule.factory @vitest+browser/dist/index.js:3232:34 ❯ ManualMockedModule.resolve @vitest+mocker/dist/chunk-registry.js:161:21 ❯ RouteHandler._handleInternal playwright-core/lib/client/network.js:696 Caused by: Error: [birpc] rpc is closed, cannot call "resolveManualMock" ``` ## Failure path 1. All tests in a file finish; vitest closes its birpc channel. 2. Playwright's `RouteHandler` keeps intercepting requests for `vi.mock`'d modules — its lifecycle is the page, not the runner. 3. A late module fetch (likely from React effect cleanup or HMR-style reload) hits the route, which calls `resolveManualMock` over the closed RPC. 4. Rejection propagates as unhandled, runner exits 1. ## Affected versions - `vitest` 4.1.5 - `@vitest/browser-playwright` 4.1.5 - `playwright-core` 1.59.1 ## Affected PRs / runs (already) - #1079 (PR) — runs #3396, #3399, #3400 (all server-only changes) - #3392, #3394, #3395, #3398 — unrelated work, same teardown rejection - Earlier fix attempts: 47b47f0a (`fix(web): wait for drawer unmount in app-shell test`), ccc3be9c (`fix(ci): await unmount in nav-sections re-render test`), 60760cca (`fix(web): serialise vitest browser test files to dodge dep-optimizer race` — adds `fileParallelism: false`). Each fixed a near-cause; none caught this race. ## Workaround currently in place `apps/web/vitest.config.ts` excludes the loudest leakers: - `src/components/app-shell.test.tsx` - `src/components/nav-sections.test.tsx` - `src/components/sidebar-nav.test.tsx` This restores green CI but cuts coverage on those three components. Re-enable as part of the fix. ## Fix attempts that did NOT work - `optimizeDeps.include` for `react-dom/client` + `@base-ui-components/react/radio[-group]` — kills the mid-suite "Vite unexpectedly reloaded a test" reload warning, but the RPC race fires regardless - Replacing the async `vi.mock("@tanstack/react-router", async () => ({ ...await vi.importActual(...), ...routerStubs }))` with a sync `vi.mock("@tanstack/react-router", () => routerStubs)` — only addresses one of ~10 async factories in the suite - `globalSetup` `process.on("unhandledRejection", ...)` swallow restricted to the exact RPC-closed message — handler attached but the rejection is not caught (the rejection appears to fire in a context where this handler is not active — possibly a child process or a different async hook chain) ## Fix directions worth trying 1. **Pin / bisect** vitest + `@vitest/browser-playwright` versions until a release without the race. 4.1.5 is the current pin; check 4.1.4 / 4.0.x or whatever the upstream issue tracker recommends. 2. **Migrate every `vi.mock(..., async () => …)` factory in the suite to a sync factory** — eliminates the source of late `resolveManualMock` calls. ~10 files: `app-shell.test.tsx`, `nav-sections.test.tsx`, `sidebar-nav.test.tsx`, `avatar-menu.test.tsx`, `watchdog-panel.test.tsx`, `settings.repos.test.tsx`, `settings.service.test.tsx`, `settings.voice-input.test.tsx`, `settings.agents.admin.test.tsx`, `settings.agent-config.test.tsx`, `agents.live.test.tsx`, `agents.facets.test.tsx`, `agents.roster.test.tsx`, `agents.sessions.test.tsx`, `settings.language.test.tsx`, `board/board.test.tsx`, `features/agent-config/history-tab.test.tsx` (grep `vi.mock.*async\\|importActual` to confirm). 3. **Patch the upstream RouteHandler** at startup to ignore RPC-closed errors — surgical but fragile. 4. **Switch off browser mode for the leaker files** and migrate them to a happy-dom or unit harness. ## Acceptance criteria - [ ] Vitest browser-mode CI run is reliably green across 5+ consecutive PRs - [ ] The three excluded files (`app-shell.test.tsx`, `nav-sections.test.tsx`, `sidebar-nav.test.tsx`) are re-enabled with no exclude entries beyond `e2e/**` + `settings-manifest.test.ts` - [ ] No `vi.mock(..., async () => { … vi.importActual(...) … })` factories remain (or the fix proves they are safe) - [ ] CI duration unchanged or improved ## References - Spec: this conversation, 2026-05-10 - Audit: PR #1079 (server-only) was blocked by this flake; opened separately to keep the engine PR scope clean.
Collaborator

🦵 @charles kicked the queue — re-running implement on @code-lead.

🦵 @charles kicked the queue — re-running implement on @code-lead.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks#1080
No description provided.