fix(ci): bound the mcp-conformance-fixture beforeAll with hookTimeout - #2239
fix(ci): bound the mcp-conformance-fixture beforeAll with hookTimeout#2239willgriffin wants to merge 1 commit into
Conversation
`packages/mcp-conformance-fixture/vitest.config.ts` set `testTimeout: 240_000` but no `hookTimeout`, so its `beforeAll` ran against vitest's 10s default. Hooks do not inherit `testTimeout`. Nearly all of that suite's work is in the hook, not the test bodies: it regenerates the MCP server with `MCPGenerator`, spawns a `tsx` child over stdio to negotiate the protocol and list tools, then spawns a second `tsx` child for the HTTP adapter. The two test bodies only issue requests against what the hook already built (25ms + 192ms locally, against 240s). On a loaded runner the hook measured 10,022ms against the 10,000ms default and ejected PR #2230 from the merge queue. The package sits in the shared `test-packages` shard, so the miss was never local to that PR: any PR in the queue could hit it, after a full merge-group validation cycle had already been spent. Verified rather than assumed. The hook takes no per-hook timeout argument; `vitest.workspace.ts` is a re-export shim, not a vitest workspace config; `smrtVitestPlugin` sets no timeouts; and CI passes no timeout flag or env. Nothing else was ever supplying a bound. Running the unmodified suite with `--hookTimeout=1` reproduces the CI signature exactly -- "Hook timed out in 1ms", 1 file failed, 2 tests skipped -- and a probe config carrying `hookTimeout: 1` fails the same way, confirming the config-level option is the operative one. Also documents the trap in TESTING_STANDARD.md and .claude/rules/testing.md. This is its third occurrence (smrt-svelte teardown in #1426, then #2220), and its signature is misleading: the file fails while every test body reports as skipped, so it does not read as a timeout in the test you budgeted for. Swept the other 32 `vitest.config.ts` files that set `testTimeout` without `hookTimeout`. Only three packages have process-spawning or generation code anywhere in their test files -- smrt-app-mcp, smrt-dev-mcp, tenancy -- and in all three that work sits in test bodies, not hooks. Their hooks are in-process HTTP listens or in-memory sqlite seeds. Measured: smrt-app-mcp's conformance test 189ms against 15s, smrt-dev-mcp's 622ms against 30s. No sibling needs the same fix. Closes #2238
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
Pull request overview
This PR prevents nondeterministic CI failures in @happyvertical/smrt-mcp-conformance-fixture by explicitly bounding Vitest hook execution time (beforeAll/etc.) with hookTimeout, and documents the repo convention that hook timeouts must be set alongside testTimeout when hooks do meaningful work.
Changes:
- Add
hookTimeout: 240_000topackages/mcp-conformance-fixture/vitest.config.tsto match the existingtestTimeout. - Document the “testTimeout does not apply to hooks” pitfall and its CI failure signature in
TESTING_STANDARD.md. - Add the same timeout guidance to
.claude/rules/testing.mdfor agent/dev workflow consistency.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
packages/mcp-conformance-fixture/vitest.config.ts |
Sets hookTimeout to match testTimeout, preventing beforeAll from hitting Vitest’s default hook timeout in CI. |
TESTING_STANDARD.md |
Adds a testing convention note explaining independent testTimeout vs hookTimeout and the “tests skipped” failure mode. |
.claude/rules/testing.md |
Captures the timeout rule in the Claude testing rules so it’s less likely to regress. |
|
Superseded by #2242, which carries this branch's commits cherry-picked verbatim (same authors, same messages) alongside the other two CI-lane fixes. Rationale: these three PRs each needed a full validation pass on the very lane they were unclogging, and #2217/#2237 both edit The merged result is the union of both workflow changes, verified job by job — the three light jobs are hosted per #2236, every heavy job keeps the The branch is preserved. Reopen if #2242 needs to be unwound. |
Summary
packages/mcp-conformance-fixture/vitest.config.tssettestTimeout: 240_000but no
hookTimeout, so itsbeforeAllran against vitest's 10s default —hooks do not inherit
testTimeout. On a loaded runner that hook measured10,022ms and ejected PR #2230 from the merge queue.
The fix is
hookTimeout: 240_000alongside the existingtestTimeout, matchingthe repo's prevailing "match testTimeout" convention (smrt-svelte, ads, chat,
cli, content, …; core goes higher at 60s/120s; bundle-gate is the one package
that deliberately diverges).
The diagnosis was verified, not assumed
Everything that could have been supplying a bound was ruled out:
beforeAll(async () => {…})).vitest.workspace.tsis a re-export shim, not a vitest workspace config —it exports
smrtVitestPluginand a setup path. There is no root vitest config.smrtVitestPluginsets no timeouts (the onlyTimeouthits inpackages/vitest/srcare twosetTimeoutsleeps intest-db.ts).testscript is a barevitest run, invoked through turbo.Two positive reproductions, both against the unmodified hook:
vitest run --hookTimeout=1Error: Hook timed out in 1ms.— 1 file failed, 2 tests skippedhookTimeout: 1That is the CI signature exactly (
Hook timed out in 10000ms, 1 failed, 2skipped), and the second probe confirms the config-level option is the
operative one, not just the CLI flag.
Why the hook, and not the tests, is the expensive part
beforeAlldoes essentially all of the suite's work — slightly more than theissue described, since it spawns two
tsxchildren, not one:rm -rf+mkdirthe generated dirMCPGenerator.generateServer()tsxover stdio → connectClient→ negotiate2026-07-28→listToolsstartHttpAdapter()→ spawn a secondtsxfor the HTTP adapterThe two test bodies only issue requests against what the hook already built:
25ms and 192ms locally, against a 240s budget. Locally the hook is ~1.94s of
the 2.16s total; CI's >10s means roughly 5x amplification under load, which is
ordinary for a shard runner spawning child processes.
Blast radius
The fixture is
private: truebut has atestscript, andscripts/test-packages-shard.mjsexcludes onlysmrt-core— so it runs in theshared
test-packagesshard. The 22ms margin was never local to #2230: any PRin the merge queue could hit it, after a full merge-group validation cycle had
already been spent.
Sibling sweep — none affected
33 of 58
vitest.config.tsfiles settestTimeoutwithouthookTimeout, butthe omission only matters where a hook does heavy work. Only three packages
have process-spawning or generation code anywhere in their test files, and in
all three it sits in test bodies rather than hooks:
smrt-app-mcpcreateHttpServer+listen(0)smrt-dev-mcpcreateHttpServer+listen(0)tenancygenerateServeris insideit()bodies)Their hooks are tens of milliseconds; even 100x CI amplification stays far under
the 10s default. No sibling needs this fix, so the change stays at one package.
Documentation
The issue notes this trap is "worth a line in the testing conventions if it bites
a third time" — it has (smrt-svelte teardown in #1426, then #2220, now this), so
it is recorded in
TESTING_STANDARD.mdand.claude/rules/testing.md. Thesignature is worth naming because it is misleading: the file fails while every
test body reports as skipped, so it does not read as a timeout in the test
you were budgeting for.
Validation
@happyvertical/smrt-mcp-conformance-fixturefull suite: 2 files/2 testspassed, 3.26s (transform 376ms, import 845ms, tests 2.33s)
tsc --noEmitgreen for the fixture packagepnpm smrt dev:knowledge-check: fresh, 0 errors, 0 warningspnpm check:agents-chain: 65 chains under the 32,768-byte cap (the twobiome checkon the three touched paths: all three are ignored bybiome.json(a vitest config and two markdown files), so there is nothing forit to lint
git statusclean apart from the three intended files;git diff-treeon thecommit confirms exactly those three
Closes #2238
{"schema":"hv-agent-run:v1","runtime":"claude","session":"53e2c43c-41e2-4f9f-a63a-996baed10afd","issue":"2238","policy_revision":"1.0.0","status":"complete","validation":["mcp-conformance-fixture full suite: 2 files / 2 tests passed in 3.26s","tsc --noEmit green for the fixture package","diagnosis reproduced twice on the unmodified hook: --hookTimeout=1 and a probe config with hookTimeout:1 both yield the CI signature (1 failed, 2 skipped)","ruled out per-hook argument, root/workspace config, smrtVitestPlugin, and CI flags as alternative sources of the bound","sibling sweep of 33 testTimeout-without-hookTimeout configs: only smrt-app-mcp (189ms/15s), smrt-dev-mcp (622ms/30s), tenancy spawn or generate, and all do so in test bodies, not hooks","pnpm smrt dev:knowledge-check fresh (0 errors, 0 warnings)","pnpm check:agents-chain: 65 chains under cap"]}