From 6f57c893cddc341ef63f9e412eeaea6e0d3e7bfe Mon Sep 17 00:00:00 2001 From: Ed_ Date: Wed, 1 Jul 2026 23:23:01 -0400 Subject: [PATCH] test(chronology): write Red tests for v2 classifier + summary extractor --- tests/test_generate_chronology.py | 227 ++++++++++++++++++++++++++++-- 1 file changed, 214 insertions(+), 13 deletions(-) diff --git a/tests/test_generate_chronology.py b/tests/test_generate_chronology.py index 7da9ebca..2ab1525c 100644 --- a/tests/test_generate_chronology.py +++ b/tests/test_generate_chronology.py @@ -1,7 +1,15 @@ from pathlib import Path import json import pytest -from scripts.audit.generate_chronology import extract_slug_date, extract_summary +from unittest.mock import patch, MagicMock +from scripts.audit.generate_chronology import ( + extract_slug_date, + extract_summary, + classify_status, + walk_track_folders, + format_markdown, +) +from scripts.audit.chronology_quality_gate import run_quality_gate def test_slug_date_extraction() -> None: @@ -14,18 +22,62 @@ def test_slug_date_extraction_handles_missing_date() -> None: assert result is None -def test_summary_extraction_from_spec_md(tmp_path: Path) -> None: - spec_content: str = "# Title\n\n## Overview\n\nFirst sentence here. Second sentence.\n" +def test_summary_extraction_rejects_priority_line(tmp_path: Path) -> None: + spec_content: str = "# Title\n\n**Priority:** A (foundational)\n\nReal description of the work.\n" (tmp_path / "spec.md").write_text(spec_content, encoding="utf-8") result: str = extract_summary(tmp_path) - assert result == "First sentence here." + assert result == "Real description of the work." -def test_summary_extraction_falls_back_to_metadata(tmp_path: Path) -> None: - metadata: dict = {"description": "From metadata."} +def test_summary_extraction_rejects_date_line(tmp_path: Path) -> None: + spec_content: str = "# Title\n\n**Date:** 2026-06-20\n\nReal description.\n" + (tmp_path / "spec.md").write_text(spec_content, encoding="utf-8") + result: str = extract_summary(tmp_path) + assert result == "Real description." + + +def test_summary_extraction_rejects_initialized_line(tmp_path: Path) -> None: + spec_content: str = "# Title\n\n**Initialized:** 2026-06-13\n\nReal description.\n" + (tmp_path / "spec.md").write_text(spec_content, encoding="utf-8") + result: str = extract_summary(tmp_path) + assert result == "Real description." + + +def test_summary_extraction_rejects_track_line(tmp_path: Path) -> None: + spec_content: str = "# Title\n\n**Track:** Some track\n\nReal description.\n" + (tmp_path / "spec.md").write_text(spec_content, encoding="utf-8") + result: str = extract_summary(tmp_path) + assert result == "Real description." + + +def test_summary_extraction_rejects_parent_umbrella_line(tmp_path: Path) -> None: + spec_content: str = "# Title\n\n**Parent umbrella:** result_migration_20260616\n\nReal description.\n" + (tmp_path / "spec.md").write_text(spec_content, encoding="utf-8") + result: str = extract_summary(tmp_path) + assert result == "Real description." + + +def test_summary_extraction_rejects_confidence_line(tmp_path: Path) -> None: + spec_content: str = "# Title\n\n**Confidence:** high\n\nReal description.\n" + (tmp_path / "spec.md").write_text(spec_content, encoding="utf-8") + result: str = extract_summary(tmp_path) + assert result == "Real description." + + +def test_summary_extraction_prefers_metadata_description_prose(tmp_path: Path) -> None: + metadata: dict = {"description": "A real prose description of the track."} (tmp_path / "metadata.json").write_text(json.dumps(metadata), encoding="utf-8") result: str = extract_summary(tmp_path) - assert result == "From metadata." + assert result == "A real prose description of the track." + + +def test_summary_extraction_rejects_metadata_description_field_text(tmp_path: Path) -> None: + metadata: dict = {"description": "**Priority:** A (foundational)"} + (tmp_path / "metadata.json").write_text(json.dumps(metadata), encoding="utf-8") + spec_content: str = "# Title\n\nReal description from spec.\n" + (tmp_path / "spec.md").write_text(spec_content, encoding="utf-8") + result: str = extract_summary(tmp_path) + assert result == "Real description from spec." def test_summary_extraction_truncates_to_25_words(tmp_path: Path) -> None: @@ -35,11 +87,160 @@ def test_summary_extraction_truncates_to_25_words(tmp_path: Path) -> None: result: str = extract_summary(tmp_path) expected: str = " ".join(["word"] * 25) + "\u2026" assert result == expected - assert result.endswith("\u2026") -def test_summary_extraction_skips_status_metadata_line(tmp_path: Path) -> None: - spec_content: str = "# Title\n\n**Status:** Spec approved 2026-06-19\n\nReal description of the work.\n" - (tmp_path / "spec.md").write_text(spec_content, encoding="utf-8") - result: str = extract_summary(tmp_path) - assert result == "Real description of the work." \ No newline at end of file +# --- Classifier tests (the new git-history classifier) --- + +def test_classify_status_completion_report_override(tmp_path: Path) -> None: + """TRACK_COMPLETION report in docs/reports/ overrides everything.""" + reports_dir = tmp_path / "docs/reports" + reports_dir.mkdir(parents=True) + (reports_dir / "TRACK_COMPLETION_my_track_20260701.md").write_text("report", encoding="utf-8") + result = classify_status( + folder_link="conductor/tracks/my_track_20260701", + current="active", + track_id="my_track_20260701", + repo_root=tmp_path, + reports_dir=reports_dir, + ) + assert result[0] == "Completed" + assert result[1] == "high" + assert "completion report" in result[2] + + +def test_classify_status_abort_report_override(tmp_path: Path) -> None: + """TRACK_ABORTED report -> Abandoned.""" + reports_dir = tmp_path / "docs/reports" + reports_dir.mkdir(parents=True) + (reports_dir / "TRACK_ABORTED_my_track_20260701.md").write_text("report", encoding="utf-8") + result = classify_status( + folder_link="conductor/tracks/my_track_20260701", + current="active", + track_id="my_track_20260701", + repo_root=tmp_path, + reports_dir=reports_dir, + has_abort_report=True, + ) + assert result[0] == "Abandoned" + assert result[1] == "high" + assert "abort report" in result[2] + + +def test_classify_status_superseded_state_toml_overrides_abort(tmp_path: Path) -> None: + """state.toml status=superseded wins over abort report.""" + reports_dir = tmp_path / "docs/reports" + reports_dir.mkdir(parents=True) + (reports_dir / "TRACK_ABORTED_my_track_20260701.md").write_text("report", encoding="utf-8") + result = classify_status( + folder_link="conductor/tracks/my_track_20260701", + current="active", + track_id="my_track_20260701", + repo_root=tmp_path, + reports_dir=reports_dir, + has_abort_report=True, + state_status="superseded", + ) + assert result[0] == "Superseded" + assert result[1] == "high" + + +def test_classify_status_work_commits_completed(tmp_path: Path) -> None: + """>=3 work commits -> Completed.""" + reports_dir = tmp_path / "docs/reports" + reports_dir.mkdir(parents=True) + with patch("scripts.audit.generate_chronology._git_log") as mock_log: + mock_log.return_value = "feat: add thing\nfix: fix thing\nrefactor: refactor thing\n" + result = classify_status( + folder_link="conductor/tracks/my_track_20260701", + current="active", + track_id="my_track_20260701", + repo_root=tmp_path, + reports_dir=reports_dir, + ) + assert result[0] == "Completed" + assert result[1] == "medium" + assert "work commits" in result[2] + + +def test_classify_status_metadata_commits_not_counted_as_work(tmp_path: Path) -> None: + """conductor(plan): commits don't count as work commits.""" + reports_dir = tmp_path / "docs/reports" + reports_dir.mkdir(parents=True) + with patch("scripts.audit.generate_chronology._git_log") as mock_log: + mock_log.return_value = "conductor(plan): mark task\nconductor(state): update\nconductor(track): init\n" + result = classify_status( + folder_link="conductor/tracks/my_track_20260701", + current="active", + track_id="my_track_20260701", + repo_root=tmp_path, + reports_dir=reports_dir, + ) + assert result[0] == "Active" + + +def test_classify_status_1_2_work_commits_in_progress(tmp_path: Path) -> None: + """1-2 work commits + in tracks/ -> In Progress.""" + reports_dir = tmp_path / "docs/reports" + reports_dir.mkdir(parents=True) + with patch("scripts.audit.generate_chronology._git_log") as mock_log: + mock_log.return_value = "feat: add thing\nfix: fix thing\n" + result = classify_status( + folder_link="conductor/tracks/my_track_20260701", + current="active", + track_id="my_track_20260701", + repo_root=tmp_path, + reports_dir=reports_dir, + ) + assert result[0] == "In Progress" + + +def test_classify_status_archive_no_override_completed_low(tmp_path: Path) -> None: + """archive/ + no completion report -> Completed (low confidence).""" + reports_dir = tmp_path / "docs/reports" + reports_dir.mkdir(parents=True) + with patch("scripts.audit.generate_chronology._git_log") as mock_log: + mock_log.return_value = "feat: thing\nfix: thing\nrefactor: thing\n" + result = classify_status( + folder_link="conductor/archive/my_track_20260701", + current="active", + track_id="my_track_20260701", + repo_root=tmp_path, + reports_dir=reports_dir, + ) + # >=3 work commits -> Completed (medium), even in archive + assert result[0] == "Completed" + + +def test_classify_status_fallback_needs_review(tmp_path: Path) -> None: + """Inconclusive -> Needs Review.""" + reports_dir = tmp_path / "docs/reports" + reports_dir.mkdir(parents=True) + with patch("scripts.audit.generate_chronology._git_log") as mock_log: + mock_log.return_value = "" + result = classify_status( + folder_link="conductor/tracks/my_track", + current="", + track_id="my_track", + repo_root=tmp_path, + reports_dir=reports_dir, + ) + assert result[0] == "Needs Review" + + +def test_classify_status_returns_3_tuple(tmp_path: Path) -> None: + """Classifier must return (status, confidence, reason).""" + reports_dir = tmp_path / "docs/reports" + reports_dir.mkdir(parents=True) + with patch("scripts.audit.generate_chronology._git_log") as mock_log: + mock_log.return_value = "" + result = classify_status( + folder_link="conductor/tracks/my_track_20260701", + current="active", + track_id="my_track_20260701", + repo_root=tmp_path, + reports_dir=reports_dir, + ) + assert len(result) == 3 + assert isinstance(result[0], str) + assert isinstance(result[1], str) + assert isinstance(result[2], str) \ No newline at end of file