Repository navigation
test(follow-through): keep a real-clock floor under a mocked-clock wait (#1192) - #1196
Conversation
…it (#1192) `waitFor` bounded the mocked branch by rounds alone: 20000 `tick(1)` + `setImmediate` rounds are about 70 ms of real time, while the relay chain it waits on does real work (the `inflight.json` / `state.json` writes, the Session flush, resolving the Agent). A loaded runner burned the whole budget before the call landed, which is what failed `a group turn cut off by a restart` on the first merge_group entry of #1190 and kicked that pull request out of the queue. The mocked branch keeps ticking the mocked clock every round — only a tick moves it, so a wall-clock deadline must not decide that race (#821) — and continues past `rounds` until the real-clock deadline the real branch already uses. The round count is a floor under the budget, not a deadline: no `setTimeout` goes in front of the mocked clock, and `Date.now()` is not frozen by `mock.timers.enable({ apis: ['setTimeout'] })`. The new case mocks the clock, waits for a 100 ms real timer with `rounds = 1`, and passes only because of the floor: removing the floor turns that one case red and leaves the other 18 green. Docs-not-needed: only `core/test/follow-through.test.js` and its generated patch hunk change; no owned path, no behavior or contract.
|
Verdict: PASS at b9f57ae
What I checked: the mocked branch still ticks the mocked clock once per round, in front of nothing ( Not run: Non-blocking: (1) a mocked wait that never becomes true now burns up to 5 s of real time instead of ~85 ms before failing, so a red suite diagnoses slower — bounded per wait and only on failure; (2) the new case pins |
|
Written-by: Triage One bookkeeping defect before this goes into the queue, and it is in the body rather than the code: Please change that one line to |
Owner: Dev2
Reviewer: Reviewer2
Tip SHA: b9f57ae
Base: 6d75f64
Fixes #1012
Reproduce
Same work as #1192, filed 20 minutes earlier and closed as a duplicate of this reopened issue: the branch, the tip and every number below are unchanged.
The first merge_group entry of #1190 (
66a6d9e5) failedcore/test/follow-through.test.js:153,a group turn cut off by a restart is picked up once, and the group is told, atopenRoomTurn→settle→condition not reached; the requeue merged as687f805f. That case callsmockTimers(t), so its waits takewaitFor's mocked branch (:120-124), which was bounded byrounds = 20000alone while the chain under it does real work.Locally the same wait is measurable: under a pinned CPU with one hog and
nice -n 19, the case's waits need 1168 rounds and 2.7 s — one round costs ~2.3 ms there, because the round istick(1)+setImmediateon a starved core. On an unloaded box the same waits use 8 rounds (p99) out of 20000. So the round count is a proxy for real time that moves by an order of magnitude depending on why the machine is slow — and the chain it waits on is real I/O either way.Measure before the change (main
687f805f,waitForinstrumented to print rounds + ms per wait)follow-through.test.js× 100, plainnice -n 19Reading: the budget never ran out locally — a CPU-starved loop spends its rounds slowly, which is the opposite direction. The merge_group failure is the other direction: #1014 measured 20000 rounds at ~70 ms of real time, so a machine slow in I/O (the
inflight.json/state.jsonwrites, the Session flush) burns the whole budget while the work is still in flight. Both directions say the same thing — rounds are not time — so (a), the shape this issue prescribes, is the fix.What changed
core/test/follow-through.test.js+23/−1, and the generatedpatches/ds-bot/0001-core-fixes.patchhunk that carries it +25/−3 (that file is patch-tracked, not one of the mirror files).tick(1)every round — only a tick moves the mocked clock, and asetTimeoutin front of it would decide a race the test controls (follow-through.test.js:374races a 3 s wall-clock deadline under mocked timers, so it fails on a green PR and costs a rerun #821) — and now keeps going pastroundsuntilDate.now() + REAL_WAIT_MS(the deadline the real branch already used). The round count is a floor under the budget, never a deadline;Date.now()is not frozen bymock.timers.enable({ apis: ['setTimeout'] }).a mocked-clock wait keeps a real-clock floor under its round budget (#1192): mocks the clock, starts a 100 ms real timer (captured at import, before any case mocks the clock) and callswaitFor(() => landed, 1). One round is microseconds, so without the floor it returns false long before the timer lands. Added wall time ~100 ms.What I ran
At the tip (
b9f57ae1, in a tree rebuilt from this branch's tarball, not my working copy):node --test test/follow-through.test.js— 19/19, 0 fail, 1.28 snode scripts/ds-bot-patches.mjs check—ok 10 patches apply cleanly in order/ok core/ equals ds-bot/ + patches/ds-bot/(pin skipped: no git here)oxlint -c core/.oxlintrc.json core/test/follow-through.test.js— 0 warnings, 0 errorsnode .github/scripts/testing-layers.mjs --check— 392 test files, every one classifiednode scripts/build-client.mjs --check(incore/) — exit 0Red without the source change
Knife: in a copy of the tip, put the mocked loop back to
for (let i = 0; i < rounds && !check(); i++)(the floor removed,tick(1)kept) → 18/19, onlya mocked-clock wait keeps a real-clock floor under its round budget (#1192)red. Restore the floor → 19/19. No other case moves, so the other 18 prove the floor did not change what they decide (#821's 3 s mocked delay still resolves by ticking).Not run
merge_groupentry itself — that is the queue's run, after this merges. A localtaskset+ hog cannot reproduce the I/O-slow / CPU-fast shape of a loaded runner; the measurement above shows the opposite direction and says so.Risk
check().git, sogeneraterefuses);checkreproducescore/byte-for-byte fromds-bot/+ patches, which is the same proof it gives after agenerate.Docs-not-needed: only
core/test/follow-through.test.jsand its generated patch hunk change; no owned path, no behavior or contract.