"""Regression tests: main-topic flags must not leak into competitor sub-runs. Based on 2026-04-22 Kanye West --competitors receipt where Drake and Kendrick Lamar sub-runs logged Kanye's resolved subreddit list as their own targeted search. Per-entity sub-runs must never inherit main-topic targeting via closure capture, config mutation, or any other path. """ from __future__ import annotations import io import unittest from contextlib import redirect_stderr from unittest import mock def _fake_report(topic: str): class _R: pass r = _R() r.topic = topic r.artifacts = {} return r class SubRunIsolationTests(unittest.TestCase): """Exercise the _competitor_runner closure pattern from main() directly. Builds the same closure shape main() uses, then invokes it with captured-in-scope main-topic flags to verify they do NOT leak into sub-run pipeline.run kwargs. """ def _run_closure(self, main_flags, competitors, config=None, mock_flag=False): """Replicate _competitor_runner closure from last30days.py main(). main_flags: dict of {x_handle, x_related, subreddits, tiktok_hashtags, tiktok_creators, ig_creators, github_user, github_repos} as they would exist in outer scope after argparse. competitors: list of entity names to run. Returns the list of kwargs dicts pipeline.run was called with. """ from lib import pipeline, resolve as resolve_mod captured: list[dict] = [] def fake_run(**kwargs): captured.append(kwargs) return _fake_report(kwargs["topic"]) # Simulate main scope variables outer_subreddits = main_flags.get("subreddits") outer_x_handle = main_flags.get("x_handle") outer_x_related = main_flags.get("x_related") outer_tiktok_hashtags = main_flags.get("tiktok_hashtags") outer_tiktok_creators = main_flags.get("tiktok_creators") outer_ig_creators = main_flags.get("ig_creators") outer_github_user = main_flags.get("github_user") outer_github_repos = main_flags.get("github_repos") class _Args: pass args = _Args() args.mock = mock_flag args.web_backend = "auto" args.lookback_days = 30 cfg = config or {} # This mirrors the real _competitor_runner closure structure. def competitor_runner(entity): entity_config = dict(cfg) resolved = { "entity": entity, "x_handle": "", "subreddits": [], "github_user": "", "github_repos": [], "context": "", } if not args.mock and resolve_mod._has_backend(entity_config): try: r = resolve_mod.auto_resolve(entity, entity_config) except Exception: r = {} resolved["x_handle"] = r.get("x_handle", "") or "" resolved["subreddits"] = list(r.get("subreddits") or []) resolved["github_user"] = r.get("github_user", "") or "" resolved["github_repos"] = list(r.get("github_repos") or []) resolved["context"] = r.get("context", "") or "" if resolved["context"]: entity_config["_auto_resolve_context"] = resolved["context"] pipeline.run( topic=entity, config=entity_config, depth="default", requested_sources=None, mock=args.mock, x_handle=resolved["x_handle"] or None, subreddits=resolved["subreddits"] or None, github_user=resolved["github_user"] or None, github_repos=resolved["github_repos"] or None, web_backend=args.web_backend, lookback_days=args.lookback_days, internal_subrun=True, ) with mock.patch.object(pipeline, "run", side_effect=fake_run): for entity in competitors: competitor_runner(entity) return captured def test_main_subreddits_do_not_leak_to_peers(self): """Kanye receipt: main --subreddits=Kanye,hiphopheads leaked to Drake/Kendrick.""" main_flags = { "subreddits": ["Kanye", "hiphopheads", "Music", "popheads", "kanyewest"], "x_handle": "kanyewest", } captured = self._run_closure(main_flags, ["Drake", "Kendrick Lamar"]) self.assertEqual(len(captured), 2) for kwargs in captured: self.assertIsNone( kwargs["subreddits"], f"Main subreddits leaked into {kwargs['topic']!r}'s sub-run: " f"{kwargs['subreddits']}", ) def test_main_x_handle_does_not_leak(self): main_flags = {"x_handle": "kanyewest"} captured = self._run_closure(main_flags, ["Drake"]) self.assertIsNone(captured[0]["x_handle"]) def test_main_github_does_not_leak(self): main_flags = { "github_user": "someuser", "github_repos": ["someuser/someproject"], } captured = self._run_closure(main_flags, ["Drake"]) self.assertIsNone(captured[0]["github_user"]) self.assertIsNone(captured[0]["github_repos"]) def test_auto_resolve_context_does_not_leak_across_peers(self): """Per-entity auto_resolve context must not bleed between sub-runs.""" from lib import resolve as resolve_mod def fake_resolve(entity, _cfg): per_topic = { "Drake": {"x_handle": "Drake", "subreddits": [], "github_user": "", "github_repos": [], "context": "Drake ICEMAN rollout", "category": None, "searches_run": 4}, "Kendrick Lamar": {"x_handle": "kendricklamar", "subreddits": [], "github_user": "", "github_repos": [], "context": "Meet The Grahams revival", "category": None, "searches_run": 4}, } return per_topic.get(entity, {}) with mock.patch.object(resolve_mod, "auto_resolve", side_effect=fake_resolve), \ mock.patch.object(resolve_mod, "_has_backend", return_value=True): captured = self._run_closure( main_flags={}, competitors=["Drake", "Kendrick Lamar"], config={"BRAVE_API_KEY": "test"}, ) by_topic = {kw["topic"]: kw for kw in captured} # Each sub-run's config got its own context string. self.assertEqual( by_topic["Drake"]["config"].get("_auto_resolve_context"), "Drake ICEMAN rollout", ) self.assertEqual( by_topic["Kendrick Lamar"]["config"].get("_auto_resolve_context"), "Meet The Grahams revival", ) # Cross-entity check: neither config contains the other's context. self.assertNotIn( "Meet The Grahams", by_topic["Drake"]["config"].get("_auto_resolve_context", ""), ) self.assertNotIn( "ICEMAN", by_topic["Kendrick Lamar"]["config"].get("_auto_resolve_context", ""), ) if __name__ == "__main__": unittest.main()