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 it’s here" reason line is the calm fallback fb = share.render_share_page(art, "http://b", summary="sum", explanation=None) assert "Why it’s 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"}