5601022cf7
- New frontend/ SvelteKit static SPA (Svelte 5), served by FastAPI from frontend/build (falls back to the legacy page if unbuilt). - Calm design system: cream/sage palette, serif headlines, generous space, no urgency colors, gentle motion (respects prefers-reduced-motion). - Home screen: mood-mode nav (Today/Wonder/People Helping/Solutions/Light Only/Grounded), the daily brief as a hero + remaining four, browsable mood lanes, an explicit calm end-state, inline Not today / Less like this / Hide affordances, and device-local Calm Filters mirroring goodnews/filters.py. - Backend: moods.py + GET /api/moods (single source of truth for the modes); FilterPrefs gains max_cortisol/max_ragebait ceilings (for Light Only). - Push categorical filters (include/mute topics+flavors, ceilings) into SQL in queries.feed so low-ranked-but-matching items (e.g. discovery for Wonder) are not truncated by ranking; only avoid-terms stay a Python pass. - PWA manifest + icon (installable; offline deferred per plan). - Multi-stage Dockerfile builds the site then serves it from the API. - Tests: queries.feed categorical filters (63 total). README updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
71 lines
2.4 KiB
Python
71 lines
2.4 KiB
Python
import pytest
|
|
|
|
from goodnews import queries
|
|
from goodnews.db import connect, init_db
|
|
|
|
|
|
@pytest.fixture
|
|
def conn():
|
|
c = connect(":memory:")
|
|
init_db(c)
|
|
c.execute("INSERT INTO sources (id, name, feed_url, trust_score) VALUES (1,'S','http://s/f',5)")
|
|
rows = [
|
|
(1, "science", "discovery", 1),
|
|
(2, "animals", "discovery", 1),
|
|
(3, "health", "breakthrough", 2),
|
|
(4, "community", "solution", 1),
|
|
(5, "environment", "solution", 7), # high cortisol
|
|
]
|
|
for aid, topic, flavor, cort in rows:
|
|
c.execute(
|
|
"INSERT INTO articles (id, source_id, canonical_url, title, url_hash) VALUES (?,1,?,?,?)",
|
|
(aid, f"http://s/{aid}", f"t{aid}", f"h{aid}"),
|
|
)
|
|
c.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 (?, 6, 2, 2, ?, 0, 2, 1, ?, ?)",
|
|
(aid, cort, topic, flavor),
|
|
)
|
|
c.commit()
|
|
yield c
|
|
c.close()
|
|
|
|
|
|
def _topics(rows):
|
|
return sorted({r["topic"] for r in rows})
|
|
|
|
|
|
def test_include_topics_in_sql(conn):
|
|
rows = queries.feed(conn, include_topics=["science", "animals"], limit=50)
|
|
assert _topics(rows) == ["animals", "science"]
|
|
|
|
|
|
def test_include_flavors_in_sql(conn):
|
|
rows = queries.feed(conn, include_flavors=["discovery"], limit=50)
|
|
assert sorted({r["flavor"] for r in rows}) == ["discovery"]
|
|
|
|
|
|
def test_include_topic_and_flavor_are_anded(conn):
|
|
# Wonder-style: (science/animals/culture) AND discovery
|
|
rows = queries.feed(conn, include_topics=["science", "animals", "culture"], include_flavors=["discovery"], limit=50)
|
|
assert _topics(rows) == ["animals", "science"] # the two discovery items
|
|
|
|
|
|
def test_mute_topics_excludes(conn):
|
|
rows = queries.feed(conn, mute_topics=["health", "environment"], limit=50)
|
|
assert "health" not in _topics(rows) and "environment" not in _topics(rows)
|
|
|
|
|
|
def test_max_cortisol_ceiling(conn):
|
|
rows = queries.feed(conn, max_cortisol=2, limit=50)
|
|
assert all((r["cortisol_score"] or 0) <= 2 for r in rows)
|
|
assert "environment" not in _topics(rows) # the cortisol=7 item is gone
|
|
|
|
|
|
def test_duplicates_excluded(conn):
|
|
conn.execute("UPDATE articles SET duplicate_of = 1 WHERE id = 2")
|
|
conn.commit()
|
|
ids = {r["id"] for r in queries.feed(conn, limit=50)}
|
|
assert 2 not in ids
|