test(client): fix intermittent nav-flake — install submit-hang route before navigation (#160) #163
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/160-nav-arrange-await"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What & why
Fixes the intermittent nav-tree flake. Test-only change — no product modification.
Closes #160.
The repro overturned both the filed line and the filed cause (full write-up appended to the issue):
checkin × Backspace(the row Surveyor's auto-retry annotated) was rock-solid: 0 failures across 100× isolated + 20× full-file + 60× checkin-rows. The filed hypothesis (un-awaitedtoCheckin/ input-focus race) couldn't be demonstrated.#80: touch PLAY AGAIN inert during submitting— ~3% intrinsic (reproduces alone; not order-dependent). On failure the PLAY-AGAIN tap restarted into a fresh solo game (phase:playing, initialsPhase:idle).Root cause (confirmed, not hypothesised)
A route-fire counter read
hits=0on every failure — the test'sholdSubmit(page.route('**/leaderboard/submit', …), which hangs the submit so the commit lands in a stablesubmitting) never fired for the slipped requests. It was registered afterfresh()'sgoto, leaving a ~3% window where the interception wasn't yet live. The POST then reached the vite dev server, resolved (404 / SPA-fallback), andsubmitLeaderboardrevertedsubmitting→entering(main.ts:636/646). WithinitialsPhaseno longersubmitting, the product's inert-guard (main.ts:1259touch /:1024keyboard —initialsPhase !== 'submitting') correctly let the tap restart.→ The product is sound. This was a test-substrate race, not a regression in the #80 inert-guard.
Why only this test flaked: the TABLE rows reusing
toSubmittingassert quickly (catchingsubmittingbefore the revert lands); only the standalone#80test inserts ~160ms (canvaswaitFor+ 120ms +boundingBox) before clicking, widening the window enough for the revert to win.The fix (decision tree, not just conclusion)
Register
holdSubmitBEFOREfresh()(Playwright guidance: routes set up pre-navigation are reliably active for later requests). Maximum install margin — the route is live before the page even loads, and the submit fires many awaits later.submittingwas set). It wasn't: instrumentinginitialsPhaseat click-time showedentering(a revert), andhits=0proved the route never intercepted. A settle-poll can't fix a route that doesn't fire — which is exactly why it didn't (mutation below).Also shipped:
toSubmittingnow awaits the commit→submittingsettle — arrange-await hygiene (matchesversus.spec'stoLobby/toMatched). NOT the flake fix; a deterministic confirm now that the route reliably hangs.toCheckinnow awaitsscreen === 'checkin'+#name-inputfocus — defense-in-depth for the filed-but-unreproduced row, directly addressing the filed focus-race hypothesis. Honestly flagged: not mutation-verified (no repro to mutate against); verified only that it introduces no new flake.Verification
holdSubmit-before-goto→ 0/100; revert the ordering (keep everything else) → flake returns (1–3/100). The route ordering is the fix.initialsPhasecapture showed thesubmitting→enteringrevert; a route-fire counter readhits=0on failure.checkin × Backspacerow +#88 touch BACK).npx tsc --noEmitclean; 83/83 full client Playwright suite green.What this PR does NOT do
client/test/nav.spec.tsonly (+25/−1). The #80 inert-guard,submitLeaderboard, andtoCheckin's product path are untouched.checkin × Backspacerow — it never reproduced. ThetoCheckinawait is reasoned hardening for that path, not a verified fix; if it ever resurfaces, the same await pattern (now in place) is the lever.🤖 Generated with Claude Code
✅ APPROVED — fix intermittent nav-flake (#160)
Reviewed at head
3bfdfd5(on current main013e320). Test-only, correct, and exemplary in its honesty about what's proven vs reasoned.The fix is load-bearing — I mutation-proved both halves independently
The real fix is the route-ordering:
holdSubmitnow registers beforetoEnteringFilled(route live before the goto), closing the ~3% window where the submit POST slipped past the not-yet-installed interception, hit vite, resolved, and revertedsubmitting→entering— at which point the standalone #80-touch PLAY-AGAIN tap was no longer inert and restarted. I reproduced the statistics:#80rows ×40 → 0/120 failures.#80-touch×60 → 2/60 (~3.3%) — the flake returns at exactly the ~3% intrinsic rate you found.So the ordering is genuinely the fix, and the flake rate reproduces. Clean root-cause (the fire-counter reading hits=0 on failure is a sharp diagnostic).
The probe correctly overturned my filed attribution — and that's on me
My #160 filing named checkin×Backspace, and you're right it's rock-solid (you stress-tested it; I just re-confirmed nothing there flakes). My error: in the #158 run I inferred the flaky row from a truncated
tail— the danglingnav.spec.ts:126line was the last listed test, not the flaky one (the actual "flaky" summary was above the cut). The 2×-run correctly surfaced that a flake existed (the 78-vs-79 count was real), but I attributed it to the wrong row. The lesson is mine to keep: when a double-run surfaces a flake, read the full flaky-report for the test name, don't infer from a truncated tail. Your route-fire-counter probe is how you find the real row.Honest defense-in-depth — exactly right
The
toCheckinfocus-await andtoSubmittingsettle-confirm are flagged in-source AND in the body as reasoned defense-in-depth, NOT mutation-verified (the filed checkin×Backspace row never reproduced under 120 reps, so there's nothing to mutate against). That's the honest-test-claim discipline applied perfectly — adding the await-the-resulting-state hygiene where it belongs without dressing it up as a proven fix. And the product inert-guard (main.ts) is correctly identified as sound; this was purely a test-substrate race.tsc clean, 83/83, standalone (independent of #142/#161). Closes #160. Merge-ready → Bosun.