main
py 586 lines 23.9 KB
Raw
1 import io
2 import tempfile
3 import unittest
4 from pathlib import Path
5 from unittest import mock
6
7 import scripts.generate_rollups as generate_rollups
8 import scripts.generate_yearly_narrative as generate_yearly_narrative
9
10 WORKSPACE_ROOT = Path(".test-workspaces")
11
12
13 def make_summary(
14 *,
15 week: str,
16 date: str,
17 top_repo: str,
18 summary: str,
19 signal: str,
20 noise: str,
21 gaps: str,
22 conclusion: str,
23 tags: tuple[str, ...] = ("ai", "agents", "developer-tooling"),
24 repo_mentions: tuple[str, ...] = (),
25 ) -> str:
26 year = int(week[:4])
27 rendered_tags = ", ".join(tags)
28 linked_mentions = " ".join(
29 f"[{repo}](https://github.com/{repo}) is part of the weekly conversation."
30 for repo in repo_mentions
31 )
32 notable_new = f"A fresh set of launches landed. {linked_mentions}".strip()
33 return f'''---
34 title: "Week {int(week[-2:])}, {year} Analysis"
35 date: {date}
36 week: "{week}"
37 year: {year}
38 tags: [{rendered_tags}]
39 categories: [weekly]
40 repos_featured: 10
41 top_repo: "{top_repo}"
42 quality_score: 80
43 summary: "{summary}"
44 stars_tracked: 1000
45 ---
46
47 ## This Week's Trends
48
49 Trend analysis for {week}. Developer activity concentrated around practical tooling and infrastructure work.
50
51 ## Where Industry Meets Code
52
53 No press data available for this automated test summary. Developer activity tells a coherent story on its own.
54
55 ## Signal & Noise
56
57 {signal} {noise}
58
59 ## Blind Spots
60
61 {gaps}
62
63 ## The Week Ahead
64
65 {conclusion}
66
67 ## Key References
68
69 ### Notable Projects
70
71 {notable_new}
72
73 ### Press & Industry
74
75 No press data was provided this week.
76 '''
77
78
79 def temporary_workspace() -> tempfile.TemporaryDirectory[str]:
80 WORKSPACE_ROOT.mkdir(exist_ok=True)
81 return tempfile.TemporaryDirectory(dir=WORKSPACE_ROOT.resolve())
82
83
84 class GenerateRollupsTests(unittest.TestCase):
85 def test_generate_rollups_creates_monthly_and_yearly_pages(self) -> None:
86 with temporary_workspace() as tmpdir:
87 base = Path(tmpdir)
88 analyzed_dir = base / "data" / "analyzed"
89 content_root = base / "content"
90 analyzed_dir.mkdir(parents=True)
91
92 (analyzed_dir / "2026-W21-summary.md").write_text(
93 make_summary(
94 week="2026-W21",
95 date="2026-05-18T12:07:20+00:00",
96 top_repo="octo/signal-kit",
97 summary="Practical agent tooling led the week.",
98 signal="Teams preferred operational automation over generic hype.",
99 noise="Exploit-heavy projects still added editorial noise.",
100 gaps="Reliable momentum data remained missing.",
101 conclusion="The strongest projects made automation safer to adopt.",
102 ),
103 encoding="utf-8",
104 )
105
106 written = generate_rollups.generate_rollups(analyzed_dir, content_root)
107
108 monthly_path = content_root / "monthly" / "2026" / "05.md"
109 yearly_path = content_root / "yearly" / "2026.md"
110 self.assertEqual(written, [monthly_path, yearly_path])
111
112 monthly = monthly_path.read_text(encoding="utf-8")
113 self.assertIn("Define the Month — May 2026", monthly)
114 self.assertIn('categories: ["monthly"]', monthly)
115 self.assertIn('weeks_covered: ["2026-W21"]', monthly)
116 self.assertIn("total_repos_featured: 1", monthly)
117 self.assertIn("---\n\n## Month Synthesis", monthly)
118 self.assertIn("## Month Overview", monthly)
119 self.assertIn("### Week 2026-W21", monthly)
120 self.assertIn("[Week 21, 2026](/weekly/2026/W21/)", monthly)
121 self.assertIn("[octo/signal-kit](https://github.com/octo/signal-kit)", monthly)
122
123 yearly = yearly_path.read_text(encoding="utf-8")
124 self.assertIn("The Ecosystem Reorganizes", yearly)
125 self.assertIn('categories: ["yearly"]', yearly)
126 self.assertIn('months_covered: ["2026-05"]', yearly)
127 self.assertIn('format: "narrative"', yearly)
128 self.assertIn("## Year in Review", yearly)
129 self.assertIn("Practical agent tooling led the week.", yearly)
130 self.assertNotIn("## Arc", yearly)
131
132 def test_generate_rollups_is_append_only_for_existing_pages(self) -> None:
133 with temporary_workspace() as tmpdir:
134 base = Path(tmpdir)
135 analyzed_dir = base / "data" / "analyzed"
136 content_root = base / "content"
137 analyzed_dir.mkdir(parents=True)
138
139 (analyzed_dir / "2026-W21-summary.md").write_text(
140 make_summary(
141 week="2026-W21",
142 date="2026-05-18T12:07:20+00:00",
143 top_repo="octo/signal-kit",
144 summary="Practical agent tooling led the week.",
145 signal="Teams preferred operational automation over generic hype.",
146 noise="Exploit-heavy projects still added editorial noise.",
147 gaps="Reliable momentum data remained missing.",
148 conclusion="The strongest projects made automation safer to adopt.",
149 tags=("alpha",),
150 repo_mentions=("octo/shared-kit",),
151 ),
152 encoding="utf-8",
153 )
154 generate_rollups.generate_rollups(analyzed_dir, content_root)
155 monthly_path = content_root / "monthly" / "2026" / "05.md"
156 yearly_path = content_root / "yearly" / "2026.md"
157 first_monthly = monthly_path.read_text(encoding="utf-8")
158 first_yearly = yearly_path.read_text(encoding="utf-8")
159
160 (analyzed_dir / "2026-W22-summary.md").write_text(
161 make_summary(
162 week="2026-W22",
163 date="2026-05-25T12:07:20+00:00",
164 top_repo="octo/steady-watch",
165 summary="Observability and release safety gained more traction.",
166 signal="Teams doubled down on measurable automation and release health.",
167 noise="Wrapper projects still outnumbered differentiated platforms.",
168 gaps="Defensive tooling still lagged behind orchestration tools.",
169 conclusion="The durable winners reduced toil without hiding trade-offs.",
170 tags=("beta",),
171 repo_mentions=("octo/shared-kit", "octo/deploy-guard"),
172 ),
173 encoding="utf-8",
174 )
175
176 generate_rollups.generate_rollups(analyzed_dir, content_root)
177 second_monthly = monthly_path.read_text(encoding="utf-8")
178 second_yearly = yearly_path.read_text(encoding="utf-8")
179
180 self.assertIn('weeks_covered: ["2026-W21", "2026-W22"]', second_monthly)
181 self.assertIn("total_repos_featured: 4", second_monthly)
182 self.assertIn('months_covered: ["2026-05"]', second_yearly)
183 for expected in [
184 "### Week 2026-W21 — [Week 21, 2026](/weekly/2026/W21/)",
185 "- [octo/signal-kit](https://github.com/octo/signal-kit) led the published weekly analysis for 2026-W21.",
186 "- Signal: Teams preferred operational automation over generic hype.",
187 "- Gap to watch: Reliable momentum data remained missing.",
188 "- Recurring themes so far: alpha.",
189 ]:
190 self.assertIn(expected, second_monthly)
191 self.assertIn("- Recurring themes so far: alpha, beta.", second_monthly)
192 self.assertIn('format: "narrative"', second_yearly)
193 self.assertIn("## Year in Review", second_yearly)
194 self.assertIn("Observability and release safety gained more traction.", second_yearly)
195 self.assertNotIn("## Arc", second_yearly)
196 self.assertEqual(second_monthly.count("### Week 2026-W21"), 4)
197 self.assertEqual(second_monthly.count("### Week 2026-W22"), 4)
198 self.assertEqual(second_yearly.count("## Year in Review"), 1)
199 self.assertNotEqual(first_monthly, second_monthly)
200 self.assertNotEqual(first_yearly, second_yearly)
201
202 def test_generate_yearly_narrative_standalone_writes_narrative_format(self) -> None:
203 with temporary_workspace() as tmpdir:
204 base = Path(tmpdir)
205 content_root = base / "content"
206 monthly_dir = content_root / "monthly" / "2026"
207 monthly_dir.mkdir(parents=True)
208
209 (monthly_dir / "05.md").write_text(
210 """---
211 title: "May 2026 Rollup"
212 date: "2026-05-25T11:56:08+00:00"
213 month: 5
214 year: 2026
215 categories: ["monthly"]
216 weeks_covered: ["2026-W21", "2026-W22"]
217 total_repos_featured: 32
218 ---
219
220 ## Month Overview
221
222 ### Week 2026-W21 — [Week 21, 2026](/weekly/2026/W21/)
223 - Summary: May defined the shift from maturing agent infrastructure toward a visible agent skills economy.
224 - Repositories featured this week: 17
225 - Recurring themes so far: agent-skills, mcp, small-models.
226
227 ## Trends Observed
228
229 ### Week 2026-W21 — [Week 21, 2026](/weekly/2026/W21/)
230 - Signal: Agent skills kept widening as a distribution format.
231 - Noise: Coordinated star-farming distorted discovery.
232
233 ## Key Takeaways
234
235 ### Week 2026-W21 — [Week 21, 2026](/weekly/2026/W21/)
236 - Gap to watch: Agent execution security remained underbuilt.
237 - Closing read: Skills were likely to spread into more teams.
238 """,
239 encoding="utf-8",
240 )
241 (monthly_dir / "06.md").write_text(
242 """---
243 title: "June 2026 Rollup"
244 date: "2026-06-08T12:40:47+00:00"
245 month: 6
246 year: 2026
247 categories: ["monthly"]
248 weeks_covered: ["2026-W23", "2026-W24"]
249 total_repos_featured: 36
250 ---
251
252 ## Month Overview
253
254 ### Week 2026-W23 — [Week 23, 2026](/weekly/2026/W23/)
255 - Summary: June pushed agent skills into East Asian workflows, self-hosted AI workspaces, and role-specific verticalization.
256 - Repositories featured this week: 36
257 - Recurring themes so far: agent-skills, self-hosted-ai, coding-agents.
258
259 ## Trends Observed
260
261 ### Week 2026-W23 — [Week 23, 2026](/weekly/2026/W23/)
262 - Signal: Agent skills globalized quickly while local-sovereignty tooling gained traction.
263 - Noise: Fork inflation replaced the earlier star-farming wave.
264
265 ## Key Takeaways
266
267 ### Week 2026-W23 — [Week 23, 2026](/weekly/2026/W23/)
268 - Gap to watch: Prompt-injection and skills supply-chain security still lacked a category winner.
269 - Closing read: Expect more vertical skills packs and more local-first AI tooling.
270 """,
271 encoding="utf-8",
272 )
273
274 written = generate_yearly_narrative.generate_yearly_narratives(content_root)
275 yearly_path = content_root / "yearly" / "2026.md"
276
277 self.assertEqual(written, [yearly_path])
278 yearly = yearly_path.read_text(encoding="utf-8")
279 self.assertIn("When Agents Became Infrastructure", yearly)
280 self.assertIn('format: "narrative"', yearly)
281 self.assertIn("## Year in Review", yearly)
282 self.assertIn("split-screen story", yearly)
283 self.assertIn("globalized", yearly)
284 self.assertIn("What was confirmed:", yearly)
285 self.assertIn("What weakened:", yearly)
286 self.assertNotIn("## Arc", yearly)
287 self.assertNotIn(
288 "agent-skills: infrastructure > economy > globalization > verticalization", yearly
289 )
290
291 def test_generate_yearly_narrative_prefers_month_synthesis_artifacts(self) -> None:
292 with temporary_workspace() as tmpdir:
293 base = Path(tmpdir)
294 content_root = base / "content"
295 monthly_dir = content_root / "monthly" / "2026"
296 analyzed_dir = base / "data" / "analyzed"
297 monthly_dir.mkdir(parents=True)
298 analyzed_dir.mkdir(parents=True)
299
300 (monthly_dir / "05.md").write_text(
301 """---
302 title: "May 2026 Rollup"
303 date: "2026-05-25T11:56:08+00:00"
304 month: 5
305 year: 2026
306 categories: ["monthly"]
307 weeks_covered: ["2026-W21", "2026-W22"]
308 total_repos_featured: 32
309 ---
310
311 ## Month Overview
312
313 ### Week 2026-W21 — [Week 21, 2026](/weekly/2026/W21/)
314 - Summary: Fallback monthly summary that should not drive the yearly opening.
315 - Repositories featured this week: 17
316 - Recurring themes so far: agent-skills, mcp.
317 """,
318 encoding="utf-8",
319 )
320 (analyzed_dir / "2026-05-month-synthesis.md").write_text(
321 """---
322 title: "May 2026 Monthly Synthesis"
323 date: "2026-05-25T11:56:08+00:00"
324 month: 5
325 year: 2026
326 ---
327
328 ## Month Synthesis
329
330 May made it clear that teams were no longer evaluating agent skills as demos; they were treating them as operating infrastructure with distribution consequences.
331
332 The strongest thread was a shift from raw capability talk toward packaging, trust, and fit inside real workflows.
333 """,
334 encoding="utf-8",
335 )
336
337 written = generate_yearly_narrative.generate_yearly_narratives(content_root)
338 yearly_path = content_root / "yearly" / "2026.md"
339
340 self.assertEqual(written, [yearly_path])
341 yearly = yearly_path.read_text(encoding="utf-8")
342 self.assertIn("operating infrastructure with distribution consequences", yearly)
343 self.assertNotIn(
344 "Fallback monthly summary that should not drive the yearly opening", yearly
345 )
346
347 def test_generate_rollups_replaces_placeholder_and_preserves_unknown_sections(self) -> None:
348 with temporary_workspace() as tmpdir:
349 base = Path(tmpdir)
350 analyzed_dir = base / "data" / "analyzed"
351 content_root = base / "content"
352 analyzed_dir.mkdir(parents=True)
353 monthly_path = content_root / "monthly" / "2026" / "05.md"
354 monthly_path.parent.mkdir(parents=True, exist_ok=True)
355 monthly_path.write_text(
356 '---\ntitle: "May 2026 Rollup"\n---\n\n## Month Overview\n\n_No updates yet._\n\n## Legacy Notes\n\nKeep this section.\n',
357 encoding="utf-8",
358 )
359
360 (analyzed_dir / "2026-W21-summary.md").write_text(
361 make_summary(
362 week="2026-W21",
363 date="2026-05-18T12:07:20+00:00",
364 top_repo="octo/signal-kit",
365 summary="Practical agent tooling led the week.",
366 signal="Teams preferred operational automation over generic hype.",
367 noise="Exploit-heavy projects still added editorial noise.",
368 gaps="Reliable momentum data remained missing.",
369 conclusion="The strongest projects made automation safer to adopt.",
370 ),
371 encoding="utf-8",
372 )
373
374 generate_rollups.generate_rollups(analyzed_dir, content_root)
375 monthly = monthly_path.read_text(encoding="utf-8")
376
377 self.assertNotIn("_No updates yet._\n\n### Week 2026-W21", monthly)
378 self.assertIn("### Week 2026-W21", monthly)
379 self.assertIn("## Legacy Notes\n\nKeep this section.", monthly)
380
381 def test_generate_rollups_returns_empty_when_no_summaries_exist(self) -> None:
382 with temporary_workspace() as tmpdir:
383 base = Path(tmpdir)
384 analyzed_dir = base / "data" / "analyzed"
385 content_root = base / "content"
386 analyzed_dir.mkdir(parents=True)
387
388 self.assertEqual(generate_rollups.generate_rollups(analyzed_dir, content_root), [])
389 stderr = io.StringIO()
390 with mock.patch("sys.stderr", stderr):
391 self.assertEqual(
392 generate_rollups.main(
393 ["--analyzed-dir", str(analyzed_dir), "--content-root", str(content_root)]
394 ),
395 0,
396 )
397 self.assertIn("No weekly summaries found", stderr.getvalue())
398
399 def test_parse_args_defaults_resolve_from_project_root(self) -> None:
400 args = generate_rollups.parse_args([])
401
402 self.assertEqual(
403 args.analyzed_dir, Path(generate_rollups.PROJECT_ROOT / "data" / "analyzed")
404 )
405 self.assertEqual(args.content_root, Path(generate_rollups.PROJECT_ROOT / "content"))
406
407 def test_generate_rolling_report_creates_last_month(self) -> None:
408 with temporary_workspace() as tmpdir:
409 base = Path(tmpdir)
410 content_root = base / "content"
411 weekly_dir = content_root / "weekly" / "2026"
412 weekly_dir.mkdir(parents=True)
413
414 for wnum, date in [
415 ("21", "2026-05-21T12:00:00+00:00"),
416 ("22", "2026-05-25T12:00:00+00:00"),
417 ("23", "2026-06-06T12:00:00+00:00"),
418 ("24", "2026-06-08T12:00:00+00:00"),
419 ]:
420 (weekly_dir / f"W{wnum}.md").write_text(
421 make_summary(
422 week=f"2026-W{wnum}",
423 date=date,
424 top_repo=f"octo/repo-{wnum}",
425 summary=f"Summary for W{wnum}.",
426 signal=f"Signal for W{wnum}.",
427 noise=f"Noise for W{wnum}.",
428 gaps=f"Gaps for W{wnum}.",
429 conclusion=f"Conclusion for W{wnum}.",
430 tags=("ai", "agents") if wnum in ("21", "22") else ("ai", "new-tag"),
431 ),
432 encoding="utf-8",
433 )
434
435 result = generate_rollups.generate_rolling_report(content_root)
436
437 self.assertIsNotNone(result)
438 self.assertEqual(result, content_root / "rolling" / "last-month.md")
439 self.assertTrue(result.exists())
440
441 report = result.read_text(encoding="utf-8")
442 self.assertIn("title: Rolling 4-Week Context", report)
443 self.assertIn("updated: 2026-W24", report)
444 self.assertIn("weeks: [W21, W22, W23, W24]", report)
445 self.assertIn("## Active Trends", report)
446 self.assertIn("## Trend Velocity", report)
447 self.assertIn("## Open Predictions", report)
448 self.assertIn("## Noise Patterns", report)
449 self.assertIn("Signal for W", report)
450 self.assertIn("Noise for W", report)
451 self.assertIn("[W24] Gaps for W24.", report)
452
453 def test_generate_rolling_report_returns_none_when_no_content(self) -> None:
454 with temporary_workspace() as tmpdir:
455 base = Path(tmpdir)
456 content_root = base / "content"
457 content_root.mkdir(parents=True)
458
459 result = generate_rollups.generate_rolling_report(content_root)
460 self.assertIsNone(result)
461
462 def test_rolling_flag_triggers_rolling_report(self) -> None:
463 with temporary_workspace() as tmpdir:
464 base = Path(tmpdir)
465 analyzed_dir = base / "data" / "analyzed"
466 content_root = base / "content"
467 analyzed_dir.mkdir(parents=True)
468 weekly_dir = content_root / "weekly" / "2026"
469 weekly_dir.mkdir(parents=True)
470
471 summary_text = make_summary(
472 week="2026-W21",
473 date="2026-05-21T12:00:00+00:00",
474 top_repo="octo/signal-kit",
475 summary="Test summary.",
476 signal="Test signal.",
477 noise="Test noise.",
478 gaps="Test gaps.",
479 conclusion="Test conclusion.",
480 )
481 (analyzed_dir / "2026-W21-summary.md").write_text(summary_text, encoding="utf-8")
482 (weekly_dir / "W21.md").write_text(summary_text, encoding="utf-8")
483
484 ret = generate_rollups.main(
485 [
486 "--analyzed-dir",
487 str(analyzed_dir),
488 "--content-root",
489 str(content_root),
490 "--rolling",
491 ]
492 )
493 self.assertEqual(ret, 0)
494 self.assertTrue((content_root / "rolling" / "last-month.md").exists())
495
496 def test_velocity_classification(self) -> None:
497 tags_by_week = [
498 ("2026-W21", {"ai", "agents", "old-tag"}),
499 ("2026-W22", {"ai", "agents"}),
500 ("2026-W23", {"ai", "new-thing"}),
501 ("2026-W24", {"ai", "new-thing"}),
502 ]
503 velocity = generate_rollups._classify_velocity(tags_by_week)
504 self.assertEqual(velocity["ai"], "accelerating")
505 self.assertEqual(velocity["old-tag"], "dying")
506 self.assertIn(velocity["new-thing"], ("accelerating", "new"))
507
508 def test_extract_summary_in_yearly_frontmatter(self) -> None:
509 """Yearly pages must include a summary field in frontmatter."""
510 with temporary_workspace() as tmpdir:
511 base = Path(tmpdir)
512 analyzed_dir = base / "data" / "analyzed"
513 content_root = base / "content"
514 analyzed_dir.mkdir(parents=True)
515
516 (analyzed_dir / "2026-W21-summary.md").write_text(
517 make_summary(
518 week="2026-W21",
519 date="2026-05-18T12:07:20+00:00",
520 top_repo="octo/signal-kit",
521 summary="Practical agent tooling led the week.",
522 signal="Teams preferred operational automation over generic hype.",
523 noise="Exploit-heavy projects still added editorial noise.",
524 gaps="Reliable momentum data remained missing.",
525 conclusion="The strongest projects made automation safer to adopt.",
526 ),
527 encoding="utf-8",
528 )
529
530 generate_rollups.generate_rollups(analyzed_dir, content_root)
531 yearly_path = content_root / "yearly" / "2026.md"
532 yearly = yearly_path.read_text(encoding="utf-8")
533 self.assertIn("summary:", yearly)
534 # Extract summary value and verify length constraint
535 for line in yearly.splitlines():
536 if line.strip().startswith("summary:"):
537 summary_value = line.split(":", 1)[1].strip().strip('"')
538 self.assertLessEqual(len(summary_value), 155)
539 break
540 else:
541 self.fail("summary field not found in yearly frontmatter")
542
543
544 class ExtractSummaryTests(unittest.TestCase):
545 def test_short_sentence_returned_as_is(self) -> None:
546 result = generate_yearly_narrative._extract_summary("Short sentence.")
547 self.assertEqual(result, "Short sentence.")
548
549 def test_exclamation_mark_terminates_sentence(self) -> None:
550 result = generate_yearly_narrative._extract_summary("Wow! More text follows here.")
551 self.assertEqual(result, "Wow!")
552
553 def test_question_mark_terminates_sentence(self) -> None:
554 result = generate_yearly_narrative._extract_summary("Why not? The rest is irrelevant.")
555 self.assertEqual(result, "Why not?")
556
557 def test_truncation_respects_max_length(self) -> None:
558 long = "A" * 200 + "."
559 result = generate_yearly_narrative._extract_summary(long, max_length=50)
560 self.assertLessEqual(len(result), 50)
561 self.assertTrue(result.endswith(""))
562
563 def test_truncation_at_word_boundary(self) -> None:
564 sentence = "This is a moderately long sentence that should be truncated at a word boundary when it exceeds the maximum allowed length for meta descriptions."
565 result = generate_yearly_narrative._extract_summary(sentence, max_length=60)
566 self.assertLessEqual(len(result), 60)
567 self.assertTrue(result.endswith(""))
568 self.assertFalse(result[-2].isspace())
569
570 def test_leading_whitespace_stripped(self) -> None:
571 result = generate_yearly_narrative._extract_summary(" Leading spaces. More text.")
572 self.assertEqual(result, "Leading spaces.")
573
574 def test_fallback_when_no_sentence_terminator(self) -> None:
575 result = generate_yearly_narrative._extract_summary("No punctuation at all")
576 self.assertEqual(result, "No punctuation at all")
577
578 def test_fallback_truncation_for_long_text_without_terminator(self) -> None:
579 long = "word " * 50
580 result = generate_yearly_narrative._extract_summary(long, max_length=30)
581 self.assertLessEqual(len(result), 30)
582 self.assertTrue(result.endswith(""))
583
584
585 if __name__ == "__main__":
586 unittest.main()