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:
parent
5b80a5d834
commit
69e6097b68
7 changed files with 228 additions and 799 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
43
clients/web/src/test/asyncUtilTimeout.test.ts
Normal file
43
clients/web/src/test/asyncUtilTimeout.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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 });
|
||||
|
|
@ -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": []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue