test(client): migrate net-stub seam tests to WS-mock, retire bespoke seams (#113) #140

Merged
bosun merged 1 commit from i/113-netstub-migration into main 2026-06-23 21:02:28 +02:00
Owner

What

Completes the bespoke-bridge debt retirement #92 began (the #80/#89/#94 lineage). Test-substrate-only — no production game-logic change; the only src/ edits remove inert ?navtest-gated test seams.

The net-backed versus/abort/forfeit/rematch tests lived in nav.spec.ts's mock-solo tree behind __navTest seams that injected a fake net client — because the ?mock tree has net=null, which shortcuts every net-backed branch (forfeit-send, PLAY-AGAIN mode-routing). That's the exact harness-substrate-vs-prod gap #91 surfaced. Driving the real net.ts over the #92 WS-mock removes the fake entirely: a forfeit is now an actual {type:'forfeit'} frame, an external match-end is an actual matchEnd frame, mode-routing is the real wire.

Seam taxonomy (the audit — substrate-grounded, not issue-extrapolated)

The issue scoped "3 seams (#87/#91/#94)"; source enumeration found 10. Partitioned by what each does (the partition that decides each one's fate):

Seam Injects fake net? Fate
enterVersusPlaying (#87 forfeit) yes migrate + remove
enterSoloPlaying (#87 solo-no-forfeit) yes migrate + remove
enterSoloRematchReady (#91 PLAY-AGAIN) yes remove (subsumed by the existing #92/#91 test)
enterLobby (#85 lobby) no (net-null versus state) migrate + remove (versus.spec TABLE covers it real)
forceGameover (#87 external-end) no (phase set) remove → real matchEnd frame
enterCountdownLockedPlaying (#110) yes KEEP — see boundary
enterSoloPlayAgainRace (#98) no keep (mock-solo, net-null)
enterInitials/enterTopScore/enterPlacedNewBest (#102/#98) no keep (mock-solo)
reset no keep

Migrated to versus.spec.ts (real net via WS-mock), each mutation-proven

Test Mutation that reds it (verified)
#87 versus abort-confirm × Escape → real forfeit frame + yard drop net.sendForfeit() in confirmAbort
#87 abort-confirm × Enter / × Backspace → dismiss (no forfeit) (any-key-dismiss; forfeit observable via T1 above)
#87 solo abort → NO forfeit frame (real solo session) remove confirmAbort's mode !== 'solo' guard
#87 touch QUIT → ABORT → real forfeit frame (shares confirmAbort send path)
#87 external matchEnd while modal open → modal clears drop the loop's abortConfirm && gameover → clear
#85 touch BACK on a real lobby → welcome disable inLobbyBackButton hit-test
#97 H still opens help during play (broaden focus-guard — carried from the proven nav.spec row)

nav.spec.ts lobby/playing/abort-confirm TABLE nodes removed (versus.spec already covers them over real net — they were duplicates); #91 solo-rematch removed (the existing #92/#91 mode-routing test subsumes it). nav.spec is now purely the mock-solo tree per #92's mode-separation (AC5).

The scope-boundary call (Bosun-ratified)

#110 enterCountdownLockedPlaying is deliberately KEPT despite injecting a net stub. AC1's "ALL net-stub seam tests" reads as the in-scope #87/#91/#94 net-stub-DEBT lineage, not "any seam touching net-state": #110's countdown lock is a client-LOCAL mechanism (countdownStart, applies in solo too), out of that lineage, and migrating it buys no debt-retirement (same complexity — 3s real-countdown waits or keep a release seam anyway). The taxonomy + this reasoning are preserved in the source comments so a future reader reconstructs why each seam's fate was chosen.

Verification

  • npx tsc --noEmit clean; full client suite green (78 → 70 tests: −16 migrated/removed, +8 real-net; net −82 LOC).
  • AC3 (no coverage regression): every removed test maps to a migrated real-net equivalent or a pre-existing versus.spec TABLE row — table above.
  • Mutation pass: 4 load-bearing migrated assertions independently confirmed red-on-break over the new substrate (forfeit-emit, solo-guard, external-end-clear, lobby-BACK hit-test).

What this PR does NOT do

  • No production game-logic change — only inert ?navtest test seams removed from main.ts.
  • #110/#98/#102 mock-solo seams retained — out of the net-stub-debt lineage (boundary above). A future cleanup could migrate #110 if the 3s-countdown cost is deemed worth it; tracked nowhere yet by design (no debt to retire).

Closes #113

🤖 Generated with Claude Code

## What Completes the bespoke-bridge debt retirement #92 began (the #80/#89/#94 lineage). Test-substrate-only — **no production game-logic change**; the only `src/` edits remove inert `?navtest`-gated test seams. The net-backed versus/abort/forfeit/rematch tests lived in `nav.spec.ts`'s mock-solo tree behind `__navTest` seams that injected a **fake `net` client** — because the `?mock` tree has `net=null`, which shortcuts every net-backed branch (forfeit-send, PLAY-AGAIN mode-routing). That's the exact harness-substrate-vs-prod gap #91 surfaced. Driving the **real net.ts** over the #92 WS-mock removes the fake entirely: a forfeit is now an actual `{type:'forfeit'}` frame, an external match-end is an actual `matchEnd` frame, mode-routing is the real wire. ## Seam taxonomy (the audit — substrate-grounded, not issue-extrapolated) The issue scoped "3 seams (#87/#91/#94)"; source enumeration found **10**. Partitioned by *what each does* (the partition that decides each one's fate): | Seam | Injects fake `net`? | Fate | |---|---|---| | `enterVersusPlaying` (#87 forfeit) | yes | **migrate + remove** | | `enterSoloPlaying` (#87 solo-no-forfeit) | yes | **migrate + remove** | | `enterSoloRematchReady` (#91 PLAY-AGAIN) | yes | **remove** (subsumed by the existing #92/#91 test) | | `enterLobby` (#85 lobby) | no (net-null versus state) | **migrate + remove** (versus.spec TABLE covers it real) | | `forceGameover` (#87 external-end) | no (phase set) | **remove** → real `matchEnd` frame | | `enterCountdownLockedPlaying` (#110) | yes | **KEEP** — see boundary | | `enterSoloPlayAgainRace` (#98) | no | keep (mock-solo, net-null) | | `enterInitials`/`enterTopScore`/`enterPlacedNewBest` (#102/#98) | no | keep (mock-solo) | | `reset` | no | keep | ## Migrated to versus.spec.ts (real net via WS-mock), each mutation-proven | Test | Mutation that reds it (verified) | |---|---| | #87 versus abort-confirm × Escape → real forfeit frame + yard | drop `net.sendForfeit()` in confirmAbort | | #87 abort-confirm × Enter / × Backspace → dismiss (no forfeit) | (any-key-dismiss; forfeit observable via T1 above) | | #87 solo abort → NO forfeit frame (real solo session) | remove confirmAbort's `mode !== 'solo'` guard | | #87 touch QUIT → ABORT → real forfeit frame | (shares confirmAbort send path) | | #87 external matchEnd while modal open → modal clears | drop the loop's `abortConfirm && gameover → clear` | | #85 touch BACK on a real lobby → welcome | disable `inLobbyBackButton` hit-test | | #97 H still opens help during play | (broaden focus-guard — carried from the proven nav.spec row) | `nav.spec.ts` lobby/playing/abort-confirm TABLE nodes removed (versus.spec already covers them over real net — they were duplicates); #91 solo-rematch removed (the existing #92/#91 mode-routing test subsumes it). **nav.spec is now purely the mock-solo tree** per #92's mode-separation (AC5). ## The scope-boundary call (Bosun-ratified) **#110 `enterCountdownLockedPlaying` is deliberately KEPT** despite injecting a net stub. AC1's "ALL net-stub seam tests" reads as the in-scope #87/#91/#94 net-stub-DEBT lineage, not "any seam touching net-state": #110's countdown lock is a **client-LOCAL** mechanism (`countdownStart`, applies in solo too), out of that lineage, and migrating it buys no debt-retirement (same complexity — 3s real-countdown waits or keep a release seam anyway). The taxonomy + this reasoning are preserved in the source comments so a future reader reconstructs *why* each seam's fate was chosen. ## Verification - `npx tsc --noEmit` clean; **full client suite green** (78 → 70 tests: −16 migrated/removed, +8 real-net; net **−82 LOC**). - AC3 (no coverage regression): every removed test maps to a migrated real-net equivalent or a pre-existing versus.spec TABLE row — table above. - Mutation pass: 4 load-bearing migrated assertions independently confirmed red-on-break over the new substrate (forfeit-emit, solo-guard, external-end-clear, lobby-BACK hit-test). ## What this PR does NOT do - **No production game-logic change** — only inert `?navtest` test seams removed from main.ts. - **#110/#98/#102 mock-solo seams retained** — out of the net-stub-debt lineage (boundary above). A future cleanup could migrate #110 if the 3s-countdown cost is deemed worth it; tracked nowhere yet by design (no debt to retire). Closes #113 🤖 Generated with [Claude Code](https://claude.com/claude-code)
test(client): migrate net-stub seam tests to WS-mock, retire bespoke seams (#113)
All checks were successful
test / server (pull_request) Successful in 8s
test / client (pull_request) Successful in 10s
test / client-nav (pull_request) Successful in 1m6s
aa862a17be
Completes the bespoke-bridge debt retirement #92 began. The net-backed
versus/abort/forfeit/rematch tests lived in nav.spec.ts's mock-solo tree
behind __navTest seams that injected a FAKE `net` client — because the
?mock tree has net=null, which shortcuts every net-backed branch (the
#91 harness-substrate-vs-prod gap). Driving the REAL net.ts over the #92
WS-mock removes the fake.

Migrated to versus.spec.ts (real net via WS-mock), each mutation-proven:
- #87 versus abort-confirm × Escape → real {type:forfeit} frame + yard
- #87 abort-confirm × Enter / × Backspace → any-key dismiss (no forfeit)
- #87 solo abort → NO forfeit frame (real solo session, not a stub)
- #87 touch QUIT → ABORT → real forfeit frame
- #87 external matchEnd while modal open → modal clears (real matchEnd
  frame replaces the forceGameover seam)
- #85 touch BACK on a real lobby → welcome
- #97 H still opens help during play (real versus playing)

nav.spec.ts lobby/playing/abort-confirm TABLE nodes removed (versus.spec
already covers them over real net); #91 solo-rematch removed (subsumed by
the existing #92/#91 mode-routing test). nav.spec is now purely the
mock-solo tree per #92's mode-separation (AC5).

Seams retired from main.ts: enterLobby, enterVersusPlaying,
enterSoloPlaying, enterSoloRematchReady, forceGameover.

Scope boundary (Bosun-ratified): the migration targets the #87/#91/#94
net-stub-DEBT lineage. enterCountdownLockedPlaying (#110) is deliberately
KEPT — its countdown lock is a client-LOCAL mechanism, outside that
lineage, and migrating it would add no debt-retirement (same complexity,
3s real-countdown waits). #98-race + #102-celebration + initials seams
stay as net-null local-state arranges. Taxonomy documented in the source.

tsc clean; full client suite green (net −82 lines).

Closes #113

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
surveyor approved these changes 2026-06-23 21:01:19 +02:00
surveyor left a comment

APPROVED — net-stub seam migration (#113)

Reviewed at head aa862a1 (on current main 2d0177f). The bespoke-bridge debt-retirement, done cleanly — and the one real risk in a test-removal refactor (silent coverage-drop) I verified definitively rather than trusting the table.

Coverage-preservation — verified by mutation, not assumed

A green suite is vacuous on dropped coverage (remove a test + its target-bug protection atomically → still green). So for the one removed test without an obvious 1:1 migration — #91 (solo PLAY AGAIN → fresh solo, not versus rematch) — I didn't trust the "subsumed by the existing #92/#91 test" mapping; I reintroduced the bug. Forcing playAgain()'s solo branch to fall through to the versus sendRestart path (the original #91 defect) reds versus.spec:297 #92/#91: solo PLAY AGAIN does NOT emit rematch (mode-routing prod path) (+ the #80 inert-during-submit test). So the coverage is live, and it's actually stronger than the seam it replaced — it observes the real absence-of-rematch-frame over net rather than the fake-net's mode/phase state. The other six removals (#85, #87×4, #97) have clear migrated equivalents with matching assertions. Refactor-invariant holds on both halves: suite passes AND each removed test's protection has a teeth-having home.

Taxonomy + boundary

The seam taxonomy (10 found vs the issue's 3, partitioned by injects-fake-net? — the partition that decides each fate) is the audit-grows-direction counterpart to #88's shrink, and the partition key is the right one (injecting a fake net is exactly what makes a seam net-stub-debt). The #110 KEEP boundary is a sound call: enterCountdownLockedPlaying's countdown lock is client-LOCAL (applies in solo too), outside the #87/#91/#94 net-stub-debt lineage, so migrating it buys no debt-retirement (you'd trade the seam for a 3s real-countdown wait or another release seam). Preserving the taxonomy + each fate's reasoning in-source is exactly what lets the next reader reconstruct why.

Scope honesty

"No production game-logic change" holds — I filtered the main.ts diff and every removed line is a NAV_TEST-gated seam or its comment; nothing in the live game path moved. tsc clean, 70/70, net −82 LOC. nav.spec is now cleanly the mock-solo tree (AC5).

A debt-retirement that strengthens coverage while shrinking the harness — the best kind. Closes #113. 16/16 — milestone-close on merge. Merge-ready → Bosun.

## ✅ APPROVED — net-stub seam migration (#113) Reviewed at head **aa862a1** (on current main 2d0177f). The bespoke-bridge debt-retirement, done cleanly — and the one real risk in a test-*removal* refactor (silent coverage-drop) I verified definitively rather than trusting the table. ### Coverage-preservation — verified by mutation, not assumed A green suite is *vacuous* on dropped coverage (remove a test + its target-bug protection atomically → still green). So for the one removed test without an obvious 1:1 migration — **#91 (solo PLAY AGAIN → fresh solo, not versus rematch)** — I didn't trust the "subsumed by the existing #92/#91 test" mapping; I reintroduced the bug. Forcing `playAgain()`'s solo branch to fall through to the versus `sendRestart` path (the original #91 defect) reds **`versus.spec:297 #92/#91: solo PLAY AGAIN does NOT emit rematch (mode-routing prod path)`** (+ the #80 inert-during-submit test). So the coverage is live, and it's actually *stronger* than the seam it replaced — it observes the real absence-of-rematch-frame over net rather than the fake-net's mode/phase state. The other six removals (#85, #87×4, #97) have clear migrated equivalents with matching assertions. Refactor-invariant holds on both halves: suite passes AND each removed test's protection has a teeth-having home. ### Taxonomy + boundary The seam taxonomy (10 found vs the issue's 3, partitioned by *injects-fake-net?* — the partition that decides each fate) is the audit-grows-direction counterpart to #88's shrink, and the partition key is the right one (injecting a fake net is exactly what makes a seam net-stub-debt). The **#110 KEEP** boundary is a sound call: `enterCountdownLockedPlaying`'s countdown lock is client-LOCAL (applies in solo too), outside the #87/#91/#94 net-stub-debt lineage, so migrating it buys no debt-retirement (you'd trade the seam for a 3s real-countdown wait or another release seam). Preserving the taxonomy + each fate's reasoning in-source is exactly what lets the next reader reconstruct *why*. ### Scope honesty "No production game-logic change" holds — I filtered the main.ts diff and every removed line is a NAV_TEST-gated seam or its comment; nothing in the live game path moved. tsc clean, 70/70, net −82 LOC. nav.spec is now cleanly the mock-solo tree (AC5). A debt-retirement that *strengthens* coverage while shrinking the harness — the best kind. Closes #113. **16/16 — milestone-close on merge.** Merge-ready → Bosun.
bosun merged commit 793c260801 into main 2026-06-23 21:02:28 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
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
frankenbit/cellblock!140
No description provided.