diff --git a/AGENTS.md b/AGENTS.md index eb0b13ca..726ab538 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -423,6 +423,14 @@ free to run a full `local:gate`). Raising a budget nobody chose hides no race. 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. +- **A budget is stated, which is not the same as raised.** Testing Library's + `asyncUtilTimeout` is pinned at its own default in both web setup files + because raising it was *measured worse*: a wait that is meant to expire — + a test asserting something never appears, a poll allowed to run out — spends + its whole budget on the happy path, so a raise is a proportional cost on + exactly those tests and starves the worker pool. Measure before raising a + budget that a passing test can spend in full; the call-site comment records + the three-arm run. - **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 diff --git a/clients/cli/__tests__/oauth-interactive.test.ts b/clients/cli/__tests__/oauth-interactive.test.ts index f6c6d77b..abc51982 100644 --- a/clients/cli/__tests__/oauth-interactive.test.ts +++ b/clients/cli/__tests__/oauth-interactive.test.ts @@ -116,7 +116,11 @@ describe("CLI interactive OAuth (integration)", () => { } catch { // ignore } - }, OAUTH_ROUND_TRIP_MS); + // No budget: stopping the server and unlinking a file is not the round trip + // this file's constant describes, and the shared `hookTimeout` is already + // 30000 — so a per-hook argument here would restate it and say the wrong + // thing about it (Copilot). + }); it( "connects to an OAuth-protected server via the loopback callback server", diff --git a/clients/web/src/test/integration/auth/node/file-lock.test.ts b/clients/web/src/test/integration/auth/node/file-lock.test.ts index ebf9cacc..fc043d0f 100644 --- a/clients/web/src/test/integration/auth/node/file-lock.test.ts +++ b/clients/web/src/test/integration/auth/node/file-lock.test.ts @@ -37,10 +37,19 @@ import { resetFileLockWarnings, } from "@inspector/core/auth/node/file-lock.js"; -/** A test that waits out a full stale-lock takeover in a second process. */ -const LOCK_TAKEOVER_MS = 90_000; -/** A test that waits on cross-process contention but not on a stale window. */ -const LOCK_CONTENTION_MS = 60_000; +/** + * A test that waits for `proper-lockfile`'s retries to run out against a holder + * that never yields — a live second process, or a stale lock it cannot clear. + * Nothing is taken over here; the budget is spent in full by construction, + * which is why it is the longer of the two. + */ +const LOCK_RETRIES_EXHAUSTED_MS = 90_000; +/** + * A test that waits out the stale window and then *succeeds* by taking the dead + * holder's lock over. Shorter, because it ends the moment the takeover lands + * rather than when the retries stop. + */ +const STALE_LOCK_TAKEOVER_MS = 60_000; import { FileSecretStore } from "@inspector/core/auth/node/file-secret-store.js"; import { SecretStoreUnavailableError } from "@inspector/core/auth/node/secret-store.js"; @@ -319,7 +328,7 @@ describe("withSecretFileLock degrades rather than failing", () => { // Nothing was written behind the holder's back. expect(existsSync(target)).toBe(false); }, - LOCK_TAKEOVER_MS, + LOCK_RETRIES_EXHAUSTED_MS, ); it( @@ -354,7 +363,7 @@ describe("withSecretFileLock degrades rather than failing", () => { expect(existsSync(target)).toBe(false); expect(warnings()).not.toContain("not protected"); }, - LOCK_TAKEOVER_MS, + LOCK_RETRIES_EXHAUSTED_MS, ); it( @@ -384,7 +393,7 @@ describe("withSecretFileLock degrades rather than failing", () => { // Took the lock over — did not fall through to an unlocked write. expect(warnings()).toBe(""); }, - LOCK_CONTENTION_MS, + STALE_LOCK_TAKEOVER_MS, ); it( @@ -415,7 +424,7 @@ describe("withSecretFileLock degrades rather than failing", () => { // …and the entry it could not delete is still there, not half-removed. expect(await store.get("srv", "env:A")).toBe("1"); }, - LOCK_TAKEOVER_MS, + LOCK_RETRIES_EXHAUSTED_MS, ); }); diff --git a/clients/web/src/test/setup.ts b/clients/web/src/test/setup.ts index 53e6621b..344c0ee2 100644 --- a/clients/web/src/test/setup.ts +++ b/clients/web/src/test/setup.ts @@ -2,24 +2,37 @@ import "@testing-library/jest-dom/vitest"; import { afterEach } from "vitest"; import { cleanup, configure } from "@testing-library/react"; -// Testing Library's own `asyncUtilTimeout` default is 1000ms -// (`@testing-library/dom/dist/config.js`), and it governs every `waitFor` and -// `findBy*` in the unit project — 788 call sites — none of which chose it. One -// `configure` call is the whole surface, which is why it is here rather than -// argued a site at a time (#2323). +// Testing Library's `asyncUtilTimeout` governs every `waitFor` and `findBy*` in +// this project — 788 call sites — and is the tighter of the two bounds on any +// async assertion here, so it is stated rather than inherited (#2323). Pinning +// it also means a Testing Library upgrade that changes its default cannot move +// every one of those sites silently. // -// 5x, because a contended happy-dom render is the worst-measured case on this -// team's machine (8 logical cores, three or four concurrent worktree sessions -// each free to run the full `local:gate`), and because once the project's -// `testTimeout` rises to 15000 this becomes the *binding* constraint on every -// async assertion in the project — the enclosing budget stops being the thing -// that cuts a wait short and this starts being it. +// ⚠️ The value is 1000 — Testing 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 and that this becomes the +// binding constraint once the project's `testTimeout` rises. Measured on this +// machine, 5000 made the suite WORSE: three arms of three full unit runs each, +// same worktree, comparable load — // -// The trade-off, stated rather than hidden: a genuinely-failing async -// assertion now takes 5s to report instead of 1s. Only failing assertions pay -// it, once each, on runs that are already the slow path. A *passing* `waitFor` -// returns the instant its callback stops throwing and is unaffected. -configure({ asyncUtilTimeout: 5000 }); +// • unmodified `v2/main`: 3/3 green +// • this change with 5000: 1, 4 and 0 files timing out at 15000ms +// • this change with the default: 3/3 green +// +// The failures were a different, unrelated set 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 is that `asyncUtilTimeout` is not +// only a ceiling: any wait that is *meant* to expire — a test asserting +// something never appears, and anything that lets a poll run out — spends the +// whole budget on the happy path, so a 5x raise is a 5x cost on those tests, +// which saturates the worker pool and starves tests that were never slow. +// +// So the general rule this repo now follows — a budget must be a value someone +// chose — is satisfied by choosing it, not by raising it. Raising it is what +// #2323's own "do not raise" list would have said had the measurement been +// taken first. The tests that assert absence by letting a wait expire are the +// real defect and are tracked on #2335; when they are gone, revisit this. +configure({ asyncUtilTimeout: 1000 }); // Node 22+ exposes an experimental `localStorage` placeholder that overrides // happy-dom's implementation. Without `--localstorage-file`, it's an empty diff --git a/clients/web/src/test/storybookSetup.ts b/clients/web/src/test/storybookSetup.ts index bb49c73a..f5f31cf2 100644 --- a/clients/web/src/test/storybookSetup.ts +++ b/clients/web/src/test/storybookSetup.ts @@ -1,12 +1,17 @@ /** * Setup file for the `storybook` Vitest project (#2323). * - * It exists for one call. Storybook's instrumented Testing Library re-exports - * the same `asyncUtilTimeout: 1000` default as the unit project's, and it - * governs the 80 `waitFor` / `findBy*` sites across the story files. Those play - * functions run in a real Chromium against a real Storybook preview, so 1000ms - * is the *tighter* of the two bounds the project has — the per-test ceiling is - * 15000 (#2292) — and it is the one nobody chose. + * 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: * @@ -27,4 +32,4 @@ import { configure } from "storybook/test"; -configure({ asyncUtilTimeout: 5000 }); +configure({ asyncUtilTimeout: 1000 }); diff --git a/docs/quality-gate.md b/docs/quality-gate.md index 833b186b..d5309172 100644 --- a/docs/quality-gate.md +++ b/docs/quality-gate.md @@ -40,7 +40,7 @@ That is the readable half, and prose rots. The enforced half is `scripts/lib/wor | `npm run test:scripts` | Table-driven unit tests (`node --test`) for the guard's own pure parsers (`scripts/lib/npm-scripts.mjs`, `scripts/lib/tsc-program.mjs` + the exported helpers of `verify-typecheck-coverage.mjs` and `verify-dep-lockstep.mjs`), one case per rule they encode, plus two suites over shared `scripts/lib` helpers that no smoke can check itself: `resolve-node-bin.test.mjs` — the cross-platform bin resolver (#1939), pinned against the real `bin`/`exports` shapes of the packages the scripts actually spawn — and `announced-child.test.mjs` — the spawn/readiness ownership helper (#2000), which drives real `node -e` children to prove a child that never announces is still published to the caller before the timeout throws, and so is reachable by teardown rather than orphaned. Four more do the same: `mcp-app-flow.test.mjs` covers the shared MCP Apps flow (#2003) — the deep link's two CSRF gates and `appArgs` encoding, plus `driveAppFlow`'s failure branches against a stand-in page, all of which are dead code from the happy-path smokes' point of view and would otherwise surface only as opaque timeouts; `render-smoke.test.mjs` and `pty.test.mjs` cover the TUI boot harness ([#2147](https://github.com/modelcontextprotocol/inspector/issues/2147)) — the former driving real `node -e` stubs to prove that a child which paints the marker and *then* dies is a **failure**, which the old harness reported as OK and which no fixed TUI can reproduce; the latter pinning the three mutually-incompatible `script(1)` invocations, where a wrong guess stops the pseudoterminal from being allocated at all; and `ensure-test-servers.test.mjs` pins the [#2111](https://github.com/modelcontextprotocol/inspector/issues/2111) invariant — that `test-servers/build` is rebuilt **even when it already exists** — which no smoke can assert about itself, since one driving a stale fixture reports a product failure rather than a staleness one. `workflow-gate.test.mjs` is a different shape again ([#2146](https://github.com/modelcontextprotocol/inspector/issues/2146)): besides the table of parser cases it runs the parser over the repo's real `.github/workflows/**`, so a workflow that invokes the local-only gate or a non-Chromium engine pass fails here rather than in a CI run nobody expected to be red. Five more arrive with the skills (#2163). `skill-manifest.test.mjs` pins the frontmatter contract one case per rule — most importantly that a fence which is not a whole line is not a terminator, and that malformed YAML is an error rather than an empty description. `verify-skills.main.test.mjs` drives that guard's `main()` over fixture directories, the sibling guards' `*.main.test.mjs` pattern, so it cannot stop enforcing while the parser tests stay green. `skill-eval.test.mjs` covers the trigger eval's stream parsing and its run classification, which an eval run's happy path never reaches and which would otherwise report a plausible hit rate for a CLI that failed to run. `verify-skills-cli.test.mjs` covers which validator gets run — a local CLI only at the exact pin — and the orchestration around it, neither of which an ordinary run on a working machine exercises. `claude-cli.test.mjs` covers the one place that decides how the CLI is spawned — the Windows `.cmd` shim needs a shell, and its arguments then need quoting — with the platform **injected**, since a win32-only bug that no runner here executes is exactly the kind that ships. Runs in `validate` — and `verify:typecheck-coverage` guards *this* gate in turn (reachable from `validate`, non-empty test set, every test file matched by the `test:scripts` glob), since `node --test` silently skips a file its glob misses and still exits 0. | | `npm run verify:typecheck-coverage` | The typecheck-coverage analog of the above (#1791): for each Node client (auto-discovered from disk — enrolled via its `typecheck` script's projects, or for a `tsc -b` client like `clients/web` via its `tsconfig.json` `references`) it runs those projects with `tsc --listFilesOnly`, unions them, and **fails** listing any tracked `.ts`/`.tsx`/`.mts`/`.cts` under the client that lands in no project (so a new top-level config/helper can't silently go untypechecked). It also requires, deny-by-default, the first-party TS no client owns (`test-servers/src`, the root `vitest.shared.mts`, all of `core/`, and any new top-level location) to land in some client project's tsc pass — so a `core` `*.tsx` web's projects don't reach is caught too. Also asserts the gate is wired (each client's typecheck pass — its `typecheck` script, or web's `tsc -b` — is reachable from its `validate`, and the root chain runs each client's `validate`). Runs in `validate`. | | `npm run verify:dep-lockstep` | Guards the "one version per install-crossing dependency" invariant (#1896). v2 is not a workspace, so a client's test project compiles the shared first-party TypeScript — `core/`, `test-servers/src`, and the root-owned `vitest.shared.mts`, all of which resolve their dependencies from the **root** install — alongside the client's own sources, putting the same package in one `tsc` program twice. At the same version that's harmless; skewed, TypeScript must relate two structurally-distinct copies of every type, which for a recursive-generic surface is exponential (zod `4.3.6` vs `4.4.3` exhausted the 4GB tsc heap in `clients/web`). Derives its candidate set from **what actually enters each program** (#1965) — every client tsconfig project listed with `tsc --listFilesOnly` via the shared `scripts/lib/tsc-program.mjs`, each resolved `node_modules` file mapped to its owning install, keeping the packages that reach one program from two installs (a package whose declarations arrive only through another package's `.d.ts`, as `@modelcontextprotocol/sdk`'s do, is invisible to a scan of first-party imports). Prices each copy from the lockfile entry for the exact install path the program resolved, compares only the installs that met in one program, and **fails deny-by-default** on any disagreement not in the annotated `TOLERATED_SKEW` allowlist — empty today — with an allowlisted package tolerated only *within a major version*. A **second tier** (#2226) runs alongside it, asking the weaker but broader question the `AGENTS.md` rule actually states: does a package this repo *declares* anywhere resolve to two versions across our installs at all? Its candidate set is every name in any install's `dependencies`/`devDependencies`/`optionalDependencies` — unioned across the root and all four clients, so a copy declared by only one of them still counts — that **more than one install holds a top-level copy of** (17 packages today). Nested copies are excluded: one exists because some dependency asked for a different version, so it is that dependency's range to govern, not ours. Neither tier subsumes the other — the program tier sees a copy no manifest names (`@modelcontextprotocol/sdk`, arriving through another package's `.d.ts`), while the declared tier sees a **transitive** copy no program loads (cli's `@types/node`, hoisted via `@types/express` — the case that motivated it), two **clients** disagreeing with no root copy involved (`@types/react`, web against tui), and the peer shadows `eslint`/`typescript`/`vitest` that never enter a program. Same deny-by-default and same within-a-major rule, against its own `TOLERATED_DECLARED_SKEW` — also empty. Two limits: it reads **lockfiles**, so an uncommitted hand-installed copy is invisible, and it compares only declared names, so a purely transitive package no manifest names stays the first tier's business. Runs in `validate`. -| `npm run verify:test-timeouts` | Guards the wall-clock budgets the test gates run under (#2323). The class it encodes against is a budget **nobody chose**: three of the six Vitest projects ran on Vitest's own `testTimeout: 5000` and five on its `hookTimeout`/`teardownTimeout: 10000`, sized for an idle machine rather than the one this team works on — three or four concurrent agent sessions in separate worktrees, each free to run a full `local:gate`, on eight logical cores. A correct, deterministic test cut off by such a budget fails a gate its diff did not break, which is the same "channel nobody trusts" failure [Lint has no warning tier](../AGENTS.md#lint-has-no-warning-tier) describes from the other direction; #2292, #2278, #2250, #1942 and #1742 were each an instance, found one site at a time. It asks **Vitest itself** to resolve each of the four configs and reads the number a test actually gets, rather than checking that a key is absent from a config block — which would pass just as happily on a config that had stopped being loaded. Three properties beyond the numbers: a **seventh project** with no row is an error rather than a silent skip, since an unstated budget is precisely the drift being prevented; **`retry` must stay unset**, because a retry turns a load-induced red into a silent green on the only pre-push gate here and would re-open #1596; and both web projects must `configure({ asyncUtilTimeout })`, the Testing Library half that no Vitest config can see — it defaults to 1000ms, governs 788 `waitFor`/`findBy*` sites, and becomes the *binding* bound on an async assertion once the enclosing per-test budget rises. Observed **red** against the pre-#2323 config before it was trusted. Runs in `validate`; its pure decision logic has a sibling `verify-test-timeouts.test.mjs` under `test:scripts`. +| `npm run verify:test-timeouts` | Guards the wall-clock budgets the test gates run under (#2323). The class it encodes against is a budget **nobody chose**: three of the six Vitest projects ran on Vitest's own `testTimeout: 5000` and five on its `hookTimeout`/`teardownTimeout: 10000`, sized for an idle machine rather than the one this team works on — three or four concurrent agent sessions in separate worktrees, each free to run a full `local:gate`, on eight logical cores. A correct, deterministic test cut off by such a budget fails a gate its diff did not break, which is the same "channel nobody trusts" failure [Lint has no warning tier](../AGENTS.md#lint-has-no-warning-tier) describes from the other direction; #2292, #2278, #2250, #1942 and #1742 were each an instance, found one site at a time. It asks **Vitest itself** to resolve each of the four configs and reads the number a test actually gets, rather than checking that a key is absent from a config block — which would pass just as happily on a config that had stopped being loaded. Three properties beyond the numbers: a **seventh project** with no row is an error rather than a silent skip, since an unstated budget is precisely the drift being prevented; **`retry` must stay unset**, because a retry turns a load-induced red into a silent green on the only pre-push gate here and would re-open #1596; and both web projects must `configure({ asyncUtilTimeout })`, the Testing Library half that no Vitest config can see — it governs 788 `waitFor`/`findBy*` sites and is the binding bound on any async assertion, being tighter than any per-test budget here. What is enforced there is that the value is **stated**, not that it is large: raising it to 5000 was tried and measured worse (three arms of three full unit runs — unmodified `v2/main` 3/3 green, the raise 1/4/0 files timing out, the raise reverted 3/3 green), because a wait that is *meant* to expire spends its whole budget on the happy path, so a 5x raise is a 5x cost on exactly those tests and starves the worker pool. The reasoning is recorded at the call site. Observed **red** against the pre-#2323 config before it was trusted. Runs in `validate`; its pure decision logic has a sibling `verify-test-timeouts.test.mjs` under `test:scripts`. | `npm run local:gate` | **Mandatory pre-push command.** `validate` → `verify:skills:cli` → `coverage` → `verify:build-gate` → `verify:bundle-externals` → `smoke` → `smoke:web:firefox` → `local:storybook`. A strict superset of GitHub CI — see [Two tiers](#two-tiers-github-ci-and-the-local-gate). Named `local:` rather than `ci` on purpose (#2146); there is no `npm run ci` alias. | | `npm run pack:verify` | Publish smoke — see [Publishing](./publishing.md). | diff --git a/scripts/lib/browser-timeouts.mjs b/scripts/lib/browser-timeouts.mjs index ffb7b6ac..ed3a8bb1 100644 --- a/scripts/lib/browser-timeouts.mjs +++ b/scripts/lib/browser-timeouts.mjs @@ -2,8 +2,9 @@ * Playwright wall-clock budgets for the web smokes (#2323). * * Before this, `smoke-web-tabs.mjs`, `smoke-web-elicitation.mjs` and - * `smoke-web-browser.mjs` carried 15 locator budgets between them across three - * independently-chosen scales, with no config anywhere to raise. They are the + * `smoke-web-browser.mjs` carried 19 budgets between them across three + * independently-chosen scales, with no config anywhere to raise — 17 locator + * operations plus a `page.goto` and a `waitForLoadState`. They are the * same three or four decisions repeated, so they are named once here and the * scripts import them — the same shape `render-smoke.mjs`'s `DEFAULTS` already * has, and for the same reason: a budget nobody can find is a budget nobody diff --git a/scripts/verify-test-timeouts.mjs b/scripts/verify-test-timeouts.mjs index 6fab43b4..82527348 100644 --- a/scripts/verify-test-timeouts.mjs +++ b/scripts/verify-test-timeouts.mjs @@ -29,9 +29,11 @@ * "do not raise" decision, so it is enforced rather than remembered. * * It also asserts the Testing Library half, which no Vitest config can see: - * `asyncUtilTimeout` governs every `waitFor` / `findBy*` in the web projects, - * defaults to 1000ms, and becomes the *binding* constraint on an async - * assertion once the enclosing per-test budget rises. + * `asyncUtilTimeout` governs every `waitFor` / `findBy*` in the web projects + * and is the binding constraint on an async assertion, since it is tighter than + * any per-test budget here. What is enforced is that each web project *states* + * it — raising it was measured and rejected, for the reason recorded in + * `clients/web/src/test/setup.ts`. * * ⚠️ Observed to FAIL against the unfixed config before it was trusted: on * `origin/v2/main` it reports `unit`, `tui` and `launcher` at @@ -118,8 +120,13 @@ export const ASYNC_UTIL_SITES = Object.freeze([ * The value those sites must configure — stated here for the same reason the * Vitest budgets are: a guard that accepts any number at all would pass on * `asyncUtilTimeout: 1` (Copilot). + * + * It is Testing Library's own default, and deliberately so: what this guard + * enforces is that the value is *stated*, not that it is large. Raising it was + * measured and rejected — see the long comment in + * `clients/web/src/test/setup.ts`. */ -export const EXPECTED_ASYNC_UTIL_TIMEOUT = 5_000; +export const EXPECTED_ASYNC_UTIL_TIMEOUT = 1_000; /** * Directory every Vitest config in this repo lives one level under. Discovery @@ -168,7 +175,7 @@ export function checkProject(name, config, expected = EXPECTED_PROJECTS) { * Strip `//` and block comments so a commented-out call cannot satisfy a check. * * Comment-aware rather than exact, deliberately: a naive scan of the raw source - * would accept a `// configure({ asyncUtilTimeout: 5000 })` left behind by + * would accept a `// configure({ asyncUtilTimeout: 1000 })` left behind by * someone disabling it, which is the most likely way this stops being * configured (Copilot). String literals are not parsed out — a `configure(` in * a string would still be accepted — but these two files are fifteen lines each @@ -363,7 +370,12 @@ export function identifyProject(project, expectedNames) { } async function main() { - const failures = []; + // Seeded from discovery, not empty: the unknown-project check below can only + // reject a project this guard actually resolves, so a config it never opens + // is invisible to it. Without this line the deny-by-default claim rested on + // `test:scripts` happening to run the same comparison — a different command, + // which a standalone `npm run verify:test-timeouts` does not invoke (Copilot). + const failures = checkConfigRootCoverage(discoverConfigRoots()); let checked = 0; for (const { root, projects: expectedNames } of CONFIG_ROOTS) { diff --git a/scripts/verify-test-timeouts.test.mjs b/scripts/verify-test-timeouts.test.mjs index b5cc6cb2..2839cefa 100644 --- a/scripts/verify-test-timeouts.test.mjs +++ b/scripts/verify-test-timeouts.test.mjs @@ -116,7 +116,7 @@ test("an unrecognized project name is not silently matched", () => { test("a setup file that configures asyncUtilTimeout passes", () => { const source = `import { cleanup, configure } from "@testing-library/react"; -configure({ asyncUtilTimeout: 5000 });`; +configure({ asyncUtilTimeout: 1000 });`; assert.deepEqual(checkAsyncUtilSource(source, "@testing-library/react"), []); }); @@ -133,7 +133,7 @@ test("the import is checked, not just the call", () => { // `@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 });`; +configure({ asyncUtilTimeout: 1000 });`; assert.deepEqual(checkAsyncUtilSource(source, "storybook/test"), [ 'does not import from "storybook/test"', ]); @@ -158,13 +158,13 @@ test("a commented-out configure() does not satisfy the check", () => { // The most likely way this stops being configured is someone disabling it in // place, and a raw-source scan would read that as configured (Copilot). const lineComment = `import { configure } from "storybook/test"; -// configure({ asyncUtilTimeout: 5000 });`; +// configure({ asyncUtilTimeout: 1000 });`; assert.deepEqual(checkAsyncUtilSource(lineComment, "storybook/test"), [ "does not call configure()", ]); const blockComment = `import { configure } from "storybook/test"; -/* configure({ asyncUtilTimeout: 5000 }); */`; +/* configure({ asyncUtilTimeout: 1000 }); */`; assert.deepEqual(checkAsyncUtilSource(blockComment, "storybook/test"), [ "does not call configure()", ]); @@ -172,24 +172,24 @@ test("a commented-out configure() does not satisfy the check", () => { test("the configured value must be the expected one, not merely present", () => { const source = `import { configure } from "storybook/test"; -configure({ asyncUtilTimeout: 1 });`; +configure({ asyncUtilTimeout: 9000 });`; assert.deepEqual(checkAsyncUtilSource(source, "storybook/test"), [ - `configures asyncUtilTimeout as 1, expected ${EXPECTED_ASYNC_UTIL_TIMEOUT}`, + `configures asyncUtilTimeout as 9000, expected ${EXPECTED_ASYNC_UTIL_TIMEOUT}`, ]); }); test("a numeric separator in the configured value is read, not rejected", () => { const source = `import { configure } from "storybook/test"; -configure({ asyncUtilTimeout: 5_000 });`; +configure({ asyncUtilTimeout: 1_000 });`; assert.deepEqual(checkAsyncUtilSource(source, "storybook/test"), []); }); test("stripComments leaves executable code alone", () => { const source = `/** header */ import { configure } from "storybook/test"; // trailing -configure({ asyncUtilTimeout: 5000 });`; +configure({ asyncUtilTimeout: 1000 });`; const code = stripComments(source); - assert.match(code, /configure\(\{ asyncUtilTimeout: 5000 \}\)/); + assert.match(code, /configure\(\{ asyncUtilTimeout: 1000 \}\)/); assert.doesNotMatch(code, /header/); });