返回 last30days-skill
test_agent_export.py
根目录 / tests / test_agent_export.py
1 import json
2 from pathlib import Path
3
4 import last30days as cli
5 from lib import health, schema
6
7
8 GOLDEN = Path(__file__).parent / "fixtures" / "agent_export_v1.json"
9
10
11 def _report() -> schema.Report:
12 reddit_item = schema.SourceItem(
13 item_id="reddit-1",
14 source="reddit",
15 title="Agents move into daily coding workflows",
16 body="Developers described where coding agents save time.",
17 url="https://www.reddit.com/r/programming/comments/agent-workflows",
18 published_at="2026-06-28",
19 snippet="Developers shared concrete agent workflows.",
20 engagement={"score": 1543, "num_comments": 201},
21 )
22 x_item = schema.SourceItem(
23 item_id="x-1",
24 source="x",
25 title="Teams compare coding-agent review loops",
26 body="A thread compared review loops across several tools.",
27 url="https://x.com/example/status/123",
28 published_at="2026-07-02",
29 snippet="Teams compared how agents fit into code review.",
30 engagement={"likes": 800, "reposts": 50},
31 )
32 digg_item = schema.SourceItem(
33 item_id="digg-1",
34 source="digg",
35 title="Agents climb the Digg AI leaderboard",
36 body="A Digg cluster collected five posts from four authors.",
37 url="https://di.gg/ai/agent-leaderboard",
38 published_at="2026-07-05",
39 snippet="A small Digg cluster appeared low on the leaderboard.",
40 engagement={"postCount": 5, "uniqueAuthors": 4, "rank": 500, "rank_score": 0.0},
41 )
42 reddit_candidate = schema.Candidate(
43 candidate_id="candidate-reddit",
44 item_id=reddit_item.item_id,
45 source="reddit",
46 title=reddit_item.title,
47 url=reddit_item.url,
48 snippet=reddit_item.snippet,
49 subquery_labels=["workflows"],
50 native_ranks={"workflows:reddit": 1},
51 local_relevance=0.95,
52 freshness=85,
53 engagement=100,
54 source_quality=0.6,
55 rrf_score=0.02,
56 final_score=92,
57 cluster_id="cluster-workflows",
58 source_items=[reddit_item],
59 )
60 x_candidate = schema.Candidate(
61 candidate_id="candidate-x",
62 item_id=x_item.item_id,
63 source="x",
64 title=x_item.title,
65 url=x_item.url,
66 snippet=x_item.snippet,
67 subquery_labels=["reviews"],
68 native_ranks={"reviews:x": 1},
69 local_relevance=0.88,
70 freshness=92,
71 engagement=80,
72 source_quality=0.68,
73 rrf_score=0.018,
74 final_score=84,
75 source_items=[x_item],
76 )
77 digg_candidate = schema.Candidate(
78 candidate_id="candidate-digg",
79 item_id=digg_item.item_id,
80 source="digg",
81 title=digg_item.title,
82 url=digg_item.url,
83 snippet=digg_item.snippet,
84 subquery_labels=["leaderboard"],
85 native_ranks={"leaderboard:digg": 500},
86 local_relevance=0.78,
87 freshness=75,
88 engagement=5,
89 source_quality=0.6,
90 rrf_score=0.01,
91 final_score=70,
92 cluster_id="cluster-digg",
93 source_items=[digg_item],
94 )
95 return schema.Report(
96 topic="AI coding agents",
97 range_from="2026-06-10",
98 range_to="2026-07-10",
99 generated_at="2026-07-10T00:00:00+00:00",
100 provider_runtime=schema.ProviderRuntime(
101 reasoning_provider="local",
102 planner_model="fixture-planner",
103 rerank_model="fixture-reranker",
104 ),
105 query_plan=schema.QueryPlan(
106 intent="research",
107 freshness_mode="strict_recent",
108 cluster_mode="story",
109 raw_topic="AI coding agents",
110 subqueries=[
111 schema.SubQuery(
112 label="workflows",
113 search_query="AI coding agent workflows",
114 ranking_query="How are developers using AI coding agents?",
115 sources=["reddit"],
116 )
117 ],
118 source_weights={"reddit": 1.0, "x": 0.8},
119 ),
120 clusters=[
121 schema.Cluster(
122 cluster_id="cluster-workflows",
123 title="Agents move into daily coding workflows",
124 candidate_ids=[reddit_candidate.candidate_id],
125 representative_ids=[reddit_candidate.candidate_id],
126 sources=["reddit"],
127 score=92,
128 ),
129 schema.Cluster(
130 cluster_id="cluster-reviews",
131 title="Teams compare coding-agent review loops",
132 candidate_ids=[x_candidate.candidate_id],
133 representative_ids=[x_candidate.candidate_id],
134 sources=["x"],
135 score=84,
136 ),
137 schema.Cluster(
138 cluster_id="cluster-digg",
139 title="Agents climb the Digg AI leaderboard",
140 candidate_ids=[digg_candidate.candidate_id],
141 representative_ids=[digg_candidate.candidate_id],
142 sources=["digg"],
143 score=70,
144 ),
145 ],
146 ranked_candidates=[reddit_candidate, x_candidate, digg_candidate],
147 items_by_source={"reddit": [reddit_item], "x": [x_item], "digg": [digg_item]},
148 errors_by_source={
149 "youtube": "HTTP 429",
150 "github": "HTTP 401",
151 "grounding": "DNS failure",
152 },
153 source_status={
154 "reddit": schema.SourceOutcome(source="reddit", state=health.OK, items_returned=1),
155 "x": schema.SourceOutcome(source="x", state=health.OK, items_returned=1),
156 "digg": schema.SourceOutcome(source="digg", state=health.OK, items_returned=1),
157 "hackernews": schema.SourceOutcome(source="hackernews", state=schema.NO_RESULTS),
158 "youtube": schema.SourceOutcome(source="youtube", state=schema.RATE_LIMITED),
159 "grounding": schema.SourceOutcome(source="grounding", state=schema.UNREACHABLE),
160 "github": schema.SourceOutcome(source="github", state=schema.AUTH_FAILED),
161 },
162 )
163
164
165 def test_agent_export_matches_v1_2_golden_contract():
166 expected = json.loads(GOLDEN.read_text(encoding="utf-8"))
167
168 assert schema.to_agent_export(_report()) == expected
169
170
171 def test_agent_export_maps_per_run_source_outcomes_to_states():
172 exported = schema.to_agent_export(_report())
173
174 assert exported["source_status"] == {
175 "digg": "ok",
176 "github": "auth-failed",
177 "grounding": "unreachable",
178 "hackernews": "no-results",
179 "reddit": "ok",
180 "x": "ok",
181 "youtube": "rate-limited",
182 }
183
184
185 def test_agent_export_uses_digg_post_count_not_rank_for_cluster_engagement():
186 exported = schema.to_agent_export(_report())
187
188 assert exported["clusters"][2]["engagement_total"] == 5
189
190
191 def test_agent_export_excludes_non_counter_metadata_from_cluster_engagement():
192 report = _report()
193 report.ranked_candidates[0].source = "web"
194 report.ranked_candidates[0].source_items[0].source = "web"
195 report.ranked_candidates[0].source_items[0].engagement = {
196 "views": 5,
197 "rank": 500,
198 "rank_score": 400,
199 "ranking_score": 300,
200 "score": 200,
201 "upvote_ratio": 0.95,
202 "rating": 4.9,
203 "trustScore": 3.4,
204 }
205
206 exported = schema.to_agent_export(report)
207
208 assert exported["clusters"][0]["engagement_total"] == 5
209
210
211 def test_raw_profile_is_byte_identical_to_legacy_report_dump():
212 report = _report()
213 legacy = json.dumps(schema.to_dict(report), indent=2, sort_keys=True)
214
215 assert cli.emit_output(report, "json", json_profile="raw") == legacy
216
217
218 def test_raw_comparison_profile_is_byte_identical_to_legacy_wrapper():
219 report = _report()
220 reports = [("AI coding agents", report)]
221 legacy = json.dumps(
222 {
223 "comparison": True,
224 "entities": ["AI coding agents"],
225 "reports": [{"entity": "AI coding agents", "report": schema.to_dict(report)}],
226 },
227 indent=2,
228 sort_keys=True,
229 )
230
231 assert cli.emit_comparison_output(reports, "json", json_profile="raw") == legacy
232
233
234 def test_json_profile_parser_defaults_to_agent_and_accepts_raw():
235 parser = cli.build_parser()
236
237 assert parser.parse_args(["topic", "--emit=json"]).json_profile == "agent"
238 assert parser.parse_args(["topic", "--emit=json", "--json-profile=raw"]).json_profile == "raw"
239
240
241 def _reach_candidate(source, engagement):
242 item = schema.SourceItem(
243 item_id=f"{source}-reach-1",
244 source=source,
245 title="reach test",
246 body="reach test body",
247 url=f"https://example.com/{source}/reach",
248 published_at="2026-07-05",
249 snippet="reach test snippet",
250 engagement=engagement,
251 )
252 return schema.Candidate(
253 candidate_id=f"candidate-{source}-reach",
254 item_id=item.item_id,
255 source=source,
256 title=item.title,
257 url=item.url,
258 snippet=item.snippet,
259 subquery_labels=["primary"],
260 native_ranks={f"primary:{source}": 1},
261 local_relevance=0.5,
262 freshness=50,
263 engagement=10,
264 source_quality=0.5,
265 rrf_score=0.01,
266 final_score=50,
267 cluster_id="cluster-reach",
268 source_items=[item],
269 )
270
271
272 def test_headline_engagement_excludes_author_reach_for_stocktwits():
273 candidate = _reach_candidate(
274 "stocktwits", {"likes": 12, "reshares": 3, "followers": 250000}
275 )
276 assert schema._headline_engagement(candidate) == 12.0
277
278
279 def test_headline_engagement_excludes_followers_generically():
280 candidate = _reach_candidate(
281 "linkedin", {"reactions": 40, "followers": 90000}
282 )
283 assert schema._headline_engagement(candidate) == 40.0
284
284 lines PYTHON