Reconcile before/after branches/ snapshots so preserved worktrees cannot disappear without removal logs or explained state transitions. - worktree_cleanup_audit.py: snapshot capture, disposition reconciliation - gitea_capture_branches_worktree_snapshot, gitea_assess_worktree_cleanup_integrity - Final-report rule author.worktree_cleanup_audit_proof - worktree-cleanup.md bulk audit section - tests/test_worktree_cleanup_audit.py (11 cases)
164 lines
5.7 KiB
Python
164 lines
5.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Regression tests for worktree cleanup audit integrity (#404)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
import unittest
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
from worktree_cleanup_audit import ( # noqa: E402
|
|
assess_cleanup_audit_final_report,
|
|
assess_cleanup_audit_integrity,
|
|
assess_worktree_cleanup_integrity,
|
|
classify_branches_entry,
|
|
parse_worktree_list_porcelain,
|
|
reconcile_cleanup_audit,
|
|
)
|
|
|
|
|
|
def _entry(
|
|
path: str,
|
|
*,
|
|
classification: str,
|
|
registered: bool = True,
|
|
preserve: bool | None = None,
|
|
) -> dict:
|
|
preserve_flag = preserve if preserve is not None else classification in {
|
|
"active_open_pr",
|
|
"active_issue_work",
|
|
"dirty_local_worktree",
|
|
"unsafe_unknown",
|
|
}
|
|
return {
|
|
"path": path,
|
|
"classification": classification,
|
|
"preserve": preserve_flag,
|
|
"registered_worktree": registered,
|
|
"worktree_state": {"exists": True, "clean": classification == "clean_stale_removable"},
|
|
"worktree_record": {"branch": "feat/x"} if registered else None,
|
|
}
|
|
|
|
|
|
def _snapshot(entries: list[dict]) -> dict:
|
|
return {"entries": entries}
|
|
|
|
|
|
class TestParseWorktreePorcelain(unittest.TestCase):
|
|
def test_parses_multiple_worktrees(self):
|
|
text = "\n".join([
|
|
"worktree /proj/branches/foo",
|
|
"HEAD abcdef0123456789abcdef0123456789abcdef0",
|
|
"branch refs/heads/feat/foo",
|
|
"",
|
|
"worktree /proj",
|
|
"HEAD 1111111111111111111111111111111111111111",
|
|
"branch refs/heads/master",
|
|
])
|
|
parsed = parse_worktree_list_porcelain(text)
|
|
self.assertEqual(len(parsed), 2)
|
|
self.assertEqual(parsed[0]["branch"], "feat/foo")
|
|
|
|
|
|
class TestClassifyEntry(unittest.TestCase):
|
|
def test_active_pr_classification(self):
|
|
result = classify_branches_entry(
|
|
rel_path="branches/feat-issue-1-x",
|
|
worktree_record={"branch": "feat/issue-1-x"},
|
|
worktree_state={"exists": True, "clean": True, "dirty_files": []},
|
|
open_pr_branches={"feat/issue-1-x"},
|
|
)
|
|
self.assertEqual(result, "active_open_pr")
|
|
|
|
def test_dirty_classification(self):
|
|
result = classify_branches_entry(
|
|
rel_path="branches/dirty-one",
|
|
worktree_record={"branch": "feat/dirty-one"},
|
|
worktree_state={"exists": True, "dirty_files": ["a.py"]},
|
|
open_pr_branches=set(),
|
|
)
|
|
self.assertEqual(result, "dirty_local_worktree")
|
|
|
|
|
|
class TestCleanupIntegrity(unittest.TestCase):
|
|
def test_preserved_worktree_remains(self):
|
|
path = "branches/keep-me"
|
|
before = _snapshot([_entry(path, classification="clean_stale_removable", preserve=False)])
|
|
after = _snapshot([_entry(path, classification="clean_stale_removable", preserve=False)])
|
|
result = assess_worktree_cleanup_integrity(before=before, after=after)
|
|
self.assertTrue(result["integrity_passed"])
|
|
|
|
def test_intentional_removal_passes(self):
|
|
path = "branches/remove-me"
|
|
before = _snapshot([_entry(path, classification="clean_stale_removable", preserve=False)])
|
|
after = _snapshot([])
|
|
result = assess_worktree_cleanup_integrity(
|
|
before=before,
|
|
after=after,
|
|
removals=[{
|
|
"path": path,
|
|
"method": "git worktree remove",
|
|
"pre_removal_proof": "clean status",
|
|
}],
|
|
)
|
|
self.assertTrue(result["integrity_passed"])
|
|
|
|
def test_dirty_worktree_disappears_fails(self):
|
|
path = "branches/dirty-wt"
|
|
before = _snapshot([_entry(path, classification="dirty_local_worktree")])
|
|
after = _snapshot([])
|
|
recon = reconcile_cleanup_audit(before, after)
|
|
result = assess_cleanup_audit_integrity(recon)
|
|
self.assertFalse(result["proven"])
|
|
|
|
def test_active_pr_worktree_disappears_fails(self):
|
|
path = "branches/review-pr99"
|
|
before = _snapshot([_entry(path, classification="active_open_pr")])
|
|
after = _snapshot([])
|
|
result = assess_worktree_cleanup_integrity(before=before, after=after)
|
|
self.assertFalse(result["integrity_passed"])
|
|
|
|
def test_explained_missing_allowed(self):
|
|
path = "branches/review-pr382"
|
|
before = _snapshot([
|
|
_entry(path, classification="detached_review_leftover", preserve=False),
|
|
])
|
|
after = _snapshot([])
|
|
result = assess_worktree_cleanup_integrity(
|
|
before=before,
|
|
after=after,
|
|
explained_missing={path: "removed concurrently by sibling session"},
|
|
)
|
|
self.assertTrue(result["integrity_passed"])
|
|
|
|
def test_removal_log_omits_clean_stale_fails(self):
|
|
path = "branches/a"
|
|
before = _snapshot([_entry(path, classification="clean_stale_removable", preserve=False)])
|
|
after = _snapshot([])
|
|
recon = reconcile_cleanup_audit(before, after, removal_log=[])
|
|
self.assertFalse(recon["removal_log_complete"])
|
|
|
|
|
|
class TestCleanupReportProof(unittest.TestCase):
|
|
def test_complete_report_passes(self):
|
|
report = "\n".join([
|
|
"Cleanup audit reconciliation table:",
|
|
"Initial count: 10",
|
|
"Removed count: 3",
|
|
"Preserved count: 7",
|
|
"Missing-unexplained count: 0",
|
|
"Final count: 7",
|
|
"Final verification: git worktree list proof attached",
|
|
])
|
|
result = assess_cleanup_audit_final_report(report)
|
|
self.assertTrue(result["proven"])
|
|
|
|
def test_incomplete_report_fails(self):
|
|
result = assess_cleanup_audit_final_report("removed some worktrees")
|
|
self.assertFalse(result["proven"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main() |