Meta: automated keyboard navigation-tree playtest harness (design conversation) #81
Labels
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/cellblock#81
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Context
Two keyboard-navigation bugs surfaced on 2026-06-22 via operator playtest:
Both are state-machine gaps that ad-hoc playtest happened to find. They're likely not the only gaps — the navigation tree has multiple screens (welcome → mode-select → solo / multiplayer-name / multiplayer-lobby / game-over → leaderboard / initials-entry / ...) and each transition has multiple keyboard paths (Enter / Esc / Tab / Arrow / Backspace / Space / etc.).
Proposal
Design + build an automated keyboard-navigation-tree playtest harness:
Open design questions (deferred — design conversation first)
Decision needed
Whether to build now, defer to next sprint after #79 + #80 land, or defer to a "post-prototype polish pass."
Lane + size
Pending design conversation. Likely size/L if full-coverage; size/M if critical-path-only. Shipwright lane primary; Engineer + Surveyor + Bosun consult on scope-shape.
Design proposal (Shipwright, primary author)
TL;DR: Build it as part of the #79+#80 sprint — but as the regression vehicle for those fixes, not after them. Critical-reachable-path scope, a hand-maintained declarative transition table with a completeness meta-check, desktop-keyboard axis. Size/M for the harness; #79/#80 stay size/S each. The one real prerequisite is a tiny state-introspection seam.
Reframe: both bugs are one shape — a missing edge
#79 and #80 aren't two unrelated bugs; they're the same failure: a state node missing an expected transition edge.
checkinnode handlesEnter → lobbybut is missingEsc → title.initials:submittednode is missing the "input is now inert" property;Backspace/Re-Enteredges that should be no-ops still fire.A navigation-tree harness is precisely "enumerate every (node, key) → expected-edge and assert it." A missing edge is the bug. So #79 and #80 are not bugs the harness would be written after — they're the first two failing assertions it produces. That reframes the whole priority question (Q5 below).
The one prerequisite: a state-introspection seam
My existing harnesses (#74/#75/#77) drive exported render functions with synthetic state — they never needed to read the app's live state. A nav-harness does: it presses a key and must assert "what state are we in now?". The app doesn't currently expose that.
Proposal: a narrow read-only getter —
window.__navState()→{ screen, phase, initialsPhase, name, pendingInitials }— wired inmain.ts. Tiny, read-only, no behavior. (Could dev-gate it, but it's so small that always-on is fine and simpler.) This is the load-bearing enabling decision; everything else builds on it. Flagging it explicitly because it's a (trivial) prod-code addition, not pure test code.Alternative considered: infer state from canvas screenshots / DOM signals (
#name-input.ni-active,tc-hidden). Rejected — brittle, partial, and slow. A 5-field getter is the honest seam.The five questions
Q1 — Coverage scope: critical reachable path × the keys that bite.
Not full-tree, not Esc+Enter-only. The real constraint is reachability in the harness: solo (title → solo → playing → gameover → initials:entering → submitted) is fully reachable via
?mock(no server) — and #80 lives there.checkinis reachable viatitle → Enter— and #79'sEsc → titleis testable with no server (just press Esc on checkin, assert title). The multiplayer lobby/match nodes need a WS server or mock → defer those to a v2 (explicitly logged, not silently uncovered). So v1 = the server-free critical path, keys{Esc, Enter, Backspace}(Backspace matters for #80). Both reported bugs are covered without standing up a server.Q2 — Maintenance: hand-maintained declarative transition table + a completeness meta-check.
A single data table —
{ from: {screen,phase,initialsPhase}, key, to: {...} | 'noop' }— that the harness iterates. Reads like a spec, but it's just data; each feature adds rows. Not a spec-language/generator (overkill at prototype stage). The teeth that stop it decaying into ad-hoc: a meta-assertion that every reachable (node × nav-key) pair has a table row (even an explicit'noop'). An unenumerated transition then fails loudly — otherwise the harness reproduces the exact gap class it exists to catch (a transition nobody thought to test). If the state machine is later formalized into an explicit FSM, generate the table from it — a graduation step, not now.Q3 — Axis: this is the desktop-keyboard axis; keep it distinct from the mobile-touch harnesses.
#74/#75/#77 cover touch+mobile per-feature. This complements them on the keyboard axis — different input modality, different failure modes (the layered-instrument lesson from #73: don't conflate axes). One overlap to note: #79's AC allows "Esc or an explicit cancel-back button." If the fix adds an on-screen BACK button, the harness should assert both the key (desktop) and the button (touch) fire the same
→ titleedge — a small, deliberate cross-axis row.Q4 — Lane: solo Shipwright build, targeted consults (not co-build).
The harness substrate is my Playwright lane. Consults: Engineer on #80's AC#4 server-dedup question — the harness can actually assert it ("after the client deactivates the input, no second leaderboard entry appears on Re-Enter"), so Engineer's probe result becomes a harness row. Herald on the genuinely-UX transition calls — e.g. #80 AC#2 says initials:submitted goes to "the natural next state"; is that game-over-with-PLAY-AGAIN or leaderboard-display? That's a creative-head call that defines the expected edge the harness asserts. So: I build, Engineer/Herald supply specific expected-edge values where they own the answer.
Q5 — Priority: build now, in the bundle, as the failing-test vehicle for #79/#80.
The tracker set the build-trigger at "n=2/n=3 nav-bugs in close succession → systematic gap." That trigger fired — two surfaced the same day. So the original "defer to post-prototype" lean is correctly superseded by the operator's bundle directive. Recommended order within the bundle:
window.__navState()(enabling seam).checkin: Esc → title) and #80 (initials:submittedinert + transitions away).This makes the table encode the intended behavior (from the ACs), and the fix satisfies a test that already exists — test-defines-correct, not test-mirrors-impl. It also gives the harness real teeth by construction: #79/#80 demonstrably fail it pre-fix (no placebo-test risk).
Recommended shape + size
__navStateseam + critical-path table + completeness check: size/M.What I'd want from consults before building
initials:submitted → ?next-state (game-over vs leaderboard-display) — defines #80's target edge.leaderboard.go Add()dedup)? Determines whether #80 is UX-only or has a correctness tail, and whether the harness asserts a server-side row.Happy to adjust scope up (full-tree + WS mock) or down (Esc+Enter only, no completeness-check) on your/operator ratification. My lean is the M-sized critical-path-with-teeth above — it covers both reported bugs, locks them as regressions, and is cheap to extend row-by-row as the tree grows.
Closing satisfaction summary — closed by PR #84 @
bd14396fe0(squashed):Harness shipped per the settled (b) design:
TDD-shape closed the loop empirically: #79 + #80 were the harnesss first failing rows; fixes pass them; mutation-proven. Both Shipwright (during build) + Surveyor (during review) verified the harness has teeth via mutation.
Discoveries during build worth holding: input-path-symmetry as a substrate-property axis (Surveyor caught touch/keyboard asymmetry); TDD-shape harnesses where bug-discovery-in-review feeds back as added-coverage-rows make the review-cycle itself substrate-extending.
Multiplayer lobby/match nodes deferred to v2 (needs WS mock; logged-not-silent per declarative-table design — the completeness meta-check will surface those uncovered nodes on next harness invocation as expected-yet-not-covered, fail-loud-not-silent).