Files
upbeatBytes/tests/test_explain.py
T
thejayman77 337dc3f901 Article pages: structured "Why it belongs" editorial read
Per Codex — make /a/<id> feel like Upbeat Bytes has editorial judgment, not just
a summary wrapper. Trust-building, short, not an essay.

* article_summaries gains what_happened / why_matters / why_belongs (+ migration).
* summarize.explain_article: a separate, fallback-able LLM pass producing three
  short notes (parsed from a labelled WHAT/MATTERS/BELONGS format). generate_summary
  now stores them alongside the summary, and tops up older summaries on next view.
  get_explanation returns them only when all three are present.
* API: share_page + /api/summary expose the explanation.
* share.py: renders the three-part section (accent rule) when complete; otherwise
  the single "Why it's here" reason line is the calm fallback. The page polls and
  swaps in both the summary and the section as they cache.

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

63 lines
3.1 KiB
Python
Raw 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