diff --git a/skills/last30days/scripts/lib/grounding.py b/skills/last30days/scripts/lib/grounding.py index 9d7835e..ad33ede 100644 --- a/skills/last30days/scripts/lib/grounding.py +++ b/skills/last30days/scripts/lib/grounding.py @@ -232,17 +232,32 @@ def web_search( raise ValueError(f"Unsupported web backend: {backend!r}") else: return [], {} - if items: + if items and not _reddit_excluded(config): items = _enrich_reddit_items(items) return items, artifact +def _reddit_excluded(config: dict) -> bool: + """Return True when EXCLUDE_SOURCES contains 'reddit'. + + Respects the same suppression knob the pipeline uses for source gating, + so a user who set EXCLUDE_SOURCES=reddit doesn't get Reddit content + smuggled back in via web-search URLs. + """ + raw = (config.get("EXCLUDE_SOURCES") or "").split(",") + return any(s.strip().lower() == "reddit" for s in raw) + + def _enrich_reddit_items(items: list[dict]) -> list[dict]: """Enrich web search results that are Reddit URLs with thread body and comments. Claude Code's WebFetch blocks reddit.com, so the model can't retrieve Reddit content from web search results. This fetches it via the public JSON API (reddit.com/.../.json) which bypasses that restriction. + + Callers should gate this with EXCLUDE_SOURCES=reddit handling (see + `_reddit_excluded`) so a user who explicitly excluded Reddit doesn't + get Reddit content via web-search URLs. """ from . import reddit_enrich diff --git a/tests/test_grounding_v3.py b/tests/test_grounding_v3.py index b9867d6..0a662f5 100644 --- a/tests/test_grounding_v3.py +++ b/tests/test_grounding_v3.py @@ -190,5 +190,42 @@ class WebSearchDispatchTests(unittest.TestCase): grounding.web_search("test", ("2026-02-25", "2026-03-27"), {}, backend="google") +class RedditEnrichmentGateTests(unittest.TestCase): + """EXCLUDE_SOURCES=reddit must suppress the web-search Reddit enrichment. + + Otherwise a user who explicitly excluded Reddit would still get Reddit + content smuggled back in via web-search URLs that happen to point at + reddit.com threads. + """ + + def test_reddit_excluded_via_exclude_sources_skips_enrichment(self): + config = {"BRAVE_API_KEY": "k", "EXCLUDE_SOURCES": "reddit"} + items = [{"url": "https://www.reddit.com/r/python/comments/abc/title/", "snippet": "original"}] + with patch("lib.grounding.brave_search", return_value=(items, {})), \ + patch("lib.grounding._enrich_reddit_items") as enrich_mock: + grounding.web_search("test", ("2026-02-25", "2026-03-27"), config, backend="auto") + enrich_mock.assert_not_called() + + def test_reddit_excluded_case_insensitive(self): + for value in ("REDDIT", "Reddit", " reddit ", "x,reddit,y"): + config = {"BRAVE_API_KEY": "k", "EXCLUDE_SOURCES": value} + self.assertTrue( + grounding._reddit_excluded(config), + msg=f"_reddit_excluded should be True for EXCLUDE_SOURCES={value!r}", + ) + + def test_reddit_not_excluded_when_other_sources_listed(self): + config = {"EXCLUDE_SOURCES": "tiktok,instagram"} + self.assertFalse(grounding._reddit_excluded(config)) + + def test_enrichment_runs_when_reddit_not_excluded(self): + config = {"BRAVE_API_KEY": "k"} + items = [{"url": "https://www.reddit.com/r/python/comments/abc/title/", "snippet": "original"}] + with patch("lib.grounding.brave_search", return_value=(items, {})), \ + patch("lib.grounding._enrich_reddit_items", return_value=items) as enrich_mock: + grounding.web_search("test", ("2026-02-25", "2026-03-27"), config, backend="auto") + enrich_mock.assert_called_once() + + if __name__ == "__main__": unittest.main()