Admin: Daily Word pool curation (lookup + add/remove)

First games admin tool. A "Games" tab in the operator console for the Daily Word
answer pool.
* Lookup: is a word real (in the guess dictionary), the right length (5/6), and
  already in the pool — instant as you type.
* Add: appends to the pool, enforcing the invariant (alpha · 5/6 letters · in the
  guess dict) so the daily answer is always guessable. Remove: drops admin-added
  words (curated static ones stay).
* Additions persist in a new word_pool table (survives redeploys, unlike the
  baked-in JSON); the daily picker reads static pool ∪ DB additions. Guess dicts
  shipped with the package (goodnews/data/words-5/6.json) for server-side
  validation. Admin-gated endpoints + tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-11 11:42:52 -04:00
parent 7e4d3e2cd9
commit 903b27fc8d
7 changed files with 230 additions and 2 deletions
+22
View File
@@ -368,3 +368,25 @@ def test_wordsearch_thin_llm_falls_back(tmp_path, monkeypatch):
rich = FakeClient("THEME: Rich Theme\nWORDS: " + ", ".join(rich_words))
p2 = games.generate_wordsearch_puzzle(c, "2026-07-02", client=rich)
assert p2["theme"] == "Rich Theme" and len(p2["words"]) >= games.WS_MIN_ACCEPT
def test_word_pool_admin(tmp_path, monkeypatch):
app, api = _make(tmp_path, monkeypatch, admin_email="boss@x.com")
assert TestClient(app).get("/api/admin/word/pool").status_code == 401 # gated
tc = _signin(app, api, "boss@x.com")
# lookup: valid dict word vs non-word vs wrong length
assert tc.get("/api/admin/word/lookup?word=thrive").json() == {
"word": "thrive", "length": 6, "alpha": True, "variant": "6", "in_dictionary": True, "in_pool": True}
assert tc.get("/api/admin/word/lookup?word=zzzzz").json()["in_dictionary"] is False
assert tc.get("/api/admin/word/lookup?word=cat").json()["variant"] is None
# add a valid word, then it shows in the pool + lookup
res = tc.post("/api/admin/word/pool", json={"word": "PLUMB"}).json()
assert res["added"] is True and "plumb" in res["pool"]["5"]["added"]
assert tc.get("/api/admin/word/lookup?word=plumb").json()["in_pool"] is True
# rejections: non-dictionary word + wrong length + duplicate
assert tc.post("/api/admin/word/pool", json={"word": "qwxzv"}).status_code == 400
assert tc.post("/api/admin/word/pool", json={"word": "cat"}).status_code == 400
assert tc.post("/api/admin/word/pool", json={"word": "plumb"}).status_code == 400
# remove the admin-added word
tc.delete("/api/admin/word/pool/plumb")
assert "plumb" not in tc.get("/api/admin/word/pool").json()["5"]["added"]