84b1fb514f
- Admin joy item route moved to /api/admin/joys/{kind}/items/{item_id} so the
/add and /repick verbs resolve to their own routes instead of 422-ing as a
non-int item id (the launch blocker). Frontend mutate URL updated to match.
- Re-pick now excludes the currently-shown item: the endpoint reads today's
daily pool_id and passes it as `avoid`, so "Re-pick today" yields a different
item. Added `avoid` to pick_daily/_candidates across wotd/quote/onthisday.
- WOTD sense selection: the LLM now proposes word + intended part of speech, and
_lookup prefers that sense (fixes "serene" returning the archaic noun).
- On This Day tone prompt tightened to favor genuinely uplifting events and
exclude merely procedural/political-administrative ones.
- Caddy @hidden now also noindexes /word /quote /onthisday /admin (+ .html).
- Regression tests: add/repick resolve (401 not 422), add/feature/block/delete,
re-pick excludes current; WOTD pos-preference + proposal parsing units.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
88 lines
3.9 KiB
Python
88 lines
3.9 KiB
Python
"""Word of the Day: LLM-proposed words validated against the dictionary (mocked),
|
||
harvest dedupes + drops unknowns, audio cached when present, deterministic pick."""
|
||
import json
|
||
|
||
import pytest
|
||
|
||
from goodnews import wotd
|
||
from goodnews.db import connect, init_db
|
||
|
||
FAKE_DICT = {
|
||
"serene": {"word": "serene", "part_of_speech": "adjective", "phonetic": "/səˈriːn/",
|
||
"audio_url": "https://a/serene.mp3", "definition": "calm, peaceful, and untroubled",
|
||
"examples": ["a serene mountain lake"]},
|
||
"dawn": {"word": "dawn", "part_of_speech": "noun", "phonetic": "/dɔːn/",
|
||
"audio_url": None, "definition": "the first appearance of light in the sky", "examples": []},
|
||
}
|
||
|
||
|
||
class FakeClient:
|
||
def chat_text(self, messages):
|
||
return '{"words": ["serene", "dawn", "xyzzyq"]}' # xyzzyq isn't a real word
|
||
|
||
|
||
@pytest.fixture
|
||
def conn(tmp_path, monkeypatch):
|
||
monkeypatch.setenv("GOODNEWS_WOTD_AUDIO", str(tmp_path / "audio"))
|
||
monkeypatch.setattr(wotd, "_lookup", lambda w, prefer_pos=None: FAKE_DICT.get(w)) # no dictionary network
|
||
monkeypatch.setattr(wotd, "_cache_audio", lambda url, word: f"{word}.mp3" if url else None)
|
||
c = connect(":memory:"); init_db(c)
|
||
yield c
|
||
c.close()
|
||
|
||
|
||
def test_harvest_validates_dedupes_and_caches_audio(conn):
|
||
r = wotd.harvest(conn, FakeClient())
|
||
assert r["added"] == 2 # serene + dawn; the nonsense word dropped
|
||
assert wotd.harvest(conn, FakeClient())["added"] == 0 # idempotent (word is UNIQUE)
|
||
assert conn.execute("SELECT audio_file FROM wotd_pool WHERE word='serene'").fetchone()[0] == "serene.mp3"
|
||
assert conn.execute("SELECT audio_file FROM wotd_pool WHERE word='dawn'").fetchone()[0] is None
|
||
|
||
|
||
def test_pick_caches_marks_shown_idempotent(conn):
|
||
wotd.harvest(conn, FakeClient())
|
||
a = wotd.pick_daily(conn, feature_date="2026-06-22")
|
||
assert a and a["word"] in ("serene", "dawn") and a["definition"]
|
||
assert json.loads(a["examples"]) == FAKE_DICT[a["word"]]["examples"]
|
||
shown = conn.execute("SELECT shown_at FROM wotd_pool WHERE id=?", (a["pool_id"],)).fetchone()[0]
|
||
assert shown == "2026-06-22"
|
||
assert wotd.pick_daily(conn, feature_date="2026-06-22")["pool_id"] == a["pool_id"]
|
||
|
||
|
||
def test_featured_pinned(conn):
|
||
wotd.harvest(conn, FakeClient())
|
||
conn.execute("UPDATE wotd_pool SET featured=1 WHERE word='dawn'"); conn.commit()
|
||
assert wotd.pick_daily(conn, feature_date="2026-06-22", force=True)["word"] == "dawn"
|
||
|
||
|
||
def test_get_today_never_empty(conn):
|
||
wotd.harvest(conn, FakeClient())
|
||
a = wotd.pick_daily(conn, feature_date="2026-06-22")
|
||
assert wotd.get_today(conn, "2099-01-01")["pool_id"] == a["pool_id"]
|
||
|
||
|
||
def test_run_daily_bootstraps(conn):
|
||
r = wotd.run_daily(conn, client=FakeClient())
|
||
assert r["pool"] == 2 and r["picked"] in ("serene", "dawn")
|
||
|
||
|
||
def test_lookup_prefers_intended_pos(monkeypatch):
|
||
"""When the LLM says 'serene' as an adjective, _lookup must pick the adjective sense,
|
||
not an earlier archaic noun sense the dictionary lists first."""
|
||
entry = {"word": "serene", "phonetics": [], "meanings": [
|
||
{"partOfSpeech": "noun", "definitions": [{"definition": "Serenity; clearness; calmness."}]},
|
||
{"partOfSpeech": "adjective", "definitions": [{"definition": "Calm, peaceful, untroubled."}]},
|
||
]}
|
||
monkeypatch.setattr(wotd.daily, "http_json", lambda url, timeout=20: [entry])
|
||
assert wotd._lookup("serene", "adjective")["part_of_speech"] == "adjective"
|
||
assert wotd._lookup("serene", "adjective")["definition"] == "Calm, peaceful, untroubled."
|
||
assert wotd._lookup("serene")["part_of_speech"] == "noun" # no preference → first usable sense
|
||
|
||
|
||
def test_propose_words_accepts_dicts_and_strings():
|
||
class C:
|
||
def chat_text(self, m):
|
||
return '{"words": [{"word": "Serene", "pos": "Adjective"}, "dawn", {"word": ""}]}'
|
||
out = wotd._propose_words(C(), 3)
|
||
assert out == [{"word": "serene", "pos": "adjective"}, {"word": "dawn", "pos": None}]
|