Files
wordle-solver/tests/test_cli.py
T
will 42c393fe50 Print possible answers in heuristic order, not alphabetical
The "possible answer(s) remain" list (shown when <=20 candidates)
was sorted alphabetically, out of step with the ranked suggestions
printed right below it. Rank it with the same heuristic instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 05:20:19 -04:00

175 lines
6.4 KiB
Python

import argparse
import pytest
from wordle_solver import cli
from wordle_solver.game import Clue, score_guess
from wordle_solver.cli import handle_slash_command, parse_line, hard_mode_violation, print_suggestions
from wordle_solver.solver import Solver, rank_guesses
from wordle_solver.wager import Wager
G = Clue.GREEN
Y = Clue.YELLOW
B = Clue.GREY
class TestParseLine:
def test_digit_encoding(self):
guess, pattern = parse_line("c2r1a0n0e0")
assert guess == "crane"
assert pattern == (G, Y, B, B, B)
def test_letter_encoding(self):
guess, pattern = parse_line("cGrYaBnBeB")
assert guess == "crane"
assert pattern == (G, Y, B, B, B)
def test_spaces_stripped(self):
guess, pattern = parse_line("c 2 r 1 a 0 n 0 e 0")
assert guess == "crane"
assert pattern == (G, Y, B, B, B)
def test_uppercase_letters_lowercased(self):
guess, _ = parse_line("C2R1A0N0E0")
assert guess == "crane"
def test_all_green(self):
_, pattern = parse_line("c2r2a2n2e2")
assert pattern == (G, G, G, G, G)
def test_wrong_length_raises(self):
with pytest.raises(ValueError):
parse_line("c2r1a0") # only 3 pairs
def test_too_long_raises(self):
with pytest.raises(ValueError):
parse_line("c2r1a0n0e0x0") # 6 pairs
def test_digit_in_letter_position_raises(self):
with pytest.raises(ValueError):
parse_line("12r1a0n0e0") # position 0 is '1', not a letter
class TestHardModeViolation:
def test_valid_guess_returns_none(self):
green = {0: "c", 1: "r"}
from collections import Counter
min_count = Counter({"a": 1})
assert hard_mode_violation("crane", green, min_count) is None
def test_wrong_green_position_detected(self):
green = {0: "c"}
from collections import Counter
result = hard_mode_violation("trace", green, Counter())
assert result is not None
assert "1" in result # position 1 (1-indexed)
def test_missing_required_letter_detected(self):
from collections import Counter
min_count = Counter({"r": 1})
result = hard_mode_violation("piano", {}, min_count)
assert result is not None
assert "R" in result
def test_missing_duplicate_letter_detected(self):
# Word needs two 'e's
from collections import Counter
min_count = Counter({"e": 2})
result = hard_mode_violation("crane", {}, min_count)
assert result is not None # "crane" has only one 'e'
def test_satisfies_duplicate_requirement(self):
from collections import Counter
min_count = Counter({"e": 2})
result = hard_mode_violation("geese", {}, min_count)
assert result is None # "geese" has three 'e's — satisfies ≥2
def test_empty_constraints_always_none(self):
from collections import Counter
assert hard_mode_violation("crane", {}, Counter()) is None
class TestAddWordCommand:
def _solver(self):
return Solver(answers=["crane", "trace"], extra_guesses=["about", "zesty"])
def _opts(self):
return argparse.Namespace(top=10, position_weight=1.0, presence_weight=1.0)
def test_rejects_wrong_arg_count(self, capsys):
solver = self._solver()
handle_slash_command("/addword", solver, Wager(), {}, solver.answers, self._opts())
assert "Usage" in capsys.readouterr().out
assert solver.answers == ["crane", "trace"]
def test_rejects_non_alpha_or_wrong_length(self, capsys):
solver = self._solver()
handle_slash_command("/addword abc12", solver, Wager(), {}, solver.answers, self._opts())
assert "5 letters" in capsys.readouterr().out
assert solver.answers == ["crane", "trace"]
def test_already_in_answers_short_circuits(self, capsys, monkeypatch):
solver = self._solver()
called = False
def fake_add(word):
nonlocal called
called = True
return {"added": False, "removed_from_guesses": False}
monkeypatch.setattr(cli, "add_answer_word", fake_add)
handle_slash_command("/addword crane", solver, Wager(), {}, solver.answers, self._opts())
assert "already" in capsys.readouterr().out
assert not called
assert solver.answers == ["crane", "trace"]
def test_adds_word_and_removes_from_guesses(self, capsys, monkeypatch):
solver = self._solver()
monkeypatch.setattr(
cli, "add_answer_word",
lambda word: {"added": True, "removed_from_guesses": True},
)
handle_slash_command("/addword zesty", solver, Wager(), {}, solver.answers, self._opts())
out = capsys.readouterr().out
assert "Added ZESTY" in out
assert "removed it from guesses.txt" in out
assert solver.answers == ["crane", "trace", "zesty"]
assert "zesty" not in solver.extra_guesses
def test_new_candidate_consistent_with_history_is_added(self, capsys, monkeypatch):
solver = self._solver()
# Derive the pattern "trace" would actually produce against "chase",
# so the new word is consistent with history by construction.
pattern = score_guess("trace", "chase")
solver.apply("trace", pattern)
monkeypatch.setattr(
cli, "add_answer_word",
lambda word: {"added": True, "removed_from_guesses": False},
)
handle_slash_command("/addword chase", solver, Wager(), {}, solver.answers, self._opts())
assert "chase" in solver.candidates
class TestPrintSuggestionsOrder:
def test_possible_answers_printed_in_heuristic_order_not_alphabetical(self, capsys):
# Deliberately not alphabetical so a bug that re-sorts alphabetically
# would be caught: alphabetical order here is amigo, aroma, mango.
answers = ["mango", "amigo", "aroma"]
solver = Solver(answers=answers, extra_guesses=[])
print_suggestions(solver, top_n=10, position_weight=1.0, presence_weight=1.0)
expected_order = [
word for word, _, _ in
rank_guesses(answers, answers, top_n=len(answers), position_weight=1.0, presence_weight=1.0)
]
assert expected_order != sorted(answers) # sanity check the fixture is non-alphabetical
printed_line = capsys.readouterr().out.splitlines()[2]
assert printed_line.strip() == ", ".join(expected_order)