| 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 = "[](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 |