Files
thejayman77 008364e922 Why-it-belongs: top-up requires all three fields (idempotency fix)
Per Codex: generate_summary treated why_belongs alone as a complete explanation,
but get_explanation requires all three — so a partial older row (e.g. only
why_belongs) would never top up and the page would fall back forever. Now the
fully-cached check requires summary + what_happened + why_matters + why_belongs.
Test covers the partial-row top-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 20:10:27 -04:00

87 lines
4.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from goodnews import summarize, share
from goodnews.db import connect, init_db
def test_parse_explain_labels():
txt = ("WHAT: A town planted 500 trees.\n"
"MATTERS: It cools streets and cuts flooding.\n"
"BELONGS: Neighbors did it themselves — local agency and clear benefit.")
ex = summarize._parse_explain(txt)
assert ex["what_happened"].startswith("A town planted")
assert "cools streets" in ex["why_matters"]
assert "local agency" in ex["why_belongs"]
def test_parse_explain_partial():
ex = summarize._parse_explain("WHAT: only this")
assert ex["what_happened"] == "only this" and ex["why_matters"] is None and ex["why_belongs"] is None
def _seed_article(c):
c.execute("INSERT INTO sources (id,name,feed_url) VALUES (1,'S','http://s/f')")
c.execute("INSERT INTO articles (id,source_id,canonical_url,title,url_hash) VALUES (1,1,'http://a/1','Town plants trees','h')")
c.execute("INSERT INTO article_scores (article_id,accepted) VALUES (1,1)")
c.commit()
def test_get_explanation_requires_all_three(tmp_path):
c = connect(str(tmp_path / "e.db")); init_db(c); _seed_article(c)
c.execute("INSERT INTO article_summaries (article_id,summary,what_happened) VALUES (1,'s','w')"); c.commit()
assert summarize.get_explanation(c, 1) is None # partial → fall back
c.execute("UPDATE article_summaries SET why_matters='m', why_belongs='b' WHERE article_id=1"); c.commit()
assert summarize.get_explanation(c, 1) == {"what_happened": "w", "why_matters": "m", "why_belongs": "b"}
def test_generate_summary_stores_explanation(tmp_path, monkeypatch):
c = connect(str(tmp_path / "g.db")); init_db(c); _seed_article(c)
class FakeClient:
model = "test"
def chat_text(self, messages):
if "three" in messages[0]["content"].lower():
return "WHAT: trees planted.\nMATTERS: cooler streets.\nBELONGS: local agency, clear benefit."
return "A town planted trees, cooling streets."
monkeypatch.setattr(summarize, "_fetch_text", lambda url: "body text")
s = summarize.generate_summary(c, 1, client=FakeClient())
assert s and "planted trees" in s
ex = summarize.get_explanation(c, 1)
assert "agency" in ex["why_belongs"] and "cooler streets" in ex["why_matters"]
def test_share_renders_structured_or_fallback():
art = {"id": 1, "title": "T", "source_name": "Src", "source_id": 1,
"canonical_url": "http://x", "reason_text": "calm reason", "tags": ""}
full = share.render_share_page(art, "http://b", summary="sum",
explanation={"what_happened": "WH", "why_matters": "WM", "why_belongs": "WB"})
assert "What happened" in full and "Why it matters" in full and "Why it belongs here" in full
assert "WH" in full and "WB" in full
assert "fetch('/api/summary/'" not in full # both cached → no poll needed
# no explanation → the single "Why its here" reason line is the calm fallback
fb = share.render_share_page(art, "http://b", summary="sum", explanation=None)
assert "Why its here" in fb and "calm reason" in fb
def test_generate_tops_up_partial_explanation(tmp_path, monkeypatch):
c = connect(str(tmp_path / "p.db")); init_db(c); _seed_article(c)
# an older row: summary + why_belongs only (what_happened/why_matters missing)
c.execute("INSERT INTO article_summaries (article_id,summary,why_belongs) VALUES (1,'old summary','b')")
c.commit()
assert summarize.get_explanation(c, 1) is None # incomplete → falls back
calls = {"explain": 0}
def fake_explain(client, t, s, b):
calls["explain"] += 1
return {"what_happened": "w", "why_matters": "m", "why_belongs": "b2"}
monkeypatch.setattr(summarize, "explain_article", fake_explain)
monkeypatch.setattr(summarize, "_fetch_text", lambda url: "body")
class FakeClient:
model = "test"
def chat_text(self, messages): return "should not be used for summary (kept)"
out = summarize.generate_summary(c, 1, client=FakeClient())
assert out == "old summary" # existing summary reused, not regenerated
assert calls["explain"] == 1 # explanation WAS topped up
assert summarize.get_explanation(c, 1) == {"what_happened": "w", "why_matters": "m", "why_belongs": "b2"}