Skip to content

Add reducer tests; require solutions to be passed into newGame - #439

Merged
jrr merged 2 commits into
devfrom
claude/reducer-tests
Aug 22, 2026
Merged

Add reducer tests; require solutions to be passed into newGame#439
jrr merged 2 commits into
devfrom
claude/reducer-tests

Conversation

@jrr

@jrr jrr commented Aug 22, 2026

Copy link
Copy Markdown
Owner

First of three test-focused PRs. Small refactors + new tests only, all using the existing vitest setup — no new dependencies or tooling.

newGame now requires its solutions

newGame takes one solution per board, and the board count derives from the array's length:

newGame({ solutions: ["CIGAR", "REBUT"], numGuesses: 3 })

game-states.ts no longer imports game-logic.ts, so the state layer is pure data construction with no dependency on the word lists. Randomness moves to the two call sites that actually want it, via a new pickSolutions(n) helper. Three call sites changed: ui.tsx, the reducer's new-game branch, and display-rows.test.ts (which previously depended on a random word — harmless, since it only checks row shapes, but a latent flake).

This is code organization, not testability. The reducer's new-game branch still reaches Math.random() — explicitly now rather than transitively. Making that path deterministic means putting the solutions in the action, which splits the "N only works once the game is over" rule across the reducer and the key handler. Better designed alongside deterministic daily mode, so it's deliberately not here.

While changing the ui.tsx call site, useReducer switched to its lazy-init form. The initial-state argument is re-evaluated on every render, so picking words inline drew throwaway solutions on every keystroke.

src/state/reducer.test.ts

The reducer is the core state machine and had no coverage at all. Because the bug below needs four actions in sequence to appear, these drive the reducer through action sequences rather than testing it one call at a time:

const guess = (state: GameState, word: string) => submit(type(state, word));

Covered: typing and row-full clamping, backspace bounds, submission before the row is full, valid and invalid words, note lifecycle, per-board colouring, single- and multi-board wins, guess exhaustion, give-up, and the N/Q keys in terminal states.

Two known bugs recorded as it.fails

Rather than leave these undocumented or turn CI red, they're written as it.fails. Vitest reports them as "expected fail", so CI stays green — and each one starts failing the moment the bug is fixed, which forces the flip to it.

  • Finding 1.1 — a two-board game runs past its guess limit when board 0 is solved first. onFinalGuess counts rows on gameBoards[0], which stops growing once that board is won. The renderer then throws RangeError on a negative blank-row count.
  • Finding 1.4 — the invalid-word note survives every later keystroke.

Both were confirmed to fail for the intended reason, not incidentally.

Checks

yarn build, yarn lint, yarn prettier --check . and the --test midgame --quit smoke run all pass. Tests go from 11 to 30 passing plus 2 expected failures.

Since the smoke run takes the initialState path and never touches newGame, the real path was verified separately: board counts, the numGuesses default (1→6, 2→7, 3→8, unchanged), the --num-guesses override, lowercase normalization, and the reducer's N path all behave as before.

Note that newGame({ solutions: [] }) still yields zero boards and crashes on the first render — finding 1.3, unchanged and simply relocated by this PR.

Next up, as separate PRs: invariant tests for colorGuess and the reducer, then extracting keyToAction and the CLI flag handling into pure functions so they can be tested directly.

claude added 2 commits August 22, 2026 14:06
The reducer is the core state machine and had no coverage. Bug 1.1 from
the architecture review needed four actions in sequence to appear, so
these drive the reducer through action sequences rather than testing it
one call at a time.

newGame gains an optional `solutions` array so tests can pin the answers
instead of working around Math.random() via pickSolution. Boards without
a supplied solution still get a random word, so existing behavior is
unchanged; numBoards now also defaults to the number of solutions given.

Two known bugs are recorded as it.fails, which keeps CI green while
making the bug executable — each starts failing the moment it is fixed,
forcing the flip to `it`:

- finding 1.1: a two-board game runs past its guess limit when board 0
  is solved first, because onFinalGuess counts rows on gameBoards[0]
- finding 1.4: the invalid-word note survives later keystrokes

11 tests -> 30 passing plus 2 expected failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EoMUcnWHajEBC54SvGaox
Follow-up to the previous commit: rather than an optional solutions
array that falls back to pickSolution(), newGame now requires one
solution per board and the board count derives from its length.

game-states.ts no longer imports game-logic.ts, so the state layer is
pure data construction with no dependency on the word lists. Randomness
is now visible at the two call sites that actually want it, via a new
pickSolutions(n) helper.

Note this is a code-organization change, not a testability one: the
reducer's new-game branch still reaches Math.random(), just explicitly
now instead of transitively. Making that path deterministic means
putting solutions in the action, which splits the "N only works once
the game is over" rule across the reducer and the key handler — better
designed alongside deterministic daily mode.

While changing the ui.tsx call site, switched useReducer to its lazy
init form. The initial-state argument is re-evaluated on every render,
so picking words inline drew throwaway solutions on each keystroke.

Verified board counts, the numGuesses default (1->6, 2->7, 3->8), the
--num-guesses override, lowercase normalization, and the reducer's 'N'
path all behave as before. reducer.test.ts needed no changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EoMUcnWHajEBC54SvGaox
@jrr jrr changed the title Add reducer tests, with a solutions seam for determinism Add reducer tests; require solutions to be passed into newGame Aug 22, 2026
@jrr
jrr merged commit 63d44a1 into dev Aug 22, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants