Pool admin: delete any word (tombstones + restore) + bulk import
Daily Word pool curation, full add/delete/import — no redeploys to fix tone: - Remove ANY pool word, curated or admin-added, via a word_pool_removed tombstone table. Runtime pool = (static ∪ added) − removed, so even a baked-in word can be pulled on negative feedback. Reversible: a "Removed" list with one-tap Restore lifts the tombstone. Lookup now surfaces a Remove button when in-pool, Restore when removed. - Import a vetted list (paste or .txt/.csv upload, read client-side): validates each word (alpha · 5–6 · in guess dictionary), ignores duplicates, and reports rejects with reasons. Re-adding/importing a removed word lifts its tombstone. - Word Search theme delete already existed (Edit/Remove per theme) — verified. Pool stays the clean 251/224; today's noisy LLM enrichment is discarded. Tests: +tests/test_pool_admin.py, extended test_word_pool_admin. 222 pytest + 11 vitest green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+18
-1
@@ -371,12 +371,14 @@ def test_wordsearch_thin_llm_falls_back(tmp_path, monkeypatch):
|
||||
|
||||
|
||||
def test_word_pool_admin(tmp_path, monkeypatch):
|
||||
from goodnews import games
|
||||
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}
|
||||
"word": "thrive", "length": 6, "alpha": True, "variant": "6",
|
||||
"in_dictionary": True, "in_pool": True, "removed": False}
|
||||
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
|
||||
@@ -390,6 +392,21 @@ def test_word_pool_admin(tmp_path, monkeypatch):
|
||||
# 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"]
|
||||
# remove a CURATED word (only a tombstone can pull it) → restore brings it back
|
||||
tc.delete("/api/admin/word/pool/thrive")
|
||||
look = tc.get("/api/admin/word/lookup?word=thrive").json()
|
||||
assert look["in_pool"] is False and look["removed"] is True
|
||||
assert "thrive" in tc.get("/api/admin/word/pool").json()["6"]["removed"]
|
||||
tc.post("/api/admin/word/pool/restore", json={"word": "thrive"})
|
||||
assert tc.get("/api/admin/word/lookup?word=thrive").json()["in_pool"] is True
|
||||
# bulk import: validates, dedupes, reports rejects
|
||||
fresh5 = next(w for w in sorted(games._DICT["5"]) if w not in set(games._POOL["5"]))
|
||||
imp = tc.post("/api/admin/word/pool/import",
|
||||
json={"text": f"{fresh5.upper()}, {fresh5}, thrive, qwxzv, hi"}).json()
|
||||
assert fresh5 in imp["added"] and imp["counts"]["added"] == 1
|
||||
assert imp["counts"]["duplicates"] >= 1 # thrive already present
|
||||
assert {r["word"] for r in imp["rejected"]} == {"qwxzv", "hi"}
|
||||
assert fresh5 in tc.get("/api/admin/word/pool").json()["5"]["added"]
|
||||
|
||||
|
||||
def test_client_error_telemetry(tmp_path, monkeypatch):
|
||||
|
||||
Reference in New Issue
Block a user