test(chronology): write Red tests for v2 classifier + summary extractor
This commit is contained in:
@@ -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."
|
||||
# --- 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)
|
||||
Reference in New Issue
Block a user