fix: address Copilot review round 2, and drop the asyncUtilTimeout raise

The inline finding was a real bug: main() never called the discovery check, so
the deny-by-default claim rested on test:scripts happening to run the same
comparison — a different command, which `npm run verify:test-timeouts` does not
invoke. `failures` is now seeded from it, and a probe config under clients/ was
observed to fail the guard.

Three suppressed findings, all correct. The afterEach in oauth-interactive
restated the shared hookTimeout and labelled it as a round trip, when it only
stops a server and unlinks a file; it now carries no budget. The file-lock
constants were named for the opposite of what they bound — 90s covers retries
running out against a holder that never yields, 60s covers the stale-lock
takeover that succeeds — so they are LOCK_RETRIES_EXHAUSTED_MS and
STALE_LOCK_TAKEOVER_MS. The browser-timeouts header said 15 budgets; it is 19.

Separately, and more importantly: the asyncUtilTimeout raise is reverted, on
measurement. Verifying round 2 turned up intermittent 15s timeouts across
unrelated unit files, so three arms of three full unit runs were taken in this
worktree at comparable load — unmodified v2/main 3/3 green, this change with
5000 producing 1, 4 and 0 timing-out files, and the same change with that one
line reverted 3/3 green. 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
saturates the worker pool, starving tests that were never slow.

Both web setup files therefore state asyncUtilTimeout at the library's own
value rather than raising it, which is what #2323's rule actually asks for: a
budget nobody chose is the defect, not a budget that is small. The tests that
assert absence by letting a wait expire are the real problem and are #2335.

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 11:49:30 -04:00
commit eff0368491
9 changed files with 102 additions and 50 deletions

View file

@ -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

View file

@ -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",

View file

@ -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,
);
});

View file

@ -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

View file

@ -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 });

View file

@ -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). |

View file

@ -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

View file

@ -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) {

View file

@ -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/);
});