2860fb5d18
Security / Dependency review (push) Has been skipped
Scorecard / Scorecard analysis (push) Failing after 0s
Validate / eval (push) Failing after 0s
Security / Dependency audit (push) Failing after 1s
Security / Secret scan (push) Failing after 1s
Validate / tests (push) Failing after 0s
Validate / mcp-tests (push) Failing after 1s
GitHub Actions Security Analysis with zizmor 🌈 / zizmor (push) Failing after 1s
Security / SAST scan (push) Failing after 13m52s
190 lines
7.3 KiB
Python
190 lines
7.3 KiB
Python
"""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()
|