Files
upbeatBytes/tests/test_digest.py
T
thejayman77 cf5cbb33c0 Daily digest (opt-in) + finite "you're caught up" ending
Reader-retention as ritual, not capture (Codex's framing). Opt-in calm morning
email of today's brief; the on-site twin is the finite end-of-feed nudge.

* Schema: users.digest_enabled + digest_unsub_token; digest_sends (dedupe +
  visibility). auth.get_user now returns the digest fields.
* goodnews/digest.py: build (dated calm subject, items w/ summary + "why it's
  here" + UB/source links + one-click unsubscribe, "you're caught up" sign-off)
  and send_due_digests (morning-window gated, >=4-item floor or skip quietly,
  deduped, reuses SMTP). No streaks/urgency/"you missed".
* API: /auth/me exposes digest_enabled; POST /api/account/digest toggle;
  GET /api/digest/unsubscribe (token, no login, calm confirmation page).
* CLI: cycle gains a morning-gated digest step (--no-digest) + a send-digests
  command (--force).
* Frontend: digest toggle on the Account profile; the Highlights end-cap now
  says "you're caught up — see you tomorrow" with a one-tap "Get tomorrow's
  brief by email" (signed-in → enable; anon → sign in).

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

61 lines
3.2 KiB
Python

from goodnews.db import connect, init_db
from goodnews import digest, email_send
def _seed(c, n=5, date="2026-06-09"):
bid = c.execute("INSERT INTO daily_briefs (brief_date, title) VALUES (?, 't')", (date,)).lastrowid
for i in range(1, n + 1):
c.execute("INSERT INTO sources (id,name,feed_url,active,content_visible) VALUES (?,?,?,1,1)",
(i, f"Src{i}", f"http://s{i}/f"))
c.execute("INSERT INTO articles (id,source_id,canonical_url,title,url_hash) VALUES (?,?,?,?,?)",
(i, i, f"http://a/{i}", f"Title {i}", f"h{i}"))
c.execute("INSERT INTO article_scores (article_id,accepted,reason_text) VALUES (?,1,?)", (i, f"reason {i}"))
c.execute("INSERT INTO article_summaries (article_id,summary) VALUES (?,?)", (i, f"summary {i}"))
c.execute("INSERT INTO daily_brief_items (brief_id,article_id,rank) VALUES (?,?,?)", (bid, i, i))
c.execute("INSERT INTO users (id,email,digest_enabled) VALUES (1,'reader@x.com',1)")
c.commit()
return date
def test_build_digest_is_calm_and_dated():
items = [{"id": 1, "title": "Good thing", "canonical_url": "http://a/1", "source": "Src", "summary": "nice", "reason_text": "wonder", "paywalled": False}]
subject, text, html = digest.build_digest(items, "2026-06-09", "http://ub/unsub")
assert "Tuesday's Upbeat Bytes" in subject and "1 calm read" in subject
assert "You're caught up" in text and "http://ub/unsub" in text
assert "Good thing" in html and "Read on Upbeat Bytes" in html and "Unsubscribe" in html
assert "you missed" not in (text + html).lower() # no guilt language
def test_send_due_digests_sends_dedupes_and_skips(monkeypatch, tmp_path):
sent = []
monkeypatch.setattr(email_send, "send_email", lambda to, subj, text, html=None: sent.append(to))
c = connect(str(tmp_path / "d.db")); init_db(c)
date = _seed(c, n=5)
monkeypatch.setattr(digest, "local_today", lambda: date)
# force=True bypasses the morning window
assert digest.send_due_digests(c, force=True) == 1
assert sent == ["reader@x.com"]
assert c.execute("SELECT item_count FROM digest_sends WHERE user_id=1").fetchone()[0] == 5
# dedupe: a second run sends nothing
assert digest.send_due_digests(c, force=True) == 0
# unsub token was generated on send
assert c.execute("SELECT digest_unsub_token FROM users WHERE id=1").fetchone()[0]
def test_send_skips_thin_day(monkeypatch, tmp_path):
monkeypatch.setattr(email_send, "send_email", lambda *a, **k: (_ for _ in ()).throw(AssertionError("should not send")))
c = connect(str(tmp_path / "d.db")); init_db(c)
date = _seed(c, n=3) # below MIN_ITEMS (4)
monkeypatch.setattr(digest, "local_today", lambda: date)
assert digest.send_due_digests(c, force=True) == 0
def test_send_respects_opt_out(monkeypatch, tmp_path):
sent = []
monkeypatch.setattr(email_send, "send_email", lambda to, *a, **k: sent.append(to))
c = connect(str(tmp_path / "d.db")); init_db(c)
date = _seed(c, n=5)
c.execute("UPDATE users SET digest_enabled=0 WHERE id=1"); c.commit()
monkeypatch.setattr(digest, "local_today", lambda: date)
assert digest.send_due_digests(c, force=True) == 0 and sent == []