Add invariant tests for colorGuess and the reducer - #440
Merged
Conversation
Where the existing tests pin down specific cases, these state the rules those cases are circling and check them across many inputs, using a small seeded LCG so failures reproduce exactly. colorGuess: one tile per guessed letter in order; green marks exactly the positions already correct; and Wordle's duplicate-letter rule, that a letter earns min(count in guess, count in solution) coloured tiles. Checked over all 144 pairings of a duplicate-heavy word set and 2000 seeded pairs from the real word lists. reducer: rules checked per state (the current row fits in a word, every guessed row is a full word) and per move (rows never disappear, a move adds at most one row, an already-solved board takes no more guesses and stays solved). Games are played out with random actions and stop as soon as they end, so the new-game key can't reset the boards mid-sequence. The guess-limit rule is split in two: single-board games stay inside the limit, while the multi-board case is another it.fails for finding 1.1 — the broad form of the targeted case added earlier. Assertions carry the inputs alongside the value being checked, so a failure inside a long loop names the pair or seed that broke it. That made expectEqual's swapped arguments actively misleading during development, so this also fixes that (finding 3.3): outcomes are unchanged, but diffs now label expected and actual the right way round. Verified by mutation: removing colorGuess's consume step, unfreezing won boards, and dropping the row-full clamp are each caught by the invariant naming that rule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013EoMUcnWHajEBC54SvGaox
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.
Second of three test-focused PRs (#439 was the first). Tests only — no production code changes, no new dependencies. The one non-test change is a one-line fix inside
test-util.ts, explained at the bottom.Where the existing tests pin down specific cases, these state the rules those cases are circling and check them across many inputs, using a small seeded LCG so failures reproduce exactly.
colorGuessThree rules, checked over all 144 pairings of a duplicate-heavy word set (
EERIE,GEESE,TEETH,MUMMY,KAYAK, …) and 2000 seeded pairs drawn from the real word lists:min(count in guess, count in solution)coloured tiles, and no moreThat third one is what the eight existing examples approximate. Before writing it as a test I brute-forced it across 92,600 real word pairs to confirm it actually holds rather than merely looking plausible.
Deliberately not asserted: "a gray letter is absent from the solution". That isn't true — a letter goes gray once its occurrences are used up.
reducerGames are played out with random actions and stop as soon as they end, so the new-game key can't reset the boards mid-sequence and legitimately break these.
Per state: the current row fits in a word; every guessed row is a full word.
Per move: rows never disappear; a move adds at most one row; an already-solved board takes no more guesses and stays solved.
The guess-limit rule is split in two. Single-board games stay inside the limit. The multi-board case is another
it.failsfor finding 1.1 — the broad form of the targeted case from #439: no random play of a multi-board game stays inside its limit.Verified by mutation
An invariant that passes against broken code is worthless, so each was checked against a deliberate break:
colorGuessstops consuming a matched letterOne invariant I first wrote was simply wrong — "a solved board takes no guesses" fired on the winning move itself, since a board becomes won on the same transition that appends the winning row. The test caught it; it's now expressed over state transitions rather than snapshots.
Why
expectEqualchangedAssertions carry their inputs alongside the value being checked, so a failure inside a 2000-iteration loop names the pair that broke it:
That made
expectEqual's swapped arguments (finding 3.3) actively misleading — the correct value was labelled "Received" and the buggy one "Expected". Since diagnosable failures are the point of this PR andtest-util.tswas already being edited for the RNG, the one-line fix is included. Outcomes are unchanged; only the labels move.Checks
yarn build,yarn lint,yarn prettier --check .and the--test midgame --quitsmoke run all pass. 30 → 36 passing plus 3 expected failures, suite still under a second.Next, as a separate PR: extracting
keyToActionand the CLI flag handling into pure functions so they can be tested directly.Generated by Claude Code