返回 last30days-skill
test_freshness.py
根目录 / tests / test_freshness.py
1 import json
2
3 import last30days as cli
4 from lib import freshness, github, health, hosted, http, planner, polymarket, render, schema, stocktwits
5
6
7 def _report(*items: schema.SourceItem) -> schema.Report:
8 candidates = []
9 for index, item in enumerate(items, start=1):
10 candidates.append(
11 schema.Candidate(
12 candidate_id=f"candidate-{index}",
13 item_id=item.item_id,
14 source=item.source,
15 title=item.title,
16 url=item.url,
17 snippet=item.snippet,
18 subquery_labels=["primary"],
19 native_ranks={f"primary:{item.source}": index},
20 local_relevance=0.9,
21 freshness=90,
22 engagement=10,
23 source_quality=0.8,
24 rrf_score=0.02,
25 final_score=90,
26 source_items=[item],
27 )
28 )
29 return schema.Report(
30 topic="freshness fixture",
31 range_from="2026-06-10",
32 range_to="2026-07-10",
33 generated_at="2026-07-10T12:00:00Z",
34 provider_runtime=schema.ProviderRuntime(
35 reasoning_provider="local",
36 planner_model="fixture",
37 rerank_model="fixture",
38 ),
39 query_plan=schema.QueryPlan(
40 intent="research",
41 freshness_mode="strict_recent",
42 cluster_mode="story",
43 raw_topic="freshness fixture",
44 subqueries=[
45 schema.SubQuery(
46 label="primary",
47 search_query="freshness fixture",
48 ranking_query="freshness fixture",
49 sources=[item.source for item in items] or ["reddit"],
50 )
51 ],
52 source_weights={item.source: 1.0 for item in items},
53 ),
54 clusters=[],
55 ranked_candidates=candidates,
56 items_by_source={
57 source: [item for item in items if item.source == source]
58 for source in {item.source for item in items}
59 },
60 errors_by_source={},
61 source_status={
62 source: schema.SourceOutcome(source=source, state=health.OK, items_returned=1)
63 for source in {item.source for item in items}
64 },
65 )
66
67
68 def _item(source: str, *, title: str, snippet: str = "", **kwargs) -> schema.SourceItem:
69 return schema.SourceItem(
70 item_id=kwargs.pop("item_id", f"{source}-1"),
71 source=source,
72 title=title,
73 body=kwargs.pop("body", snippet),
74 url=kwargs.pop("url", f"https://example.com/{source}/1"),
75 published_at=kwargs.pop("published_at", "2026-07-09T12:00:00Z"),
76 snippet=snippet,
77 **kwargs,
78 )
79
80
81 def test_extract_claims_is_conservative_and_structurally_grounded():
82 prose = _item(
83 "reddit",
84 title="10 ways teams discussed a launch",
85 snippet="The post has 2,000 votes and mentions 2026-08-01 in passing.",
86 )
87 market = _item(
88 "polymarket",
89 title="Will the bill pass?",
90 metadata={
91 "event_id": "123",
92 "outcome_prices": [["Yes", 0.42], ["No", 0.58]],
93 "end_date": "2026-11-03",
94 },
95 )
96
97 claims = freshness.extract_claims(_report(prose, market))
98
99 assert [claim.datum_kind for claim in claims] == [
100 "polymarket_probability",
101 "polymarket_probability",
102 "polymarket_end_date",
103 ]
104 assert all(claim.source_item_id == market.item_id for claim in claims)
105
106
107 def test_verify_report_assigns_current_and_stale_for_each_point_source():
108 market = _item(
109 "polymarket",
110 title="Will the bill pass?",
111 metadata={"outcome_prices": [["Yes", 0.42]], "end_date": "2026-11-03"},
112 )
113 github = _item(
114 "github",
115 title="owner/repo (1K stars)",
116 url="https://github.com/owner/repo",
117 container="owner/repo",
118 engagement={"stars": 1000},
119 )
120 stocktwits = _item(
121 "stocktwits",
122 title="$ACME traders debate earnings",
123 container="ACME",
124 metadata={
125 "symbol": "ACME",
126 "sentiment_aggregate": {"pct_bullish": 60},
127 },
128 )
129 report = _report(market, github, stocktwits)
130
131 verdicts = freshness.verify_report(
132 report,
133 checked_at="2026-07-10T13:00:00Z",
134 refetchers={
135 "polymarket": lambda _item, key: {
136 "value": 0.47 if key == "Yes" else "2026-11-03",
137 "values": {"Yes": 0.47, "end_date": "2026-11-03"},
138 "url": "https://polymarket.com/event/bill",
139 "timestamp": "2026-07-10T12:59:00Z",
140 },
141 "github": lambda _item, _key: {"value": 1000, "url": _item.url},
142 "stocktwits": lambda _item, _key: {"value": 55, "url": _item.url},
143 },
144 )
145
146 by_kind = {claim.datum_kind: claim for claim in freshness.extract_claims(report)}
147 by_id = {verdict.claim_id: verdict for verdict in verdicts}
148 assert by_id[by_kind["polymarket_probability"].claim_id].verdict == "stale"
149 assert by_id[by_kind["polymarket_end_date"].claim_id].verdict == "current"
150 assert by_id[by_kind["github_stars"].claim_id].verdict == "current"
151 stock_verdict = by_id[by_kind["stocktwits_bullish_pct"].claim_id]
152 assert stock_verdict.verdict == "stale"
153 assert stock_verdict.original_value == 60
154 assert stock_verdict.current_value == 55
155 assert report.freshness_verdicts == verdicts
156
157
158 def test_duplicate_polymarket_labels_keep_distinct_refetch_identity():
159 market = _item(
160 "polymarket",
161 title="What price will Bitcoin hit?",
162 metadata={"outcome_prices": [["Bitcoin", 0.86], ["Bitcoin", 0.75]]},
163 )
164 report = _report(market)
165
166 claims = freshness.extract_claims(report)
167 verdicts = freshness.verify_report(
168 report,
169 refetchers={
170 "polymarket": lambda _item, key: {
171 "value": [0.86, 0.75][int(key.rsplit("\x1f", 1)[1])],
172 "values": {"Bitcoin\x1f0": 0.86, "Bitcoin\x1f1": 0.75},
173 "url": _item.url,
174 }
175 },
176 )
177
178 assert len({claim.datum_key for claim in claims}) == 2
179 assert [verdict.verdict for verdict in verdicts] == ["current", "current"]
180
181
182 def test_source_refetch_helpers_use_shared_http_wrapper(monkeypatch):
183 github_item = _item(
184 "github",
185 title="owner/repo",
186 url="https://github.com/owner/repo",
187 container="owner/repo",
188 )
189 monkeypatch.setattr(github.env, "read_secret_env", lambda _key: None)
190 monkeypatch.setattr(
191 github.http,
192 "request",
193 lambda *_args, **_kwargs: {
194 "stargazers_count": 123,
195 "html_url": github_item.url,
196 "updated_at": "2026-07-10T12:00:00Z",
197 },
198 )
199 assert github.refetch_datum(github_item, "stars")["value"] == 123
200
201 market_item = _item(
202 "polymarket",
203 title="Market",
204 url="https://polymarket.com/event/market-slug",
205 metadata={"event_id": "42"},
206 )
207 monkeypatch.setattr(
208 polymarket.http,
209 "request",
210 lambda *_args, **_kwargs: {"id": "42", "updatedAt": "2026-07-10T12:00:00Z"},
211 )
212 monkeypatch.setattr(
213 polymarket,
214 "parse_polymarket_response",
215 lambda _payload, **_kwargs: [{"outcome_prices": [["Yes", 0.51]], "end_date": "2026-12-01"}],
216 )
217 assert polymarket.refetch_datum(market_item, "Yes")["value"] == 0.51
218
219 stock_item = _item(
220 "stocktwits",
221 title="$ACME",
222 metadata={"symbol": "ACME"},
223 )
224 monkeypatch.setattr(
225 http,
226 "request",
227 lambda *_args, **_kwargs: {
228 "messages": [
229 {
230 "created_at": "2026-07-10T12:00:00Z",
231 "entities": {"sentiment": {"basic": "Bullish"}},
232 }
233 ]
234 },
235 )
236 assert stocktwits.refetch_datum(stock_item, "pct_bullish")["value"] == 100
237
238
239 def test_source_item_lookup_is_scoped_by_source_when_ids_collide():
240 github_item = _item(
241 "github",
242 item_id="42",
243 title="owner/repo",
244 url="https://github.com/owner/repo",
245 container="owner/repo",
246 engagement={"stars": 10},
247 )
248 market_item = _item(
249 "polymarket",
250 item_id="42",
251 title="Will it pass?",
252 metadata={"outcome_prices": [["Yes", 0.5]]},
253 )
254 report = _report(github_item, market_item)
255
256 verdicts = freshness.verify_report(
257 report,
258 refetchers={
259 "github": lambda item, _key: {"value": item.engagement["stars"], "url": item.url},
260 "polymarket": lambda item, _key: {
261 "value": item.metadata["outcome_prices"][0][1],
262 "url": item.url,
263 },
264 },
265 )
266
267 assert [verdict.verdict for verdict in verdicts] == ["current", "current"]
268
269
270 def test_degraded_source_is_unsupported_not_stale():
271 github = _item(
272 "github",
273 title="owner/repo (1K stars)",
274 url="https://github.com/owner/repo",
275 container="owner/repo",
276 engagement={"stars": 1000},
277 )
278 report = _report(github)
279 report.source_status["github"] = schema.SourceOutcome(
280 source="github", state=schema.UNREACHABLE,
281 )
282 called = False
283
284 def refetcher(_item, _key):
285 nonlocal called
286 called = True
287 return 999
288
289 verdict = freshness.verify_report(report, refetchers={"github": refetcher})[0]
290
291 assert verdict.verdict == "unsupported"
292 assert "unreachable" in (verdict.detail or "")
293 assert called is False
294
295
296 def test_newer_in_report_status_disagreement_is_contradicted():
297 original = _item(
298 "reddit",
299 title="Widget API is open",
300 published_at="2026-07-08T10:00:00Z",
301 )
302 report = _report(original)
303 contradiction = _item(
304 "grounding",
305 item_id="web-2",
306 title="Widget API is closed",
307 published_at="2026-07-09T10:00:00Z",
308 url="https://status.example.com/widget",
309 )
310 report.items_by_source["grounding"] = [contradiction]
311
312 verdict = freshness.verify_report(report)[0]
313
314 assert verdict.verdict == "contradicted"
315 assert verdict.current_value == "closed"
316 assert verdict.evidence_url == contradiction.url
317 assert verdict.evidence_timestamp == contradiction.published_at
318
319
320 def test_status_assertion_without_positive_rederivation_is_unsupported():
321 report = _report(_item("reddit", title="Widget API is open"))
322
323 verdict = freshness.verify_report(report)[0]
324
325 assert verdict.verdict == "unsupported"
326 assert "re-derived" in (verdict.detail or "")
327
328
329 def test_status_contradiction_requires_subject_bound_to_opposite_status():
330 original = _item(
331 "reddit",
332 title="Widget API is open",
333 published_at="2026-07-08T10:00:00Z",
334 )
335 report = _report(original)
336 report.items_by_source["grounding"] = [
337 _item(
338 "grounding",
339 item_id="web-2",
340 title="Widget API remains open while legacy access is closed",
341 published_at="2026-07-09T10:00:00Z",
342 )
343 ]
344
345 verdict = freshness.verify_report(report)[0]
346
347 assert verdict.verdict == "unsupported"
348
349
350 def test_verify_report_caches_one_point_refetch_per_source_key():
351 first = _item(
352 "stocktwits",
353 item_id="stock-1",
354 title="$ACME first post",
355 metadata={"symbol": "ACME", "sentiment_aggregate": {"pct_bullish": 60}},
356 )
357 second = _item(
358 "stocktwits",
359 item_id="stock-2",
360 title="$ACME second post",
361 metadata={"symbol": "ACME", "sentiment_aggregate": {"pct_bullish": 60}},
362 )
363 calls = []
364
365 verdicts = freshness.verify_report(
366 _report(first, second),
367 refetchers={
368 "stocktwits": lambda item, key: calls.append((item, key)) or {
369 "value": 60,
370 "url": item.url,
371 }
372 },
373 )
374
375 assert [verdict.verdict for verdict in verdicts] == ["current", "current"]
376 assert len(calls) == 1
377
378
379 def test_stocktwits_refetch_cache_separates_depth_and_date_populations():
380 default = _item(
381 "stocktwits",
382 item_id="stock-default",
383 title="$ACME default population",
384 metadata={
385 "symbol": "ACME",
386 "sentiment_aggregate": {"pct_bullish": 60},
387 "freshness_window": {
388 "depth": "default",
389 "from_date": "2026-07-01",
390 "to_date": "2026-07-10",
391 },
392 },
393 )
394 deep = _item(
395 "stocktwits",
396 item_id="stock-deep",
397 title="$ACME deep population",
398 metadata={
399 "symbol": "ACME",
400 "sentiment_aggregate": {"pct_bullish": 75},
401 "freshness_window": {
402 "depth": "deep",
403 "from_date": "2026-06-10",
404 "to_date": "2026-07-10",
405 },
406 },
407 )
408 calls = []
409
410 verdicts = freshness.verify_report(
411 _report(default, deep),
412 refetchers={
413 "stocktwits": lambda item, _key: calls.append(item.item_id) or {
414 "value": item.metadata["sentiment_aggregate"]["pct_bullish"],
415 "url": item.url,
416 }
417 },
418 )
419
420 assert [verdict.verdict for verdict in verdicts] == ["current", "current"]
421 assert calls == ["stock-default", "stock-deep"]
422
423
424 def test_stocktwits_refetch_reuses_retrieval_depth_and_date_window(monkeypatch):
425 item = _item(
426 "stocktwits",
427 title="$ACME",
428 metadata={
429 "symbol": "ACME",
430 "freshness_window": {
431 "depth": "deep",
432 "from_date": "2026-07-01",
433 "to_date": "2026-07-10",
434 },
435 },
436 )
437 calls = []
438
439 def fake_request(_method, _url, **kwargs):
440 calls.append(kwargs.get("params"))
441 if len(calls) == 1:
442 return {
443 "messages": [
444 {"id": 3, "created_at": "2026-07-10T12:00:00Z", "entities": {"sentiment": {"basic": "Bullish"}}},
445 {"id": 2, "created_at": "2026-06-30T12:00:00Z", "entities": {"sentiment": {"basic": "Bearish"}}},
446 ],
447 "cursor": {"more": True, "max": 2},
448 }
449 return {
450 "messages": [
451 {"id": 1, "created_at": "2026-07-09T12:00:00Z", "entities": {"sentiment": {"basic": "Bullish"}}},
452 ],
453 "cursor": {"more": False},
454 }
455
456 monkeypatch.setattr(http, "request", fake_request)
457
458 refreshed = stocktwits.refetch_datum(item, "pct_bullish")
459
460 assert refreshed["value"] == 100
461 assert calls == [None, {"max": 2}]
462
463
464 def test_unverified_drill_clears_inherited_freshness_verdicts(tmp_path, monkeypatch):
465 cached = _report(_item("reddit", title="Widget API is open"))
466 freshness.verify_report(cached)
467 merged = _report(_item("reddit", title="Widget API is open"))
468 merged.freshness_verdicts = list(cached.freshness_verdicts)
469 drill_report = _report(_item("reddit", title="Widget API is closed"))
470 monkeypatch.setattr(cli, "_load_last_report_cache", lambda *_args, **_kwargs: (cached, None, tmp_path / "last-report.json"))
471 monkeypatch.setattr(planner, "resolve_drill_clusters", lambda *_args, **_kwargs: [])
472 monkeypatch.setattr(planner, "build_drill_plan", lambda *_args, **_kwargs: cached.query_plan)
473 monkeypatch.setattr(cli.pipeline, "diagnose", lambda *_args, **_kwargs: {})
474 monkeypatch.setattr(cli.pipeline, "run", lambda **_kwargs: drill_report)
475 monkeypatch.setattr(cli.pipeline, "merge_drill_report", lambda *_args, **_kwargs: merged)
476 monkeypatch.setattr(cli, "_show_runtime_ui", lambda *_args, **_kwargs: None)
477 monkeypatch.setattr(cli, "_write_last_run", lambda *_args, **_kwargs: True)
478 monkeypatch.setattr(cli, "_render_save_and_print", lambda *_args, **_kwargs: 0)
479 args = cli.build_parser().parse_args(["--drill", "cluster 1", "--mock"])
480
481 assert cli._run_drill(args, {}) == 0
482 assert merged.freshness_verdicts == []
483
484
485 def test_polymarket_refetch_uses_complete_outcomes_and_one_event_snapshot(monkeypatch):
486 item = _item(
487 "polymarket",
488 title="Who will win?",
489 url="https://polymarket.com/event/winner",
490 metadata={
491 "event_id": "42",
492 "outcome_prices": [["Delta", 0.05]],
493 "end_date": "2026-12-01",
494 },
495 )
496 event = {
497 "id": "42",
498 "slug": "winner",
499 "title": "Who will win?",
500 "updatedAt": "2026-07-10T12:00:00Z",
501 "markets": [{
502 "active": True,
503 "closed": False,
504 "question": "Who will win?",
505 "liquidity": "100",
506 "outcomes": '["Alpha", "Beta", "Gamma", "Delta"]',
507 "outcomePrices": '["0.4", "0.3", "0.25", "0.05"]',
508 "endDate": "2026-12-01T00:00:00Z",
509 }],
510 }
511 calls = []
512 monkeypatch.setattr(
513 polymarket.http,
514 "request",
515 lambda *_args, **_kwargs: calls.append(1) or event,
516 )
517
518 verdicts = freshness.verify_report(_report(item))
519
520 assert [verdict.verdict for verdict in verdicts] == ["current", "current"]
521 assert len(calls) == 1
522
523
524 def test_polymarket_refetch_marks_resolved_market_movement_stale(monkeypatch):
525 item = _item(
526 "polymarket",
527 title="Will the bill pass?",
528 url="https://polymarket.com/event/bill",
529 metadata={
530 "event_id": "42",
531 "outcome_prices": [["Yes", 0.42]],
532 },
533 )
534 monkeypatch.setattr(
535 polymarket.http,
536 "request",
537 lambda *_args, **_kwargs: {
538 "id": "42",
539 "slug": "bill",
540 "title": "Will the bill pass?",
541 "active": False,
542 "closed": True,
543 "markets": [{
544 "active": False,
545 "closed": True,
546 "question": "Will the bill pass?",
547 "liquidity": "0",
548 "outcomes": '["Yes", "No"]',
549 "outcomePrices": '["1", "0"]',
550 }],
551 },
552 )
553
554 verdict = freshness.verify_report(_report(item))[0]
555
556 assert verdict.verdict == "stale"
557 assert verdict.current_value == 1.0
558
559
560 def test_github_refetch_reuses_resolved_gh_cli_token(monkeypatch):
561 item = _item(
562 "github",
563 title="owner/private",
564 url="https://github.com/owner/private",
565 container="owner/private",
566 )
567 seen_headers = []
568 monkeypatch.setattr(github, "_resolve_token", lambda: "gh-cli-token")
569 monkeypatch.setattr(
570 github.http,
571 "request",
572 lambda *_args, **kwargs: seen_headers.append(kwargs["headers"]) or {
573 "stargazers_count": 7,
574 "html_url": item.url,
575 },
576 )
577
578 github.refetch_datum(item, "stars")
579
580 assert seen_headers == [{
581 "Accept": "application/vnd.github+json",
582 "Authorization": "Bearer gh-cli-token",
583 }]
584
585
586 def test_early_return_paths_do_not_ignore_freshness_verification(tmp_path, monkeypatch, capsys):
587 monkeypatch.setenv("LAST30DAYS_API_KEY", "dummy-hosted-key")
588 monkeypatch.setenv("LAST30DAYS_API_BASE", "https://api.example.test")
589 monkeypatch.setattr(cli.env, "get_config", lambda **_kwargs: {})
590 parser = cli.build_parser()
591 hosted_args = parser.parse_args(["topic", "--verify-freshness"])
592
593 assert cli._main(parser, hosted_args, []) == 2
594 assert "not supported by the hosted backend" in capsys.readouterr().err
595
596 monkeypatch.delenv("LAST30DAYS_API_KEY")
597 monkeypatch.delenv("LAST30DAYS_API_BASE")
598 monkeypatch.setenv("LAST30DAYS_SKIP_PREFLIGHT", "1")
599 synthesis = tmp_path / "synthesis.md"
600 synthesis.write_text("# Synthesis", encoding="utf-8")
601 report = _report(_item("reddit", title="Widget API is open"))
602 calls = []
603 monkeypatch.setattr(cli.pipeline, "diagnose", lambda *_args, **_kwargs: {})
604 monkeypatch.setattr(cli, "_load_last_report_cache", lambda *_args, **_kwargs: (report, None, tmp_path / "last-report.json"))
605 monkeypatch.setattr(cli, "_verify_report_set", lambda *_args, **_kwargs: calls.append("verified"))
606 monkeypatch.setattr(cli, "_render_save_and_print", lambda *_args, **_kwargs: calls.append("rendered") or 0)
607 cached_args = parser.parse_args([
608 "topic", "--emit=html", f"--synthesis-file={synthesis}", "--verify-freshness",
609 ])
610
611 assert cli._main(parser, cached_args, []) == 0
612 assert calls == ["verified", "rendered"]
613
614
615 def test_hosted_configured_freshness_skips_unless_cli_explicitly_enables(monkeypatch, capsys):
616 monkeypatch.setenv("LAST30DAYS_API_KEY", "dummy-hosted-key")
617 monkeypatch.setenv("LAST30DAYS_API_BASE", "https://api.example.test")
618 monkeypatch.setattr(
619 cli.env,
620 "get_config",
621 lambda **_kwargs: {"LAST30DAYS_VERIFY_FRESHNESS": "on"},
622 )
623 calls = []
624 monkeypatch.setattr(
625 hosted,
626 "run_hosted",
627 lambda topic, depth, **_kwargs: calls.append((topic, depth)) or 0,
628 )
629 parser = cli.build_parser()
630
631 assert cli._main(parser, parser.parse_args(["topic"]), []) == 0
632 assert calls == [("topic", "default")]
633 assert capsys.readouterr().err == (
634 "hosted backend does not support freshness verification; skipping\n"
635 )
636 no_verify_args = parser.parse_args(["topic", "--no-verify-freshness"])
637 assert cli._freshness_enabled(
638 no_verify_args,
639 {"LAST30DAYS_VERIFY_FRESHNESS": "on"},
640 ) is False
641
642
643 def test_agent_export_includes_typed_claim_metadata():
644 item = _item("reddit", title="Widget API is open")
645 report = _report(item)
646 freshness.verify_report(report, checked_at="2026-07-10T13:00:00Z")
647
648 exported = schema.to_agent_export(report)
649
650 assert exported["schema_version"] == "1.2"
651 assert exported["freshness_verdicts"][0]["verdict"] == "unsupported"
652 assert exported["freshness_verdicts"][0]["source_item_id"] == item.item_id
653
654
655 def test_render_surfaces_inline_flag_and_freshness_footer_table():
656 item = _item(
657 "github",
658 title="owner/repo (1K stars)",
659 url="https://github.com/owner/repo",
660 container="owner/repo",
661 engagement={"stars": 1000},
662 )
663 report = _report(item)
664 report.clusters = [
665 schema.Cluster(
666 cluster_id="cluster-1",
667 title="Repository traction",
668 candidate_ids=["candidate-1"],
669 representative_ids=["candidate-1"],
670 sources=["github"],
671 score=90,
672 )
673 ]
674 report.ranked_candidates[0].cluster_id = "cluster-1"
675 freshness.verify_report(
676 report,
677 checked_at="2026-07-10T13:00:00Z",
678 refetchers={"github": lambda _item, _key: {"value": 1010, "url": _item.url}},
679 )
680
681 rendered = render.render_compact(report)
682
683 assert "[freshness:stale]" in rendered
684 assert "## Freshness Verification" in rendered
685 assert "(moved: 1,000 -> 1,010)" in rendered
686 assert "## Freshness Verification" in render.render_for_html(report)
687 assert "## Freshness Verification" in render.render_brief(report)
688
689
690 def test_post_hoc_path_loads_updates_and_rewrites_cache(tmp_path, monkeypatch, capsys):
691 item = _item("reddit", title="Widget API is open")
692 report = _report(item)
693 monkeypatch.setattr(cli.env, "CONFIG_DIR", tmp_path)
694 assert cli._write_last_run(report.topic, report)
695 before = json.loads((tmp_path / "last-report.json").read_text(encoding="utf-8"))
696 args = cli.build_parser().parse_args(["--verify-freshness", "--mock", "--emit=json"])
697
698 rc = cli._run_cached_freshness(args, {})
699
700 assert rc == 0
701 payload = json.loads((tmp_path / "last-report.json").read_text(encoding="utf-8"))
702 cached_verdicts = payload["reports"][0]["report"]["freshness_verdicts"]
703 assert cached_verdicts[0]["verdict"] == "unsupported"
704 assert payload["timestamp"] == before["timestamp"]
705 assert "Updated freshness verdicts" in capsys.readouterr().err
706
707
708 def test_opt_in_gating_accepts_flag_or_truthy_config():
709 parser = cli.build_parser()
710 default_args = parser.parse_args(["topic"])
711 flag_args = parser.parse_args(["topic", "--verify-freshness"])
712
713 assert cli._freshness_enabled(default_args, {}) is False
714 assert cli._freshness_enabled(default_args, {"LAST30DAYS_VERIFY_FRESHNESS": "on"}) is True
715 assert cli._freshness_enabled(flag_args, {}) is True
716
717
718 def test_mixed_event_prefers_active_markets(monkeypatch):
719 from types import SimpleNamespace
720 from lib import polymarket
721
722 event = {
723 "id": "ev1",
724 "title": "Mixed event",
725 "active": True,
726 "markets": [
727 {"id": "m-closed", "active": False, "closed": True,
728 "question": "Old market", "volume": "900000",
729 "outcomePrices": '["0.99","0.01"]', "outcomes": '["Yes","No"]'},
730 {"id": "m-active", "active": True, "closed": False,
731 "question": "Live market", "volume": "1000",
732 "outcomePrices": '["0.60","0.40"]', "outcomes": '["Yes","No"]',
733 "liquidity": "5000"},
734 ],
735 }
736 captured = {}
737 real_parse = polymarket.parse_polymarket_response
738
739 def spy(payload, **kwargs):
740 captured.update(kwargs)
741 return real_parse(payload, **kwargs)
742
743 monkeypatch.setattr(polymarket, "parse_polymarket_response", spy)
744 monkeypatch.setattr(polymarket.http, "request", lambda *a, **k: [event])
745 item = SimpleNamespace(metadata={"event_id": "ev1"}, url="")
746 try:
747 polymarket.refetch_datum(item, "probability")
748 except Exception:
749 pass
750 assert captured.get("include_closed") is False
751
752 # Fully resolved event falls back to closed markets.
753 captured.clear()
754 resolved = dict(event)
755 resolved["markets"] = [dict(event["markets"][0])]
756 monkeypatch.setattr(polymarket.http, "request", lambda *a, **k: [resolved])
757 try:
758 polymarket.refetch_datum(item, "probability")
759 except Exception:
760 pass
761 assert captured.get("include_closed") is True
762
763
764 def test_slug_refetch_requires_identity_match(monkeypatch):
765 from types import SimpleNamespace
766 import pytest
767 from lib import polymarket
768
769 wrong = {"id": "222", "slug": "different-market", "title": "Other",
770 "markets": [{"id": "m1", "active": True, "closed": False,
771 "question": "q", "volume": "10",
772 "outcomePrices": '["0.5","0.5"]', "outcomes": '["Yes","No"]',
773 "liquidity": "10"}]}
774 monkeypatch.setattr(polymarket.http, "request", lambda *a, **k: [wrong])
775 item = SimpleNamespace(
776 metadata={}, item_id="777",
777 url="https://polymarket.com/event/fed-rate-cut-2026",
778 )
779 with pytest.raises(KeyError):
780 polymarket.refetch_datum(item, "probability")
781
782 right = dict(wrong)
783 right["slug"] = "fed-rate-cut-2026"
784 right["id"] = "777"
785 monkeypatch.setattr(polymarket.http, "request", lambda *a, **k: [wrong, right])
786 values = polymarket.refetch_datum(item, "Yes")
787 assert values is not None
788
789
790 def _stale_fixture_report() -> schema.Report:
791 market = _item(
792 "polymarket",
793 title="Will the bill pass?",
794 metadata={"outcome_prices": [["Yes", 0.42]]},
795 )
796 gh = _item(
797 "github",
798 title="owner/repo (1K stars)",
799 url="https://github.com/owner/repo",
800 container="owner/repo",
801 engagement={"stars": 1000},
802 )
803 stock = _item(
804 "stocktwits",
805 title="$ACME traders debate earnings",
806 container="ACME",
807 metadata={"symbol": "ACME", "sentiment_aggregate": {"pct_bullish": 60}},
808 )
809 return _report(market, gh, stock)
810
811
812 def test_stale_detail_carries_original_and_current_values():
813 report = _stale_fixture_report()
814 verdicts = freshness.verify_report(
815 report,
816 checked_at="2026-07-10T13:00:00Z",
817 refetchers={
818 "polymarket": lambda _item, _key: {"value": 0.47, "url": _item.url},
819 "github": lambda _item, _key: {"value": 1200, "url": _item.url},
820 "stocktwits": lambda _item, _key: {"value": 55, "url": _item.url},
821 },
822 )
823
824 details = {verdict.source: verdict.detail for verdict in verdicts}
825 assert details["polymarket"] == "moved: 42% -> 47%"
826 assert details["github"] == "moved: 1,000 -> 1,200"
827 assert details["stocktwits"] == "moved: 60% -> 55%"
828
829
830 def test_current_verdicts_keep_detail_none():
831 report = _stale_fixture_report()
832 verdicts = freshness.verify_report(
833 report,
834 checked_at="2026-07-10T13:00:00Z",
835 refetchers={
836 "polymarket": lambda _item, _key: {"value": 0.42, "url": _item.url},
837 "github": lambda _item, _key: {"value": 1000, "url": _item.url},
838 "stocktwits": lambda _item, _key: {"value": 60, "url": _item.url},
839 },
840 )
841
842 assert all(verdict.verdict == "current" for verdict in verdicts)
843 assert all(verdict.detail is None for verdict in verdicts)
844
845
846 def test_agent_export_carries_enriched_stale_detail():
847 report = _stale_fixture_report()
848 freshness.verify_report(
849 report,
850 checked_at="2026-07-10T13:00:00Z",
851 refetchers={
852 "polymarket": lambda _item, _key: {"value": 0.47, "url": _item.url},
853 "github": lambda _item, _key: {"value": 1200, "url": _item.url},
854 "stocktwits": lambda _item, _key: {"value": 55, "url": _item.url},
855 },
856 )
857
858 exported = schema.to_agent_export(report)
859 stale = [v for v in exported["freshness_verdicts"] if v["verdict"] == "stale"]
860 assert stale and all(v["detail"].startswith("moved: ") for v in stale)
861
862
863 def test_zero_claim_verification_notes_outcome_on_stderr(capsys):
864 prose_only = _report(_item("reddit", title="10 ways teams discussed a launch"))
865
866 cli._verify_report_set(prose_only, None, allow_network=False)
867
868 err = capsys.readouterr().err
869 assert "no re-checkable claims" in err
870 assert prose_only.freshness_verdicts == []
871
872
873 def test_zero_claim_note_prints_once_across_entity_reports(capsys):
874 first = _report(_item("reddit", title="Thread one", item_id="reddit-a"))
875 second = _report(_item("reddit", title="Thread two", item_id="reddit-b"))
876
877 cli._verify_report_set(first, [("one", first), ("two", second)], allow_network=False)
878
879 err = capsys.readouterr().err
880 assert err.count("no re-checkable claims") == 1
881
882
883 def test_zero_claim_note_absent_when_any_report_has_verdicts(capsys):
884 market = _report(
885 _item("polymarket", title="Will it?", metadata={"outcome_prices": [["Yes", 0.42]]})
886 )
887
888 cli._verify_report_set(market, None, allow_network=False)
889
890 err = capsys.readouterr().err
891 assert "no re-checkable claims" not in err
892 assert market.freshness_verdicts # unsupported verdicts still count as outcomes
893
894
895 def _candidate_star_report(*, repos: dict[str, int], item_source: str = "reddit") -> schema.Report:
896 """A report whose only star facts live in candidate enrichment metadata."""
897 primary = _item(item_source, title="Agent tooling thread")
898 report = _report(primary)
899 report.ranked_candidates[0].metadata["github_stars"] = dict(repos)
900 return report
901
902
903 def test_candidate_star_claims_dispatch_despite_non_github_primary_item():
904 """Regression: star facts attached during post-rerank enrichment carried
905 no claims because extraction read only item-level engagement, so a
906 GitHub-flavored run produced zero verdicts."""
907 report = _candidate_star_report(repos={"a/b": 100})
908
909 calls: list[tuple[object, str]] = []
910
911 def fake_refetch(item, key):
912 calls.append((item, key))
913 return {"value": 100, "url": f"https://github.com/{key}"}
914
915 verdicts = freshness.verify_report(
916 report,
917 checked_at="2026-07-11T13:00:00Z",
918 refetchers={"github": fake_refetch},
919 )
920
921 assert [verdict.verdict for verdict in verdicts] == ["current"]
922 assert calls == [(None, "a/b")]
923 assert verdicts[0].source == "github"
924 assert verdicts[0].source_url == "https://github.com/a/b"
925
926
927 def test_candidate_star_claim_moved_value_is_stale_with_detail():
928 report = _candidate_star_report(repos={"a/b": 100})
929
930 verdicts = freshness.verify_report(
931 report,
932 checked_at="2026-07-11T13:00:00Z",
933 refetchers={"github": lambda _item, key: {"value": 250, "url": f"https://github.com/{key}"}},
934 )
935
936 assert [verdict.verdict for verdict in verdicts] == ["stale"]
937 assert verdicts[0].detail == "moved: 100 -> 250"
938 assert verdicts[0].original_value == 100
939 assert verdicts[0].current_value == 250
940
941
942 def test_repo_claimed_at_item_level_is_not_claimed_again_from_metadata():
943 gh_item = _item(
944 "github",
945 title="a/b (100 stars)",
946 url="https://github.com/a/b",
947 container="a/b",
948 engagement={"stars": 100},
949 )
950 report = _report(gh_item)
951 report.ranked_candidates[0].metadata["github_stars"] = {"a/b": 100}
952
953 claims = [
954 claim for claim in freshness.extract_claims(report)
955 if claim.datum_kind == "github_stars"
956 ]
957
958 assert len(claims) == 1
959 assert claims[0].datum_key == "stars"
960
961
962 def test_multiple_candidates_citing_same_repo_share_one_refetch():
963 first = _item("reddit", title="Thread one", item_id="reddit-a")
964 second = _item("reddit", title="Thread two", item_id="reddit-b")
965 report = _report(first, second)
966 for candidate in report.ranked_candidates:
967 candidate.metadata["github_stars"] = {"a/b": 100}
968
969 calls: list[str] = []
970
971 def fake_refetch(_item, key):
972 calls.append(key)
973 return {"value": 100, "url": f"https://github.com/{key}"}
974
975 verdicts = freshness.verify_report(
976 report,
977 checked_at="2026-07-11T13:00:00Z",
978 refetchers={"github": fake_refetch},
979 )
980
981 assert [verdict.verdict for verdict in verdicts] == ["current", "current"]
982 assert calls == ["a/b"]
983
984
985 def test_candidate_star_claims_bypass_github_source_outcome_gate():
986 report = _candidate_star_report(repos={"a/b": 100})
987 report.source_status["github"] = schema.SourceOutcome(
988 source="github", state="timeout", items_returned=0,
989 )
990
991 verdicts = freshness.verify_report(
992 report,
993 checked_at="2026-07-11T13:00:00Z",
994 refetchers={"github": lambda _item, key: {"value": 100, "url": f"https://github.com/{key}"}},
995 )
996
997 assert [verdict.verdict for verdict in verdicts] == ["current"]
998
999
1000 def test_candidates_without_star_metadata_emit_no_claims():
1001 report = _report(_item("reddit", title="Agent tooling thread"))
1002
1003 assert freshness.extract_claims(report) == []
1004
1005
1006 def test_refetch_datum_accepts_repo_slug_datum_key(monkeypatch):
1007 seen_urls: list[str] = []
1008 monkeypatch.setattr(github, "_resolve_token", lambda: "")
1009 monkeypatch.setattr(
1010 github.http,
1011 "request",
1012 lambda _method, url, **_kwargs: seen_urls.append(url) or {
1013 "stargazers_count": 42,
1014 "html_url": "https://github.com/a/b",
1015 },
1016 )
1017
1018 refreshed = github.refetch_datum(None, "a/b")
1019
1020 assert seen_urls == ["https://api.github.com/repos/a/b"]
1021 assert refreshed["value"] == 42
1022
1023
1024 def test_unsupported_verdicts_carry_no_fabricated_evidence():
1025 report = _report(_item("reddit", title="Widget API is open"))
1026
1027 verdicts = freshness.verify_report(report, checked_at="2026-07-11T13:00:00Z")
1028
1029 assert [verdict.verdict for verdict in verdicts] == ["unsupported"]
1030 assert verdicts[0].evidence_url == ""
1031 assert verdicts[0].evidence_timestamp is None
1032 assert verdicts[0].source_url # provenance stays on the source fields
1033
1034
1035 def test_rendered_table_shows_unsupported_reason_from_detail():
1036 report = _report(_item("reddit", title="Widget API is open"))
1037 freshness.verify_report(report, checked_at="2026-07-11T13:00:00Z")
1038
1039 rendered = render.render_compact(report)
1040
1041 assert "(Status could not be positively re-derived from a current source)" in rendered
1042
1043
1044 def test_agent_export_results_expose_candidate_id_join_key():
1045 report = _candidate_star_report(repos={"a/b": 100})
1046 freshness.verify_report(
1047 report,
1048 checked_at="2026-07-11T13:00:00Z",
1049 refetchers={"github": lambda _item, key: {"value": 100, "url": f"https://github.com/{key}"}},
1050 )
1051
1052 exported = schema.to_agent_export(report)
1053
1054 result_ids = {result["candidate_id"] for result in exported["results"]}
1055 verdict_ids = {verdict["candidate_id"] for verdict in exported["freshness_verdicts"]}
1056 assert verdict_ids and verdict_ids <= result_ids
1057
1058
1059 def test_other_candidates_item_level_claim_does_not_suppress_enriched_claim():
1060 gh_item = _item(
1061 "github",
1062 title="a/b (100 stars)",
1063 url="https://github.com/a/b",
1064 container="a/b",
1065 engagement={"stars": 100},
1066 )
1067 reddit_item = _item("reddit", title="Thread about a/b", item_id="reddit-a")
1068 report = _report(gh_item, reddit_item)
1069 report.ranked_candidates[1].metadata["github_stars"] = {"a/b": 100}
1070
1071 calls: list[str] = []
1072
1073 def fake_refetch(_item, key):
1074 calls.append(key)
1075 return {"value": 100, "url": "https://github.com/a/b"}
1076
1077 verdicts = freshness.verify_report(
1078 report,
1079 checked_at="2026-07-11T13:00:00Z",
1080 refetchers={"github": fake_refetch},
1081 )
1082
1083 # Both candidates get their own verdict, sharing one repo snapshot.
1084 assert [verdict.verdict for verdict in verdicts] == ["current", "current"]
1085 assert {verdict.candidate_id for verdict in verdicts} == {"candidate-1", "candidate-2"}
1086 assert calls == ["stars"]
1087
1088
1089 def test_slug_refetch_rejects_same_slug_different_event_id(monkeypatch):
1090 from types import SimpleNamespace
1091 import pytest
1092 from lib import polymarket
1093
1094 recreated = {"id": "999", "slug": "fed-rate-cut-2026", "title": "Recreated",
1095 "markets": [{"id": "m1", "active": True, "closed": False,
1096 "question": "q", "volume": "10",
1097 "outcomePrices": '["0.5","0.5"]', "outcomes": '["Yes","No"]',
1098 "liquidity": "10"}]}
1099 monkeypatch.setattr(polymarket.http, "request", lambda *a, **k: [recreated])
1100 cached = SimpleNamespace(
1101 metadata={}, item_id="123",
1102 url="https://polymarket.com/event/fed-rate-cut-2026",
1103 )
1104 with pytest.raises(KeyError):
1105 polymarket.refetch_datum(cached, "Yes")
1106
1107 original = dict(recreated)
1108 original["id"] = "123"
1109 monkeypatch.setattr(polymarket.http, "request", lambda *a, **k: [recreated, original])
1110 assert polymarket.refetch_datum(cached, "Yes") is not None
1111
1112
1113 def test_slug_refetch_fails_closed_without_any_event_identity(monkeypatch):
1114 """A synthetic PM<N> parse id carries no identity, so slug equality alone
1115 must not verify a claim; the refetch fails closed to unsupported."""
1116 from types import SimpleNamespace
1117 import pytest
1118 from lib import polymarket
1119
1120 calls: list[object] = []
1121 monkeypatch.setattr(
1122 polymarket.http, "request", lambda *a, **k: calls.append(a) or []
1123 )
1124 cached = SimpleNamespace(
1125 metadata={}, item_id="PM3",
1126 url="https://polymarket.com/event/fed-rate-cut-2026",
1127 )
1128 with pytest.raises(ValueError):
1129 polymarket.refetch_datum(cached, "Yes")
1130 assert calls == [] # fails closed before any network request
1131
1131 lines PYTHON