Waiting for engine state in tests
Issue #930 separates the condition a test asserts from how quickly its host can run git, a shell, or a filesystem operation. A fixed 1.5–120 second timeout around that work was also measuring unrelated host load.
In-process workflows
The runtime fan-out/resume-barrier regressions and the runner's parked human gate run inside testing/synctest.Test. Go's virtual clock advances when the bubble's goroutines are durably blocked; CPU scheduling and filesystem work do not spend the test's virtual deadline. Existing cancellation-grace and lost barrier bounds still detect a workflow that cannot progress.
The runner human-gate test explicitly waits for quiescence, checks that the child persisted paused_waiting_human and the parent has not returned, then cancels the parent and joins the result. It no longer needs to spend 1.5 real seconds proving that a human has not answered.
Real-process service fixtures
Service launch, resume, subbot control, reconciliation and restart tests keep a real clock. waitForSubbotStatus observes the persisted state and reports a terminal child or parent immediately. awaitRunCompletion joins the service's Done channel. Neither predicts how long creating a worktree or running a shell should take.
runWaitContext bounds one wait by the EARLIER of two figures (runWaitTimeout derives them; its own unit test pins the edge cases). First, a per-operation ceiling — 30s * waitSlowdown, the package's existing scale-up of the figure these waits carried. t.Deadline() alone would not do: it is one absolute instant for the whole test BINARY, so a single wait that never satisfies would spend the package's entire remaining -timeout (~10 min on the unit job's default, ~30 min under the race job's -timeout 1800s) and hand every test scheduled after it a near-expired deadline. Second, what is left of the harness deadline minus waitDeadlineMargin (30 s), so the wait fails as its own named assertion — with the run goroutine joined and the t.TempDir removable — rather than as a package-wide harness panic naming whichever test was in flight. A deadline already inside that margin yields a non-positive bound, i.e. an already-expired context: the caller reports immediately instead of waiting past the harness. go test -timeout=0 has no deadline and keeps its meaning: no wall-clock ceiling at all. Polling every 50 ms is an observation cadence, not an execution budget.
runWaitTimeout is the package's ONE wait policy: the older waitBudget (scale a caller's hand-picked figure, clamp it to the harness) lost its last pkg/runview caller here and was removed with it, keeping only its two constants. The e2e package keeps its own copy — it is a separate package with live callers.
Do not wrap these real-process service fixtures in synctest: external process I/O and their polling/background workers do not provide the same durable-block contract as the in-process fixtures.
Nearby wait audit
The service test sweep included time.After, context.WithTimeout and time.Now().Add; not every duration is an estimate of engine speed.
| Test family | Disposition |
|---|---|
subbot_human_gate, subbot_reconcile, subbot_child_control, subbot_restart | Persisted child-state and joined-run waits use the operation ceiling clamped to the harness deadline. The explicit ten reconciliation passes remain the reconciliation oracle. |
service_launch_{budget,dispatch_fields,loop_budget,pause} | Completion waits use the operation ceiling clamped to the harness deadline. |
service_resume_{budget,hash,snapshot} | Completion waits use the operation ceiling clamped to the harness deadline. |
broker, service_hook_observers | No bound: the fact is already true when the select is entered. Publish fans out with a non-blocking send performed by its caller, and Cancel/CloseRun close the subscriber channel under the write lock, so each is read with a default (see the two recipes below). |
file_event_source, file_log_source, service_eventsource, service_stream | Retain file-tail/event delivery bounds and deliberate no-event observations. |
manager | No real-clock bound left: the waits run in a testing/synctest bubble and assert exact virtual figures, and what a Cancel/Deregister leaves behind is read with a default. |
service_drain, reconcile_shutdown | Retain shutdown and cancellation contracts: the budget handed to Drain/Stop IS what they exercise, and the facts they wait for come from a spawned goroutine. |
service_stop_background | StopBackground awaits the reconcile goroutine, so that done is read with a default. pipelineStop is only the scheduler's stop SIGNAL — StopBackground closes it itself — so the default there witnesses the signal and the WORKER is witnessed by its goroutine frame, as the alert manager's is: a poll-until-settled, not a bound. |
periodic_reconcile, reattach | Retain bounded observations of reconciliation/timer behaviour in their separate fixtures. |
merge_claim, service_runs_skip_log, service_test | Backdated timestamps construct old records; they are not wait deadlines. |
subbot_{restart,child_control,human_gate,reconcile} service teardown | stopService keeps the separate 30-second shutdown budget (waitDeadlineMargin); it bounds service teardown, not child workflow progress. A context.Background() there is unbounded: Manager.Stop's ctx.Done() arm is nil and can never fire, so a wedged run goroutine blocks teardown forever. |
Falsification performed
With a temporary two-second real subprocess delay immediately before the runner starts its child engine, the old human-gate test failed after 2.06 seconds; the new test passed after 2.07 seconds. The delay was removed afterward.
Changing the park to use the already-cancelled child context made the new test fail with park returned before the parent was cancelled. Removing the resume barrier release made both each/all error-path regressions fail with resume hung. Those production mutations were also removed: this change contains only tests and this wait audit.
TestFanOutAbandonedBranchDoesNotRaceRunState states its value as "running exactly this interleaving under the CI -race job", and virtual time elapses its 100 ms branchCancelGracePeriod the instant the bubble blocks durably — so the conversion was falsified on the oracle itself, twice, under go test -race -count=3:
- Removing the retired-epoch guard (
parallelExecutionState.updateBranch'sif p.retired { return false }, the ADR-095 write the final assertion covers) failed the converted test with sixDATA RACEreports. - A deliberately unsynchronized probe — written by the main loop in
finalizeright after it releases the wedged branch, read by that branch as it wakes — was reported as a race, naming the abandoned branch's goroutine throughlaunchBranches→execBranch→executeNodeForBranch. The overlap the test exists for is therefore still real inside a bubble: synctest virtualizes the clock, not goroutine scheduling.
Both mutations were removed. Two earlier candidates did NOT discriminate (sharing the trunk's loop-counter map into enclosingLoopCounters; the main loop writing rs.vars on the loop-edge traversal) — they are simply not on the abandoned branch's read path, in either the synctest or the pre-conversion shape, so they say nothing about the conversion.
Three recipes for a bound a scheduler can decide
#1471 asks a liveness bound to keep >= 10x headroom over the legitimate latency, or to be replaced by a synchronisation point. These three shapes are what "replaced" looks like in practice; #2362 and #2447 applied the first two to the last tight bounds of pkg/runview and the duration-deadline spend witness, #2487 the third to the node-session sweep, #2499 the first to pkg/eventbus's overrun post-check.
An in-process lifecycle witness reads VIRTUAL time. pkg/runview's Manager is pure in-process state, so every hand-off test that reads a WAIT runs in a bubble and asserts an exact figure: a refusal that must not pay the grace spends == 0, a wait for the previous runner == previousRunnerTeardown, a caller whose deadline expires == callerBudget, a Stop that drains == runnerTeardown. The production grace then costs the suite nothing, so the test exercises the real registerHandoffGrace instead of shortening it. The facts that need no wait at all — a context closed by Cancel, a done closed by Deregister — are read with the default recipe below instead, outside any bubble. Two traps:
- A
t.Fatalfwhile a goroutine the bubble spawned is still blocked exits the main bubble goroutine with a blocked one behind it, and synctest reportsdeadlock: main bubble goroutine has exited but blocked goroutines remain— a panic that aborts the test BINARY. The failing assertion is still printed; what is lost is every test after it (measured onpkg/runview: 247 of 739 tests ran, 1 failure reported instead of 3). Buying that back takes two separate things, and a join alone is not one of them. First, a goroutine the bubble spawns must be unblockable WITHOUT the test's cooperation —releaseAfter's only block is atime.Sleep, which virtual time always completes — or carry an escape the test owns, like thereleasechannelTestManager_StopDrainscloses in its defer, after the last assertion so it can never decide one. Second, it must be joined by a receive the test DEFERS, so the join runs on the Goexit path too:releaseAfter'sreleased(which the goroutine closes, hence a join and never an escape), StopDrains'goneper run. Measured on this tree, each shape under a failingt.Fatalf: the deferred join ALONE over a goroutine blocked on a channel trades one binary-aborting panic for another —deadlock: all goroutines in bubble are blocked, and the next test in the file does not run — while escape + join, and virtual sleep + join, each report the failure as their own and let the next test run. time.Afterracing a fact that is already true is a verdict the host decides: a select picks at random among the arms that are ready, and a second of starvation makes the timer one of them (measured on a ready timer against a closed channel: the timer wins 500/1000). Such a fact is read with a non-blocking select and adefault, or joined withsynctest.Wait— never with a timer. "Already true" is a claim about the PRODUCER: a non-blocking send the test's own goroutine performed, acloseunder a lock the callee took, a worker the callee awaited before returning.
A duration-budget witness owns the budget's clock. A short max_duration is two things at once: how long the node under observation is HELD, and a race against everything upstream of it — the engine's construction, the store, the entry node. expireDurationBudget (pkg/runtime/budget_deadline_test.go) separates them: the budget runs on budgetTestClock through Engine.budgetClock, frozen, so the pre-exec gate always sees the full allowance and the node is always reached and always armed; the node itself spends the cap once it has started, which is what makes the engine's post-exec verdict read a budget genuinely out of time. The per-node deadline is untouched — it is still armed from the remaining budget and still kills the node on the real clock — and the fixture reports started/armed so a premise that never held fails as itself.
Falsified both ways, with 400ms of upstream work standing in for a loaded merge-queue runner: the pre-change spend witness fails with the queue's own message (the timed-out node's tokens never reached the run: 0), and the pre-change TestNodeDeadlineFromDurationBudget PASSES with context.WithDeadline removed from engine_exec.go — the regression it guards, green. Converted, both are green 3/3 under the same load and both go red in 0.03s on that mutant, where the old witness only reached the harness timeout.
A witness of a sweep's own cadence is HELD in it. pkg/internal/proc's node-session sweep scans every NodeSessionSettle (50 ms) and ends on two scans in a row that find nothing, so TestSweepStopsAProcessThatShowsAfterAnEmptyScan had one settle to start a process and have its exec visible: #2487 measured it losing in a merge-queue run that touched no file of the package, reading the product as broken (late (pid 57351) belongs to the session and is still running). Both implementations now carry a scan gate the sweep calls after every scan and BEFORE it acts on what the scan found — sweepScanGate, a func of the members the scan found, for the sweep that runs in-process; sweepScanGateFile, a path the generated sh program appends each scan to and waits at, for the program a sandbox runs. Both are inert by default (nil, and an empty GATE='' whose gate() returns at once), and the program's gate is bounded: a gate nobody releases prints an iterion-node-session error line, so a held sweep is never read as a sweep that found nothing. EVERY scan is held and released one at a time, the release naming the scan it frees (.resume.<n> for the program), so a witness reads what each scan found from the sweep's own view and stages its window at the scan its premise names.
Which scan the premise is about, and why the first one is a bet. Holding the FIRST scan put the premise back where the sleep had been: a seed's exec can still be running when that scan reads it, and a process in the middle of an execve takes its /proc/<pid>/environ with it for that instant, so the scan files it as unreadable instead of a member. TestSweepStopsAProcessThatShowsAfterANonEmptyScan asserted its held scan had found the seed, and reddened on 2 of 40 serial runs of unchanged code (premise: the scan the sweep is held at found nothing), then on 2 of the 3 go arms of a second series — a verdict on an execve, and idle-biased: 0 of 200 arms of the same binary under eight-way load, so a green loaded run says nothing about it. (The sandbox is the right place to hunt it: it is a PID namespace of its own, nine processes, which is the throwaway namespace this package's header asks for.) The premise is now a predicate given to scanWindow.at, which releases the scans that do not meet it and judges the first that does — the pattern TestGuardFoundRefusesALiveIntruder already applied to its own scan. And what awaitSessionHolds proves is where a launcher BECOMES visible: until its own execve it is a fork of the test binary, carrying the test binary's environment. It never proves where a process stops being missable, so a witness that needs ONE named scan to hold a process it started adds awaitExeced, which waits for the name /proc/<pid>/stat gives — the file the last execve ran.
The window the product opens is not only the first one. A sweep that files the members of its first scans and then stops re-discovering passed both witnesses while their single window sat between scan 1 and scan 2: a mutant that drops members new at the third scan of a phase was green on 18 of 18 arms. Each witness stages its window a second time, at a later scan, and the non-empty one a third time two scans further on. A window witnesses that drop only when it sits in the phase that ENDS the sweep: a member dropped in a phase that hands over to the next one is re-discovered for free by that phase's own first scan. The non-empty witness has that by construction — its later windows sit in the KILL phase, which has no successor, so a member dropped there is left running and unreported, on the product's own lines (the join's left running [latest (pid N)], then latest (pid N) was stopped but not reported — while that phase still had a deadline, latest (pid N) belongs to the session and is still running). Those lines come free only for the LAST window: a member left running is a session never quiet again, so when it is an earlier window's member the NEXT window's premise — a scan that found nothing — expires first, and a premise names the scans it waited through, not the process left alive. So every premise staged after a window goes through atAfterStaging, which joins the sweep and judges it as well as failing on the premise. Measured on the mutant that drops re-discovery in the KILL phase alone, whose dropped member is the SECOND window's: with the join, the red carries both premise, within 30s: no scan of the sweep found nothing once more, two scans later and later (pid N) belongs to the session and is still running, 3 of 3 arms; without it, the same 3 arms carried the premise alone and no product line at all. The empty-scan witness could not have any of this: its windows sit in the TERM phase, which ends that sweep only while the session goes quiet, and the very mutants it hunts leave a member alive — with a 10s budget, the phase burned it and handed the member to KILL, which found it. That is how the third-scan mutant and its sh twin stayed green on it, 3 of 3 arms each at 10.1s, with the second window already in place.
A premise, not an assumption — and no clock in it. The empty-scan witness reads the sweep's own scan log once it has returned (endedQuiet): the log must end on the two empty scans a quiet phase returns on. It first did so against the 10s budget, counting at most a quarter of the scans that budget allows — 6 scans on each of 9 arms of sound code, against a ceiling of 50, where a phase that burned the budget logged 201 scans in the go implementation and 160 to 191 in the program — on the claim that a count is not a duration. The budget was one. With 150 extra processes and 48 busy loops in the sandbox's PID namespace, one scan of the program without grep -z took 4.5s, so the 10s TERM phase ran out after its third scan and handed both windows to KILL. Traced, the log read [0 1 0 1 0 0]: KILL's first scan re-discovered later for free and its two empty scans read as a quiet end, the premise passing the very handover it exists to refuse. With 300, scans took up to 8s, KILL ran out too, and the premise reddened unchanged code 3 of 3 arms ([0 1 0 1], [0 1 0]) — the shape seen locally at load 45 and 102 ([0 1 0 1 0]). The phases a window sits in now have no deadline (sweepNoDeadline), so that witness's TERM phase ends on two empty scans or not at all, on any host, and endedQuiet asks only that, within scanBound (50) scans. Under the 300: both witnesses green 3 of 3 on every arm, the empty-scan one's TERM phase ending quiet with no KILL phase at all (traced). The third-scan mutant and its sh twin keep the session from ever going quiet: the join names the member left running once the log passes the bound (left running [later (pid N)]) and stops it, and the premise reddens too — endedQuiet in the empty-scan witness, the third window's in the non-empty one — 6 of 6 arms at rest, where the old premise caught it after 77 to 186 scans of its burned budget. Under the 300 the old witness PASSED that mutant 2 of 2, its TERM phase handing later to KILL; the new one reddens on the join's verdict, after 53 scans and 321s. No log shape says WHICH phase stopped a member, so the witness also reads each member's own end, the kernel's wait status (endedOfTerm): late and later — and replacement, below — must die of the SIGTERM their phase sends. A mutant that drops members new from the TERM phase's 3rd scan AND hands over to KILL after its 5th ends on two empty scans in 7, every member stopped and reported — it passed the old witness 3 of 3 arms at rest, and reddened the witness as f5de1b70f had it 3 of 3 on later (pid N) signal: killed, not of the TERM its window's phase sends.
The respawn window, kept in the phase that ends the sweep. A supervisor forks its replacement because TERM reached it, so the replacement shows only after a scan of the TERM phase that found the supervisor, and the scan that first finds it is that phase's too while the grace lasts — a scan that found members ends a phase only on its deadline. The non-empty witness staged that window while its TERM phase had a 1s budget; with a deadline already past, its TERM phase is one scan that hands over, its first window's member is first found by KILL's first scan, and no window of the package sat after a TERM scan that found members with another TERM scan to follow. Measured with a mutant that signals a member the TERM phase finds just after a scan that found members but never files it, Go and its sh twin: the witness as 9cdd04bae had it reddens 3 of 3 arms (late (pid N) was stopped but not reported); the package at f5de1b70f passed ENTIRELY, rc 0 — the respawner's replacement in TestSweepStopsTheSessionAndNothingElse is judged only when the sweep sighted it, which this sweep never does, so it SKIPPED on all three arms. The empty-scan witness now also holds the scan that found late, before the sweep signals it, and stages replacement there; its TERM phase has no deadline, so the scan that finds replacement is TERM's, and endedOfTerm reads that it ended of TERM. That mutant reddens it 3 of 3 arms on replacement (pid N) was stopped but not reported alone, its scan log the sound one, 7 scans: [0 1 1 0 1 0 0]; the respawner's subtests still SKIP. The third-scan drop still reddens all six arms of the two witnesses, the empty-scan one now on left running [replacement (pid N)], and so does its variant that hands over after the TERM phase's 5th scan — later then lands at KILL's third scan and is dropped there (left running [later (pid N)], 53 scans). A variant that drops only in the TERM phase and hands over after its 5th scan ends on two empty scans with every member reported ([0 1 1 1 1 1 0 1 0 0]) and reddens the empty-scan witness 3 of 3 on endedOfTerm alone: replacement (pid N) signal: killed, not of the TERM its window's phase sends. A member the join stopped itself is told apart (… by the join: no phase of the sweep stopped it), measured with a sweep stalled 35s after its 5th scan, 3 of 3 arms.
How far the windows reach. A window's member is new to the scan AFTER it, so the two windows the non-empty witness stages in the KILL phase pin re-discovery at its 3rd and 5th scans, and the empty-scan one pins the 2nd and 3rd of its own and, at rest, the 5th. The non-empty witness's FIRST window pins no index: that one sits in the TERM phase, which hands over in that witness, so KILL's own first scan re-discovers the member for free — a mutant that classifies no new member at all through the whole TERM phase, and one that drops them from its 2nd scan on, both pass it 3 of 3 arms, while reddening the empty-scan witness 3 of 3 on endedQuiet. What the first window asserts is that its member is STOPPED, by whichever phase reaches it; the TERM indexes are the empty-scan witness's to hold. Measured with mutants that drop a member first seen at the Kth scan of a phase: K=3 and K=4 redden both witnesses, K=5 the non-empty one, K=6 passes both — and with the respawn window in place, K=5 reddens the empty-scan witness too (3 of 3), while K=6 still passes both at rest. The third window is what K=4 and K=5 cost: with only two, K=4 and its sh twin passed the ENTIRE package, 69 green and rc 0. No finite set of windows reaches every index, which is why the product's clause stays a loop over every scan, and why both witnesses say where their proof stops.
The scan INDEX is also the only axis these windows bound, and that bound is load-free but not time-free: at rest both witnesses reach their last window within a second or two of the sweep's start, where production gives the phases 5s and 3s, so a sweep that stops re-discovering after a few seconds of ELAPSED time is invisible to them. Measured, a mutant that classifies no new member more than 3s after the sweep started: the ENTIRE package green, 69 green and rc 0, both witnesses passing on all three of their arms. Nothing here covers it, and a window cannot — a window is an index.
The KILL-phase window also turns a capture that never was one: the mutant that drops re-discovery in the KILL phase ALONE read as caught by TestSweepSignalsBeforeItsDeadline, and is caught there 1 arm in 15 — by the same execve miss, so the verdict was a race's. It reddens the non-empty witness 3 of 3 instead, and deterministically: the member it drops keeps the session from ever being quiet again, so the third window's premise cannot be met and fails as itself, naming the scans it waited through — and, since atAfterStaging joins the sweep on that failure, alongside the product's own verdict on the process it left running. The deadline witness still catches it only by that race, and that is no loss — when a sweep gives up is that witness's subject, re-discovery is not.
No deadline where a window sits, and every wait counted in scans. A phase that ends on its deadline skips the scan a window was staged for and reads as a verdict on how long this host took to scan — when a sweep gives up is TestSweepSignalsBeforeItsDeadline's subject, and it keeps the knob. Wide budgets were the first answer, and a wide budget is a bigger constant: the non-empty witness's KILL phase got 20s for its 7 scans and 2 stagings — measured then at most 5.28s an arm over 1056 arms, eight workers in parallel — and under the load above it ran out before the third window's scan, 2 of 2 arms (premise, within 30s: … [1 2 0 1]). So sweepBudget gives the phases the windows sit in sweepNoDeadline, and that witness's TERM phase — which only its deadline can end, the seed ignoring TERM — a deadline already past, one scan whatever the host. Every wait that spans scans is bounded by the sweep's progress, not the wait's age: scanBound scans in all, premiseWait with no new one (atOrNot, the join). A sweep that never ends is the product's verdict, named with what it left running; the join then stops that session itself, so the sweep comes back with a report to judge. The program's own net follows: a held program ends by its witness, and shSweep gives it the test binary's deadline, never less than sweepProgramCeiling — under that load the sh-without-grep-z arms took 43s to 57s, the go and sh ones 1.2s to 10.7s.
Where the gate sits is what keeps the mutation honest. At the settle instead, the mutant that ends on the FIRST empty scan never settles, and the witness would redden on a premise timeout rather than on its own assertion. Measured, one implementation at a time: quiet == 2 → quiet == 1 in nodesession_linux.go reddens the go subtest in ~5s with the queue's own two lines (the seconds are check's wait for a process that never dies, not the sweep's) and leaves the sh ones green; [ $quiet -ge 2 ] → -ge 1 in nodeSessionSweepBody reddens both sh subtests and leaves go green. Green -count=50, -race -count=10, and -count=20 under two competing jailed runs of the same package.
Three sibling premises of the same witnesses rested on a 200 ms sleep — a leader thread that must have exited, a prctl(PR_SET_DUMPABLE, 0) that must have run. A loaded host does not fail them, it makes them PASS vacuously: the sweep then finds an ordinary process and stops it, and the fallback path the witness exists for is never exercised. Each is now an awaitPremise poll on the fact itself — the process-wide environ unreadable while a thread's answers; an environment nobody of this uid can read while the process still runs — so a slow host costs time instead of a verdict, and a premise that never holds fails as itself.
The seeds are the other side of the same wall, and there load FAILS the witness instead of passing it vacuously. A foreground seed returns the pid of a launcher the test binary has only just forked, and until that launcher's own execve runs it carries the test binary's environment and not the entry, so a scan misses it. What it does not prove is the exec of the program the seed goes on to become: that is the bet the two paragraphs above pay for. TestSweepStopsTheSessionAndNothingElse/go bet all of populate() on those execs and lost in a merge queue that touched no file of the package (#2539): the guard refused (the session misses [respawner …]), which is correct, but read as a product verdict. The world now awaits its seeds: w.awaitSeeded() holds the test until every marked seed is visible to a scan (awaitSessionHolds) — from populate() itself, and from the two witnesses that seed directly (TestSweepSignalsBeforeItsDeadline, TestSweepDoesNotWaitOnASinceFileThatIsNoFile). The controls need no await: a sweep can only stop carriers of the entry, so a control a scan does not see yet is trivially spared. The refusal stays witnessed: short-circuiting environHolds in scanNodeSession, so seeded carriers are never found, reddens the unfixed witnesses on the queue's own line and the fixed ones on the await's premise. Green -count=30, -race -count=2, and -count=10 under two competing jailed runs of the same package.
A production timer whose losing side is goroutine scheduling is arbitrated in a bubble. pkg/eventbus's cancel path gives a subscriber one overrunPostCheckWindow (20 ms) after the budget wait ends on an already-spent caller ctx before it names the subscriber as an overrun; whether the worker's done channel closes inside the window is decided by the scheduler, so TestBusSubscribeCancelExpiredCtxDoesNotWarnCleanSub was a 100-iteration real-clock bet — under merge-queue load it lost at iteration 99 and ejected two PRs that touched no file of the package (#2499). The fact cannot be staged the way the sweep's is: the worker's settle is CAUSED by the cancel the witness is exercising, and the decision point sits inside a sync.Once closure that cannot be driven stepwise. So the witness moved the arbitration itself into a synctest bubble: virtual time advances only when every goroutine of the bubble is durably blocked, and the goroutines the cancel path just signalled are runnable while the post-check waits, so the window cannot elapse before done closes — the witness decides the arbitration every iteration instead of sampling it. The bubble's one blind spot: shrinking the production window toward zero goes green here, since virtual time holds it open — that miscalibration surfaces as operator-visible overrun warnings in real shutdowns, not as a red queue. Mutants (the warn moved to the settled side of either implementation's post-check; the post-check dropped for an immediate warn) redden it on its own line at iteration 0.
