"""Closer to Home: /api/feed?home=... sections the feed into near / country / world, state-match only on high/medium confidence, sparse tiers fold down, Brief untouched.""" import pytest from fastapi.testclient import TestClient from goodnews.db import connect, init_db @pytest.fixture def app_db(tmp_path, monkeypatch): db = tmp_path / "t.sqlite3" monkeypatch.setenv("GOODNEWS_DB", str(db)) monkeypatch.setenv("GOODNEWS_PUBLIC_BASE_URL", "http://testserver") import importlib import goodnews.api as api importlib.reload(api) c = connect(str(db)); init_db(c) c.execute("INSERT INTO sources (id,name,feed_url,trust_score,content_visible) VALUES (1,'S','http://s/f',5,1)") def art(aid, *, breadth, conf, country=None, state=None): c.execute("INSERT INTO articles (id,source_id,canonical_url,title,url_hash,published_at) " "VALUES (?,1,?,?,?,?)", (aid, f"https://x/{aid}", f"Story {aid}", f"h{aid}", "2026-06-18T08:00:00")) c.execute("INSERT INTO article_scores (article_id,accepted,novelty_score,constructive_score) " "VALUES (?,1,5,5)", (aid,)) c.execute("INSERT INTO article_geo (article_id,breadth,confidence,geo_version) VALUES (?,?,?, 'geo-v1')", (aid, breadth, conf)) if country: c.execute("INSERT INTO article_places (article_id,country_code,state_code,ord) VALUES (?,?,?,0)", (aid, country, state)) # 4 NY high-conf (near), 1 NY LOW-conf (must NOT be near), 3 US/CA (country), 4 world for i in range(1, 5): art(i, breadth="locality", conf="high", country="US", state="NY") art(5, breadth="locality", conf="low", country="US", state="NY") for i in range(6, 9): art(i, breadth="regional", conf="high", country="US", state="CA") for i in range(9, 13): art(i, breadth="global", conf="high") # placeless -> world c.commit(); c.close() return api.create_app() def _sections(items): return [it["section"] for it in items] def test_home_state_sections_near_country_world(app_db): r = TestClient(app_db).get("/api/feed?home=US-NY&limit=50").json() items = r["items"] secs = _sections(items) # order: all near, then all country, then all world (no interleaving) assert secs == sorted(secs, key=["near", "country", "world"].index) near = [it for it in items if it["section"] == "near"] assert len(near) == 4 and all(it["geo_confidence"] == "high" for it in near) # the LOW-confidence NY story is NOT "near" — state match needs high/medium a5 = next(it for it in items if it["id"] == 5) assert a5["section"] == "country" # world holds the placeless/global ones assert {it["id"] for it in items if it["section"] == "world"} == {9, 10, 11, 12} def test_sparse_near_folds_into_country(app_db): # home in a state with no high-conf local stories -> "near" is empty, no near header; # its (none) items fold away and we still get country + world, never an empty tier. r = TestClient(app_db).get("/api/feed?home=US-TX&limit=50").json() assert "near" not in _sections(r["items"]) # nothing local to TX -> no near section assert "country" in _sections(r["items"]) # US stories still surface as your country def test_country_only_home_gates_near_on_confidence(app_db): r = TestClient(app_db).get("/api/feed?home=US&limit=50").json() items = r["items"] secs = set(_sections(items)) assert "country" not in secs # no state -> near IS the whole country assert secs <= {"near", "world"} near_ids = {it["id"] for it in items if it["section"] == "near"} # "Near you" requires high/medium confidence: high-conf US stories only, NOT the # low-confidence US story (#5), which must still appear, in "world". assert near_ids == {1, 2, 3, 4, 6, 7, 8} a5 = next(it for it in items if it["id"] == 5) assert a5["section"] == "world" # low-conf home-country -> world, not vanished def test_home_brief_leads_with_local(app_db): # /api/brief?home=US-NY defaults to the 'nearby' scope: leads with the 'state' tier # (high-confidence NY), titles it "Close to home", and never elevates the low-conf # NY story (#5). CA stories (a different census region) fall to 'country'. r = TestClient(app_db).get("/api/brief?home=US-NY&limit=10").json() assert r["title"] == "Close to home" assert r["items"][0]["section"] == "state" # hero comes from the closest section state_ids = {it["id"] for it in r["items"] if it["section"] == "state"} assert state_ids == {1, 2, 3, 4} # high-conf NY only a5 = next(it for it in r["items"] if it["id"] == 5) assert a5["section"] == "country" # low-conf NY -> not 'state' secs = [it["section"] for it in r["items"]] if "state" in secs and "world" in secs: # local leads, world trails assert max(i for i, s in enumerate(secs) if s == "state") < \ min(i for i, s in enumerate(secs) if s == "world") def test_home_brief_scope_country_has_no_state_tier(app_db): # The 'country' scope drops the local lead: all US stories sit in 'country', none 'state'. r = TestClient(app_db).get("/api/brief?home=US-NY&scope=country&limit=10").json() secs = {it["section"] for it in r["items"]} assert "state" not in secs and "country" in secs def test_no_home_is_unchanged_and_unsectioned(app_db): r = TestClient(app_db).get("/api/feed?limit=50").json() assert all(it["section"] is None for it in r["items"]) assert r["next_offset"] is None or isinstance(r["next_offset"], int)