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:
parent
336e045210
commit
58e53d5a92
1 changed files with 1276 additions and 394 deletions
150
scripts/verify-test-timeouts.test.mjs
Normal file
150
scripts/verify-test-timeouts.test.mjs
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
/**
|
||||
* Unit tests for `verify-test-timeouts.mjs`'s pure decision logic.
|
||||
*
|
||||
* The guard's own resolution half needs a real Vitest and four real configs, so
|
||||
* it is exercised by running it (it is in `validate`). What is tested here is
|
||||
* everything that decides PASS or FAIL once a project is resolved — including
|
||||
* the two failure modes a green repo can never produce on its own: an unknown
|
||||
* project, and a `retry` someone added.
|
||||
*
|
||||
* ⚠️ Keep the filename exactly `verify-test-timeouts.test.mjs`. `node --test`
|
||||
* silently SKIPS a file its glob misses and still exits 0, so a typo here reads
|
||||
* as a passing suite.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
ASYNC_UTIL_SITES,
|
||||
CONFIG_ROOTS,
|
||||
EXPECTED_INTEGRATION_TIMEOUTS,
|
||||
EXPECTED_PROJECTS,
|
||||
EXPECTED_TIMEOUTS,
|
||||
checkAsyncUtilSource,
|
||||
checkProject,
|
||||
identifyProject,
|
||||
} from "./verify-test-timeouts.mjs";
|
||||
|
||||
const ok = { ...EXPECTED_TIMEOUTS };
|
||||
|
||||
test("a project on the stated budgets passes", () => {
|
||||
assert.deepEqual(checkProject("unit", ok), []);
|
||||
assert.deepEqual(checkProject("cli", ok), []);
|
||||
assert.deepEqual(
|
||||
checkProject("integration", { ...EXPECTED_INTEGRATION_TIMEOUTS }),
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
test("each budget is checked independently", () => {
|
||||
for (const key of Object.keys(EXPECTED_TIMEOUTS)) {
|
||||
const failures = checkProject("unit", { ...ok, [key]: 1234 });
|
||||
assert.equal(failures.length, 1, `${key} was not checked`);
|
||||
assert.match(failures[0], new RegExp(`${key} to 1234, expected`));
|
||||
}
|
||||
});
|
||||
|
||||
test("a budget left on a library default is a failure, not an omission", () => {
|
||||
// The exact shape this guard exists for: Vitest's own 5000/10000, which is
|
||||
// what five of the six projects resolved to before #2323.
|
||||
const failures = checkProject("tui", {
|
||||
testTimeout: 5000,
|
||||
hookTimeout: 10000,
|
||||
teardownTimeout: 10000,
|
||||
});
|
||||
assert.equal(failures.length, 3);
|
||||
});
|
||||
|
||||
test("an absent budget fails rather than being treated as fine", () => {
|
||||
// `undefined !== 15000`. Worth pinning: a config that stopped being loaded
|
||||
// resolves every key to undefined, and that must not read as a pass.
|
||||
assert.equal(checkProject("unit", {}).length, 3);
|
||||
});
|
||||
|
||||
test("a project with no row fails loudly", () => {
|
||||
const failures = checkProject("brand-new", ok);
|
||||
assert.equal(failures.length, 1);
|
||||
assert.match(failures[0], /no row in EXPECTED_PROJECTS/);
|
||||
});
|
||||
|
||||
test("retry is allowed to be unset or 0, and nothing else", () => {
|
||||
assert.deepEqual(checkProject("unit", { ...ok }), []);
|
||||
assert.deepEqual(checkProject("unit", { ...ok, retry: 0 }), []);
|
||||
for (const retry of [1, 2]) {
|
||||
const failures = checkProject("unit", { ...ok, retry });
|
||||
assert.equal(failures.length, 1);
|
||||
assert.match(failures[0], /retry/);
|
||||
}
|
||||
});
|
||||
|
||||
test("every project in the table is one the configs are asked for", () => {
|
||||
const declared = CONFIG_ROOTS.flatMap((c) => c.projects).sort();
|
||||
assert.deepEqual(declared, Object.keys(EXPECTED_PROJECTS).sort());
|
||||
});
|
||||
|
||||
test("a nameless single project takes its name from the config entry", () => {
|
||||
// The node clients set no `name`, so their one project resolves as "".
|
||||
assert.equal(identifyProject({ name: "" }, ["cli"]), "cli");
|
||||
});
|
||||
|
||||
test("a browser project is matched past its instance suffix", () => {
|
||||
assert.equal(
|
||||
identifyProject({ name: "storybook (chromium)" }, [
|
||||
"unit",
|
||||
"integration",
|
||||
"storybook",
|
||||
]),
|
||||
"storybook",
|
||||
);
|
||||
});
|
||||
|
||||
test("an unrecognized project name is not silently matched", () => {
|
||||
assert.equal(
|
||||
identifyProject({ name: "e2e" }, ["unit", "storybook"]),
|
||||
undefined,
|
||||
);
|
||||
// Nameless only resolves when the entry expects exactly one project —
|
||||
// otherwise the guard cannot tell which row it belongs to.
|
||||
assert.equal(identifyProject({ name: "" }, ["unit", "storybook"]), undefined);
|
||||
});
|
||||
|
||||
test("a setup file that configures asyncUtilTimeout passes", () => {
|
||||
const source = `import { cleanup, configure } from "@testing-library/react";
|
||||
configure({ asyncUtilTimeout: 5000 });`;
|
||||
assert.deepEqual(checkAsyncUtilSource(source, "@testing-library/react"), []);
|
||||
});
|
||||
|
||||
test("configure() without the timeout is caught", () => {
|
||||
const source = `import { configure } from "storybook/test";
|
||||
configure({ testIdAttribute: "data-testid" });`;
|
||||
assert.deepEqual(checkAsyncUtilSource(source, "storybook/test"), [
|
||||
"calls configure() without an asyncUtilTimeout",
|
||||
]);
|
||||
});
|
||||
|
||||
test("the import is checked, not just the call", () => {
|
||||
// Storybook instruments its own Testing Library copy, so configuring
|
||||
// `@testing-library/*` from a story setup configures a copy no play function
|
||||
// ever calls — green by inspection, dead in practice.
|
||||
const source = `import { configure } from "@testing-library/dom";
|
||||
configure({ asyncUtilTimeout: 5000 });`;
|
||||
assert.deepEqual(checkAsyncUtilSource(source, "storybook/test"), [
|
||||
'does not import from "storybook/test"',
|
||||
]);
|
||||
});
|
||||
|
||||
test("a setup file with no configure() at all is caught", () => {
|
||||
const failures = checkAsyncUtilSource(
|
||||
"export const nothing = 1;\n",
|
||||
"storybook/test",
|
||||
);
|
||||
assert.equal(failures.length, 2);
|
||||
});
|
||||
|
||||
test("both web projects have an asyncUtilTimeout site", () => {
|
||||
assert.deepEqual(ASYNC_UTIL_SITES.map((s) => s.project).sort(), [
|
||||
"storybook",
|
||||
"unit",
|
||||
]);
|
||||
});
|
||||
Loading…
Reference in a new issue