fix: state every test-gate timeout instead of inheriting a library default

Three of the six Vitest projects ran on Vitest's own testTimeout: 5000 and
five on its hookTimeout/teardownTimeout: 10000, and all 788 Testing Library
waits in the web unit project ran on asyncUtilTimeout: 1000. None of those
numbers was chosen here, and all of them were sized for an idle machine rather
than for three or four concurrent worktree sessions each running a full
local:gate on eight cores. #2292, #2278, #2250, #1942 and #1742 were each that
class, diagnosed one site at a time.

This is the complement of #1596, not a retreat from it: every fix that issue
produced stands, and no race is hidden here. What changes is that a correct,
deterministic test is no longer cut off by a budget nobody picked.

- vitest.shared.mts exports frozen TIMEOUTS / INTEGRATION_TIMEOUTS; all six
  projects spread one of them, clients/launcher importing the shared module for
  the first time to do so.
- Both web projects configure Testing Library's asyncUtilTimeout. The storybook
  one needs its own setup file, deliberately outside .storybook/ and free of
  setProjectAnnotations so the automatic preview-annotation provisioning
  (#1898) is untouched.
- 28 per-suite restatements of the integration project's own budget are gone,
  so raising the shared value now moves every site under it. The handful that
  genuinely differ — cross-process lock contention, a full OAuth round trip —
  keep their budget as a named constant that says why.
- AppRenderer's waitFor no longer asks for exactly its enclosing budget, which
  it could never win (#2292's shape); cli's e2e child timer drops below the
  project budget so the bound carrying the diagnostic is the one that fires.
- The transition auto-settle's slack term is named RAF_SLACK_MS and raised;
  the term derived from HEADER_ANIM_MS is untouched. The two answer to
  different things and must not be scaled as one number.
- The 19 Playwright budgets scattered across three web smokes become four
  named constants in scripts/lib/browser-timeouts.mjs.

scripts/verify-test-timeouts.mjs resolves each project through Vitest itself
and asserts the budgets, the asyncUtilTimeout sites, and that retry stays
unset. It was observed red against the pre-change config: unit/tui/launcher at
testTimeout 5000, five projects at hookTimeout 10000, both asyncUtilTimeout
sites unmet.

Closes #2323

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-09-12 08:32:34 -04:00
commit 58e53d5a92

View file

@ -396,6 +396,50 @@ diagnose a failing gate — is the `testing` skill. These are the rules.
- **Render React components through `renderWithMantine`** (`src/test/renderWithMantine.tsx`); do not hand-roll a bare `MantineProvider`, which skips the project theme and the helper's options and drifts from every other test. Pass the `colorScheme` option to exercise a forced scheme rather than hand-rolling `defaultColorScheme`. Use `renderWithMantineTransitions` **only** when a test must assert mid-flight transition state, and read the long comment on the helper before changing anything about it.
- **The web coverage `include` is a whitelist.** It names `components`/`hooks`/`theme`/`lib`/`utils`/`server` plus the browser-consumed `core/*` runtime, so a module placed **outside** those directories falls out of the gate entirely, silently. Place new modules inside a gated directory. The documented exceptions — `src/App.tsx` and the `src/main.tsx` / `src/index.ts` bootstraps — are called out in a comment on the `include` array itself.
### Test-gate timeouts are chosen values, stated once
**Every wall-clock budget a test gate runs under is a value somebody picked,
and it lives in one place (#2323).** The shared budgets are `TIMEOUTS` and
`INTEGRATION_TIMEOUTS` in `vitest.shared.mts`; all six Vitest projects spread
one of them, and Testing Library's `asyncUtilTimeout` — which no Vitest config
can see — is configured in each web project's setup file
(`src/test/setup.ts`, `src/test/storybookSetup.ts`).
`npm run verify:test-timeouts` resolves each project through Vitest itself and
enforces all of it.
This is not a licence to relax the bar. #1596 settled that: **a test that races
is fixed with fake timers or an awaited condition, never with headroom.** The
complementary case is what these budgets exist for — a test that is correct and
deterministic and is simply cut off, because the number bounding it was a
library default sized for an idle machine rather than for the one this repo is
worked on (three or four concurrent agent sessions in separate worktrees, each
free to run a full `local:gate`). Raising a budget nobody chose hides no race.
- **Raise a budget in the shared object, not at a suite.** A per-suite
`}, 30_000)` moves one site and leaves every future file where it was — which
is how five of the six projects came to have no stated budget at all. Delete a
restatement of the project's own value rather than keeping it in sync.
- **A per-suite raise is right only where the work is genuinely different**, and
then it says so: real cross-process lock contention (`file-lock.test.ts`), a
full interactive OAuth round trip (`oauth-interactive.test.ts`). Name the
constant and state the reason at the site.
- **Never nest a budget inside an equal-or-larger one.** `waitFor({ timeout: N })`
inside a test whose own budget is `N` can never win, and it reports as a
timeout naming neither the wait nor its subject (#2292). Two bounds on the
same work must differ, and the one carrying the useful diagnostic must be the
tighter (`clients/cli/__tests__/e2e.test.ts`).
- **`retry` stays unset.** A retry turns a load-induced red into a silent green
on the only pre-push gate this repo has, and hides a real race behind a second
attempt. The guard enforces this.
- **Do not scale a fixed sleep.** A `setTimeout(r, N)` with no condition is not
a timeout: it always waits the full window, so raising it slows every passing
run and still races on a loaded one. Replace one with a condition wait when it
actually flakes (#2250). The single exception is the transition auto-settle in
`clients/web/src/test/renderWithMantine.tsx`, whose failure is silent and
displaced rather than loud and local — and there only its `RAF_SLACK_MS` term
moves, never the term derived from the component's own animation constant.
## Mandatory pre-push gate
- **ALWAYS run `npm run format` before committing.** The **root** `format` auto-fixes `core/`, the root `scripts/` tooling, the root shared surface, and every client's scope in one shot. `validate` runs the non-fixing `format:check` and will fail in CI on any unformatted file, so run the auto-fixer first rather than letting `format:check` catch it.