490
self.assertEqual(velocity["old-tag"], "dying")
491
self.assertIn(velocity["new-thing"], ("accelerating", "new"))
492
493
+ def test_extract_summary_in_yearly_frontmatter(self) -> None:
494
+ """Yearly pages must include a summary field in frontmatter."""
495
+ with temporary_workspace() as tmpdir:
496
+ base = Path(tmpdir)
497
+ analyzed_dir = base / "data" / "analyzed"
498
+ content_root = base / "content"
499
+ analyzed_dir.mkdir(parents=True)
500
+
501
+ (analyzed_dir / "2026-W21-summary.md").write_text(
502
+ make_summary(
503
+ week="2026-W21",
504
+ date="2026-05-18T12:07:20+00:00",
505
+ top_repo="octo/signal-kit",
506
+ summary="Practical agent tooling led the week.",
507
+ signal="Teams preferred operational automation over generic hype.",
508
+ noise="Exploit-heavy projects still added editorial noise.",
509
+ gaps="Reliable momentum data remained missing.",
510
+ conclusion="The strongest projects made automation safer to adopt.",
511
+ ),
512
+ encoding="utf-8",
513
+ )
514
+
515
+ generate_rollups.generate_rollups(analyzed_dir, content_root)
516
+ yearly_path = content_root / "yearly" / "2026.md"
517
+ yearly = yearly_path.read_text(encoding="utf-8")
518
+ self.assertIn("summary:", yearly)
519
+ # Extract summary value and verify length constraint
520
+ for line in yearly.splitlines():
521
+ if line.strip().startswith("summary:"):
522
+ summary_value = line.split(":", 1)[1].strip().strip('"')
523
+ self.assertLessEqual(len(summary_value), 155)
524
+ break
525
+ else:
526
+ self.fail("summary field not found in yearly frontmatter")
527
+
528
+
529
+class ExtractSummaryTests(unittest.TestCase):
530
+ def test_short_sentence_returned_as_is(self) -> None:
531
+ result = generate_yearly_narrative._extract_summary("Short sentence.")
532
+ self.assertEqual(result, "Short sentence.")
533
+
534
+ def test_exclamation_mark_terminates_sentence(self) -> None:
535
+ result = generate_yearly_narrative._extract_summary("Wow! More text follows here.")
536
+ self.assertEqual(result, "Wow!")
537
+
538
+ def test_question_mark_terminates_sentence(self) -> None:
539
+ result = generate_yearly_narrative._extract_summary("Why not? The rest is irrelevant.")
540
+ self.assertEqual(result, "Why not?")
541
+
542
+ def test_truncation_respects_max_length(self) -> None:
543
+ long = "A" * 200 + "."
544
+ result = generate_yearly_narrative._extract_summary(long, max_length=50)
545
+ self.assertLessEqual(len(result), 50)
546
+ self.assertTrue(result.endswith("…"))
547
+
548
+ def test_truncation_at_word_boundary(self) -> None:
549
+ 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."
550
+ result = generate_yearly_narrative._extract_summary(sentence, max_length=60)
551
+ self.assertLessEqual(len(result), 60)
552
+ self.assertTrue(result.endswith("…"))
553
+ self.assertFalse(result[-2].isspace())
554
+
555
+ def test_leading_whitespace_stripped(self) -> None:
556
+ result = generate_yearly_narrative._extract_summary(" Leading spaces. More text.")
557
+ self.assertEqual(result, "Leading spaces.")
558
+
559
+ def test_fallback_when_no_sentence_terminator(self) -> None:
560
+ result = generate_yearly_narrative._extract_summary("No punctuation at all")
561
+ self.assertEqual(result, "No punctuation at all")
562
+
563
+ def test_fallback_truncation_for_long_text_without_terminator(self) -> None:
564
+ long = "word " * 50
565
+ result = generate_yearly_narrative._extract_summary(long, max_length=30)
566
+ self.assertLessEqual(len(result), 30)
567
+ self.assertTrue(result.endswith("…"))
568
+
569
570
if __name__ == "__main__":
571
unittest.main()