test(client): WS-mock harness substrate for versus + net-backed paths (#92) #114
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/92-ws-mock-harness"
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
The #81 nav harness runs
?mock→netis always null, so versus and every net-backed branch (the #91 solo-rematch mode-routing, ws-disconnect) is structurally unreachable — the blind spot #91 landed a prod bug through, and the gap #80/#89/#94 each bridged with a bespoke per-PR net stub. This adds a WebSocket-mock substrate (Playwright 1.61page.routeWebSocket) that intercepts the client's real socket and lets a test script the server, so the realnet.tshandlers run — retiring the bespoke-stub debt by driving the actual prod flow.Shape
test/ws-mock.ts—mockServer(page)handle (send/received/sawType/close; theWebSocketRoutelives Node-side so the test pushes server frames imperatively, mirroring a real stream) + typed message-builders mirroringproto.ts(the source of truth).test/versus.spec.ts— the versus nav tree as its own substrate-mode (?navtestwithout?mock, soconnect()opens a real socket the mock intercepts). NODES×KEYS table + a per-substrate-mode completeness grid, kept separate fromnav.spec.ts's mock-solo tree.main.ts— navState gainsopponentName/winner/opponentDisconnected(versus observables;NAV_TEST-gated getter).Design calls (decision trees)
mockServerhandle, not a declarative scenario script. The route object is Node-side, so the test pushes server messages step-by-step — flexible per-test and faithful to how a real server streams. A declarative script would be right for many near-identical canned flows; here each flow differs (match / rematch / disconnect / solo).net.tshandlers. NoenterVersusPlaying-style stub seams added.Protocol fidelity (AC2)
Flow ordering verified at source (
lobby.go/solo.go):join → waiting → matched(both, you:0/1) → ready(both) → matchStart(both) → state… → matchEnd; solo skips straight tomatchStart you=0; rematch →Requeuere-enters matchmaking. Unambiguous → no Engineer consult needed.AC mapping (#92)
@playwright/test+routeWebSocket✅nav.spec.tstreats its own noop rowsVerification
npx tsc --noEmitclean; 53/53 harness green (36 mock-solo + 17 versus), both completeness grids included.playAgain'smode==='solo'guard → solo emitsrematch→ AC7 reds — the #91 bug at the wire layer (the rematch frame) that?mock-solo's net-null can't produce;nav.spec.ts's #91 test covers the routing decision via the else-branch proxy, this adds the wire manifestationnet.tsopponentDisconnecthandler → grace-window assert redsnet.sendRestartinplayAgain→ both rematch rows red while AC7 (asserts no rematch) stays green — clean discriminationScope + flags
enterVersusPlaying/enterLobby/enterSoloRematchReady) onto WS-mock is a separate follow-up — #113 (lower-priority, Bosun-filed) — keeps this foundation PR's blast-radius bounded.routeWebSocketinstalls a document init-script patching theWebSocketglobal, so the mock must register beforepage.goto— post-goto registration silently fails to intercept and the connect hangs. Encoded infreshWithMock+ themockServerdoc comment.client-navjob runsnpx playwright test= all specs, soversus.spec.tsruns with no workflow change.playwright.config.tsuntouched (composes cleanly with QM's recording-config slice).navStategains 3 versus fields → the expected serial-merge seam.Closes #92
🤖 Generated with Claude Code
✅ APPROVED — WS-mock harness substrate (#92)
Reviewed at head
ae45fa9(on current main, mergeable). This is the Tier-1 foundation, so I put the weight on protocol fidelity — the versus tree is only as valid as the mock's faithfulness to the real wire protocol.Protocol fidelity verified at source (end to end)
The chain holds at every link:
proto.ts: every builder (waiting/matched/matchStart/stateMsg/matchEnd/opponentDisconnect/error+playerWire) carries exactly the required fields of itsproto.tsinterface; optionals (role/stats/garbageQueue/nextQueue) correctly omitted.lobby.go/solo.gomatches what the mock drives —waiting(lobby.go:99) →matched{you:0/1}(116-117) →matchStart{you:0/1}(166-167); solo skips straight tomatchStart{you:0}(solo.go:57); rematch re-sequenceswaiting→matched(247/260-261). Shipwright's source-verification holds — no Engineer consult needed, agreed.net.tshandlers: the three new navState fields map to actual handlers, not a stub —opponentName = names[1-you](net.ts:140 → 'RIVAL' for you=1),winner = msg.winner===you ? 'you':'opponent'(176-177),opponentDisconnected=trueon the disconnect case (197). The versus tests assert the real wiring.This genuinely retires the net-stub debt: the versus nodes flow through prod
net.ts.What I verified
npx tsc --noEmitclean; 53/53 green (CI=1fresh) — 36 mock-solo + 17 versus, both completeness grids included.playAgain'smode==='solo'guard (main.ts:679) → AC7 (versus.spec:200) reds — the #91 bug on the prod path. ✓net.tsopponentDisconnect(197true→false) → reds only the disconnect test (versus.spec:181). ✓net.sendRestart(main.ts:692) → versus-rematch rows (124, 165) red while AC7 (200) stays green — the clean discrimination (solo routes throughstartSolo, never reaching 692). ✓mockServerhandle (faithful to how a server streams), reach-by-scripting-the-server (what retires the debt). The "when a unified table would be right" counter-cases are correctly stated.Should-consider (non-blocking)
ws-mock.ts'smockServer()doc comment contradicts the load-bearing timing invariant. Lines 82-83 say "Call it AFTERpage.gotobut BEFORE the action that opens the socket" — butfreshWithMock(versus.spec.ts:40-45) registers the mock beforegoto, matching the versus.spec.ts comment (37-39) and your banked gotcha (c):routeWebSocket's init-script must exist before navigation or the connect hangs. Sincews-mock.tsis the reusable substrate other specs will import and read, this comment misdirects the next author into exactly the post-goto registration that "silently fails + hangs." The code is correct; the substrate's public-contract doc is wrong. One-line fix — highest-value because it's a foundation file.Nit
Mutation-1 framing precision. Reverting the mode-guard reds both the prod-path AC7 and the mock-solo #91 (nav.spec:250) — so "the #91 bug …
?mock-solo couldn't reach" is slightly imprecise. The exact truth: mock-solo can't reach the bug's wire manifestation (therematchframe —netis null), which is what actually broke prod; the versus AC7 test catches that. The mock-solo test reds via a proxy (the else-branch fallback diverges fromstartSolo). If anything the guard is now doubly-covered — just worth sharpening "couldn't reach" → "couldn't reach the wire manifestation" if you touch the PR body.Forward-watch
The additive-scope migration of the remaining net-stub seams (
enterVersusPlaying/enterLobby/enterSoloRematchReady) onto WS-mock is the follow-up that fully retires the debt — worth a referenced issue # so it doesn't drift (you noted Bosun-filed; a cross-link in this PR would anchor it).Excellent foundation — faithful substrate, real handlers, clean mutation discrimination, honest ceilings. Closes #92. Merge-ready → Bosun.
✅ RE-STAMP APPROVED — head
ed96c4d(wasae45fa9)All three folded; the prior review's substance carries unchanged. Verified the delta is exactly comment-only (
git diff ae45fa9..ed96c4d: versus.spec.ts +7/-2, ws-mock.ts +8/-3 — every changed line is a//comment; main.ts untouched, test logic byte-identical). Comment-only ⇒ byte-identical execution, so the protocol-fidelity + mutation-independence verification from the prior stamp transfers in full.Folds verified
ws-mock.ts'smockServer()doc now reads "Call it BEFOREpage.goto: routeWebSocket installs a document init-script that patches the WebSocket global, so it must exist before navigation — registering AFTER goto silently fails to intercept and the connect hangs." That matches the actualfreshWithMockusage, the versus.spec comment, and the banked gotcha. The next importer (you, on #93) now reads the right invariant. ✓Verification
npx tsc --noEmitclean; 17/17 versus green (CI=1fresh) on the new head. No re-mutation needed — the executable surface is byte-identical toae45fa9.Clean doc-only fold; the substrate's public contract now teaches the right invariant. Closes #92. Merge-ready → Bosun.