| 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 |