fix: per-entity Step 0.55, LAW 7 sub-run quiet, default 2, canonical SKILL.md (#311)
Four fixes based on 2026-04-22 test-window feedback on v3.0.11 --competitors:
- Each competitor sub-run now runs Step 0.55 (X handle / subreddits /
GitHub) via resolve.auto_resolve inside the fanout closure. Deep-copied
config per entity prevents _auto_resolve_context leak across sub-runs.
Resolved data stored on report.artifacts["resolved"] for the renderer.
- New internal_subrun keyword on planner.plan_query and pipeline.run
suppresses the LAW 7 "No --plan passed" stderr for engine-internal
fan-out only. Default path unchanged.
- Default --competitors count is now 2 (3-way total). --competitors=N
still customizes; range 1..6.
- SKILL.md STEP 0 canonical-path self-check forces readers who loaded
from marketplaces/ (auto-restored to origin/main, stale) to re-read
from plugins/cache/last30days-skill/last30days/{VERSION}/SKILL.md.
Two of three 2026-04-22 test windows hit this stale-path trap.
- New ## Resolved Entities block in render_comparison_multi shows
per-entity handles/subs/github for debug visibility.
Bumps plugin.json to 3.0.12. 12 new tests; 1,175 total passing.
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,13 +29,19 @@ class CompetitorsCliTests(unittest.TestCase):
|
||||
self.assertEqual(count, 0)
|
||||
self.assertEqual(explicit, [])
|
||||
|
||||
def test_bare_flag_defaults_to_three(self):
|
||||
def test_bare_flag_defaults_to_two(self):
|
||||
args = _parse("Kanye West", "--competitors")
|
||||
enabled, count, explicit = cli.resolve_competitors_args(args)
|
||||
self.assertTrue(enabled)
|
||||
self.assertEqual(count, 3)
|
||||
self.assertEqual(count, 2)
|
||||
self.assertEqual(explicit, [])
|
||||
|
||||
def test_explicit_three_still_supported(self):
|
||||
args = _parse("OpenAI", "--competitors", "3")
|
||||
enabled, count, _explicit = cli.resolve_competitors_args(args)
|
||||
self.assertTrue(enabled)
|
||||
self.assertEqual(count, 3)
|
||||
|
||||
def test_explicit_count(self):
|
||||
args = _parse("OpenAI", "--competitors", "4")
|
||||
enabled, count, explicit = cli.resolve_competitors_args(args)
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
# ruff: noqa: E402
|
||||
"""Integration tests for per-entity Step 0.55 resolution inside competitor fan-out."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import sys
|
||||
import unittest
|
||||
from contextlib import redirect_stderr
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
|
||||
|
||||
def _fake_report(topic: str):
|
||||
"""Minimal Report stand-in for runner return values."""
|
||||
class _R:
|
||||
pass
|
||||
|
||||
r = _R()
|
||||
r.topic = topic
|
||||
r.artifacts = {}
|
||||
return r
|
||||
|
||||
|
||||
def _build_main_args(*overrides):
|
||||
"""Minimal argparse.Namespace-like object for the competitor path."""
|
||||
import argparse
|
||||
ns = argparse.Namespace(
|
||||
topic=["Kanye West"],
|
||||
mock=False,
|
||||
competitors=2,
|
||||
competitors_list=None,
|
||||
quick=False,
|
||||
deep=False,
|
||||
emit="compact",
|
||||
search=None,
|
||||
debug=False,
|
||||
diagnose=False,
|
||||
save_dir=None,
|
||||
save_suffix=None,
|
||||
store=False,
|
||||
x_handle=None,
|
||||
x_related=None,
|
||||
web_backend="auto",
|
||||
deep_research=False,
|
||||
plan=None,
|
||||
subreddits=None,
|
||||
tiktok_hashtags=None,
|
||||
tiktok_creators=None,
|
||||
ig_creators=None,
|
||||
lookback_days=30,
|
||||
auto_resolve=False,
|
||||
github_user=None,
|
||||
github_repo=None,
|
||||
)
|
||||
return ns
|
||||
|
||||
|
||||
class PerEntityResolveTests(unittest.TestCase):
|
||||
"""Verify each competitor sub-run calls auto_resolve with its own topic and
|
||||
that the resolved fields are threaded into pipeline.run."""
|
||||
|
||||
def test_auto_resolve_called_per_competitor(self):
|
||||
from lib import resolve as resolve_mod
|
||||
from lib import pipeline as pipeline_mod
|
||||
|
||||
config = {"BRAVE_API_KEY": "test-key"}
|
||||
|
||||
captured_resolve_topics: list[str] = []
|
||||
captured_pipeline_kwargs: list[dict] = []
|
||||
|
||||
def fake_resolve(topic, _cfg):
|
||||
captured_resolve_topics.append(topic)
|
||||
per_topic = {
|
||||
"Drake": {
|
||||
"x_handle": "Drake",
|
||||
"subreddits": ["DrakeTheType", "hiphopheads"],
|
||||
"github_user": "",
|
||||
"github_repos": [],
|
||||
"context": "Drake ICEMAN rollout",
|
||||
"category": None,
|
||||
"searches_run": 4,
|
||||
},
|
||||
"Kendrick Lamar": {
|
||||
"x_handle": "kendricklamar",
|
||||
"subreddits": ["KendrickLamar", "hiphopheads"],
|
||||
"github_user": "",
|
||||
"github_repos": [],
|
||||
"context": "Meet The Grahams revival",
|
||||
"category": None,
|
||||
"searches_run": 4,
|
||||
},
|
||||
}
|
||||
return per_topic.get(topic, {
|
||||
"x_handle": "", "subreddits": [], "github_user": "",
|
||||
"github_repos": [], "context": "",
|
||||
"category": None, "searches_run": 0,
|
||||
})
|
||||
|
||||
def fake_pipeline_run(**kwargs):
|
||||
captured_pipeline_kwargs.append(kwargs)
|
||||
return _fake_report(kwargs["topic"])
|
||||
|
||||
with mock.patch.object(resolve_mod, "auto_resolve", side_effect=fake_resolve), \
|
||||
mock.patch.object(resolve_mod, "_has_backend", return_value=True), \
|
||||
mock.patch.object(pipeline_mod, "run", side_effect=fake_pipeline_run):
|
||||
# Exercise the competitor_runner closure pattern from main() by
|
||||
# calling it directly with two competitors.
|
||||
self._run_competitor_closure(
|
||||
config=config,
|
||||
competitors=["Drake", "Kendrick Lamar"],
|
||||
mock_flag=False,
|
||||
)
|
||||
|
||||
# auto_resolve was called once per competitor
|
||||
self.assertEqual(sorted(captured_resolve_topics), ["Drake", "Kendrick Lamar"])
|
||||
# pipeline.run received resolved fields per entity
|
||||
by_topic = {kw["topic"]: kw for kw in captured_pipeline_kwargs}
|
||||
self.assertEqual(by_topic["Drake"]["x_handle"], "Drake")
|
||||
self.assertEqual(
|
||||
by_topic["Drake"]["subreddits"], ["DrakeTheType", "hiphopheads"],
|
||||
)
|
||||
self.assertEqual(by_topic["Kendrick Lamar"]["x_handle"], "kendricklamar")
|
||||
# internal_subrun=True on all competitor sub-runs
|
||||
self.assertTrue(all(kw["internal_subrun"] for kw in captured_pipeline_kwargs))
|
||||
|
||||
def test_mock_mode_skips_auto_resolve(self):
|
||||
from lib import resolve as resolve_mod
|
||||
from lib import pipeline as pipeline_mod
|
||||
|
||||
resolve_called = []
|
||||
|
||||
def fake_resolve(*a, **k):
|
||||
resolve_called.append((a, k))
|
||||
return {}
|
||||
|
||||
with mock.patch.object(resolve_mod, "auto_resolve", side_effect=fake_resolve), \
|
||||
mock.patch.object(pipeline_mod, "run", side_effect=lambda **kw: _fake_report(kw["topic"])):
|
||||
self._run_competitor_closure(
|
||||
config={"BRAVE_API_KEY": "test-key"},
|
||||
competitors=["Anthropic"],
|
||||
mock_flag=True,
|
||||
)
|
||||
|
||||
self.assertEqual(resolve_called, [])
|
||||
|
||||
def test_no_backend_skips_auto_resolve(self):
|
||||
from lib import resolve as resolve_mod
|
||||
from lib import pipeline as pipeline_mod
|
||||
|
||||
resolve_called = []
|
||||
|
||||
def fake_resolve(*a, **k):
|
||||
resolve_called.append((a, k))
|
||||
return {}
|
||||
|
||||
with mock.patch.object(resolve_mod, "auto_resolve", side_effect=fake_resolve), \
|
||||
mock.patch.object(resolve_mod, "_has_backend", return_value=False), \
|
||||
mock.patch.object(pipeline_mod, "run", side_effect=lambda **kw: _fake_report(kw["topic"])):
|
||||
self._run_competitor_closure(
|
||||
config={},
|
||||
competitors=["Anthropic"],
|
||||
mock_flag=False,
|
||||
)
|
||||
|
||||
self.assertEqual(resolve_called, [])
|
||||
|
||||
def test_resolve_failure_degrades_gracefully(self):
|
||||
from lib import resolve as resolve_mod
|
||||
from lib import pipeline as pipeline_mod
|
||||
|
||||
captured_pipeline_kwargs: list[dict] = []
|
||||
|
||||
def fake_resolve(_topic, _cfg):
|
||||
raise RuntimeError("upstream offline")
|
||||
|
||||
def fake_pipeline_run(**kwargs):
|
||||
captured_pipeline_kwargs.append(kwargs)
|
||||
return _fake_report(kwargs["topic"])
|
||||
|
||||
err = io.StringIO()
|
||||
with redirect_stderr(err), \
|
||||
mock.patch.object(resolve_mod, "auto_resolve", side_effect=fake_resolve), \
|
||||
mock.patch.object(resolve_mod, "_has_backend", return_value=True), \
|
||||
mock.patch.object(pipeline_mod, "run", side_effect=fake_pipeline_run):
|
||||
self._run_competitor_closure(
|
||||
config={"BRAVE_API_KEY": "test-key"},
|
||||
competitors=["Anthropic"],
|
||||
mock_flag=False,
|
||||
)
|
||||
|
||||
# Warning logged but run continues with planner defaults
|
||||
self.assertIn("auto_resolve failed for 'Anthropic'", err.getvalue())
|
||||
self.assertEqual(len(captured_pipeline_kwargs), 1)
|
||||
self.assertIsNone(captured_pipeline_kwargs[0]["x_handle"])
|
||||
self.assertIsNone(captured_pipeline_kwargs[0]["subreddits"])
|
||||
|
||||
def test_resolved_artifact_stored_on_report(self):
|
||||
from lib import resolve as resolve_mod
|
||||
from lib import pipeline as pipeline_mod
|
||||
|
||||
with mock.patch.object(resolve_mod, "auto_resolve", return_value={
|
||||
"x_handle": "Drake",
|
||||
"subreddits": ["DrakeTheType"],
|
||||
"github_user": "",
|
||||
"github_repos": [],
|
||||
"context": "Drake context",
|
||||
"category": None,
|
||||
"searches_run": 4,
|
||||
}), \
|
||||
mock.patch.object(resolve_mod, "_has_backend", return_value=True), \
|
||||
mock.patch.object(pipeline_mod, "run", side_effect=lambda **kw: _fake_report(kw["topic"])):
|
||||
results = self._run_competitor_closure(
|
||||
config={"BRAVE_API_KEY": "test-key"},
|
||||
competitors=["Drake"],
|
||||
mock_flag=False,
|
||||
)
|
||||
|
||||
self.assertIn("resolved", results[0].artifacts)
|
||||
resolved = results[0].artifacts["resolved"]
|
||||
self.assertEqual(resolved["entity"], "Drake")
|
||||
self.assertEqual(resolved["x_handle"], "Drake")
|
||||
self.assertEqual(resolved["subreddits"], ["DrakeTheType"])
|
||||
self.assertEqual(resolved["context"], "Drake context")
|
||||
|
||||
def test_config_not_mutated_across_sub_runs(self):
|
||||
"""_auto_resolve_context from entity A must not leak into entity B."""
|
||||
from lib import resolve as resolve_mod
|
||||
from lib import pipeline as pipeline_mod
|
||||
|
||||
captured_contexts: list[str] = []
|
||||
|
||||
def fake_resolve(topic, _cfg):
|
||||
per_topic = {
|
||||
"Drake": {"x_handle": "Drake", "subreddits": [], "github_user": "",
|
||||
"github_repos": [], "context": "Drake unique context",
|
||||
"category": None, "searches_run": 4},
|
||||
"Kendrick Lamar": {"x_handle": "kendricklamar", "subreddits": [],
|
||||
"github_user": "", "github_repos": [],
|
||||
"context": "Kendrick unique context",
|
||||
"category": None, "searches_run": 4},
|
||||
}
|
||||
return per_topic[topic]
|
||||
|
||||
def fake_pipeline_run(**kwargs):
|
||||
captured_contexts.append(
|
||||
kwargs["config"].get("_auto_resolve_context", "")
|
||||
)
|
||||
return _fake_report(kwargs["topic"])
|
||||
|
||||
shared_config = {"BRAVE_API_KEY": "test-key"}
|
||||
with mock.patch.object(resolve_mod, "auto_resolve", side_effect=fake_resolve), \
|
||||
mock.patch.object(resolve_mod, "_has_backend", return_value=True), \
|
||||
mock.patch.object(pipeline_mod, "run", side_effect=fake_pipeline_run):
|
||||
self._run_competitor_closure(
|
||||
config=shared_config,
|
||||
competitors=["Drake", "Kendrick Lamar"],
|
||||
mock_flag=False,
|
||||
)
|
||||
|
||||
# Each sub-run received its own entity's context — no cross-leak.
|
||||
self.assertIn("Drake unique context", captured_contexts)
|
||||
self.assertIn("Kendrick unique context", captured_contexts)
|
||||
# The shared outer config was not mutated
|
||||
self.assertNotIn("_auto_resolve_context", shared_config)
|
||||
|
||||
# --- test helpers -----------------------------------------------------
|
||||
|
||||
def _run_competitor_closure(self, *, config, competitors, mock_flag):
|
||||
"""Replicate the competitor_runner closure from last30days.main() and
|
||||
call it against each competitor. Returns the list of Reports."""
|
||||
from lib import pipeline, resolve as resolve_mod
|
||||
|
||||
class _Args:
|
||||
pass
|
||||
args = _Args()
|
||||
args.mock = mock_flag
|
||||
args.web_backend = "auto"
|
||||
args.lookback_days = 30
|
||||
|
||||
def runner(entity: str):
|
||||
entity_config = dict(config)
|
||||
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 as exc:
|
||||
sys.stderr.write(
|
||||
f"[Competitors] auto_resolve failed for {entity!r}: "
|
||||
f"{type(exc).__name__}: {exc}\n"
|
||||
)
|
||||
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"]
|
||||
report = 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,
|
||||
)
|
||||
report.artifacts["resolved"] = resolved
|
||||
return report
|
||||
|
||||
return [runner(c) for c in competitors]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,55 @@
|
||||
# ruff: noqa: E402
|
||||
"""Tests for planner.plan_query internal_subrun quiet mode."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import sys
|
||||
import unittest
|
||||
from contextlib import redirect_stderr
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
|
||||
from lib import planner
|
||||
|
||||
|
||||
class PlannerQuietModeTests(unittest.TestCase):
|
||||
def _call(self, *, internal_subrun: bool):
|
||||
err = io.StringIO()
|
||||
with redirect_stderr(err):
|
||||
plan = planner.plan_query(
|
||||
topic="Acme Corp",
|
||||
available_sources=["grounding", "reddit"],
|
||||
requested_sources=None,
|
||||
depth="default",
|
||||
provider=None,
|
||||
model=None,
|
||||
internal_subrun=internal_subrun,
|
||||
)
|
||||
return plan, err.getvalue()
|
||||
|
||||
def test_default_emits_law7_warning(self):
|
||||
plan, stderr = self._call(internal_subrun=False)
|
||||
self.assertIn("No --plan passed", stderr)
|
||||
self.assertIn("YOU ARE the planner", stderr)
|
||||
self.assertTrue(plan.subqueries)
|
||||
|
||||
def test_internal_subrun_suppresses_warning(self):
|
||||
plan, stderr = self._call(internal_subrun=True)
|
||||
self.assertNotIn("No --plan passed", stderr)
|
||||
self.assertNotIn("YOU ARE the planner", stderr)
|
||||
# Still returns a valid fallback plan
|
||||
self.assertTrue(plan.subqueries)
|
||||
|
||||
def test_internal_subrun_still_allows_other_warnings(self):
|
||||
"""Quiet mode only silences the LAW 7 block, not all planner output."""
|
||||
plan, _stderr = self._call(internal_subrun=True)
|
||||
# The plan itself is deterministic fallback; verify note carries
|
||||
# no planner-error indication.
|
||||
self.assertGreater(len(plan.subqueries), 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -159,6 +159,110 @@ class RenderComparisonMultiTests(unittest.TestCase):
|
||||
self.assertIn("GPT-5 drop", out)
|
||||
|
||||
|
||||
class ResolvedEntitiesBlockTests(unittest.TestCase):
|
||||
def _build_with_resolved(self, label, topic, resolved):
|
||||
r = _build_report(topic, ["Cluster A"])
|
||||
if resolved is not None:
|
||||
r.artifacts["resolved"] = resolved
|
||||
return (label, r)
|
||||
|
||||
def test_block_emitted_when_any_entity_has_resolved(self):
|
||||
reports = [
|
||||
self._build_with_resolved("OpenAI", "OpenAI", {
|
||||
"entity": "OpenAI",
|
||||
"x_handle": "OpenAI",
|
||||
"subreddits": ["OpenAI", "MachineLearning"],
|
||||
"github_user": "openai",
|
||||
"github_repos": ["openai/gpt"],
|
||||
"context": "GPT-5 release signals are strong",
|
||||
}),
|
||||
self._build_with_resolved("Anthropic", "Anthropic", {
|
||||
"entity": "Anthropic",
|
||||
"x_handle": "AnthropicAI",
|
||||
"subreddits": ["ClaudeAI"],
|
||||
"github_user": "anthropics",
|
||||
"github_repos": [],
|
||||
"context": "",
|
||||
}),
|
||||
]
|
||||
rendered = render.render_comparison_multi(reports)
|
||||
self.assertIn("## Resolved Entities", rendered)
|
||||
self.assertIn("**OpenAI**: X @OpenAI", rendered)
|
||||
self.assertIn("r/OpenAI, r/MachineLearning", rendered)
|
||||
self.assertIn("@openai (openai/gpt)", rendered)
|
||||
self.assertIn("**Anthropic**: X @AnthropicAI", rendered)
|
||||
# Missing context renders as "-"
|
||||
self.assertIn("Context: -", rendered)
|
||||
|
||||
def test_block_omitted_when_no_resolved_artifacts(self):
|
||||
reports = [
|
||||
self._build_with_resolved("A", "A", None),
|
||||
self._build_with_resolved("B", "B", None),
|
||||
]
|
||||
rendered = render.render_comparison_multi(reports)
|
||||
self.assertNotIn("## Resolved Entities", rendered)
|
||||
|
||||
def test_missing_fields_render_as_dash(self):
|
||||
reports = [
|
||||
self._build_with_resolved("OpenAI", "OpenAI", {
|
||||
"entity": "OpenAI",
|
||||
"x_handle": "",
|
||||
"subreddits": [],
|
||||
"github_user": "",
|
||||
"github_repos": [],
|
||||
"context": "",
|
||||
}),
|
||||
]
|
||||
rendered = render.render_comparison_multi(reports)
|
||||
self.assertIn("**OpenAI**: X - | Subs - | GitHub - | Context: -", rendered)
|
||||
|
||||
def test_long_context_truncated(self):
|
||||
long = "a" * 200
|
||||
reports = [
|
||||
self._build_with_resolved("X", "X", {
|
||||
"entity": "X",
|
||||
"x_handle": "",
|
||||
"subreddits": [],
|
||||
"github_user": "",
|
||||
"github_repos": [],
|
||||
"context": long,
|
||||
}),
|
||||
]
|
||||
rendered = render.render_comparison_multi(reports)
|
||||
# The truncate helper adds an ellipsis; context line should not show
|
||||
# the full 200-char string.
|
||||
self.assertNotIn("a" * 200, rendered)
|
||||
|
||||
def test_context_emit_includes_resolved_block(self):
|
||||
reports = [
|
||||
self._build_with_resolved("OpenAI", "OpenAI", {
|
||||
"entity": "OpenAI",
|
||||
"x_handle": "OpenAI",
|
||||
"subreddits": ["OpenAI"],
|
||||
"github_user": "",
|
||||
"github_repos": [],
|
||||
"context": "",
|
||||
}),
|
||||
]
|
||||
out = render.render_comparison_multi_context(reports)
|
||||
self.assertIn("## Resolved Entities", out)
|
||||
self.assertIn("**OpenAI**: X @OpenAI", out)
|
||||
|
||||
def test_subreddit_overflow_truncated(self):
|
||||
reports = [
|
||||
self._build_with_resolved("X", "X", {
|
||||
"entity": "X",
|
||||
"x_handle": "",
|
||||
"subreddits": ["a", "b", "c", "d", "e", "f", "g"],
|
||||
"github_user": "",
|
||||
"github_repos": [],
|
||||
"context": "",
|
||||
}),
|
||||
]
|
||||
rendered = render.render_comparison_multi(reports)
|
||||
self.assertIn("r/a, r/b, r/c, r/d, r/e (+2)", rendered)
|
||||
|
||||
|
||||
class EmitComparisonOutputTests(unittest.TestCase):
|
||||
def test_json_emit_nests_per_entity(self):
|
||||
reports = [
|
||||
|
||||
Reference in New Issue
Block a user