返回 last30days-skill
test_http_fixtures.py
根目录 / tests / test_http_fixtures.py
1 from __future__ import annotations
2
3 import json
4 from unittest.mock import MagicMock
5
6 import pytest
7
8 import last30days as cli
9 from lib import http, pipeline, schema
10
11
12 def _response(body: str):
13 response = MagicMock()
14 response.__enter__.return_value = response
15 response.__exit__.return_value = False
16 response.read.return_value = body.encode("utf-8")
17 response.status = 200
18 return response
19
20
21 def test_http_recording_scrubs_credentials_and_replays_offline(tmp_path, monkeypatch):
22 monkeypatch.setattr(http.urllib.request, "urlopen", lambda *_args, **_kwargs: _response('{"items": [{"url": "https://example.test/item"}]}'))
23 fixture_dir = tmp_path / "fixture"
24
25 with http.recording_requests(fixture_dir):
26 live = http.get("https://api.example.test/search?api_key=live-secret&q=agents")
27
28 fixture_text = (fixture_dir / "http.json").read_text(encoding="utf-8")
29 assert "live-secret" not in fixture_text
30 assert "%3Credacted%3E" in fixture_text
31
32 monkeypatch.setattr(
33 http.urllib.request,
34 "urlopen",
35 lambda *_args, **_kwargs: pytest.fail("fixture replay attempted the network"),
36 )
37 with http.replaying_requests(fixture_dir):
38 replayed = http.get("https://api.example.test/search?api_key=another-secret&q=agents")
39
40 assert replayed == live
41
42
43 def test_http_recording_redacts_credentials_echoed_in_response_values(tmp_path, monkeypatch):
44 monkeypatch.setattr(
45 http.urllib.request,
46 "urlopen",
47 lambda *_args, **_kwargs: _response('{"echo": "Bearer live-secret"}'),
48 )
49 fixture_dir = tmp_path / "fixture"
50
51 with http.recording_requests(fixture_dir):
52 http.get(
53 "https://api.example.test/profile",
54 headers={"Authorization": "Bearer live-secret"},
55 )
56
57 fixture_text = (fixture_dir / "http.json").read_text(encoding="utf-8")
58 assert "live-secret" not in fixture_text
59 assert '"echo": "<redacted>"' in fixture_text
60
61
62 def test_aborted_recording_does_not_overwrite_existing_fixture(tmp_path):
63 fixture = tmp_path / "http.json"
64 fixture.write_text("existing fixture\n", encoding="utf-8")
65
66 with pytest.raises(RuntimeError, match="capture failed"):
67 with http.recording_requests(fixture):
68 raise RuntimeError("capture failed")
69
70 assert fixture.read_text(encoding="utf-8") == "existing fixture\n"
71
72
73 def test_http_replay_rejects_unrecorded_requests(tmp_path):
74 fixture = tmp_path / "http.json"
75 fixture.write_text(
76 json.dumps({"format": "last30days-http-fixture/v1", "exchanges": []}),
77 encoding="utf-8",
78 )
79
80 with pytest.raises(AssertionError, match="Unrecorded HTTP request"), \
81 http.replaying_requests(fixture):
82 http.get("https://example.test/not-recorded")
83
84
85 def test_cli_backed_source_results_record_at_the_module_seam(tmp_path):
86 fixture_dir = tmp_path / "fixture"
87 request = {
88 "source": "digg",
89 "topic": "agents",
90 "search_query": "agents",
91 "date_range": ["2026-06-10", "2026-07-10"],
92 "depth": "quick",
93 }
94 value = [[{"url": "https://di.gg/ai/example"}], {"provider": "fixture"}]
95
96 with http.recording_requests(fixture_dir):
97 http.fixture_source_record(request, value)
98
99 with http.replaying_requests(fixture_dir):
100 matched, replayed = http.fixture_source_replay(request)
101
102 assert matched is True
103 assert replayed == value
104
105
106 def test_module_seam_capture_omits_nested_http_exchanges(tmp_path, monkeypatch):
107 monkeypatch.setattr(http.urllib.request, "urlopen", lambda *_args, **_kwargs: _response('{"ok": true}'))
108 fixture_dir = tmp_path / "fixture"
109 request = {
110 "source": "youtube",
111 "topic": "agents",
112 "search_query": "agents",
113 "date_range": ["2026-06-10", "2026-07-10"],
114 "depth": "quick",
115 }
116
117 with http.recording_requests(fixture_dir):
118 with http.fixture_module_capture(True):
119 http.get("https://api.example.test/nested-enrichment")
120 http.fixture_source_record(request, [[], {}])
121
122 payload = json.loads((fixture_dir / "http.json").read_text(encoding="utf-8"))
123 assert payload["exchanges"] == []
124 assert len(payload["source_exchanges"]) == 1
125
126
127 def test_module_seam_records_and_replays_adapter_failures(tmp_path, monkeypatch):
128 fixture_dir = tmp_path / "fixture"
129 kwargs = {
130 "source": "digg",
131 "topic": "agents",
132 "subquery": type("SubQuery", (), {"search_query": "agents"})(),
133 "date_range": ("2026-06-10", "2026-07-10"),
134 "depth": "quick",
135 }
136 monkeypatch.setattr(
137 pipeline,
138 "_retrieve_stream_impl",
139 lambda **_kwargs: (_ for _ in ()).throw(RuntimeError("adapter failed")),
140 )
141
142 with http.recording_requests(fixture_dir):
143 with pytest.raises(RuntimeError, match="adapter failed"):
144 pipeline._retrieve_stream(**kwargs)
145
146 payload = json.loads((fixture_dir / "http.json").read_text(encoding="utf-8"))
147 assert payload["source_exchanges"] == [
148 {
149 "request": {
150 "source": "digg",
151 "topic": "agents",
152 "search_query": "agents",
153 "date_range": ["2026-06-10", "2026-07-10"],
154 "depth": "quick",
155 },
156 "type": "error",
157 "error": {
158 "exception_type": "RuntimeError",
159 "message": "adapter failed",
160 "outcome_state": None,
161 },
162 }
163 ]
164
165 monkeypatch.setattr(
166 pipeline,
167 "_retrieve_stream_impl",
168 lambda **_kwargs: pytest.fail("replay called the live adapter"),
169 )
170 with http.replaying_requests(fixture_dir):
171 with pytest.raises(http.RecordedSourceError, match="adapter failed") as replayed:
172 pipeline._retrieve_stream(**kwargs)
173
174 assert replayed.value.exception_type == "RuntimeError"
175
176
177 @pytest.mark.parametrize("source", ["youtube", "digg"])
178 def test_post_ranking_cli_enrichment_records_and_replays(
179 tmp_path,
180 monkeypatch,
181 source,
182 ):
183 fixture_dir = tmp_path / source
184 item = schema.SourceItem(
185 item_id="item-1",
186 source=source,
187 title="Fixture item",
188 body="Fixture body",
189 url=f"https://example.test/{source}/item-1",
190 engagement={"postCount": 1} if source == "digg" else {},
191 metadata={"clusterUrlId": "cluster-1"} if source == "digg" else {},
192 )
193 if source == "youtube":
194 def enrich(items, **_kwargs):
195 items[0].metadata["transcript_snippet"] = "recorded transcript"
196
197 monkeypatch.setattr(pipeline.youtube_yt, "backfill_transcripts", enrich)
198 else:
199 def enrich(items, **_kwargs):
200 items[0].metadata["posts"] = [{"url": "https://x.com/example/status/1"}]
201 return items
202
203 monkeypatch.setattr(pipeline.digg, "enrich_source_items", enrich)
204
205 with http.recording_requests(fixture_dir):
206 recorded = pipeline._finalize_items_by_source(
207 {source: [item]}, topic="agents", depth="quick",
208 )
209
210 expected_metadata = recorded[source][0].metadata
211 replay_item = schema.SourceItem(
212 item_id="item-1",
213 source=source,
214 title="Fixture item",
215 body="Fixture body",
216 url=f"https://example.test/{source}/item-1",
217 engagement={"postCount": 1} if source == "digg" else {},
218 metadata={"clusterUrlId": "cluster-1"} if source == "digg" else {},
219 )
220 monkeypatch.setattr(
221 pipeline.youtube_yt if source == "youtube" else pipeline.digg,
222 "backfill_transcripts" if source == "youtube" else "enrich_source_items",
223 lambda *_args, **_kwargs: pytest.fail("replay executed CLI enrichment"),
224 )
225
226 with http.replaying_requests(fixture_dir):
227 replayed = pipeline._finalize_items_by_source(
228 {source: [replay_item]}, topic="agents", depth="quick",
229 )
230
231 assert replayed[source][0].metadata == expected_metadata
232
233
234 def test_record_fixtures_flag_is_dev_only_and_hidden_from_help():
235 parser = cli.build_parser()
236 args = parser.parse_args(["topic", "--record-fixtures", "tmp/eval-topic"])
237
238 assert args.record_fixtures == "tmp/eval-topic"
239 assert "--record-fixtures" not in parser.format_help()
240
240 lines PYTHON