返回 last30days-skill
test_render_footer.py
根目录 / tests / test_render_footer.py
1 """Footer rendering: non-populated sources are dropped from the emoji tree.
2
3 Covers U1 of the doctor-classification-and-footer-noresults plan. A source that
4 returned zero items - whether it completed cleanly (NO_RESULTS) or failed
5 (RATE_LIMITED / UNREACHABLE) - must not appear as its own emoji-tree line. The
6 failure signal stays visible in the evidence blocks (## Partial Coverage /
7 ## Source Coverage) so synthesis still sees it (R7), just not in the user-facing
8 footer.
9 """
10
11 from lib import health, render, schema
12
13
14 def _report(*, items_by_source=None, source_status=None, errors_by_source=None):
15 return schema.Report(
16 topic="test topic",
17 range_from="2026-06-10",
18 range_to="2026-07-10",
19 generated_at="2026-07-10T18:22:03Z",
20 provider_runtime=schema.ProviderRuntime(
21 reasoning_provider="gemini",
22 planner_model="test-planner",
23 rerank_model="test-reranker",
24 ),
25 query_plan=schema.QueryPlan(
26 intent="general",
27 freshness_mode="balanced_recent",
28 cluster_mode="story",
29 raw_topic="test topic",
30 subqueries=[
31 schema.SubQuery(
32 label="primary",
33 search_query="test topic",
34 ranking_query="test topic",
35 sources=["reddit"],
36 )
37 ],
38 source_weights={"reddit": 1.0},
39 ),
40 clusters=[],
41 ranked_candidates=[],
42 items_by_source=items_by_source or {},
43 errors_by_source=errors_by_source or {},
44 source_status=source_status or {},
45 )
46
47
48 def _reddit_item():
49 return schema.SourceItem(
50 item_id="r1",
51 source="reddit",
52 title="A thread",
53 body="body",
54 url="https://reddit.com/r/test/comments/1",
55 )
56
57
58 def test_footer_omits_clean_no_results_sources():
59 report = _report(
60 items_by_source={"reddit": [_reddit_item()]},
61 source_status={
62 "reddit": schema.SourceOutcome(source="reddit", state=health.OK, items_returned=1),
63 "jobs": schema.SourceOutcome(source="jobs", state=schema.NO_RESULTS),
64 "polymarket": schema.SourceOutcome(source="polymarket", state=schema.NO_RESULTS),
65 "youtube": schema.SourceOutcome(source="youtube", state=schema.NO_RESULTS),
66 },
67 )
68
69 text = render.render_compact(report)
70
71 # Populated source stays.
72 assert "🟠 Reddit: 1 thread" in text
73 # Clean zero-result sources do not get a footer line.
74 assert "Jobs: no results" not in text
75 assert "Polymarket: no results" not in text
76 assert "YouTube: no results" not in text
77
78
79 def test_footer_omits_errored_zero_item_source_but_keeps_evidence():
80 report = _report(
81 items_by_source={"reddit": [_reddit_item()]},
82 source_status={
83 "reddit": schema.SourceOutcome(source="reddit", state=health.OK, items_returned=1),
84 "x": schema.SourceOutcome(
85 source="x",
86 state=schema.RATE_LIMITED,
87 detail="HTTP 429 after retry budget",
88 fix_hint="doctor",
89 ),
90 },
91 errors_by_source={"x": "HTTP 429 after retry budget"},
92 )
93
94 text = render.render_compact(report)
95
96 # The failed zero-item source is dropped from the emoji-tree footer.
97 assert "🔵 X: rate-limited" not in text
98 # ... but its failure is still visible to synthesis in the evidence blocks (R7).
99 assert "## Partial Coverage" in text
100 assert "Do not interpret a failed source as no discussion" in text
101
102
103 def test_footer_preserves_save_path_when_all_sources_empty():
104 # Every source returned zero items -> no source lines, but the durable
105 # raw-file citation must still render (regression guard for the U1 loop
106 # removal, which previously suppressed the whole footer incl. save path).
107 report = _report(
108 source_status={
109 "jobs": schema.SourceOutcome(source="jobs", state=schema.NO_RESULTS),
110 "x": schema.SourceOutcome(
111 source="x", state=schema.RATE_LIMITED, detail="429", fix_hint="doctor"
112 ),
113 },
114 )
115 footer = render._render_emoji_footer(report, "/tmp/l30d-scratch/topic-raw.md")
116
117 text = "\n".join(footer)
118 assert "✅ All agents reported back!" in text
119 assert "Raw results saved to /tmp/l30d-scratch/topic-raw.md" in text
120 # No per-source line for the zero-item sources.
121 assert "Jobs" not in text
122 assert "rate-limited" not in text
123
124
125 def test_footer_empty_with_no_save_path_returns_nothing():
126 report = _report(
127 source_status={"jobs": schema.SourceOutcome(source="jobs", state=schema.NO_RESULTS)},
128 )
129 assert render._render_emoji_footer(report, None) == []
130
131
132 def test_library_block_carries_explainer_when_populated():
133 report = _report(items_by_source={"reddit": [_reddit_item()]})
134 report.library_context = [
135 schema.LibraryContext(
136 topic="test topic",
137 published_date="2026-07-01",
138 headline="a prior finding",
139 summary="a prior finding",
140 source_kind="brief",
141 )
142 ]
143
144 text = render.render_compact(report)
145
146 assert "## From your library" in text
147 assert "Prior saved runs" in text
148 assert "LAST30DAYS_LIBRARY_CONTEXT=off" in text
149
150
151 def test_library_block_and_explainer_absent_when_empty():
152 report = _report(items_by_source={"reddit": [_reddit_item()]})
153 text = render.render_compact(report)
154 assert "## From your library" not in text
155 assert "Prior saved runs" not in text
156
157
158 def test_footer_keeps_partial_populated_source_without_warning_text():
159 """A source that returned SOME items but then failed stays in the footer
160 as counts only: run diagnostics live in doctor --postmortem, the saved raw
161 file, and the model-facing ## Partial Coverage note, never on the
162 user-facing conclusion surface."""
163 ig_item = schema.SourceItem(
164 item_id="ig1",
165 source="instagram",
166 title="A reel",
167 body="caption",
168 url="https://instagram.com/reel/1",
169 )
170 report = _report(
171 items_by_source={"reddit": [_reddit_item()], "instagram": [ig_item]},
172 source_status={
173 "reddit": schema.SourceOutcome(source="reddit", state=health.OK, items_returned=1),
174 "instagram": schema.SourceOutcome(
175 source="instagram",
176 state=schema.PARTIAL,
177 items_returned=1,
178 detail="HTTP 400: Bad Request",
179 fix_hint="doctor",
180 ),
181 },
182 )
183
184 text = render.render_compact(report)
185
186 assert "📸 Instagram: 1 reel" in text
187 footer = text.split("✅ All agents reported back!", 1)[1]
188 assert "⚠" not in footer
189 assert "run doctor" not in footer
190 assert "## Partial Coverage" in text
191 assert "Instagram" in text.split("## Partial Coverage", 1)[1].split("\n\n", 1)[0] or "Instagram partial" in text
192
193
194 def test_footer_auth_failed_populated_source_has_no_warning_text():
195 ig_item = schema.SourceItem(
196 item_id="ig1",
197 source="instagram",
198 title="A reel",
199 body="caption",
200 url="https://instagram.com/reel/1",
201 )
202 report = _report(
203 items_by_source={"reddit": [_reddit_item()], "instagram": [ig_item]},
204 source_status={
205 "reddit": schema.SourceOutcome(source="reddit", state=health.OK, items_returned=1),
206 "instagram": schema.SourceOutcome(
207 source="instagram",
208 state=schema.AUTH_FAILED,
209 items_returned=1,
210 detail="HTTP 401",
211 fix_hint="doctor",
212 ),
213 },
214 )
215
216 text = render.render_compact(report)
217 footer = text.split("✅ All agents reported back!", 1)[1]
218
219 assert "📸 Instagram: 1 reel" in footer
220 assert "⚠" not in footer
221 assert "auth-failed" in text.split("## Partial Coverage", 1)[1]
222
223
224 def test_compact_drops_source_failure_warnings_and_source_errors_block():
225 report = _report(
226 items_by_source={"reddit": [_reddit_item()]},
227 source_status={
228 "reddit": schema.SourceOutcome(source="reddit", state=health.OK, items_returned=1),
229 "jobs": schema.SourceOutcome(source="jobs", state=schema.UNREACHABLE, items_returned=0, detail="DNS"),
230 },
231 )
232 report.warnings = [
233 "Some sources failed: jobs",
234 "Some sources returned partial results (degraded): reddit",
235 "Evidence is thin for this topic.",
236 ]
237 report.errors_by_source = {"jobs": "URL Error: nodename nor servname provided"}
238
239 compact = render.render_compact(report)
240 assert "## Source Errors" not in compact
241 assert "Some sources failed" not in compact
242 assert "returned partial results" not in compact
243 assert "Evidence is thin for this topic." in compact
244
245 full = render.render_full(report)
246 assert "## Source Errors" in full
247
248 payload = schema.to_dict(report)
249 assert payload["warnings"] == report.warnings
250 assert payload["errors_by_source"] == {"jobs": "URL Error: nodename nor servname provided"}
251 assert payload["source_status"]["jobs"]["state"] == schema.UNREACHABLE
252
253
254 def test_footer_carries_freshness_verdict():
255 """The freshness verdict reaches the footer, not just the report body."""
256 stale = [
257 schema.SourceItem(
258 item_id=f"r{i}",
259 source="reddit",
260 title=f"Old thread {i}",
261 body="body",
262 url=f"https://reddit.com/r/test/comments/{i}",
263 published_at="2026-06-12",
264 date_confidence="high",
265 )
266 for i in range(6)
267 ]
268 report = _report(
269 items_by_source={"reddit": stale},
270 source_status={
271 "reddit": schema.SourceOutcome(source="reddit", state=health.OK, items_returned=6),
272 },
273 )
274
275 text = render.render_compact(report)
276
277 assert "🕒" in text
278 assert "from the last 7 days" in text
279
280
281 def test_footer_freshness_line_absent_when_evidence_is_recent():
282 fresh = [
283 schema.SourceItem(
284 item_id=f"r{i}",
285 source="reddit",
286 title=f"Fresh thread {i}",
287 body="body",
288 url=f"https://reddit.com/r/test/comments/{i}",
289 published_at="2026-07-08",
290 date_confidence="high",
291 )
292 for i in range(6)
293 ]
294 report = _report(
295 items_by_source={"reddit": fresh},
296 source_status={
297 "reddit": schema.SourceOutcome(source="reddit", state=health.OK, items_returned=6),
298 },
299 )
300
301 assert "🕒" not in render.render_compact(report)
302
303
304 def test_raw_results_only_footer_includes_freshness_line():
305 """AE3: raw-results-only footer still includes the freshness line.
306
307 When every source returns zero items (so source_lines would be empty and
308 body starts empty) but a save_path is provided, the freshness verdict
309 must still appear alongside the raw-results line. Without the fix,
310 `if freshness_line and body:` would be False because body was empty.
311 """
312 report = _report(
313 items_by_source={},
314 source_status={},
315 )
316
317 footer = render._render_emoji_footer(report, "/tmp/l30d-scratch/topic-raw.md")
318 text = "\n".join(footer)
319
320 assert "🕒" in text
321 assert "no usable dated evidence" in text
322 assert "Raw results saved to /tmp/l30d-scratch/topic-raw.md" in text
323
323 lines PYTHON