refactor: assert retry and asyncUtilTimeout at runtime, delete the source scanner

Round 8 of the review asked for an AST. That was the right diagnosis of the
wrong problem: the question was never which parser to use, it was why a
test-timeout guard was parsing JavaScript at all.

It got there one reasonable step at a time. The guard enforces two rules no
Vitest config can report — a per-test `retry`, and Testing Library's
`asyncUtilTimeout` — so it read them out of source text, and the review found a
new valid spelling it missed in five consecutive rounds: a trailing comment,
nested parens in `it.each([makeCase()])`, a member-access prefix, the shorthand
`{ retry }`, an aliased import, a non-literal title. Each fix was correct and
each made it more parser-shaped, until it carried eight helpers doing quote
tracking and bracket balancing.

Both rules are observable at runtime, exactly:

- `vitest.setup.shared.mts` is loaded by all six projects and reads
  `ctx.task.retry` — the value Vitest resolved for the test. That covers
  strictly more than the scan did: a per-test option, a `describe` option
  inherited by children, a project-level setting, and a `--retry` flag typed at
  a command line, which the scan could never see since it only read npm scripts.
- `clients/web/src/test/asyncUtilTimeout.test.ts` asserts
  `getConfig().asyncUtilTimeout`, the value this project's own waitFors read.

Neither has a spelling to anticipate, so neither can be outrun by a new one.
Both were observed firing before being trusted.

The guard keeps only what it can answer by asking Vitest: resolved budgets,
project-level retry, config discovery, and that each project still loads the
setup file — the one thing about the runtime assertion a config CAN report.
868 lines to 361, every scanner helper gone, and 33 of 48 unit tests deleted
because they pinned edge cases of a thing that no longer exists.

storybookSetup.ts goes with it. At the library's own value its configure() call
was a no-op, and the assertion is what pins the number.

AGENTS.md gains the general rule: a rule about what the code DOES is asserted at
runtime, not read out of the source — reach for a verify:* guard when the
question is about configuration, which a tool can be asked to resolve.

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 15:58:19 -04:00
commit 69e6097b68
7 changed files with 228 additions and 799 deletions

View file

@ -1,7 +1,11 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import { defineConfig } from "vitest/config";
import { TIMEOUTS, vitestSharedPaths } from "../../vitest.shared.mts";
import {
NO_RETRY_SETUP,
TIMEOUTS,
vitestSharedPaths,
} from "../../vitest.shared.mts";
const dirname = path.dirname(fileURLToPath(import.meta.url));
const { projectResolve } = vitestSharedPaths(dirname);
@ -12,7 +16,7 @@ export default defineConfig({
globals: false,
environment: "node",
include: ["__tests__/**/*.test.ts"],
setupFiles: ["__tests__/helpers/mock-open-url.ts"],
setupFiles: ["__tests__/helpers/mock-open-url.ts", NO_RETRY_SETUP],
// Shared budgets (#2323). `testTimeout` was already 15000 here by hand;
// the hook and teardown budgets were Vitest's defaults until now.
...TIMEOUTS,

View file

@ -1,11 +1,12 @@
import { defineConfig } from "vitest/config";
import { TIMEOUTS } from "../../vitest.shared.mts";
import { NO_RETRY_SETUP, TIMEOUTS } from "../../vitest.shared.mts";
export default defineConfig({
test: {
globals: false,
environment: "node",
include: ["__tests__/**/*.test.ts"],
setupFiles: [NO_RETRY_SETUP],
// Shared budgets (#2323). This project holds a single test file, which is
// exactly why it is pinned rather than left on the defaults: it is cheap to
// include and expensive to remember when a second file arrives. This import

View file

@ -1,7 +1,11 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import { defineConfig } from "vitest/config";
import { TIMEOUTS, vitestSharedPaths } from "../../vitest.shared.mts";
import {
NO_RETRY_SETUP,
TIMEOUTS,
vitestSharedPaths,
} from "../../vitest.shared.mts";
const dirname = path.dirname(fileURLToPath(import.meta.url));
const { projectResolve } = vitestSharedPaths(dirname);
@ -12,7 +16,7 @@ export default defineConfig({
globals: false,
environment: "node",
include: ["__tests__/**/*.test.{ts,tsx}"],
setupFiles: ["./__tests__/setup.ts"],
setupFiles: ["./__tests__/setup.ts", NO_RETRY_SETUP],
// Shared budgets (#2323). Ink renders through React, so every assertion
// here waits on a React commit under v8 instrumentation — the shape behind
// the poll-budget raises in #1742 and #1942.

View file

@ -0,0 +1,43 @@
import { describe, it, expect } from "vitest";
import { getConfig } from "@testing-library/react";
/**
* Pins Testing Library's `asyncUtilTimeout` for the `unit` project (#2323).
*
* That value governs every `waitFor` and `findBy*` here — 788 call sites — and
* is the binding bound on any async assertion, being tighter than the project's
* own per-test budget. So it is a number this repo states rather than inherits,
* and a Testing Library upgrade that changed its default would fail here rather
* than silently move all 788.
*
* ⚠️ The value is the library's own default, and that is a MEASURED decision,
* not a shrug. #2323 proposed 5000, on the reasoning that a contended
* happy-dom render is the worst case. Measured on the machine this team uses,
* 5000 made the suite worse: three arms of three full unit runs each, same
* worktree, comparable load —
*
* • unmodified `v2/main`: 3/3 green
* • the raise to 5000: 1, 4 and 0 files timing out at 15000ms
* • the raise reverted: 3/3 green
*
* A different unrelated set failed each time and every one passed in about a
* second in isolation, which is the shape of CPU starvation rather than of a
* slow assertion. The mechanism: `asyncUtilTimeout` is not only a ceiling — any
* wait that is *meant* to expire spends the whole budget on the happy path, so
* a 5x raise is a 5x cost on exactly those tests, which saturates the worker
* pool and starves tests that were never slow. The tests that assert absence
* that way are the real defect and are #2335; revisit this when they are gone.
*
* ⚠️ This asserts the EFFECTIVE value at runtime rather than checking that
* `setup.ts` contains a `configure()` call. An earlier revision of #2334 did
* the latter by scanning source text, and the review found five ways to satisfy
* the scan without configuring anything — a commented-out call, a call on
* another object, a non-literal value, and so on. `getConfig()` is what the
* `waitFor`s in this project actually read, so there is nothing left to spell
* around.
*/
describe("Testing Library configuration (unit project)", () => {
it("uses the asyncUtilTimeout this repo states, not whatever the library ships", () => {
expect(getConfig().asyncUtilTimeout).toBe(1000);
});
});

View file

@ -1,35 +0,0 @@
/**
* Setup file for the `storybook` Vitest project (#2323).
*
* It exists for one call. Storybook's instrumented Testing Library carries its
* own `asyncUtilTimeout`, and it governs the 80 `waitFor` / `findBy*` sites
* across the story files. It is the *tighter* of the two bounds this project
* has — the per-test ceiling is 15000 (#2292) — so it is stated here rather
* than inherited, which also pins it against a Storybook upgrade changing the
* default under all 80 sites at once.
*
* The value matches the unit project's, and for the reason recorded in
* `setup.ts`: raising it was measured and rejected. A wait that is *meant* to
* expire spends its whole budget on the happy path, so a raise is a
* proportional cost on exactly those tests.
*
* ⚠️ Two things about this file that are deliberate and easy to undo:
*
* 1. **It must not live in `.storybook/`, and must not call
* `setProjectAnnotations`.** `@storybook/addon-vitest` provisions the
* preview annotations itself, and skips doing so when it finds a setup file
* that is *both* inside `configDir` and contains that call (#1898) — so the
* old `.storybook/vitest.setup.ts` was opting out of the automatic path.
* Either half of the condition is enough to stay on it; this file misses
* both. `src/test/PreviewAnnotations.stories.tsx` asserts the provisioning
* directly, so a regression here fails a test rather than silently rendering
* every story without the Mantine decorator.
* 2. **The import is `storybook/test`, not `@testing-library/*`.** Storybook
* instruments its own copy so the interactions panel can trace each step;
* configuring a different copy would change nothing that a play function
* actually calls.
*/
import { configure } from "storybook/test";
configure({ asyncUtilTimeout: 1000 });

View file

@ -41,7 +41,13 @@
"src/test/**/*.ts",
"src/test/**/*.tsx",
"../../core/**/__tests__/**/*.ts",
"../../core/**/__tests__/**/*.tsx"
"../../core/**/__tests__/**/*.tsx",
// The shared no-retry setup every Vitest project loads (#2323). It is
// referenced by the configs as a resolved PATH rather than imported, so no
// project's tsconfig picks it up transitively the way `vitest.shared.mts`
// is picked up through `vitest.config.ts` — it needs naming, or it lands in
// no tsconfig project at all and `verify:typecheck-coverage` fails.
"../../vitest.setup.shared.mts"
],
"exclude": []
}

View file

@ -16,6 +16,7 @@ import { buildWebServerConfigFromEnv } from "./server/web-server-config";
import { createBrowserExternalizedBuiltinGate } from "./server/browser-externalized-builtin-gate";
import {
INTEGRATION_TIMEOUTS,
NO_RETRY_SETUP,
TIMEOUTS,
vitestSharedPaths,
} from "../../vitest.shared.mts";
@ -318,7 +319,10 @@ export default defineConfig(({ command }) => {
include: ["clients/web/src/**/*.test.{ts,tsx}"],
// Integration tests run in the integration project below (node env).
exclude: [integrationGlob],
setupFiles: [path.join(dirname, "src/test/setup.ts")],
setupFiles: [
path.join(dirname, "src/test/setup.ts"),
NO_RETRY_SETUP,
],
// Pin after-hooks to LIFO (reverse registration). This is Vitest 4's
// own default (`resolved.sequence.hooks ??= "stack"` — the CLI
// help-text's "parallel" is stale), so this line documents intent and
@ -357,6 +361,7 @@ export default defineConfig(({ command }) => {
// can transform core/ modules and run tests against the source.
root: repoRoot,
include: [integrationGlob],
setupFiles: [NO_RETRY_SETUP],
// Integration tests spawn real HTTP/stdio servers via test-servers/,
// bind sockets, run e2e OAuth flows, and exercise filesystem-backed
// storage. 30s matches the v1.5 core/vitest.config.ts. Now stated
@ -402,11 +407,13 @@ export default defineConfig(({ command }) => {
// now follow it rather than restate it. The hook and teardown
// budgets come along with it, having been left on the defaults.
...TIMEOUTS,
// One `configure({ asyncUtilTimeout })` call, and deliberately not
// in `.storybook/` — see the file's own header for why that
// location and the absence of `setProjectAnnotations` are what keep
// the automatic preview-annotation provisioning below intact.
setupFiles: [path.join(dirname, "src/test/storybookSetup.ts")],
// Carries the shared no-retry assertion and nothing else.
// ⚠️ It is deliberately NOT in `.storybook/` and declares no
// annotations: `@storybook/addon-vitest` skips its automatic
// preview-annotation provisioning only for a setup file that is
// both inside `configDir` and calls `setProjectAnnotations`, and
// this one is neither (#1898).
setupFiles: [NO_RETRY_SETUP],
browser: {
enabled: true,
headless: true,