返回 last30days-skill
test_discover_render.py
根目录 / tests / test_discover_render.py
1 """U6 - rich discovery rendering: enriched trend cards, the global-trending
2 header, and the honest nothing-solid empty state."""
3
4 from unittest import mock
5
6 from lib import pipeline, render, schema
7
8
9 def _topic(rank: int, name: str, **overrides) -> schema.DiscoveryTopic:
10 fields = dict(
11 rank=rank,
12 name=name,
13 why_spiking=f"3 evidence items about {name}.",
14 momentum="building",
15 velocity_score=42.5,
16 sources=["hackernews", "reddit"],
17 engagement_by_source={"hackernews": {"points": 500}},
18 command=f'/last30days "{name}"',
19 evidence_urls=[f"https://example.com/{rank}"],
20 )
21 fields.update(overrides)
22 return schema.DiscoveryTopic(**fields)
23
24
25 def _report(**overrides) -> schema.DiscoveryReport:
26 fields = dict(
27 domain="AI agents",
28 range_from="2026-06-10",
29 range_to="2026-07-10",
30 generated_at="2026-07-10T00:00:00+00:00",
31 plan=schema.DiscoveryPlan(
32 domain="AI agents", category=None, subreddits=["all"],
33 sources=["reddit", "hackernews"],
34 ),
35 topics=[_topic(1, "OpenAI Agent SDK")],
36 source_status={},
37 warnings=[],
38 )
39 fields.update(overrides)
40 return schema.DiscoveryReport(**fields)
41
42
43 def test_global_trending_header():
44 report = _report(
45 domain="",
46 plan=schema.DiscoveryPlan(
47 domain="", category=None, subreddits=["all"],
48 sources=["reddit", "hackernews", "digg"],
49 ),
50 )
51 rendered = render.render_discovery(report)
52 assert "# Trending now" in rendered
53 assert "Trending discovery:" not in rendered
54
55
56 def test_domain_header_unchanged():
57 rendered = render.render_discovery(_report())
58 assert "# Trending discovery: AI agents" in rendered
59
60
61 def test_nothing_solid_renders_honest_empty_state():
62 report = _report(
63 topics=[],
64 outcome="nothing-solid",
65 weak_signal="Wii Sports nostalgia thread",
66 warnings=["No topic cleared the discovery confidence floor this window."],
67 )
68 rendered = render.render_discovery(report)
69 assert "Nothing solid this window." in rendered
70 assert "Wii Sports nostalgia thread" in rendered
71 assert "## 1." not in rendered # no fabricated topic cards
72
73
74 def test_community_voice_and_corroboration_render():
75 report = _report(topics=[_topic(
76 1, "OpenAI Agent SDK",
77 top_comment='"This changes everything about agent tooling" - dev_voice (1,200 votes)',
78 corroboration_count=3,
79 )])
80 rendered = render.render_discovery(report)
81 assert "**Community voice:**" in rendered
82 assert "dev_voice" in rendered
83 assert "confirmed across 3 sources" in rendered
84
85
86 def test_no_voice_line_when_topic_has_no_comment():
87 rendered = render.render_discovery(_report())
88 assert "**Community voice:**" not in rendered
89
90
91 def test_best_community_comment_prefers_platform_normalized_strength():
92 items = [
93 schema.SourceItem(
94 item_id="a", source="reddit", title="t", body="t",
95 url="https://reddit.com/a", metadata={"top_comments": [
96 {"text": "the sharpest take in the thread by far", "score": 4000, "author": "u/sharp"},
97 {"text": "short", "score": 9999},
98 ]},
99 ),
100 schema.SourceItem(
101 item_id="b", source="hackernews", title="t", body="t",
102 url="https://news.ycombinator.com/b", metadata={"top_comments": [
103 {"text": "a modest but thoughtful comment here", "score": 3, "author": "hn_user"},
104 ]},
105 ),
106 ]
107 comment = pipeline._best_community_comment(items)
108 assert comment is not None
109 assert "u/sharp" in comment
110 assert "4,000 votes" in comment
111 # Sub-12-char comment bodies never surface.
112 assert "short" not in comment
113
114
115 def test_best_community_comment_strips_leading_quote_chars():
116 """A comment body that itself starts with a quote must not render as
117 doubled quotes inside the wrapping quotes."""
118 items = [schema.SourceItem(
119 item_id="a", source="reddit", title="t", body="t",
120 url="https://r.example/a", metadata={"top_comments": [
121 {"text": '"This much is clear: the quote should not double up', "score": 100, "author": "u/q"},
122 ]},
123 )]
124 comment = pipeline._best_community_comment(items)
125 assert comment is not None
126 assert '""' not in comment
127 assert comment.startswith('"This much is clear')
128
129
130 def test_best_community_comment_none_when_no_comments():
131 items = [schema.SourceItem(
132 item_id="a", source="reddit", title="t", body="t", url="https://r.example/a",
133 )]
134 assert pipeline._best_community_comment(items) is None
135
136
137 # --- U5 angle and pipeline card lines ----------------------------------------
138
139
140 def test_angle_lines_render_in_order_between_voice_and_evidence():
141 report = _report(topics=[_topic(
142 1, "OpenAI Agent SDK",
143 top_comment='"This changes agent tooling" - dev_voice (1,200 votes)',
144 podcast_angle="Is the Agent SDK a platform play or a lock-in play?",
145 x_article_angle="Five signs the agent stack just consolidated around one SDK.",
146 )])
147 rendered = render.render_discovery(report)
148 assert (
149 "**Podcast angle:** Is the Agent SDK a platform play or a lock-in play?"
150 in rendered
151 )
152 assert (
153 "**X article angle:** Five signs the agent stack just consolidated around one SDK."
154 in rendered
155 )
156 voice = rendered.index("**Community voice:**")
157 podcast = rendered.index("**Podcast angle:**")
158 article = rendered.index("**X article angle:**")
159 evidence = rendered.index("**Evidence:**")
160 assert voice < podcast < article < evidence
161
162
163 def test_host_authored_angles_render_verbatim_and_capped(tmp_path):
164 """Finalize-leg path: host angle sentences pass through the handoff
165 reader (word-boundary capped at 200 chars) and render verbatim on the
166 card - relayable text, never paraphrased or re-wrapped."""
167 import dataclasses
168 import json
169
170 from lib import discovery_handoff
171
172 pending = discovery_handoff.PendingReport(
173 schema_version="1.0",
174 bundle_id="cafe1234cafe1234",
175 generated_at="2026-07-10T00:00:00+00:00",
176 run_ref="discover:AI agents:2026-07-10T00:00:00+00:00",
177 report={},
178 angle_inputs={"n1": {"name": "OpenAI Agent SDK"}},
179 )
180 long_angle = " ".join(["angle"] * 60) # well over the 200-char cap
181 angles_path = tmp_path / "angles.json"
182 angles_path.write_text(json.dumps({
183 "bundle_id": pending.bundle_id,
184 "angles": [{
185 "id": "n1",
186 "podcast": "Is the Agent SDK a platform play or a lock-in play?",
187 "x_article": long_angle,
188 }],
189 }), encoding="utf-8")
190 host = discovery_handoff.read_angles(angles_path, pending)["n1"]
191
192 topic = dataclasses.replace(
193 _topic(1, "OpenAI Agent SDK"),
194 podcast_angle=host.podcast,
195 x_article_angle=host.x_article,
196 )
197 rendered = render.render_discovery(_report(topics=[topic]))
198 assert (
199 "**Podcast angle:** Is the Agent SDK a platform play or a lock-in play?"
200 in rendered
201 )
202 capped = host.x_article
203 assert capped is not None
204 assert len(capped) <= 200
205 assert f"**X article angle:** {capped}" in rendered
206
207
208 def test_no_angle_lines_when_fields_none():
209 rendered = render.render_discovery(_report())
210 assert "**Podcast angle:**" not in rendered
211 assert "**X article angle:**" not in rendered
212
213
214 def test_single_angle_renders_without_empty_sibling_label():
215 report = _report(topics=[_topic(
216 1, "OpenAI Agent SDK",
217 podcast_angle="What breaks first when every agent shares one SDK?",
218 )])
219 rendered = render.render_discovery(report)
220 assert (
221 "**Podcast angle:** What breaks first when every agent shares one SDK?"
222 in rendered
223 )
224 assert "**X article angle:**" not in rendered
225
226
227 def test_pipeline_line_renders_surfaced_and_covered_comma_joined():
228 report = _report(topics=[_topic(
229 1, "OpenAI Agent SDK",
230 previously_surfaced_count=3,
231 last_surfaced="2026-07-14",
232 covered=True,
233 )])
234 rendered = render.render_discovery(report)
235 assert (
236 "**Pipeline:** surfaced 4th time, marked covered"
237 in rendered
238 )
239 assert "marked covered 2026" not in rendered # covered date is never shown
240
241
242 def test_pipeline_line_surfaced_only():
243 report = _report(topics=[_topic(
244 1, "OpenAI Agent SDK", previously_surfaced_count=1,
245 )])
246 rendered = render.render_discovery(report)
247 assert "**Pipeline:** surfaced 2nd time" in rendered
248 assert "marked covered" not in rendered
249
250
251 def test_pipeline_line_covered_only_without_date():
252 report = _report(topics=[_topic(1, "OpenAI Agent SDK", covered=True)])
253 rendered = render.render_discovery(report)
254 assert "**Pipeline:** marked covered" in rendered
255 assert "surfaced" not in rendered
256
257
258 def test_pipeline_line_sits_after_angles_before_evidence():
259 report = _report(topics=[_topic(
260 1, "OpenAI Agent SDK",
261 podcast_angle="A tension to talk through?",
262 x_article_angle="A claim worth writing down.",
263 previously_surfaced_count=2,
264 )])
265 rendered = render.render_discovery(report)
266 assert (
267 rendered.index("**X article angle:**")
268 < rendered.index("**Pipeline:**")
269 < rendered.index("**Evidence:**")
270 )
271
272
273 def test_no_pipeline_line_for_fresh_topic():
274 """previously_surfaced_count=0 and covered=False never render the line."""
275 rendered = render.render_discovery(_report())
276 assert "**Pipeline:**" not in rendered
277
278
279 # --- _ordinal (Pipeline card line regression guard) ------------------------
280
281
282 def test_ordinal_teens_branch_always_th():
283 """10-20 (mod 100) are the '11st'-style regression risk: %10 alone would
284 misclassify 11/12/13 as 'st'/'nd'/'rd'. All of 10-13 and the 20 boundary
285 must render 'th'."""
286 cases = {10: "10th", 11: "11th", 12: "12th", 13: "13th", 20: "20th"}
287 for count, expected in cases.items():
288 assert render._ordinal(count) == expected
289
290
291 def test_ordinal_regular_suffixes():
292 assert render._ordinal(2) == "2nd"
293 assert render._ordinal(3) == "3rd"
294 assert render._ordinal(21) == "21st"
295
296
297 def test_ordinal_hundreds_teens_still_th():
298 """111 falls in the 10 <= n % 100 <= 20 band (111 % 100 == 11), so it must
299 render 'th', not 'st' from a naive %10 check."""
300 assert render._ordinal(111) == "111th"
301
302
303 def test_pipeline_line_ordinal_teens_in_rendered_card():
304 """End-to-end: previously_surfaced_count=10 means this is appearance 11,
305 which must render 'surfaced 11th time', not 'surfaced 11st time'."""
306 report = _report(topics=[_topic(
307 1, "OpenAI Agent SDK", previously_surfaced_count=10,
308 )])
309 rendered = render.render_discovery(report)
310 assert "surfaced 11th time" in rendered
311
312
313 def test_nothing_solid_output_stays_byte_identical():
314 """The empty state predates U5 and must not grow angle or pipeline text."""
315 report = _report(
316 topics=[],
317 outcome="nothing-solid",
318 weak_signal="Wii Sports nostalgia thread",
319 warnings=[
320 "No topic cleared the discovery confidence floor this window; "
321 "reporting nothing solid instead of ranked noise."
322 ],
323 )
324 with mock.patch.object(render, "_render_badge", return_value=["BADGE", ""]):
325 rendered = render.render_discovery(report)
326 assert rendered == (
327 "BADGE\n\n"
328 "# Trending discovery: AI agents\n\n"
329 "Window: 2026-06-10 to 2026-07-10\n"
330 "Feeds: reddit, hackernews\n"
331 "Communities: r/all\n\n"
332 "**Nothing solid this window.** No topic cleared the confidence "
333 "floor - not enough cross-source confirmation or engagement to "
334 "call anything a trend, and ranked noise would be worse than an "
335 "honest empty result.\n\n"
336 "Closest weak signal: Wii Sports nostalgia thread (sub-floor; "
337 "single-source or too little engagement).\n\n"
338 "### Coverage notes\n\n"
339 "- No topic cleared the discovery confidence floor this window; "
340 "reporting nothing solid instead of ranked noise.\n"
341 )
342
342 lines PYTHON