Widen the callback-firing margin DistantMapClicksBothSurviveJourneyTests races on #195

Merged
rob merged 1 commit from fix/red-main-e2e into main 2026-08-17 10:27:48 +00:00
Owner

What broke main

Run 791 (merge of #189, 1124f83c) failed the e2e job with three failures. Two (CreateGroupJourneyTests, AddPlaceJourneyTests) timed out in RegisterAndSignInAsync waiting for #register-email/#login-email — a shared helper nearly every journey uses. Only 2 of 42 tests failed, with different failure sets on two other PRs' e2e runs queued at the same time on the same single-threaded runner, matching the CI-load signature task 233's own record already documents. No code change here for these; they read as load, not regression.

The real finding

DistantMapClicksBothSurviveJourneyTests failed on its second assertion with both asserted latitudes identical (-11.178401873711772) — the shape you get when the read happens after both deferred callbacks have already fired, not a read that lands correctly and then never observes a later change.

ADR-0160 (#173, merged hours before this run) widened DOUBLE_TAP_WINDOW_MS to fix a delivery race — but the gap between the two callbacks firing is click1_time + window vs click2_time + window; the window cancels out of that difference, leaving only StaggerMs. At the original 250ms stagger that left ~130ms between reading the first callback's value and the second overwriting it, and that read isn't at a fixed instant — ToBeVisibleAsync auto-retries until the panel renders, so CI-load contention that delays a setTimeout can just as easily delay the Blazor render the visibility check is waiting on, eating into the same 130ms from the other side. ADR-0160's own fix never touched this margin.

Raised StaggerMs from 250ms to 1000ms (still a plausible gap between two deliberate clicks), and replaced the second wait's literal 480 with a derivation from named constants, so a future change to StaggerMs can't silently narrow this margin again the way this one did.

What I couldn't do

Both the fixed ports (5017/5169) this fixture needs are held by Rob's own dev session on the primary checkout, so I could not run this suite locally at all — not once, let alone repeatedly. The evidence here is the arithmetic above plus the exact shape of run 791's own failure; ADR-0160's own Consequences already treat that as the right evidentiary weight for this specific test, over a local run count under conditions that don't reproduce CI load anyway.

Build of the modified project is clean under the pinned SDK (10.0.100).

## What broke `main` Run 791 (merge of #189, `1124f83c`) failed the `e2e` job with three failures. Two (`CreateGroupJourneyTests`, `AddPlaceJourneyTests`) timed out in `RegisterAndSignInAsync` waiting for `#register-email`/`#login-email` — a shared helper nearly every journey uses. Only 2 of 42 tests failed, with different failure sets on two other PRs' `e2e` runs queued at the same time on the same single-threaded runner, matching the CI-load signature task 233's own record already documents. No code change here for these; they read as load, not regression. ## The real finding `DistantMapClicksBothSurviveJourneyTests` failed on its second assertion with both asserted latitudes identical (`-11.178401873711772`) — the shape you get when the read happens *after* both deferred callbacks have already fired, not a read that lands correctly and then never observes a later change. ADR-0160 (#173, merged hours before this run) widened `DOUBLE_TAP_WINDOW_MS` to fix a *delivery* race — but the gap between the two callbacks *firing* is `click1_time + window` vs `click2_time + window`; the window cancels out of that difference, leaving only `StaggerMs`. At the original 250ms stagger that left ~130ms between reading the first callback's value and the second overwriting it, and that read isn't at a fixed instant — `ToBeVisibleAsync` auto-retries until the panel renders, so CI-load contention that delays a `setTimeout` can just as easily delay the Blazor render the visibility check is waiting on, eating into the same 130ms from the other side. ADR-0160's own fix never touched this margin. Raised `StaggerMs` from 250ms to 1000ms (still a plausible gap between two deliberate clicks), and replaced the second wait's literal `480` with a derivation from named constants, so a future change to `StaggerMs` can't silently narrow this margin again the way this one did. ## What I couldn't do Both the fixed ports (`5017`/`5169`) this fixture needs are held by Rob's own dev session on the primary checkout, so I could not run this suite locally at all — not once, let alone repeatedly. The evidence here is the arithmetic above plus the exact shape of run 791's own failure; ADR-0160's own Consequences already treat that as the right evidentiary weight for this specific test, over a local run count under conditions that don't reproduce CI load anyway. Build of the modified project is clean under the pinned SDK (10.0.100).
Widen the deferred-callback stagger DistantMapClicksBothSurviveJourneyTests races on
All checks were successful
CI / build (pull_request) Successful in 3m11s
CI / container-images (pull_request) Successful in 4s
CI / e2e (pull_request) Successful in 3m40s
4cc78a0269
rob left a comment

Verdict: mergeable

CI green at 4cc78a02 (build, container-images, e2e all success).

Checked the load-bearing claim against map.js itself, not just the prose: deferSingleClick's cancellation (matchIndex = pending.findIndex(entry => point.distanceTo(entry.point) < CLICK_CANCEL_SLOP_PIXELS)) is purely positional and never consults elapsed time. With StaggerMs (1000ms) still well inside TestDoubleTapWindowMs (3000ms) and the two click coordinates hundreds of pixels apart, both clicks are still concurrently pending when the second lands, so the positional guard is genuinely exercised — a 1000ms stagger doesn't push this into being discriminated by time. Reverting deferSingleClick to its pre-ADR-0145 single-slot shape would cancel the pending entry unconditionally, so neither callback fires and the first Expect(latitudeField).ToBeVisibleAsync() times out red — consistent with ADR-0160's own Consequences, which record exactly that mutation being tried and watched fail.

Cancellation arithmetic checks out: each click schedules its own callback at click_time + DOUBLE_TAP_WINDOW_MS, so the gap between the two firings really is click2_time − click1_time = StaggerMs, independent of the window. Re-derived both Task.Delay expressions by hand from the four named constants and they land at the intended absolute times (3120ms / 4500ms from click 1). PR #173's fix was correct but incomplete; this is the missing half.

PR is candid about zero local runs (ports held by another session) and reasons instead from the arithmetic plus run 791's exact failure shape (both latitudes identical) — adequate given the constraint, and honestly stated rather than implied as verified.

No new ADR is the right call — ADR-0160's Consequences already scope locators/coordinates/assertions as this test's ordinary detail, not the override mechanism itself, and StaggerMs is the same class of thing (an original task-229 parameter, only retuned here). Only the test file changed; no ADR body touched. British spelling and no AI attribution both fine.

One non-blocking edge worth recording for a later reader: if StaggerMs is ever raised past TestDoubleTapWindowMs + FirstCallbackBufferMs, the first Task.Delay argument goes negative and throws. Not a live risk at today's values (1000 vs 3120), and not worth a guard on its own, but worth knowing if either constant moves again.

Verdict: mergeable CI green at `4cc78a02` (build, container-images, e2e all `success`). Checked the load-bearing claim against `map.js` itself, not just the prose: `deferSingleClick`'s cancellation (`matchIndex = pending.findIndex(entry => point.distanceTo(entry.point) < CLICK_CANCEL_SLOP_PIXELS)`) is purely positional and never consults elapsed time. With `StaggerMs` (1000ms) still well inside `TestDoubleTapWindowMs` (3000ms) and the two click coordinates hundreds of pixels apart, both clicks are still concurrently pending when the second lands, so the positional guard is genuinely exercised — a 1000ms stagger doesn't push this into being discriminated by time. Reverting `deferSingleClick` to its pre-ADR-0145 single-slot shape would cancel the pending entry unconditionally, so neither callback fires and the first `Expect(latitudeField).ToBeVisibleAsync()` times out red — consistent with ADR-0160's own Consequences, which record exactly that mutation being tried and watched fail. Cancellation arithmetic checks out: each click schedules its own callback at `click_time + DOUBLE_TAP_WINDOW_MS`, so the gap between the two firings really is `click2_time − click1_time = StaggerMs`, independent of the window. Re-derived both `Task.Delay` expressions by hand from the four named constants and they land at the intended absolute times (3120ms / 4500ms from click 1). PR #173's fix was correct but incomplete; this is the missing half. PR is candid about zero local runs (ports held by another session) and reasons instead from the arithmetic plus run 791's exact failure shape (both latitudes identical) — adequate given the constraint, and honestly stated rather than implied as verified. No new ADR is the right call — ADR-0160's Consequences already scope locators/coordinates/assertions as this test's ordinary detail, not the override mechanism itself, and `StaggerMs` is the same class of thing (an original task-229 parameter, only retuned here). Only the test file changed; no ADR body touched. British spelling and no AI attribution both fine. One non-blocking edge worth recording for a later reader: if `StaggerMs` is ever raised past `TestDoubleTapWindowMs + FirstCallbackBufferMs`, the first `Task.Delay` argument goes negative and throws. Not a live risk at today's values (1000 vs 3120), and not worth a guard on its own, but worth knowing if either constant moves again.
rob merged commit 0b41d1cae0 into main 2026-08-17 10:27:48 +00:00
rob deleted branch fix/red-main-e2e 2026-08-17 10:27:48 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rob/PlaceMark!195
No description provided.