Repository navigation
test(missions): wait for the missed passage admission before Stop - #935
Conversation
recurring-day.e2e test E failed intermittently on CI with 'stop: 500 Recurrence control conflict' (seen on #925, #931 and feat/provider-account-rotation). After the second Play, the schedule admits the passage missed while paused through background writes (pending passage, start, accepted admission: revisions 5 to 8). The fixture's control() reads the expected revision from a snapshot after a short real-time flush; on a slower runner the admission writes land after that snapshot, so Stop's compare-and-set meets a newer revision and is refused. Reproduced locally by shortening the flush to one setImmediate: the unchanged test fails with the exact CI error. The test now waits, through the fixture's bounded until(), for the accepted admission before Stop, modelling Stop as a later human action at the settled revision. With the shortened flush it passes; the full recurring-day file passes (22/22). Product behaviour is unchanged.
pascalandr
left a comment
There was a problem hiding this comment.
Autonomous review: 0 findings
Reviewed head 130fcfa1e030d91ab2183289e2b45d725ca82da3 against dev (a4c12dab; the only file difference is this PR's three lines).
Correctness of the fix
- After Play, the schedule Job's wake runs
MissionRecurrenceRunner.tick:reserve→ native admission →recordAdmission.recordAdmissionis the last revision-bearing write of that wake. - The post-wake
clearRecurrenceWakeErrorwrites a display-only sibling key (native-recurrence-wake-status.ts). That key is outside the schedule document's revision/CAS, so it cannot conflict with Stop. - The next wake only follows virtual time or the settled-event debounce, which is also a virtual sleep. Neither advances inside
until(), so waiting forpending.admissionreaches the settled revision thatcontrol("stop")then reads. until()is bounded (5 s real time, virtual clock untouched) and fails with an explicit message. Stop still exercises the terminal path with an admitted pending passage, as before on fast runners. Product code is unchanged.
Validation (isolated temporary worktree, no shared daemon)
recurring-day.e2e.test.tswith the normal flush: 22/22 pass.flush()temporarily shortened to onesetImmediate(not committed):- without this change: test E fails 3/3 with the exact CI error,
stop: {"statusCode":500,…,"message":"Recurrence control conflict"} - with this change: 3/3 pass
- without this change: test E fails 3/3 with the exact CI error,
- Server typecheck passes.
CI note
The red tests job on this run is two tests/browser/panel-extensions.test.ts API2 timeouts. The same two failures appear on unrelated branches (for example feat/git-init-action, run 38330596020), so they are not caused by this PR. In this run, test E itself passed.
The 409-vs-500 mapping for genuine Stop/admission races, noted in the description, remains a separate product question.
Round 1 corrections pushed (6cb30c2)
Validated with the shortened flush and with Play's control record delayed by 8 and 25 microtasks (the interleavings that hung the previous version); full file 22/22. Out of scope and recorded: the same slow-runner fragility in other tests of the file, the 500 for a racing human Stop, and Play/Job first-wake interleaving delaying a missed passage. |
…s before Stop (#939) Follow-up to #935, which merged before its Gatekeeper round 1 corrections were pushed. ## Problem - `RecurringDayFixture.until()` computed its deadline with `Date.now`, which the fixture replaces with its virtual clock, so a wait that never succeeds never timed out. - Test E waited for `pending.admission` after the second Play. When Play's completion record lands just after the schedule Job's first wake, the admission is deferred and that wait hangs. ## Fix - `until()` measures its deadline with `performance.now()`. - New `quiescent()` waits until every running Job is parked in a virtual sleep. Test E calls it before Stop, so Stop follows a settled revision whatever the admission outcome (admitted, revision conflict, or reserved but not admitted). ## Validation - Test E passes with the fixture flush shortened to one `setImmediate`, and with Play's control record delayed by 0 to 40 microtasks, covering all three admission outcomes. - The full `recurring-day.e2e.test.ts` file passes 22/22. - Gatekeeper round 2 on these exact changes: ZERO FINDINGS. ## Out of scope, recorded - Other tests in the file remain sensitive to a slow flush. - A Stop that hits a revision conflict returns 500 rather than 409. - Play's completion record can collide with its Job's first wake and delay a missed passage.
Summary
recurring-day.e2e.test.tstest E ("Pause cancels wakeups and future admissions; Stop is terminal") fails intermittently on CI withstop: {"statusCode":500,"error":"Internal Server Error","message":"Recurrence control conflict"}. It blocked thetestsjob on #925, #931 andfeat/provider-account-rotation(run 38279531443).Cause: after the second Play, the schedule admits the passage missed while it was paused, through background writes (pending passage, start, accepted admission: revisions 5 → 8). The fixture's
control()reads the expected revision from a snapshot after a short real-timeflush()(4 × 1 ms). On a slower runner the admission writes land after that snapshot, so Stop's compare-and-set meets a newer revision and is refused.Reproduced locally by shortening
flush()to a singlesetImmediate: the unchanged test fails with the exact CI error.Fix (test only): wait, through the fixture's bounded
until(), for the accepted admission before Stop. Stop is modelled as a later human action at the settled revision rather than a race with the admission. Product behaviour is unchanged.Validation
recurring-day.e2e.test.tswith the normal flush: 22/22.Observation (not changed here)
A real Stop that races the same background admission is also refused by the optimistic revision check, and the route reports that refusal as a 500. Whether conflicts should map to 409 and how the UI retries is a separate product question.