Files
mvanhorn--last30days-skill/tests/test_render_comparison_multi.py
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:05:33 +08:00

305 lines
12 KiB
Python

"""Tests for render.render_comparison_multi and emit_comparison_output."""
from __future__ import annotations
import json
import unittest
import last30days as cli
from lib import render, schema
def _build_report(topic: str, cluster_titles: list[str]) -> schema.Report:
query_plan = schema.QueryPlan(
intent="comparison",
freshness_mode="balanced_recent",
cluster_mode="debate",
raw_topic=topic,
subqueries=[
schema.SubQuery(
label="primary",
search_query=topic,
ranking_query=topic,
sources=["grounding"],
)
],
source_weights={"grounding": 1.0},
)
clusters: list[schema.Cluster] = []
candidates: list[schema.Candidate] = []
for idx, title in enumerate(cluster_titles):
candidate_id = f"{topic.lower().replace(' ', '-')}-c{idx}"
item = schema.SourceItem(
source="grounding",
item_id=f"g-{candidate_id}",
title=f"{title} evidence",
body=f"Body for {title}",
url=f"https://example.test/{candidate_id}",
snippet=f"Snippet for {title}",
published_at="2026-04-20",
)
candidate = schema.Candidate(
candidate_id=candidate_id,
item_id=item.item_id,
source="grounding",
title=item.title,
url=item.url,
snippet=item.snippet,
subquery_labels=["primary"],
native_ranks={"grounding": idx + 1},
local_relevance=0.8 - idx * 0.1,
freshness=5,
engagement=10,
source_quality=0.9,
rrf_score=0.6 - idx * 0.05,
sources=["grounding"],
source_items=[item],
final_score=80.0 - idx * 5,
)
candidates.append(candidate)
clusters.append(
schema.Cluster(
cluster_id=f"cl-{idx}",
title=title,
candidate_ids=[candidate_id],
representative_ids=[candidate_id],
score=80.0 - idx * 5,
sources=["grounding"],
)
)
return schema.Report(
topic=topic,
range_from="2026-03-23",
range_to="2026-04-22",
generated_at="2026-04-22T00:00:00+00:00",
provider_runtime=schema.ProviderRuntime(
reasoning_provider="mock",
planner_model="mock-planner",
rerank_model="mock-rerank",
),
query_plan=query_plan,
clusters=clusters,
ranked_candidates=candidates,
items_by_source={"grounding": [c.source_items[0] for c in candidates]},
errors_by_source={},
)
class RenderComparisonMultiTests(unittest.TestCase):
def test_three_entity_table(self):
reports = [
("OpenAI", _build_report("OpenAI", ["GPT-5 drop", "API pricing cut"])),
("Anthropic", _build_report("Anthropic", ["Claude 4.7 ship", "MCP rollout"])),
("xAI", _build_report("xAI", ["Grok 4 release", "Memphis cluster"])),
]
rendered = render.render_comparison_multi(reports)
# All three entities appear in the header
self.assertIn("OpenAI vs Anthropic vs xAI", rendered)
# Each entity has its own evidence section
self.assertIn("## OpenAI", rendered)
self.assertIn("## Anthropic", rendered)
self.assertIn("## xAI", rendered)
# Scaffold table header has a column per entity
self.assertIn("| Dimension | OpenAI | Anthropic | xAI |", rendered)
# No verdict row: the pitch-vs-pulse signal ships as synthesis prose,
# not a table axis (early drafts emitted a "Setting the narrative?" row)
self.assertNotIn("Setting the narrative?", rendered)
# "What it is" grounds in positioning fetched this run, never memory
self.assertIn("never from memory", rendered)
# Envelope scaffolding present
self.assertIn("EVIDENCE FOR SYNTHESIS", rendered)
self.assertIn("END OF last30days CANONICAL OUTPUT", rendered)
def test_two_entity_table_has_two_columns(self):
reports = [
("Kanye West", _build_report("Kanye West", ["Donda 2 release"])),
("Drake", _build_report("Drake", ["For All The Dogs"])),
]
rendered = render.render_comparison_multi(reports)
self.assertIn("| Dimension | Kanye West | Drake |", rendered)
self.assertIn("## Kanye West", rendered)
self.assertIn("## Drake", rendered)
def test_empty_clusters_renders_placeholder(self):
reports = [
("OpenAI", _build_report("OpenAI", ["GPT-5 drop"])),
("ObscureCompetitor", _build_report("ObscureCompetitor", [])),
]
rendered = render.render_comparison_multi(reports)
self.assertIn("## ObscureCompetitor", rendered)
self.assertIn("no significant discussion this month", rendered)
# Main still has its cluster
self.assertIn("GPT-5 drop", rendered)
def test_warnings_aggregated_and_labeled(self):
report_a = _build_report("OpenAI", ["GPT-5 drop"])
report_b = _build_report("Anthropic", ["Claude 4.7"])
report_a.warnings.append("Brave quota exhausted")
report_b.warnings.append("Exa returned 0 results")
rendered = render.render_comparison_multi(
[("OpenAI", report_a), ("Anthropic", report_b)]
)
self.assertIn("[OpenAI] Brave quota exhausted", rendered)
self.assertIn("[Anthropic] Exa returned 0 results", rendered)
def test_raises_on_empty_input(self):
with self.assertRaises(ValueError):
render.render_comparison_multi([])
def test_context_emit(self):
reports = [
("OpenAI", _build_report("OpenAI", ["GPT-5 drop"])),
("Anthropic", _build_report("Anthropic", ["Claude 4.7"])),
]
out = render.render_comparison_multi_context(reports)
self.assertIn("Comparison: OpenAI vs Anthropic", out)
self.assertIn("## OpenAI", out)
self.assertIn("## Anthropic", out)
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 = [
("OpenAI", _build_report("OpenAI", ["GPT-5 drop"])),
("Anthropic", _build_report("Anthropic", ["Claude 4.7"])),
]
out = cli.emit_comparison_output(reports, emit="json")
payload = json.loads(out)
self.assertTrue(payload["comparison"])
self.assertEqual(payload["entities"], ["OpenAI", "Anthropic"])
self.assertEqual(len(payload["reports"]), 2)
self.assertEqual(payload["reports"][0]["entity"], "OpenAI")
self.assertEqual(payload["schema_version"], "1.2")
self.assertIn("query", payload["reports"][0]["report"])
def test_compact_and_md_both_route_to_multi(self):
reports = [
("A", _build_report("A", ["Thing A"])),
("B", _build_report("B", ["Thing B"])),
]
compact = cli.emit_comparison_output(reports, emit="compact")
md = cli.emit_comparison_output(reports, emit="md")
self.assertIn("| Dimension | A | B |", compact)
self.assertEqual(compact, md)
def test_context_emit_goes_to_context_renderer(self):
reports = [
("A", _build_report("A", ["Thing A"])),
("B", _build_report("B", ["Thing B"])),
]
out = cli.emit_comparison_output(reports, emit="context")
self.assertIn("Comparison: A vs B", out)
def test_unsupported_emit_raises(self):
reports = [("A", _build_report("A", ["Thing A"]))]
with self.assertRaises(SystemExit):
cli.emit_comparison_output(reports, emit="xml")
if __name__ == "__main__":
unittest.main()