You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
$ npx vitest run src/pages/ChiefOfStaff.test.jsx
Test Files 1 passed (1) Tests 35 passed (35) Duration 9.18s
$ npm test # full re-run
Test Files 864 passed (864) Tests 10778 passed (10778)
Two other files failed in the same run. Their names were lost to output truncation and did not reproduce; this issue is scoped to the confirmed ChiefOfStaff case. If the other two resurface, file them separately rather than widening this.
Why "raise the timeout" is the wrong fix here
This exact test has already defeated two rounds of exactly that mitigation:
client/vitest.config.js:9-12 already caps the client runner at two jsdom workers, and its comment names this very test as the reason: "Four jsdom workers exhausted Testing Library's existing 3s async budget on the public runner before ChiefOfStaff's config panel settled. Keep the proven two-worker client cap."
A 3s budget and a halved worker count were both spent on this panel and it still misses. A third bump buys a little more headroom and makes every genuinely-hung assertion in 10,778 tests slower to fail. Fix the settle, not the budget.
Root cause
renderConfigTab (ChiefOfStaff.test.jsx:107) mounts the entire ChiefOfStaff page through a MemoryRouter at /cos/config. The page fans out several mocked API reads on mount, and Force Evaluate only exists once ConfigTab has rendered behind all of them. The test then queries the button with a bare findByRole, which polls blind — it has no idea whether the page is still in its loading branch. Every mocked promise resolution is a separate macrotask; on a contended worker that chain is what runs past the budget.
The page already exposes a precise settle signal that the test does not use: the loading branch renders role="status" with aria-busy="true" and aria-label="Loading Chief of Staff", asserted directly at ChiefOfStaff.test.jsx:126-128.
Decision (already made — do not re-litigate)
Wait on the page's own settle signal before querying, and move panel-behavior assertions down to the component that owns them. Do not change asyncUtilTimeout, the worker cap, or testTimeout.
In ChiefOfStaff.test.jsx, have renderConfigTab (or a helper beside it) await the busy region's disappearance before returning — e.g. await waitForElementToBeRemoved(() => screen.queryByRole('status')), or await waitFor(() => expect(screen.queryByRole('status')).toBeNull()). Then the subsequent findByRole starts against a settled tree and spends its budget on one render, not on the whole mount chain. Apply it to every test in this file that reaches into a tab's contents (the Force Evaluate cases at lines ~163 and ~180, and any sibling with the same shape). Leave the loading-skeleton tests at lines 121-133 alone — the busy region is what they assert.
ConfigTab is already a standalone component with its own suite (client/src/components/cos/tabs/ConfigTab.test.jsx); Force Evaluate lives at ConfigTab.jsx:335. Assertions about what the button does (the toast.error / toast.success / "Evaluating tasks..." expectations) belong there, rendered directly, with no page mount to wait on. Keep in ChiefOfStaff.test.jsx only what is genuinely about the page: that /cos/config routes to and renders the config tab. Do not delete coverage — move it, then confirm the count is preserved.
Acceptance criteria
cd client && npm test is green across three consecutive full runs.
client/src/test/setup.js's asyncUtilTimeout, client/vitest.config.js's maxWorkers, and testTimeout are all unchanged.
No findByRole / findByText in ChiefOfStaff.test.jsx runs against an unsettled page mount.
Total assertions across ChiefOfStaff.test.jsx + ConfigTab.test.jsx are the same or higher than today; nothing is dropped in the move.
Problem
client/src/pages/ChiefOfStaff.test.jsxflakes on a full-suitenpm testand passes in isolation. Observed on 2026-09-02:Isolated, and on a full re-run, it is green:
Two other files failed in the same run. Their names were lost to output truncation and did not reproduce; this issue is scoped to the confirmed
ChiefOfStaffcase. If the other two resurface, file them separately rather than widening this.Why "raise the timeout" is the wrong fix here
This exact test has already defeated two rounds of exactly that mitigation:
client/src/test/setup.js:12already raises testing-library's async budget from the 1000ms default:configure({ asyncUtilTimeout: 3000 })(QuotaBurn retry-budget test flakes ~1 in 3 on testing-library's 1000ms default timeout #3474).client/vitest.config.js:9-12already caps the client runner at two jsdom workers, and its comment names this very test as the reason: "Four jsdom workers exhausted Testing Library's existing 3s async budget on the public runner before ChiefOfStaff's config panel settled. Keep the proven two-worker client cap."A 3s budget and a halved worker count were both spent on this panel and it still misses. A third bump buys a little more headroom and makes every genuinely-hung assertion in 10,778 tests slower to fail. Fix the settle, not the budget.
Root cause
renderConfigTab(ChiefOfStaff.test.jsx:107) mounts the entireChiefOfStaffpage through aMemoryRouterat/cos/config. The page fans out several mocked API reads on mount, andForce Evaluateonly exists onceConfigTabhas rendered behind all of them. The test then queries the button with a barefindByRole, which polls blind — it has no idea whether the page is still in its loading branch. Every mocked promise resolution is a separate macrotask; on a contended worker that chain is what runs past the budget.The page already exposes a precise settle signal that the test does not use: the loading branch renders
role="status"witharia-busy="true"andaria-label="Loading Chief of Staff", asserted directly atChiefOfStaff.test.jsx:126-128.Decision (already made — do not re-litigate)
Wait on the page's own settle signal before querying, and move panel-behavior assertions down to the component that owns them. Do not change
asyncUtilTimeout, the worker cap, ortestTimeout.ChiefOfStaff.test.jsx, haverenderConfigTab(or a helper beside it) await the busy region's disappearance before returning — e.g.await waitForElementToBeRemoved(() => screen.queryByRole('status')), orawait waitFor(() => expect(screen.queryByRole('status')).toBeNull()). Then the subsequentfindByRolestarts against a settled tree and spends its budget on one render, not on the whole mount chain. Apply it to every test in this file that reaches into a tab's contents (theForce Evaluatecases at lines ~163 and ~180, and any sibling with the same shape). Leave the loading-skeleton tests at lines 121-133 alone — the busy region is what they assert.ConfigTabis already a standalone component with its own suite (client/src/components/cos/tabs/ConfigTab.test.jsx);Force Evaluatelives atConfigTab.jsx:335. Assertions about what the button does (thetoast.error/toast.success/ "Evaluating tasks..." expectations) belong there, rendered directly, with no page mount to wait on. Keep inChiefOfStaff.test.jsxonly what is genuinely about the page: that/cos/configroutes to and renders the config tab. Do not delete coverage — move it, then confirm the count is preserved.Acceptance criteria
cd client && npm testis green across three consecutive full runs.client/src/test/setup.js'sasyncUtilTimeout,client/vitest.config.js'smaxWorkers, andtestTimeoutare all unchanged.findByRole/findByTextinChiefOfStaff.test.jsxruns against an unsettled page mount.ChiefOfStaff.test.jsx+ConfigTab.test.jsxare the same or higher than today; nothing is dropped in the move.