Add reducer tests; require solutions to be passed into newGame - #439
Merged
Conversation
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
This was referenced Aug 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First of three test-focused PRs. Small refactors + new tests only, all using the existing vitest setup — no new dependencies or tooling.
newGamenow requires its solutionsnewGametakes one solution per board, and the board count derives from the array's length:game-states.tsno longer importsgame-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 newpickSolutions(n)helper. Three call sites changed:ui.tsx, the reducer's new-game branch, anddisplay-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.tsxcall site,useReducerswitched 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.tsThe 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:
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/Qkeys in terminal states.Two known bugs recorded as
it.failsRather 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 toit.onFinalGuesscounts rows ongameBoards[0], which stops growing once that board is won. The renderer then throwsRangeErroron a negative blank-row count.Both were confirmed to fail for the intended reason, not incidentally.
Checks
yarn build,yarn lint,yarn prettier --check .and the--test midgame --quitsmoke run all pass. Tests go from 11 to 30 passing plus 2 expected failures.Since the smoke run takes the
initialStatepath and never touchesnewGame, the real path was verified separately: board counts, thenumGuessesdefault (1→6, 2→7, 3→8, unchanged), the--num-guessesoverride, lowercase normalization, and the reducer'sNpath 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
colorGuessand the reducer, then extractingkeyToActionand the CLI flag handling into pure functions so they can be tested directly.