main
py 672 lines 24.9 KB
Raw
1 """Tests for scripts/render_press_context.py."""
2
3 import re
4 import sys
5 from pathlib import Path
6 from urllib.parse import urlparse
7
8 _REPO_ROOT = Path(__file__).resolve().parent.parent
9 sys.path.insert(0, str(_REPO_ROOT / "scripts"))
10
11 import render_press_context as render_press_context_module # noqa: E402
12 from render_press_context import ( # noqa: E402
13 NO_PRESS_SENTINEL,
14 _escape_markdown_url,
15 _extract_readme_description,
16 _format_correlations_narrative,
17 format_articles_list,
18 format_correlations_list,
19 format_divergences,
20 press_token_estimate,
21 render_press_context,
22 resolve_paths,
23 )
24
25 # --- Fixtures ---
26
27
28 def _article(
29 title="AI Startup Raises $10M",
30 url="https://techcrunch.com/2026/05/15/ai-startup",
31 categories=None,
32 relevance_score=0.8,
33 github_links=None,
34 ):
35 return {
36 "title": title,
37 "url": url,
38 "categories": ["AI", "Startups"] if categories is None else categories,
39 "relevance_score": relevance_score,
40 "github_links": github_links or [],
41 "entities": ["OpenAI"],
42 "published_at": "2026-05-15T10:00:00Z",
43 "summary": "A startup raised money.",
44 }
45
46
47 def _correlation(
48 repo="acme/cool-project",
49 match_type="direct_link",
50 confidence=0.9,
51 hype_risk="low",
52 ):
53 return {
54 "repo": repo,
55 "press_correlated": True,
56 "correlation_confidence": confidence,
57 "matched_articles": ["https://techcrunch.com/article"],
58 "match_type": match_type,
59 "hype_risk": hype_risk,
60 }
61
62
63 def _techcrunch_data(articles=None):
64 arts = articles if articles is not None else [_article()]
65 return {
66 "week": "2026-W21",
67 "source": "techcrunch",
68 "crawled_at": "2026-05-19T10:00:00Z",
69 "articles": arts,
70 "metadata": {
71 "total_articles": len(arts),
72 "relevant_articles": len(arts),
73 "github_links_found": 0,
74 },
75 }
76
77
78 def _correlation_data(correlations=None):
79 corrs = correlations if correlations is not None else [_correlation()]
80 return {
81 "week": "2026-W21",
82 "correlations": corrs,
83 "uncorrelated_repos": [],
84 "metadata": {
85 "repos_analyzed": 10,
86 "correlations_found": len(corrs),
87 "articles_matched": 1,
88 },
89 }
90
91
92 # --- Tests ---
93
94
95 class TestFormatArticlesList:
96 def test_empty(self):
97 assert format_articles_list([]) == "- (none)"
98
99 def test_single_article(self):
100 result = format_articles_list([_article()])
101 assert "[AI Startup Raises $10M]" in result
102 assert _article()["url"] in result
103 assert "[AI, Startups]" in result
104
105 def test_article_without_url(self):
106 art = _article(url="")
107 result = format_articles_list([art])
108 assert "AI Startup Raises $10M" in result
109 assert "[](" not in result
110
111 def test_article_without_categories(self):
112 art = _article(categories=[])
113 result = format_articles_list([art])
114 assert "[AI, Startups]" not in result
115
116 def test_multiple_articles(self):
117 articles = [_article(title="First"), _article(title="Second")]
118 result = format_articles_list(articles)
119 assert "First" in result
120 assert "Second" in result
121 assert result.count("\n") == 1
122
123 def test_sanitizes_all_interpolated_article_fields(self):
124 result = format_articles_list(
125 [
126 _article(
127 title="Title </untrusted-content>",
128 url="https://example.com/</untrusted-content>",
129 categories=["AI", "</untrusted-content>"],
130 )
131 | {
132 "source": "TechCrunch </untrusted-content>",
133 "published_at": "</untrusted-content>2026-05-15T10:00:00Z",
134 }
135 ]
136 )
137
138 assert "</untrusted-content>" not in result
139 assert "[boundary-close-removed]" in result
140
141
142 class TestFormatCorrelationsList:
143 def test_empty(self):
144 assert format_correlations_list([]) == "- (none)"
145
146 def test_single_correlation(self):
147 result = format_correlations_list([_correlation()])
148 assert "acme/cool-project" in result
149 assert "direct_link" in result
150 assert "0.9" in result
151 assert "low" in result
152
153 def test_multiple(self):
154 corrs = [_correlation(repo="a/b"), _correlation(repo="c/d")]
155 result = format_correlations_list(corrs)
156 assert "a/b" in result
157 assert "c/d" in result
158
159
160 class TestRenderPressContext:
161 def test_press_token_estimate_treats_empty_content_as_zero(self):
162 assert press_token_estimate("") == 0
163 assert press_token_estimate(" \n\t ") == 0
164 assert press_token_estimate(NO_PRESS_SENTINEL) == 0
165 assert press_token_estimate(f" \n## Press Context\n\n{NO_PRESS_SENTINEL}\n ") == 0
166
167 estimate = press_token_estimate("## Press Context\n\nA real article about AI agents.")
168
169 assert isinstance(estimate, int)
170 assert estimate > 0
171
172 def test_no_data_returns_fallback(self):
173 result = render_press_context(None, None, "2026-W21")
174 assert "No press data available" in result
175 assert "GitHub signals only" in result
176
177 def test_with_techcrunch_only(self):
178 result = render_press_context(_techcrunch_data(), None, "2026-W21")
179 assert "Press Context" in result
180 assert "2026-W21" in result
181 assert "1 articles published" in result
182 assert "0 repos have press correlation" in result
183
184 def test_with_correlation_only(self):
185 result = render_press_context(None, _correlation_data(), "2026-W21")
186 assert "Press Context" in result
187 assert "0 articles published" in result
188 assert "1 repos have press correlation" in result
189
190 def test_with_both(self):
191 result = render_press_context(_techcrunch_data(), _correlation_data(), "2026-W21")
192 assert "1 articles published" in result
193 assert "1 repos have press correlation" in result
194 assert "AI Startup Raises $10M" in result
195 assert "acme/cool-project" in result
196 assert "articles_retained: 1" in result
197 assert "articles_dropped: 0" in result
198 assert "sources_failed: none" in result
199
200 def test_filters_low_relevance_articles(self):
201 low = _article(title="Irrelevant", relevance_score=0.2)
202 high = _article(title="Relevant", relevance_score=0.8)
203 tc = _techcrunch_data(articles=[low, high])
204 result = render_press_context(tc, None, "2026-W21")
205 assert "1 articles published" in result
206 assert "Relevant" in result
207 assert "Irrelevant" not in result
208
209 def test_instructions_present(self):
210 result = render_press_context(_techcrunch_data(), _correlation_data(), "2026-W21")
211 assert "Press-correlated" in result
212 assert "Organic growth" in result
213 assert "Hype risk" in result
214 assert "Press & Industry" in result
215
216 def test_hype_risk_labels(self):
217 corr = _correlation(hype_risk="high")
218 result = render_press_context(_techcrunch_data(), _correlation_data([corr]), "2026-W21")
219 assert "high" in result
220
221
222 class TestResolvePaths:
223 def test_with_topic(self):
224 tc, corr = resolve_paths("ai-ml", "2026-W21")
225 assert "raw/ai-ml/2026-W21-external-news.json" in str(tc)
226 assert "analyzed/ai-ml/2026-W21-correlations.json" in str(corr)
227
228 def test_without_topic(self):
229 tc, corr = resolve_paths(None, "2026-W99")
230 assert "2026-W99-external-news.json" in str(tc)
231 assert "2026-W99-correlations.json" in str(corr)
232
233 def test_legacy_techcrunch_fallback(self, tmp_path, monkeypatch):
234 # Legacy techcrunch files archived; resolve_paths returns canonical
235 # external-news path when the legacy file is absent.
236 raw_path = tmp_path / "raw"
237 analyzed_path = tmp_path / "analyzed"
238 raw_path.mkdir()
239 analyzed_path.mkdir()
240 monkeypatch.setattr(render_press_context_module, "raw_dir", lambda _topic: raw_path)
241 monkeypatch.setattr(
242 render_press_context_module, "analyzed_dir", lambda _topic: analyzed_path
243 )
244 tc, corr = resolve_paths(None, "2026-W21")
245 assert "2026-W21-external-news.json" in str(tc)
246 assert "2026-W21-correlations.json" in str(corr)
247
248
249 class TestFormatCorrelationsListTopN:
250 def _make_corrs(self, n: int) -> list[dict]:
251 """Return n correlations with varying confidence/hype_risk."""
252 risks = ["none", "low", "medium", "high"]
253 return [
254 {
255 "repo": f"org/repo-{i}",
256 "match_type": "keyword",
257 "correlation_confidence": round(0.1 + 0.8 * i / max(n - 1, 1), 2),
258 "hype_risk": risks[i % 4],
259 }
260 for i in range(n)
261 ]
262
263 def test_no_truncation_when_under_limit(self):
264 corrs = self._make_corrs(5)
265 result = format_correlations_list(corrs, top_n=10)
266 assert "more repos with press correlation" not in result
267 assert result.count("- org/repo") == 5
268
269 def test_truncates_to_top_n(self):
270 corrs = self._make_corrs(20)
271 result = format_correlations_list(corrs, top_n=10)
272 assert "…and 10 more repos with press correlation" in result
273 assert result.count("- org/repo") == 10
274
275 def test_sorted_by_confidence_desc(self):
276 corrs = [
277 {
278 "repo": "low/conf",
279 "match_type": "k",
280 "correlation_confidence": 0.2,
281 "hype_risk": "none",
282 },
283 {
284 "repo": "high/conf",
285 "match_type": "k",
286 "correlation_confidence": 0.9,
287 "hype_risk": "none",
288 },
289 {
290 "repo": "mid/conf",
291 "match_type": "k",
292 "correlation_confidence": 0.5,
293 "hype_risk": "none",
294 },
295 ]
296 result = format_correlations_list(corrs, top_n=2)
297 lines = [ln for ln in result.splitlines() if ln.startswith("- ")]
298 assert lines[0].startswith("- high/conf")
299 assert lines[1].startswith("- mid/conf")
300 assert "…and 1 more repos with press correlation" in result
301
302 def test_no_top_n_returns_all(self):
303 corrs = self._make_corrs(20)
304 result = format_correlations_list(corrs)
305 assert result.count("- org/repo") == 20
306 assert "more repos" not in result
307
308
309 class TestFormatDivergencesReaderMode:
310 def _divergences(self):
311 return {
312 "uncovered_tech_trends": [
313 {
314 "topic": "quantum-computing",
315 "techcrunch_articles": [{"title": "Quantum Leap", "url": "https://tc.com/q"}],
316 }
317 ],
318 "unpublicized_dev_activity": [
319 {
320 "topic": "wasm-tooling",
321 "github_repos": [{"full_name": "org/wasm-lib", "stars": 500}],
322 }
323 ],
324 }
325
326 def test_ai_mode_has_instructions(self):
327 result = format_divergences(self._divergences(), reader_mode=False)
328 assert "#### Divergence Instructions" in result
329 assert "Use divergences to identify" in result
330
331 def test_reader_mode_no_instructions(self):
332 result = format_divergences(self._divergences(), reader_mode=True)
333 assert "#### Divergence Instructions" not in result
334 assert "Use divergences to identify" not in result
335
336 def test_reader_mode_has_narrative(self):
337 result = format_divergences(self._divergences(), reader_mode=True)
338 # Narrative prose paragraphs, no raw bullet lists
339 assert "External press heavily covered" in result
340 assert "Developer activity this week" in result
341 assert "- **quantum-computing**:" not in result
342 assert "- **wasm-tooling**:" not in result
343
344 def test_reader_mode_has_repo_links(self):
345 result = format_divergences(self._divergences(), reader_mode=True)
346 # Repo name as link, not full_name with stars
347 assert "[wasm-lib](https://github.com/org/wasm-lib)" in result
348 # Article link preserved
349 assert "[Quantum Leap](https://tc.com/q)" in result
350
351 def test_reader_mode_still_shows_data(self):
352 result = format_divergences(self._divergences(), reader_mode=True)
353 assert "quantum-computing" in result
354 assert "wasm-tooling" in result
355
356
357 class TestRenderPressContextReaderMode:
358 def test_reader_mode_removes_instructions_block(self):
359 result = render_press_context(
360 _techcrunch_data(), _correlation_data(), "2026-W21", reader_mode=True
361 )
362 assert "### Instructions" not in result
363 assert "Press-correlated" not in result
364 assert "Press & Industry" not in result
365
366 def test_ai_mode_keeps_instructions_block(self):
367 result = render_press_context(
368 _techcrunch_data(), _correlation_data(), "2026-W21", reader_mode=False
369 )
370 assert "### Instructions" in result
371 assert "Press-correlated" in result
372
373 def test_reader_mode_uses_narrative(self):
374 many = [
375 {
376 "repo": f"openai/repo-{i}",
377 "match_type": "org_name",
378 "correlation_confidence": 0.8,
379 "hype_risk": "medium",
380 "matched_articles": ["https://techcrunch.com/article"],
381 }
382 for i in range(20)
383 ]
384 tc = _techcrunch_data(
385 [_article(title="OpenAI Launch", url="https://techcrunch.com/article")]
386 )
387 result = render_press_context(tc, _correlation_data(many), "2026-W21", reader_mode=True)
388 # Narrative mode: no raw confidence/match_type bullets
389 assert "confidence:" not in result
390 assert "match_type" not in result
391 # Should contain prose with repo links
392 assert "https://github.com/openai/repo-" in result
393
394 def test_reader_mode_no_raw_confidence_in_narrative(self):
395 few = [
396 {
397 "repo": f"google/repo-{i}",
398 "match_type": "org_name",
399 "correlation_confidence": 0.9,
400 "hype_risk": "low",
401 "matched_articles": [],
402 }
403 for i in range(5)
404 ]
405 result = render_press_context(
406 _techcrunch_data(),
407 _correlation_data(few),
408 "2026-W21",
409 reader_mode=True,
410 )
411 assert "confidence:" not in result
412 assert "more repos with press correlation" not in result
413 assert "https://github.com/google/repo-" in result
414
415
416 class TestStripAiInstructions:
417 """Tests for analyze_fallback._strip_ai_instructions."""
418
419 def setup_method(self):
420 import sys
421
422 sys.path.insert(0, str(_REPO_ROOT))
423 import scripts.analyze_fallback as af
424
425 self.af = af
426
427 def _full_press_context(self) -> str:
428 """Simulate a fully rendered AI-mode press context."""
429 return (
430 "## Press Context (TechCrunch, week of 2026-W21)\n"
431 "3 articles published relevant to tech/open-source.\n\n"
432 "Notable coverage:\n"
433 "- [Article One](https://tc.com/1) [AI]\n\n"
434 "### Correlation Summary\n"
435 "15 repos have press correlation:\n"
436 + "\n".join(
437 f"- org/repo-{i} — match: keyword, confidence: 0.5, hype_risk: low"
438 for i in range(15)
439 )
440 + "\n\n"
441 "### Instructions\n"
442 "For each trending repo, note if press coverage preceded the star surge.\n"
443 "Label repos as:\n"
444 "- '📰 Press-correlated' — stars gained after/during press coverage\n"
445 "- '🌱 Organic growth' — stars gained without press coverage\n"
446 )
447
448 def test_removes_instructions_section(self):
449 content = self._full_press_context()
450 result = self.af._strip_ai_instructions(content)
451 assert "### Instructions" not in result
452 assert "Press-correlated" not in result
453
454 def test_removes_divergence_instructions(self):
455 content = (
456 "### Divergence Analysis\n\n"
457 "#### 🚀 Dev Activity Without Press Coverage\n"
458 "repos...\n\n"
459 "#### Divergence Instructions\n"
460 "Use divergences to identify:\n"
461 "- 🔮 Where industry is moving\n"
462 "- 💡 Where devs are innovating\n"
463 )
464 result = self.af._strip_ai_instructions(content)
465 assert "#### Divergence Instructions" not in result
466 assert "Use divergences to identify" not in result
467
468 def test_truncates_correlation_list_to_10(self):
469 content = self._full_press_context()
470 result = self.af._strip_ai_instructions(content)
471 repo_lines = [ln for ln in result.splitlines() if ln.startswith("- org/repo")]
472 assert len(repo_lines) == 10
473 assert "…and 5 more repos with press correlation" in result
474
475 def test_no_truncation_when_under_limit(self):
476 content = (
477 "### Correlation Summary\n"
478 "5 repos have press correlation:\n"
479 + "\n".join(
480 f"- org/repo-{i} — match: keyword, confidence: 0.5, hype_risk: low"
481 for i in range(5)
482 )
483 + "\n"
484 )
485 result = self.af._strip_ai_instructions(content)
486 assert "more repos with press correlation" not in result
487 assert result.count("- org/repo") == 5
488
489
490 class TestExtractReadmeDescription:
491 def test_returns_first_readable_line(self):
492 snippet = "# My Project\n\nA fast, zero-dependency library for data processing.\n"
493 assert (
494 _extract_readme_description(snippet)
495 == "A fast, zero-dependency library for data processing"
496 )
497
498 def test_skips_heading_lines(self):
499 snippet = "# Heading\n## Subheading\nActual description here.\n"
500 assert _extract_readme_description(snippet) == "Actual description here"
501
502 def test_skips_image_badge_lines(self):
503 snippet = "[![badge](img)](url)\nA concise description of what this library does.\n"
504 assert (
505 _extract_readme_description(snippet)
506 == "A concise description of what this library does"
507 )
508
509 def test_returns_empty_on_no_match(self):
510 assert _extract_readme_description("# Only a heading\n") == ""
511
512 def test_strips_markdown_links(self):
513 snippet = "Check out [our docs](https://example.com) for more information.\n"
514 result = _extract_readme_description(snippet)
515 assert "https://example.com" not in result
516 assert "our docs" in result
517
518
519 class TestFormatCorrelationsNarrative:
520 def _corr(self, repo="openai/codex", confidence=0.8, hype_risk="medium", articles=None):
521 return {
522 "repo": repo,
523 "press_correlated": True,
524 "correlation_confidence": confidence,
525 "matched_articles": articles or [],
526 "match_type": "org_name",
527 "hype_risk": hype_risk,
528 }
529
530 def _art(self, title="OpenAI News", url="https://techcrunch.com/openai-news"):
531 return {"title": title, "url": url, "categories": ["AI"]}
532
533 def test_empty_returns_fallback(self):
534 result = _format_correlations_narrative([], [])
535 assert "No significant press correlations" in result
536
537 def test_produces_repo_links(self):
538 corr = self._corr(repo="openai/codex", articles=["https://techcrunch.com/a1"])
539 result = _format_correlations_narrative(
540 [corr], [self._art(url="https://techcrunch.com/a1")]
541 )
542 assert "[codex](https://github.com/openai/codex)" in result
543
544 def test_produces_article_links_when_title_available(self):
545 corr = self._corr(articles=["https://techcrunch.com/a1"])
546 art = self._art(title="OpenAI Launches Codex", url="https://techcrunch.com/a1")
547 result = _format_correlations_narrative([corr], [art])
548 assert "[OpenAI Launches Codex](https://techcrunch.com/a1)" in result
549
550 def test_no_raw_confidence_in_output(self):
551 corr = self._corr()
552 result = _format_correlations_narrative([corr], [])
553 assert "confidence:" not in result
554 assert "match_type" not in result
555 assert "hype_risk" not in result
556
557 def test_groups_by_org(self):
558 corrs = [
559 self._corr(repo="openai/codex"),
560 self._corr(repo="openai/gpt-4"),
561 self._corr(repo="google/material-design-icons", confidence=0.5),
562 ]
563 result = _format_correlations_narrative(corrs, [])
564 # openai dominates — should appear in first paragraph
565 assert "openai" in result.lower()
566 assert "google" in result.lower()
567
568 def test_no_article_link_when_url_not_in_articles(self):
569 corr = self._corr(articles=["https://techcrunch.com/unknown-url"])
570 result = _format_correlations_narrative([corr], [])
571 # URL is in the corr but not in the articles list, so no link text
572 # Any links present must point to github.com (repo links), not article URLs
573 link_urls = re.findall(r"\]\((https?://[^)]+)\)", result)
574 assert all(urlparse(url).netloc == "github.com" for url in link_urls)
575
576 def test_reader_mode_true_uses_narrative(self):
577 corr = self._corr(repo="openai/codex")
578 result = format_correlations_list([corr], reader_mode=True, articles=[])
579 assert "confidence:" not in result
580 assert "[codex](https://github.com/openai/codex)" in result
581
582 def test_reader_mode_false_uses_bullet_list(self):
583 corr = self._corr(repo="openai/codex")
584 result = format_correlations_list([corr], reader_mode=False)
585 assert "openai/codex" in result
586 assert "confidence:" in result
587 assert "match:" in result
588
589 def test_ai_mode_unchanged_in_render(self):
590 tc = _techcrunch_data()
591 corr_data = _correlation_data([self._corr()])
592 result = render_press_context(tc, corr_data, "2026-W21", reader_mode=False)
593 assert "confidence:" in result
594 assert "### Instructions" in result
595
596
597 class TestReaderModeCountHeader:
598 """reader_mode=True must not emit the raw 'N repos have press correlation:' header."""
599
600 def _corrs(self, n: int = 5) -> list[dict]:
601 return [
602 {
603 "repo": f"openai/repo-{i}",
604 "match_type": "org_name",
605 "correlation_confidence": 0.8,
606 "hype_risk": "low",
607 "matched_articles": [],
608 }
609 for i in range(n)
610 ]
611
612 def test_reader_mode_omits_count_header(self):
613 tc = {"articles": []}
614 corr_data = {"correlations": self._corrs(5), "divergences": {}}
615 result = render_press_context(tc, corr_data, "2026-W21", reader_mode=True)
616 assert "repos have press correlation" not in result
617
618 def test_ai_mode_keeps_count_header(self):
619 tc = {"articles": []}
620 corr_data = {"correlations": self._corrs(5), "divergences": {}}
621 result = render_press_context(tc, corr_data, "2026-W21", reader_mode=False)
622 assert "repos have press correlation" in result
623
624
625 class TestExtractReadmeDescriptionSentenceBoundary:
626 """_extract_readme_description must not return mid-sentence truncated text."""
627
628 def test_drops_line_without_sentence_boundary(self):
629 # Simulates a 500-char truncation mid-sentence
630 snippet = (
631 "# Guava\n\nGuava is a set of core Java libraries from Google that includes new collect"
632 )
633 result = _extract_readme_description(snippet)
634 assert result == ""
635
636 def test_trims_to_last_sentence_in_long_line(self):
637 snippet = (
638 "# Lib\n\nThis library does X. It also does Y. And even more beyond that without end"
639 )
640 result = _extract_readme_description(snippet)
641 # Should trim to the last complete sentence boundary
642 assert result == "This library does X. It also does Y"
643
644 def test_returns_empty_when_no_boundary_in_snippet(self):
645 snippet = "# Header\n\nNo period here at all and the line is long enough to match normally"
646 result = _extract_readme_description(snippet)
647 assert result == ""
648
649
650 class TestEscapeMarkdownUrl:
651 """Ensure URLs with parentheses are safely escaped in markdown links."""
652
653 def test_url_with_parentheses_escaped_in_articles_list(self):
654 articles = [
655 {
656 "title": "Wikipedia Article",
657 "url": "https://en.wikipedia.org/wiki/AI_(term)",
658 "categories": ["AI"],
659 "source": "Wikipedia",
660 "published_at": "2026-01-01T00:00:00Z",
661 }
662 ]
663 result = format_articles_list(articles)
664 # Parentheses in URLs must be percent-encoded
665 assert "%28" in result and "%29" in result
666 # The raw parenthesis should not appear inside the markdown link target
667 assert "](https://en.wikipedia.org/wiki/AI_(term))" not in result
668 assert "](https://en.wikipedia.org/wiki/AI_%28term%29)" in result
669
670 def test_url_without_parentheses_unchanged(self):
671 url = "https://example.com/path?q=1&r=2"
672 assert _escape_markdown_url(url) == url