返回 last30days-skill
test_dripstack.py
根目录 / tests / test_dripstack.py
1 """DripStack source: requested-only gating, windowing, and normalization."""
2
3 from unittest import mock
4
5 from lib import dripstack, pipeline
6
7
8 def _api_item(**overrides):
9 item = {
10 "publicationSlug": "newsletter.semianalysis.com",
11 "slug": "nvidia-gpu-debt-backstop",
12 "title": "Nvidia GPU Debt Backstop",
13 "subtitle": "Capital, offtake and datacenters.",
14 "snippet": "Nvidia's backstop economics explained.",
15 "publishedAt": "2026-07-06T21:53:44.000Z",
16 "relevanceScore": 84,
17 "matchConfidence": "strong",
18 "topicCoverageRatio": 1,
19 "whyMatched": ["Matched article body for: nvidia", "Hybrid RRF — blended"],
20 }
21 item.update(overrides)
22 return item
23
24
25 def test_dripstack_absent_from_available_sources_by_default():
26 assert "dripstack" not in pipeline.available_sources({}, None, x_pending=False)
27
28
29 def test_dripstack_available_only_when_explicitly_requested():
30 available = pipeline.available_sources({}, ["dripstack"], x_pending=False)
31 assert "dripstack" in available
32
33
34 def test_search_goes_through_the_shared_http_choke_point(monkeypatch):
35 seen = {}
36
37 def fake_get(url, headers=None, **kwargs):
38 seen["url"] = url
39 seen["retries"] = kwargs.get("retries")
40 return {"items": [_api_item()], "matchConfidence": "strong"}
41
42 monkeypatch.setattr(dripstack.http, "get", fake_get)
43 items = dripstack.search_dripstack("Nvidia earnings", "2026-06-12", "2026-07-12")
44
45 assert len(items) == 1
46 assert seen["url"].startswith("https://dripstack.xyz/api/v1/search?")
47 assert seen["retries"] == 2
48
49
50 def test_search_drops_results_outside_the_window(monkeypatch):
51 stale = _api_item(publishedAt="2026-05-20T19:06:01.000Z", slug="old-post")
52 fresh = _api_item()
53 undated = _api_item(publishedAt="", slug="undated-post")
54 monkeypatch.setattr(
55 dripstack.http, "get",
56 lambda *a, **k: {"items": [stale, fresh, undated], "matchConfidence": "strong"},
57 )
58
59 items = dripstack.search_dripstack("Nvidia", "2026-06-12", "2026-07-12")
60
61 slugs = [item["slug"] for item in items]
62 assert "old-post" not in slugs
63 assert "nvidia-gpu-debt-backstop" in slugs
64 assert "undated-post" in slugs # undated results are kept, not guessed
65
66
67 def test_search_failure_returns_empty_list(monkeypatch):
68 def boom(*a, **k):
69 raise OSError("connection reset")
70
71 monkeypatch.setattr(dripstack.http, "get", boom)
72 assert dripstack.search_dripstack("Nvidia", "2026-06-12", "2026-07-12") == []
73
74
75 def test_parse_normalizes_fields_and_relevance():
76 parsed = dripstack.parse_dripstack_response([_api_item()], query="nvidia")
77
78 item = parsed[0]
79 assert item["url"] == "https://newsletter.semianalysis.com/nvidia-gpu-debt-backstop"
80 assert item["date"] == "2026-07-06"
81 assert item["relevance"] == 0.84
82 assert item["engagement"] == {}
83 assert item["author"] == "newsletter.semianalysis"
84 assert "Hybrid" not in item["why_relevant"]
85 assert item["metadata"]["publication_slug"] == "newsletter.semianalysis.com"
86
87
88 def test_parse_handles_missing_fields_conservatively():
89 parsed = dripstack.parse_dripstack_response(
90 [{"title": "", "relevanceScore": "not-a-number"}], query="x"
91 )
92
93 item = parsed[0]
94 assert item["title"] == "DripStack result 1"
95 assert item["url"] == ""
96 assert item["relevance"] == 0.5
97
98
99 def test_parse_emits_body_and_normalizer_prefers_it():
100 parsed = dripstack.parse_dripstack_response([_api_item()], query="nvidia")
101
102 assert parsed[0]["body"] == "Capital, offtake and datacenters."
103
104 from lib import normalize
105 normalized = normalize._normalize_dripstack(
106 "dripstack", parsed[0], 0, "2026-06-12", "2026-07-12"
107 )
108 assert normalized.body == "Capital, offtake and datacenters."
109
110
111 def test_dripstack_activates_via_persisted_include_sources():
112 """INCLUDE_SOURCES is the .env checkbox for persistent opt-ins (the
113 LinkedIn/Perplexity pattern); dripstack must honor it, not only --search."""
114 available = pipeline.available_sources(
115 {"INCLUDE_SOURCES": "dripstack"}, None, x_pending=False
116 )
117 assert "dripstack" in available
118
119
120 def test_unrelated_include_sources_keeps_dripstack_off():
121 available = pipeline.available_sources(
122 {"INCLUDE_SOURCES": "linkedin,tiktok"}, None, x_pending=False
123 )
124 assert "dripstack" not in available
125
126
127 def test_include_sources_tolerates_whitespace_around_commas():
128 available = pipeline.available_sources(
129 {"INCLUDE_SOURCES": "linkedin, dripstack"}, None, x_pending=False
130 )
131 assert "dripstack" in available
132
132 lines PYTHON