0ccd5554d2
A reader who swaps a story away should keep that swap after a refresh; before, the server re-served the original brief. - localStorage now persists seen / dismissed / history (loadJSON/saveJSON). - /api/brief accepts an exclude list; dismissed (replaced-away) ids are dropped and the highlights refill around them, so swaps stick and stay full. - Replace records the swap to dismissed+seen and persists; the seen-set (persisted) keeps Replace from recycling across refreshes too. - History panel survives refresh and gains 'Clear what I've seen (start fresh)' so it never feels suffocating. Saved history/favorites still come with sign-in. Tests: brief exclude + refill (90 total). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
59 lines
2.6 KiB
Python
59 lines
2.6 KiB
Python
import json
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from goodnews.db import connect, init_db
|
|
|
|
|
|
@pytest.fixture
|
|
def client(tmp_path, monkeypatch):
|
|
db = tmp_path / "t.sqlite3"
|
|
monkeypatch.setenv("GOODNEWS_DB", str(db))
|
|
conn = connect(db); init_db(conn)
|
|
conn.execute("INSERT INTO sources (id,name,feed_url,trust_score) VALUES (1,'S','http://s/f',6)")
|
|
|
|
def add(aid, topic, in_brief_rank=None, title=None):
|
|
conn.execute("INSERT INTO articles (id,source_id,canonical_url,title,published_at,url_hash) "
|
|
"VALUES (?,1,?,?, '2026-05-31T10:00:00+00:00', ?)",
|
|
(aid, f"http://s/{aid}", title or f"t{aid}", f"h{aid}"))
|
|
conn.execute("INSERT INTO article_scores (article_id,constructive_score,agency_score,human_benefit_score,"
|
|
"cortisol_score,ragebait_score,pr_risk_score,accepted,topic,flavor) "
|
|
"VALUES (?,7,2,2,1,0,2,1,?,'solution')", (aid, topic))
|
|
if in_brief_rank:
|
|
conn.execute("INSERT INTO daily_brief_items (brief_id,article_id,rank) VALUES (1,?,?)", (aid, in_brief_rank))
|
|
|
|
conn.execute("INSERT INTO daily_briefs (id,brief_date,title) VALUES (1,'2026-05-31','B')")
|
|
add(1, "health", 1) # will be muted
|
|
add(2, "science", 2)
|
|
add(3, "environment", 3)
|
|
add(4, "community") # refill candidates (accepted, not in brief)
|
|
add(5, "animals")
|
|
add(6, "culture")
|
|
conn.commit(); conn.close()
|
|
from goodnews.api import create_app
|
|
return TestClient(create_app())
|
|
|
|
|
|
def test_brief_refills_to_full_count_under_boundary(client):
|
|
full = client.get("/api/brief", params={"limit": 3}).json()
|
|
assert len(full["items"]) == 3
|
|
|
|
muted = client.get("/api/brief", params={"limit": 3, "prefs": json.dumps({"mute_topics": ["health"]})}).json()
|
|
topics = [i["topic"] for i in muted["items"]]
|
|
assert len(muted["items"]) == 3 # stayed full (refilled)
|
|
assert "health" not in topics # boundary respected
|
|
|
|
|
|
def test_brief_refill_respects_avoid_terms(client):
|
|
# avoid a word in the health item's title
|
|
client_resp = client.get("/api/brief", params={"limit": 3, "prefs": json.dumps({"avoid_terms": ["t1"]})}).json()
|
|
assert len(client_resp["items"]) == 3
|
|
assert all(i["id"] != 1 for i in client_resp["items"])
|
|
|
|
|
|
def test_brief_excludes_dismissed_and_refills(client):
|
|
# Dismiss the first brief item; the highlights should stay full and not show it.
|
|
out = client.get("/api/brief", params={"limit": 3, "exclude": "1"}).json()
|
|
ids = [i["id"] for i in out["items"]]
|
|
assert len(out["items"]) == 3
|
|
assert 1 not in ids
|