246 lines
9.0 KiB
Python
246 lines
9.0 KiB
Python
from pathlib import Path
|
|
import json
|
|
import pytest
|
|
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:
|
|
result: str = extract_slug_date("gencpp_python_bindings_20260308")
|
|
assert result == "2026-03-08"
|
|
|
|
|
|
def test_slug_date_extraction_handles_missing_date() -> None:
|
|
result = extract_slug_date("my_folder")
|
|
assert result is None
|
|
|
|
|
|
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 == "Real description of the work."
|
|
|
|
|
|
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 == "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:
|
|
long_line: str = " ".join(["word"] * 50)
|
|
spec_content: str = f"# Title\n\n{long_line}\n"
|
|
(tmp_path / "spec.md").write_text(spec_content, encoding="utf-8")
|
|
result: str = extract_summary(tmp_path)
|
|
expected: str = " ".join(["word"] * 25) + "\u2026"
|
|
assert result == expected
|
|
|
|
|
|
# --- 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 = "abc1234 feat: add thing\ndef5678 fix: fix thing\nghi9012 refactor: 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_countd_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 = "abc1234 conductor(plan): mark task\ndef5678 conductor(state): update\nghi9012 conductor(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 = "abc1234 feat: add thing\ndef5678 fix: 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 = "abc1234 feat: thing\ndef5678 fix: thing\nghi9012 refactor: 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 (path is neither tracks/ nor archive/)."""
|
|
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="some/other/path/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) |