Skip to content

ChiefOfStaff page tests flake under full-suite load by querying an unsettled page mount #5857

Description

@atomantic

Problem

client/src/pages/ChiefOfStaff.test.jsx flakes on a full-suite npm test and passes in isolation. Observed on 2026-09-02:

 ❯ waitForWrapper node_modules/@testing-library/dom/dist/wait-for.js:163:27
 ❯ src/pages/ChiefOfStaff.test.jsx:165:33
    165|     const button = await screen.findByRole('button', { name: /Force Ev…

Test Files  3 failed | 860 passed (863)
Tests  3 failed | 10767 passed (10770)

Isolated, and on a full re-run, it is green:

$ 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/src/test/setup.js:12 already 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-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.

  1. 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.
  2. 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.

Activity

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

Metadata

Metadata

Assignees

Labels

model:mediumModel size: mediumplanTracked by /do:replantestsTest suite / test infrastructure

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions