返回 last30days-skill
test_reddit_transport_outcomes.py
根目录 / tests / test_reddit_transport_outcomes.py
1 """Reddit transport failures must not be reported as a clean no-results.
2
3 Regression coverage for issue #899: on a datacenter egress Reddit answers
4 429/403 on the keyless lanes, the adapters swallow that into an empty list, and
5 the run used to export ``"reddit": "no-results"`` with ``doctor --postmortem``
6 printing "No failures on the last run." These tests drive the real pipeline and
7 the real postmortem renderer over a mocked socket, so the whole chain --
8 capture sink -> source outcome -> postmortem bucket -- stays honest.
9 """
10
11 import urllib.error
12 from unittest import mock
13
14 from lib import doctor, pipeline, schema
15
16
17 def _plan(source):
18 return {
19 "intent": "general",
20 "freshness_mode": "balanced_recent",
21 "cluster_mode": "none",
22 "source_weights": {source: 1.0},
23 "subqueries": [{
24 "label": "primary",
25 "search_query": "claude code user feedback",
26 "ranking_query": "claude code user feedback",
27 "sources": [source],
28 }],
29 }
30
31
32 def _run_reddit_against(error):
33 runtime = schema.ProviderRuntime("local", "test-planner", "test-reranker")
34 with mock.patch.object(
35 pipeline.providers, "resolve_runtime", return_value=(runtime, mock.Mock())
36 ), mock.patch.object(
37 pipeline, "available_sources", return_value=["reddit"]
38 ), mock.patch("lib.http.time.sleep"), mock.patch(
39 "lib.http.urllib.request.urlopen", side_effect=error
40 ):
41 return pipeline.run(
42 topic="claude code user feedback",
43 config={"EXCLUDE_SOURCES": ""},
44 depth="quick",
45 requested_sources=["reddit"],
46 mock=False,
47 as_of_date="2026-07-10",
48 external_plan=_plan("reddit"),
49 )
50
51
52 def _postmortem_from(report):
53 return {
54 "engine_version": "test",
55 "mode": "postmortem",
56 "present": True,
57 "topic": report.topic,
58 "at": report.generated_at,
59 "outcomes": {
60 source: schema.to_dict(outcome)
61 for source, outcome in report.source_status.items()
62 },
63 }
64
65
66 def test_reddit_rate_limit_is_not_reported_as_clean_no_results():
67 report = _run_reddit_against(
68 urllib.error.HTTPError(
69 "https://www.reddit.com/search.rss", 429, "Too Many Requests", {}, None
70 )
71 )
72
73 outcome = report.source_status["reddit"]
74 assert outcome.state == schema.RATE_LIMITED
75 assert "429" in (outcome.detail or "")
76
77
78 def test_reddit_block_is_not_reported_as_clean_no_results():
79 report = _run_reddit_against(
80 urllib.error.HTTPError(
81 "https://www.reddit.com/search.rss", 403, "Blocked", {}, None
82 )
83 )
84
85 # 403 lands on auth-failed via http.classify_failure. The point of the test
86 # is that a blocked host is a failure state at all, not which noun it gets.
87 assert report.source_status["reddit"].state != schema.NO_RESULTS
88
89
90 def test_postmortem_does_not_claim_success_after_a_reddit_block():
91 report = _run_reddit_against(
92 urllib.error.HTTPError(
93 "https://www.reddit.com/search.rss", 429, "Too Many Requests", {}, None
94 )
95 )
96
97 text = doctor.render_postmortem_text(_postmortem_from(report))
98
99 assert "No failures on the last run." not in text
100 assert "Succeeded: reddit" not in text
101 assert "Failed:" in text
102 assert schema.RATE_LIMITED in text
103
103 lines PYTHON