Small joys: Quote of the Day + Word of the Day engines

- quote.py: curated public-domain quote pool (16 seeded, admin-grows), deterministic daily
  pick, lazy AI "what it means" explainer of the real quote (cached). No LLM-invented quotes.
- wotd.py: LLM proposes positive words → validated/enriched against dictionaryapi.dev (real
  definition, IPA, examples, audio) → audio clip cached to our origin (TTS fallback) →
  deterministic daily pick. Tops the pool up toward 30/day.
- db.py: quote_pool/daily_quote + wotd_pool/daily_wotd tables.
- api.py: /api/quote/today, /api/word/today, /api/word/audio/{word} (GET+HEAD).
- cli.py: cycle steps for both (under --no-joys), shared LLM client.
- tests: test_quote.py (6) + test_wotd.py (5). 393 backend tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-22 17:28:55 -04:00
parent a7da8362ab
commit 67d4bc32cb
7 changed files with 550 additions and 4 deletions
+36 -1
View File
@@ -36,7 +36,7 @@ from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from . import art, auth, bloom, email_send, feeds, games, oauth_google, onthisday, publishing, queries, share, sources, summarize
from . import art, auth, bloom, email_send, feeds, games, oauth_google, onthisday, publishing, queries, quote, share, sources, summarize, wotd
from .localtime import local_today
from .markup import reply_html_to_text, sanitize_reply_html
from .db import connect
@@ -2304,6 +2304,41 @@ def create_app() -> FastAPI:
"source": a["source"],
}
@app.get("/api/quote/today")
def quote_today(response: Response) -> dict:
with get_conn() as conn:
q = quote.get_today(conn)
if not q:
response.headers["Cache-Control"] = _PRIVATE
raise HTTPException(status_code=404, detail="No quote yet.")
response.headers["Cache-Control"] = _EDGE_FEED
return {"date": q["feature_date"], "text": q["text"], "author": q["author"],
"work": q["work"], "year": q["year"], "meaning": q["meaning"], "source": q["source"]}
@app.get("/api/word/today")
def word_today(response: Response) -> dict:
with get_conn() as conn:
w = wotd.get_today(conn)
if not w:
response.headers["Cache-Control"] = _PRIVATE
raise HTTPException(status_code=404, detail="No word yet.")
response.headers["Cache-Control"] = _EDGE_FEED
try:
examples = json.loads(w["examples"]) if w["examples"] else []
except (ValueError, TypeError):
examples = []
return {"date": w["feature_date"], "word": w["word"], "part_of_speech": w["part_of_speech"],
"phonetic": w["phonetic"], "definition": w["definition"], "examples": examples,
"audio_url": f"/api/word/audio/{w['word']}" if w["audio_file"] else None}
@app.api_route("/api/word/audio/{word}", methods=["GET", "HEAD"])
def word_audio(word: str) -> FileResponse:
matches = sorted(wotd.cache_dir().glob(f"{word.lower()}.*"))
matches = [m for m in matches if not m.name.startswith(".")]
if not matches:
raise HTTPException(status_code=404, detail="No audio.")
return FileResponse(str(matches[0]), headers={"Cache-Control": "public, max-age=31536000, immutable"})
@app.get("/api/replacement", response_model=Article | None)
def replacement(
exclude: str = Query("", description="comma-separated article ids already shown"),
+15 -3
View File
@@ -13,7 +13,7 @@ from .games import generate_daily_puzzles
from .localtime import local_today
from .dedup import DEFAULT_THRESHOLD, DEFAULT_WINDOW_DAYS, cluster_duplicates, dedup as run_dedup
from .geo import tag_articles as tag_geo
from . import art, onthisday
from . import art, onthisday, quote, wotd
from .enrich import enrich_brief_images, enrich_recent_images, enrich_summarized_images
from .summarize import generate_summary, get_summary
from .feeds import (
@@ -561,13 +561,25 @@ def _run_cycle_locked(conn: sqlite3.Connection, args: argparse.Namespace) -> Non
except Exception as exc:
print(f"art: skipped ({exc})")
# On This Day: harvest + tone-filter today's date in history, then pick one good fact.
# Small joys: On This Day (history), Quote of the Day, Word of the Day. Each is
# bounded + non-fatal; one shared LLM client for tone/explainer/word proposals.
if not args.no_joys:
joy_client = LocalModelClient.from_env()
try:
o = onthisday.run_daily(conn, client=LocalModelClient.from_env())
o = onthisday.run_daily(conn, client=joy_client)
print(f"onthisday: md={o['md']} picked={'yes' if o['picked'] else 'no'}")
except Exception as exc:
print(f"onthisday: skipped ({exc})")
try:
q = quote.run_daily(conn, client=joy_client)
print(f"quote: pool={q['pool']} picked={q['picked']}")
except Exception as exc:
print(f"quote: skipped ({exc})")
try:
w = wotd.run_daily(conn, client=joy_client)
print(f"word: pool={w['pool']} picked={w['picked']}")
except Exception as exc:
print(f"word: skipped ({exc})")
if not args.no_brief:
today = local_today()
+49
View File
@@ -304,6 +304,55 @@ CREATE TABLE IF NOT EXISTS daily_onthisday (
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Quote of the Day: curated public-domain quotes; one picked per day. `meaning` is an
-- AI explainer of the (real) quote, filled lazily the first time it's shown.
CREATE TABLE IF NOT EXISTS quote_pool (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source TEXT NOT NULL DEFAULT 'curated',
ckey TEXT NOT NULL UNIQUE,
text TEXT NOT NULL,
author TEXT,
work TEXT,
year TEXT,
meaning TEXT,
shown_at TEXT,
blocked INTEGER NOT NULL DEFAULT 0,
featured INTEGER NOT NULL DEFAULT 0,
added_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS daily_quote (
feature_date TEXT PRIMARY KEY,
pool_id INTEGER NOT NULL,
source TEXT NOT NULL DEFAULT 'curated',
text TEXT, author TEXT, work TEXT, year TEXT, meaning TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Word of the Day: LLM-proposed positive words, validated/enriched against a real
-- dictionary (definition, IPA, examples, cached audio clip); one picked per day.
CREATE TABLE IF NOT EXISTS wotd_pool (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source TEXT NOT NULL DEFAULT 'llm',
word TEXT NOT NULL UNIQUE,
part_of_speech TEXT,
phonetic TEXT, -- IPA
audio_file TEXT, -- our cached pronunciation clip (or null → browser TTS)
audio_url TEXT, -- source clip URL
definition TEXT NOT NULL,
examples TEXT, -- JSON array of example sentences
shown_at TEXT,
blocked INTEGER NOT NULL DEFAULT 0,
featured INTEGER NOT NULL DEFAULT 0,
added_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS daily_wotd (
feature_date TEXT PRIMARY KEY,
pool_id INTEGER NOT NULL,
word TEXT, part_of_speech TEXT, phonetic TEXT, audio_file TEXT,
definition TEXT, examples TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Privacy-respecting, first-party analytics. NO IP / user-agent / referrer / raw
-- URL. visitor_hash is a hash of a random localStorage token (never email/IP).
-- The UNIQUE key dedups to one row per (kind, article, visitor, day) — that both
+118
View File
@@ -0,0 +1,118 @@
"""Quote of the Day — a hopeful, public-domain quote a day.
Curated, never LLM-invented (misattribution is the one thing that would burn trust). A
vetted starter set seeds the pool; admin grows it. The "what it means" explainer IS
LLM-generated, but it only *interprets a real, known quote* — low risk — and is filled
lazily the first time a quote is shown, then cached.
Same lifecycle as the other small joys: pool → deterministic daily pick → cached row.
"""
from __future__ import annotations
import sqlite3
from . import daily
from .localtime import local_today
_NO_REPEAT_POOL = 60
# Public-domain (ancient / author died well over a century ago), uplifting. Admin curates.
SEED = [
("Very little is needed to make a happy life; it is all within yourself, in your way of thinking.", "Marcus Aurelius", "Meditations"),
("The happiness of your life depends upon the quality of your thoughts.", "Marcus Aurelius", "Meditations"),
("The journey of a thousand miles begins with a single step.", "Lao Tzu", "Tao Te Ching"),
("Nature does not hurry, yet everything is accomplished.", "Lao Tzu", "Tao Te Ching"),
("It does not matter how slowly you go as long as you do not stop.", "Confucius", None),
("Knowing yourself is the beginning of all wisdom.", "Aristotle", None),
("We suffer more often in imagination than in reality.", "Seneca", None),
("It's not what happens to you, but how you react to it that matters.", "Epictetus", None),
("The wound is the place where the Light enters you.", "Rumi", None),
("Write it on your heart that every day is the best day in the year.", "Ralph Waldo Emerson", None),
("Go confidently in the direction of your dreams. Live the life you have imagined.", "Henry David Thoreau", None),
("Hope is the thing with feathers that perches in the soul.", "Emily Dickinson", None),
("The best portion of a good man's life: his little, nameless, unremembered acts of kindness and of love.", "William Wordsworth", None),
("Great things are done by a series of small things brought together.", "Vincent van Gogh", None),
("I am not afraid of storms, for I am learning how to sail my ship.", "Louisa May Alcott", "Little Women"),
("I exist as I am, that is enough.", "Walt Whitman", "Leaves of Grass"),
]
def seed(conn: sqlite3.Connection) -> int:
"""Insert the curated starter quotes (idempotent via content key). Returns # added."""
before = conn.execute("SELECT COUNT(*) FROM quote_pool").fetchone()[0]
conn.executemany(
"INSERT OR IGNORE INTO quote_pool (source, ckey, text, author, work) VALUES ('curated', ?, ?, ?, ?)",
[(daily.content_key(text, author), text, author, work) for text, author, work in SEED],
)
conn.commit()
return conn.execute("SELECT COUNT(*) FROM quote_pool").fetchone()[0] - before
def _explain(client, text: str, author: str | None) -> str | None:
user = (
"In one or two plain, warm sentences, explain what this quote means and why it's worth "
"remembering, for a general audience who may not be familiar with it. No preamble, no "
f'quoting it back.\n\nQuote: "{text}"' + (f"\n{author}" if author else "")
)
out = " ".join(client.chat_text([{"role": "user", "content": user}]).split()).strip()
return out or None
def _candidates(conn: sqlite3.Connection) -> list[int]:
featured = conn.execute(
"SELECT id FROM quote_pool WHERE blocked=0 AND featured=1 ORDER BY id"
).fetchall()
if featured:
return [r[0] for r in featured]
rows = conn.execute(
"SELECT id FROM quote_pool WHERE blocked=0 ORDER BY shown_at IS NOT NULL, shown_at, id LIMIT ?",
(_NO_REPEAT_POOL,),
).fetchall()
return [r[0] for r in rows]
def pick_daily(conn: sqlite3.Connection, feature_date: str | None = None, client=None, force: bool = False) -> dict | None:
feature_date = feature_date or local_today()
existing = conn.execute("SELECT * FROM daily_quote WHERE feature_date=?", (feature_date,)).fetchone()
if existing and not force:
return dict(existing)
ids = _candidates(conn)
if not ids:
return None
pick_id = daily.seeded_order(ids, feature_date)[0]
row = conn.execute("SELECT * FROM quote_pool WHERE id=?", (pick_id,)).fetchone()
meaning = row["meaning"]
if not meaning and client: # lazy, cached; LLM done before the write
try:
meaning = _explain(client, row["text"], row["author"])
if meaning:
conn.execute("UPDATE quote_pool SET meaning=? WHERE id=?", (meaning, pick_id))
except Exception: # noqa: BLE001 — explainer is optional
meaning = None
conn.execute(
"INSERT INTO daily_quote (feature_date, pool_id, source, text, author, work, year, meaning) "
"VALUES (?,?,?,?,?,?,?,?) "
"ON CONFLICT(feature_date) DO UPDATE SET pool_id=excluded.pool_id, text=excluded.text, "
"author=excluded.author, work=excluded.work, year=excluded.year, meaning=excluded.meaning",
(feature_date, row["id"], row["source"], row["text"], row["author"], row["work"], row["year"], meaning),
)
conn.execute("UPDATE quote_pool SET shown_at=? WHERE id=?", (feature_date, pick_id))
conn.commit()
return dict(conn.execute("SELECT * FROM daily_quote WHERE feature_date=?", (feature_date,)).fetchone())
def get_today(conn: sqlite3.Connection, feature_date: str | None = None) -> dict | None:
if feature_date:
row = conn.execute("SELECT * FROM daily_quote WHERE feature_date=?", (feature_date,)).fetchone()
if row:
return dict(row)
row = conn.execute("SELECT * FROM daily_quote ORDER BY feature_date DESC LIMIT 1").fetchone()
return dict(row) if row else None
def run_daily(conn: sqlite3.Connection, client=None) -> dict:
if conn.execute("SELECT COUNT(*) FROM quote_pool").fetchone()[0] == 0:
seed(conn)
picked = pick_daily(conn, client=client)
return {"pool": conn.execute("SELECT COUNT(*) FROM quote_pool").fetchone()[0],
"picked": (picked or {}).get("author")}
+208
View File
@@ -0,0 +1,208 @@
"""Word of the Day — an uplifting word a day, grounded in a real dictionary.
"LLM proposes, dictionary disposes": the LLM suggests positive/calming words; each is
validated + enriched against the free Dictionary API (dictionaryapi.dev) for the REAL
definition, IPA pronunciation, example sentences, and a human pronunciation clip. That
rules out hallucinated definitions — the authoritative data is the dictionary's. The
audio clip (public-domain, usually Wiktionary) is cached to our origin; the page falls
back to the browser's speech synthesis when a word has no clip.
All network/LLM work happens before the brief DB write. Same pick lifecycle as the
other small joys.
"""
from __future__ import annotations
import json
import os
import re
import sqlite3
import urllib.parse
import urllib.request
from pathlib import Path
from . import daily
from .localtime import local_today
DICT_BASE = "https://api.dictionaryapi.dev/api/v2/entries/en"
_UA = {"User-Agent": "upbeatBytes/1.0 (+https://upbeatbytes.com)"}
_NO_REPEAT_POOL = 60
_TARGET_POOL = 30 # keep harvesting (a batch/day) until the pool reaches this
_HARVEST_BATCH = 12
_MIN_AUDIO_BYTES = 500
def cache_dir() -> Path:
override = os.environ.get("GOODNEWS_WOTD_AUDIO")
d = Path(override) if override else Path(os.environ.get("GOODNEWS_DB", "data/goodnews.sqlite3")).parent / "wotd_audio"
d.mkdir(parents=True, exist_ok=True)
return d
def _http_bytes(url: str, timeout: int = 30) -> tuple[bytes, str]:
req = urllib.request.Request(url, headers=_UA)
with urllib.request.urlopen(req, timeout=timeout) as r:
return r.read(), (r.headers.get("Content-Type") or "")
def _propose_words(client, n: int) -> list[str]:
user = (
f"Suggest {n} English vocabulary words for an uplifting 'word of the day' — positive, "
"calming, hopeful, or quietly beautiful in meaning (e.g. serene, kindness, dawn, "
"resilience, wonder). Real, usable words; vary common and slightly elevated. "
'Reply with JSON only: {"words": ["...", "..."]}'
)
txt = client.chat_text([{"role": "user", "content": user}])
m = re.search(r"\{.*\}", txt, re.S)
if not m:
return []
words = json.loads(m.group(0)).get("words", [])
return [str(w).strip().lower() for w in words if isinstance(w, str) and w.strip()]
def _lookup(word: str) -> dict | None:
"""Validate + enrich a word via the dictionary. Returns None if it's not a real word."""
try:
data = daily.http_json(f"{DICT_BASE}/{urllib.parse.quote(word)}")
except Exception: # noqa: BLE001 — unknown word / network → just skip it
return None
if not isinstance(data, list) or not data:
return None
entry = data[0]
meanings = entry.get("meanings") or []
if not meanings or not (meanings[0].get("definitions") or []):
return None
definition = (meanings[0]["definitions"][0].get("definition") or "").strip()
if not definition:
return None
phonetic = entry.get("phonetic")
audio_url = None
for p in (entry.get("phonetics") or []):
if not phonetic and p.get("text"):
phonetic = p["text"]
if not audio_url and p.get("audio"):
audio_url = p["audio"]
examples = []
for m in meanings:
for d in (m.get("definitions") or []):
if d.get("example"):
examples.append(d["example"].strip())
return {
"word": (entry.get("word") or word).strip().lower(),
"part_of_speech": meanings[0].get("partOfSpeech"),
"phonetic": phonetic,
"audio_url": audio_url,
"definition": definition,
"examples": examples[:3],
}
def _cache_audio(audio_url: str | None, word: str) -> str | None:
"""Download the pronunciation clip to our origin (atomic). Returns filename or None."""
if not audio_url:
return None
if audio_url.startswith("//"):
audio_url = "https:" + audio_url
try:
data, ctype = _http_bytes(audio_url)
except Exception: # noqa: BLE001
return None
if len(data) < _MIN_AUDIO_BYTES:
return None
ext = ".ogg" if ("ogg" in ctype or audio_url.endswith(".ogg")) else ".mp3"
fname = f"{word}{ext}"
cdir = cache_dir()
tmp = cdir / f".{word}.tmp"
try:
tmp.write_bytes(data)
os.replace(tmp, cdir / fname)
except OSError:
try:
tmp.unlink()
except OSError:
pass
return None
return fname
def _pool_count(conn: sqlite3.Connection) -> int:
return conn.execute("SELECT COUNT(*) FROM wotd_pool").fetchone()[0]
def harvest(conn: sqlite3.Connection, client, count: int = _HARVEST_BATCH) -> dict:
"""Propose words → validate/enrich via dictionary → cache audio → add new ones.
All network up front; one brief write at the end."""
try:
words = _propose_words(client, count)
except Exception: # noqa: BLE001
return {"proposed": 0, "added": 0, "pool": _pool_count(conn)}
rows = []
for w in words:
if not w.isalpha() or conn.execute("SELECT 1 FROM wotd_pool WHERE word=?", (w,)).fetchone():
continue
info = _lookup(w)
if not info:
continue
audio_file = _cache_audio(info["audio_url"], info["word"])
rows.append((info["word"], info["part_of_speech"], info["phonetic"], audio_file,
info["audio_url"], info["definition"], json.dumps(info["examples"])))
before = _pool_count(conn)
conn.executemany(
"INSERT OR IGNORE INTO wotd_pool (source, word, part_of_speech, phonetic, audio_file, audio_url, definition, examples) "
"VALUES ('llm', ?, ?, ?, ?, ?, ?, ?)", rows,
)
conn.commit()
after = _pool_count(conn)
return {"proposed": len(words), "added": after - before, "pool": after}
def _candidates(conn: sqlite3.Connection) -> list[int]:
featured = conn.execute("SELECT id FROM wotd_pool WHERE blocked=0 AND featured=1 ORDER BY id").fetchall()
if featured:
return [r[0] for r in featured]
rows = conn.execute(
"SELECT id FROM wotd_pool WHERE blocked=0 ORDER BY shown_at IS NOT NULL, shown_at, id LIMIT ?",
(_NO_REPEAT_POOL,),
).fetchall()
return [r[0] for r in rows]
def pick_daily(conn: sqlite3.Connection, feature_date: str | None = None, force: bool = False) -> dict | None:
feature_date = feature_date or local_today()
existing = conn.execute("SELECT * FROM daily_wotd WHERE feature_date=?", (feature_date,)).fetchone()
if existing and not force:
return dict(existing)
ids = _candidates(conn)
if not ids:
return None
pick_id = daily.seeded_order(ids, feature_date)[0]
row = conn.execute("SELECT * FROM wotd_pool WHERE id=?", (pick_id,)).fetchone()
conn.execute(
"INSERT INTO daily_wotd (feature_date, pool_id, word, part_of_speech, phonetic, audio_file, definition, examples) "
"VALUES (?,?,?,?,?,?,?,?) "
"ON CONFLICT(feature_date) DO UPDATE SET pool_id=excluded.pool_id, word=excluded.word, "
"part_of_speech=excluded.part_of_speech, phonetic=excluded.phonetic, audio_file=excluded.audio_file, "
"definition=excluded.definition, examples=excluded.examples",
(feature_date, row["id"], row["word"], row["part_of_speech"], row["phonetic"],
row["audio_file"], row["definition"], row["examples"]),
)
conn.execute("UPDATE wotd_pool SET shown_at=? WHERE id=?", (feature_date, pick_id))
conn.commit()
return dict(conn.execute("SELECT * FROM daily_wotd WHERE feature_date=?", (feature_date,)).fetchone())
def get_today(conn: sqlite3.Connection, feature_date: str | None = None) -> dict | None:
if feature_date:
row = conn.execute("SELECT * FROM daily_wotd WHERE feature_date=?", (feature_date,)).fetchone()
if row:
return dict(row)
row = conn.execute("SELECT * FROM daily_wotd ORDER BY feature_date DESC LIMIT 1").fetchone()
return dict(row) if row else None
def run_daily(conn: sqlite3.Connection, client=None) -> dict:
"""Top the pool up toward _TARGET_POOL (a batch a day), then pick today's word."""
harvested = None
if client and _pool_count(conn) < _TARGET_POOL:
harvested = harvest(conn, client)
picked = pick_daily(conn)
return {"pool": _pool_count(conn), "harvested": harvested, "picked": (picked or {}).get("word")}
+58
View File
@@ -0,0 +1,58 @@
"""Quote of the Day: curated seed (idempotent), deterministic pick (idempotent,
blocked/featured), lazy AI meaning, never-empty get_today."""
import pytest
from goodnews import quote
from goodnews.db import connect, init_db
class FakeClient:
def chat_text(self, messages):
return "It means contentment is built from within, not from circumstance."
@pytest.fixture
def conn():
c = connect(":memory:"); init_db(c)
yield c
c.close()
def test_seed_idempotent(conn):
assert quote.seed(conn) == len(quote.SEED)
assert quote.seed(conn) == 0
assert conn.execute("SELECT COUNT(*) FROM quote_pool").fetchone()[0] == len(quote.SEED)
def test_pick_caches_marks_shown_idempotent(conn):
quote.seed(conn)
a = quote.pick_daily(conn, feature_date="2026-06-22")
assert a and a["text"] and a["author"]
shown = conn.execute("SELECT shown_at FROM quote_pool WHERE id=?", (a["pool_id"],)).fetchone()[0]
assert shown == "2026-06-22"
assert quote.pick_daily(conn, feature_date="2026-06-22")["pool_id"] == a["pool_id"]
def test_meaning_generated_lazily_and_cached(conn):
quote.seed(conn)
a = quote.pick_daily(conn, feature_date="2026-06-22", client=FakeClient())
assert a["meaning"].startswith("It means")
pool_meaning = conn.execute("SELECT meaning FROM quote_pool WHERE id=?", (a["pool_id"],)).fetchone()[0]
assert pool_meaning == a["meaning"] # cached on the pool row, not regenerated
def test_featured_pinned(conn):
quote.seed(conn)
conn.execute("UPDATE quote_pool SET featured=1 WHERE author='Rumi'"); conn.commit()
assert quote.pick_daily(conn, feature_date="2026-06-22", force=True)["author"] == "Rumi"
def test_get_today_never_empty(conn):
quote.seed(conn)
a = quote.pick_daily(conn, feature_date="2026-06-22")
assert quote.get_today(conn, "2099-01-01")["pool_id"] == a["pool_id"]
def test_run_daily_seeds_then_picks(conn):
r = quote.run_daily(conn)
assert r["pool"] == len(quote.SEED) and r["picked"]
+66
View File
@@ -0,0 +1,66 @@
"""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: 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")