Files
upbeatBytes/tests/test_pool_admin.py
T
thejayman77 90da4be083 Pool admin: empty-pool safety net + honest removal copy (Codex audit)
Two hardening fixes from Codex's audit:
- _pick_answer falls back to the curated baseline if the live pool is empty,
  so an admin tombstoning every answer in a variant can't divide-by-zero the
  daily picker. Test added (test_picker_survives_empty_live_pool). Chosen over
  a minimum-count block: robust without refusing legitimate removals.
- Removal copy is now honest — "Removed from future puzzles (today's answer is
  already set)" — since a tombstone doesn't rewrite today's generated
  daily_puzzles row. Panel intro updated to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 19:04:49 -04:00

105 lines
3.8 KiB
Python

"""Admin curation of the Daily Word answer pool: add, remove (tombstone any
word — curated or added), restore, and bulk import with dedupe + validation."""
import pytest
from goodnews import games
from goodnews.db import connect, init_db
@pytest.fixture
def conn(tmp_path):
c = connect(str(tmp_path / "t.db"))
init_db(c)
return c
def _a_curated(variant):
return games._POOL[variant][0] # a known baked-in word
def _fresh(variant, n=1):
"""n dictionary words of the given length that are NOT already in the pool."""
pool = set(games._POOL[variant])
out = [w for w in sorted(games._DICT[variant]) if w not in pool and w.isalpha()]
return out[:n]
def test_add_and_remove_added_word(conn):
w = _fresh("5")[0]
res = games.add_pool_word(conn, w)
assert res.get("added") and res["variant"] == "5"
assert w in games.answer_pool(conn, "5")
# Removing an added word tombstones it → drops out of the live pool.
games.remove_pool_word(conn, w)
assert w not in games.answer_pool(conn, "5")
# ...and is reversible.
games.restore_pool_word(conn, w)
assert w in games.answer_pool(conn, "5")
def test_remove_curated_word_via_tombstone(conn):
w = _a_curated("5")
assert w in games.answer_pool(conn, "5")
games.remove_pool_word(conn, w) # curated word — only a tombstone can pull it
assert w not in games.answer_pool(conn, "5")
summary = games.pool_summary(conn)
assert w in summary["5"]["removed"]
games.restore_pool_word(conn, w)
assert w in games.answer_pool(conn, "5")
def test_add_rejects_non_dictionary_and_bad_length(conn):
assert "error" in games.add_pool_word(conn, "zzzzz") # not in dict
assert "error" in games.add_pool_word(conn, "hi") # wrong length
assert "error" in games.add_pool_word(conn, _a_curated("5")) # already in pool
def test_add_relifts_tombstone(conn):
w = _a_curated("6")
games.remove_pool_word(conn, w)
assert w not in games.answer_pool(conn, "6")
# Re-adding a removed word lifts the tombstone rather than erroring.
res = games.add_pool_word(conn, w)
assert res.get("added")
assert w in games.answer_pool(conn, "6")
def test_import_dedupes_and_validates(conn):
new5 = _fresh("5", 2) # two fresh 5-letter words
added_word = new5[0]
games.add_pool_word(conn, added_word)
existing_curated = _a_curated("5")
res = games.import_pool_words(conn, [
new5[1], # valid new
new5[1].upper(), # same word, different case → counted once
added_word, # dup (already added)
existing_curated, # dup (curated)
"zzzzz", # rejected: not in dictionary
"hi", # rejected: wrong length
" ", # blank → ignored
])
assert set(res["added"]) == {new5[1]}
assert res["counts"]["added"] == 1
assert res["counts"]["duplicates"] >= 2
assert {r["word"] for r in res["rejected"]} == {"zzzzz", "hi"}
assert new5[1] in games.answer_pool(conn, "5")
def test_picker_survives_empty_live_pool(conn):
"""An overzealous admin could tombstone every answer in a variant. The live
pool then reads empty, but the daily picker must not divide-by-zero — it falls
back to the curated baseline rather than crashing."""
for w in list(games._POOL["5"]):
games.remove_pool_word(conn, w)
assert games.answer_pool(conn, "5") == [] # live pool truly empty
ans = games._pick_answer(conn, "2026-06-11", "5") # must not raise
assert ans in set(games._POOL["5"]) # fell back to curated
def test_import_relifts_removed_word(conn):
w = _a_curated("5")
games.remove_pool_word(conn, w)
res = games.import_pool_words(conn, [w])
assert w in res["added"]
assert w in games.answer_pool(conn, "5")