fix(qa): take the wall clock out of the preflight sweep's CONTROL leg - #19382
Conversation
`config-wiring-sweep.test.ts` spawned a real `vitest list` child inside each
clocked `it()`. The CONTROL leg's verdict was therefore partly a function of
runner load: it reddened `Test Core (1/6)` on pull requests that touch no file
under `packages/qa/` with `Test timed out in 5000ms.`
Two measurements say the budget was judging something it cannot act on:
1. a `testTimeout` cannot interrupt a synchronous body — `execFileSync`
blocks the worker's event loop, so the timer fires only after the call has
already returned with the right answer (probed on vitest 4.1.11: a 1200 ms
sync spin under a 300 ms budget runs to completion and is then failed);
2. eight subjects x five runs on this container put the CONTROL leg at
432-704 ms for seven subjects and 1521-1942 ms (median 1663) for
`packages/cli` — 33% of the 5000 ms budget, which the ~3x loaded-runner
factor in the card lands on 5 s exactly.
So the clock leaves the verdict rather than growing: both children are spawned
once at module top, outside every clocked window, and each leg asserts on the
recorded status and output. Nothing is skipped, retried or quarantined and no
budget is raised or disabled; the legs assert exactly what they asserted
before, minus the wall clock.
The liveness bound moves onto the thing that can hang: a 60 s
`CHILD_LIVENESS_TIMEOUT_MS` on the child, which spawnSync enforces by killing
it, plus `expectChildRan` so a killed or unspawnable child can never read as a
refusal.
Claude-Session: https://claude.ai/code/session_01QCdUBjM47SxioST9z5Zwdf
Co-authored-by: Claude <noreply@anthropic.com>
`check:cross-package-test-inputs` collects quoted paths out of comments without parsing them, so naming the stall-guard script in the new docblock declared an input this suite never reads. Describing it keeps the declared radius equal to the real one — the same trade the `packageRoots` docblock already records for the example-app config it deliberately does not spell. Claude-Session: https://claude.ai/code/session_01QCdUBjM47SxioST9z5Zwdf Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
Seat grading — ACCEPT. The obvious fix was refused, and the reason it was refused is the finding.⛔ No budget was raised, lowered or disabled. Nothing was skipped, retried or quarantined. The dispatch named those as the wrong answer, and the round did better than avoid them — it explained why they were wrong. ⭐ The diagnosis, which is not "the budget was too small"
Probed rather than reasoned: a 1200 ms synchronous spin under an explicit 300 ms budget runs to completion, reports ⇒ the 5000 ms budget was never protecting anything. It could only take a run that had already produced the correct answer and convert it into a red. ⭐ That reframes the card: it was never a too-tight budget, it was a post-hoc wall-clock assertion attached to a verdict it cannot influence. The fix: the clock leaves the verdict instead of growingBoth children are now spawned once at module top, outside every clocked window — citing ⭐ And the liveness bound did not vanish — it moved onto the child, ⭐ The part that shows the round understood its own riskMoving the clock off a verdict creates a new way to be wrong: a child that was killed or never spawned could read as a successful refusal. The round anticipated exactly that and added Ablation C measures the size of that hole: setting The three ablations, and what each one buys
⭐ B is the one this lane should keep. The dispatch asked "after this change, what makes this leg go red, and can it still go red for the original reason?" — and a negative assertion is the hardest case, because it passes when the thing it names is simply absent. B proves the constant cannot silently drift out from under it. This lane spent the shift on impostor-satisfied controls (#18897, #19371); this is the same class answered before it could happen. The flake, quantified8 subjects × 5 runs: CONTROL 432–704 ms for seven subjects, 1521–1942 ms (median 1663) for In-passing fix, correctly declaredThe refusal leg carried the identical exposure (775–980 ms for ⭐ A rejected alternative, recorded instead of discardedAn in-process CONTROL (import each config with a clean ⇒ it is recorded in the file header so the option stays visible if wall time ever becomes the problem. ⛔ A faster instrument that answers a narrower question is not an improvement, and saying so in the file is worth more than the speed. Seat verification, ⛔ not taken from the report
Generated by Claude Code |
Fixes #18982
Clause-②: no
The finding
⭐ CONTROL — refuses NOTHING when no override is namedspawned a realvitest listchild inside its own clockedit()window, so its verdict carried a wall-clock component. That is what reddenedTest Core (1/6)on pull requests that change no file underpackages/qa/.⛔ The budget was not too small. Two measurements say it was judging something it cannot act on.
1. On a synchronous body a
testTimeoutcannot interrupt anything — here it was a post-hoc wall-clock assertion and nothing else.execFileSyncblocks the worker's event loop, so vitest's timer cannot fire until the call has already returned, with the right answer. Probed on vitest 4.1.11, a 1200 ms synchronous spin under an explicit 300 ms budget:The body ran to completion — reported duration 1210 ms — and was failed afterwards. So the 5000 ms budget protected nothing (a child that truly hangs blocks that same timer forever) and its only reachable effect was to fail a leg that had already produced the correct verdict.
2. What it measured was runner load, not the property. Eight subjects x five runs, this container, under the shared verify lock:
packages/clivitest list --filesOnly--hookTimeout=11663 ms is 33 % of the 5000 ms budget, the band #18982 names as the crossing condition, and
packages/cliis the subject that actually failed. The card's own CI readings of this leg on sibling subjects (2098 ms and 1442 ms, where this container reads 432–704 ms) put the loaded-runner factor near 3x — which landspackages/clion 5 s exactly.What changed
⛔ Nothing is skipped, retried, quarantined or deleted, and no budget anywhere is raised, lowered or disabled. The clock leaves the verdict instead:
it()now asserts on the recordedstatusandoutput, in microseconds.CHILD_LIVENESS_TIMEOUT_MS = 60_000on the child itself, whichspawnSyncenforces by killing it. That is protection this file did not have — 60 s is ~31x the measured p100 (1942 ms) and ~10x its 3x-loaded projection, so it cannot police slowness; it converts "never exits" into a named failure.expectChildRanrefuses to let a killed or unspawnable child read as a refusal. A child that never exited carries a NULL status, and the refusal leg'sstatus !== 0was satisfied by null.Before/after of the same 65 assertions doing the same work on the same tree (
HEAD~1file restored, byte-proven, then restored again):tests)import)After this change, what makes this leg go red — and can it still go red for the original reason?
Yes, and it is the only leg that does. Three ablations, each mutated and restored through
scripts/ablation-replace.mjs(anchor hit, blob change and byte-exact restore printed by the tool):A. A config that refuses every run —
packages/types/vitest.config.ts'srunProjectCliOverridePreflight({ argv: process.argv, … })replaced by a constantargv: ['vitest', 'run', '--hookTimeout=1'], which is exactly the defect the CONTROL's own comment names:The refusal leg stayed green — it is satisfied by a config that refuses everything — so the CONTROL is carrying the discrimination alone, exactly as its comment claims.
B. The
REFUSALconstant drifts from whatrenderInertOverrideNoticeprints: 8 failed, all of them the refusal leg. So the CONTROL's negativenot.toContain(REFUSAL)cannot go vacuous without eight positive assertions on the same constant, same output channel, same child going red first.C. The new liveness bound set to 1 ms: 16 failed, every one of them by name —
— which is the guard in 3 above doing its job: under the old code eight of those sixteen would have passed.
What no longer makes it red: wall-clock time. There is no timing component left in either leg's verdict.
Verification
At
73e9cc8c64:pnpm --filter @objectstack/vitest-filter-preflight test— 111 passed (111),tests 201mspnpm --filter @objectstack/vitest-filter-preflight typecheck— exit 0 (tsc --listFilesconfirms all three test files are in the program)pnpm lint— repo-wideeslint . --no-inline-config, exit 0, no narrowingscripts/pm/dispatch-gates.mjs --commands: 41 green, 5 NOT MEASURED (check:dts-closure,check:dual-build-cjs-loads,check:lean-entry-closure,check:sourcemap-no-sources-content,check:type-check-debt— all exit 3,PREREQUISITE NOT MET, nodist/in the tree; a whole-repo build is CI's).--ranreconciliation: 46 derived, 46 accounted, 0 UNRUN.check:cross-package-test-inputsreddened on the first draft because the new docblock spelled a script path in backticks and that gate collects quoted paths out of comments; the mention is now described rather than spelled, the way this file's ownpackageRootsdocblock already handles the example-app config it deliberately does not name. Declaring the path instead would have widened this suite's real input radius to a file it never reads.No changeset:
packages/qa/vitest-filter-preflightisprivate: true, has no build and nofiles[], so this diff publishes nothing —skip-changeset.Acceptance notes
packages/cliagainst the same 5000 ms budget, ~2.6 s at the card's 3x load factor. It is fixed by the same move rather than left for the next card, because the two legs are onedescribe.eachbody and fixing one would have left the file's shape unchanged. Noted, not filed.-t-filtered run inside this file now pays all sixteen children even when it selects oneit()(one pass, medians: ~9.0 s, the same total as before). Recorded in the file header.process.argvand watching stderr — was built and measured (10–55 ms for seven subjects, ~1.0 s forpackages/cli; it halves the children) and rejected: it executes the config module but never vitest's project resolution or its glob, so it cannot see a config that refuses nothing and still resolves no runnable project, whichexpect(run.status).toBe(0)does see. Trading the flake for a narrower instrument is the [finding] two of three per-package pins had their lit control satisfied by an ECHO, not a real survivor — at least four more fixtures were never checked against the discriminant #18897 direction. Recorded in the file header so the option stays visible if wall time ever becomes the problem.Generated by Claude Code