197 lines
7.8 KiB
Python
197 lines
7.8 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import pathlib
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
import zipfile
|
|
|
|
|
|
SKILL_ROOT = pathlib.Path(__file__).resolve().parents[1]
|
|
REPO_ROOT = pathlib.Path(__file__).resolve().parents[3]
|
|
SCRIPT = SKILL_ROOT / "scripts" / "review_bundle_audit.py"
|
|
TMP_ROOT = REPO_ROOT / "tmp" / "review-bundle-audit-tests"
|
|
|
|
|
|
def run_cli(*args: str) -> subprocess.CompletedProcess[str]:
|
|
return subprocess.run(
|
|
[sys.executable, str(SCRIPT), *args],
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
|
|
|
|
def write_text(path: pathlib.Path, text: str = "content") -> pathlib.Path:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(text, encoding="utf-8")
|
|
return path
|
|
|
|
|
|
def write_json(path: pathlib.Path, data: object) -> pathlib.Path:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
return path
|
|
|
|
|
|
def write_zip(path: pathlib.Path) -> pathlib.Path:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
with zipfile.ZipFile(path, "w") as archive:
|
|
archive.writestr("included.txt", "ok")
|
|
return path
|
|
|
|
|
|
def load_report(output_dir: pathlib.Path) -> dict[str, object]:
|
|
return json.loads((output_dir / "bundle-audit.json").read_text(encoding="utf-8"))
|
|
|
|
|
|
class ReviewBundleAuditTest(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
TMP_ROOT.mkdir(parents=True, exist_ok=True)
|
|
self.tmp_dir = tempfile.TemporaryDirectory(dir=TMP_ROOT)
|
|
self.bundle_root = pathlib.Path(self.tmp_dir.name) / "ccra_review_bundle" / "round-05"
|
|
self.output_dir = pathlib.Path(self.tmp_dir.name) / "outputs"
|
|
self.bundle_root.mkdir(parents=True)
|
|
|
|
def tearDown(self) -> None:
|
|
self.tmp_dir.cleanup()
|
|
|
|
@classmethod
|
|
def tearDownClass(cls) -> None:
|
|
if TMP_ROOT.exists():
|
|
shutil.rmtree(TMP_ROOT)
|
|
|
|
def write_complete_bundle(self) -> None:
|
|
write_text(self.bundle_root / "review-brief.md", "# Brief\n")
|
|
write_json(self.bundle_root / "review-manifest.json", {"files": ["review-brief.md"]})
|
|
write_json(self.bundle_root / "validation-sidecar.json", {"status": "pass"})
|
|
write_text(self.bundle_root / "local-review-report.md", "# Report\n")
|
|
write_text(self.bundle_root / "changed-files.md", "- src/model.json\n")
|
|
write_zip(self.bundle_root / "raw-review-bundle.zip")
|
|
|
|
def run_audit(self, *extra_args: str) -> tuple[subprocess.CompletedProcess[str], dict[str, object]]:
|
|
result = run_cli(
|
|
"--bundle-root",
|
|
str(self.bundle_root),
|
|
"--output-dir",
|
|
str(self.output_dir),
|
|
*extra_args,
|
|
)
|
|
return result, load_report(self.output_dir)
|
|
|
|
def test_complete_bundle_passes_and_reports_present_files(self) -> None:
|
|
self.write_complete_bundle()
|
|
|
|
result, report = self.run_audit()
|
|
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertEqual(report["machine_readable_summary"]["status"], "PASS")
|
|
self.assertEqual(report["required_files_missing"], [])
|
|
self.assertEqual(report["manifest_status"]["status"], "present")
|
|
self.assertEqual(report["zip_status"]["status"], "readable")
|
|
self.assertEqual(report["validation_sidecar_status"]["status"], "present")
|
|
self.assertEqual(report["report_status"]["status"], "present")
|
|
self.assertIn("review-brief.md", report["files_discovered"])
|
|
|
|
def test_missing_required_brief_is_blocking(self) -> None:
|
|
self.write_complete_bundle()
|
|
(self.bundle_root / "review-brief.md").unlink()
|
|
|
|
result, report = self.run_audit()
|
|
|
|
self.assertEqual(result.returncode, 1, result.stderr)
|
|
self.assertEqual(report["machine_readable_summary"]["status"], "FAIL")
|
|
self.assertIn("brief", report["required_files_missing"])
|
|
self.assertTrue(any("brief" in error for error in report["blocking_errors"]))
|
|
|
|
def test_missing_manifest_is_blocking(self) -> None:
|
|
self.write_complete_bundle()
|
|
(self.bundle_root / "review-manifest.json").unlink()
|
|
|
|
result, report = self.run_audit()
|
|
|
|
self.assertEqual(result.returncode, 1, result.stderr)
|
|
self.assertEqual(report["manifest_status"]["status"], "missing")
|
|
self.assertIn("manifest", report["required_files_missing"])
|
|
|
|
def test_missing_validation_sidecar_is_blocking(self) -> None:
|
|
self.write_complete_bundle()
|
|
(self.bundle_root / "validation-sidecar.json").unlink()
|
|
|
|
result, report = self.run_audit()
|
|
|
|
self.assertEqual(result.returncode, 1, result.stderr)
|
|
self.assertEqual(report["validation_sidecar_status"]["status"], "missing")
|
|
self.assertIn("validation_sidecar", report["required_files_missing"])
|
|
|
|
def test_unreadable_zip_is_blocking_when_zip_is_present(self) -> None:
|
|
self.write_complete_bundle()
|
|
write_text(self.bundle_root / "raw-review-bundle.zip", "not a real zip")
|
|
|
|
result, report = self.run_audit()
|
|
|
|
self.assertEqual(result.returncode, 1, result.stderr)
|
|
self.assertEqual(report["zip_status"]["status"], "unreadable")
|
|
self.assertTrue(any("zip" in error.casefold() for error in report["blocking_errors"]))
|
|
|
|
def test_extra_files_are_warnings_not_blockers(self) -> None:
|
|
self.write_complete_bundle()
|
|
write_text(self.bundle_root / "scratch.tmp", "operator note")
|
|
|
|
result, report = self.run_audit()
|
|
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertEqual(report["machine_readable_summary"]["status"], "PASS")
|
|
self.assertEqual(report["blocking_errors"], [])
|
|
self.assertTrue(any("scratch.tmp" in warning for warning in report["warnings"]))
|
|
|
|
def test_utf8_paths_and_chinese_filenames_are_reported(self) -> None:
|
|
self.bundle_root = pathlib.Path(self.tmp_dir.name) / "审查包"
|
|
self.bundle_root.mkdir()
|
|
write_text(self.bundle_root / "审查-brief.md", "# Brief\n")
|
|
write_json(self.bundle_root / "审查-manifest.json", {"files": ["审查-brief.md"]})
|
|
write_json(self.bundle_root / "验证-validation-sidecar.json", {"status": "pass"})
|
|
write_text(self.bundle_root / "审查-report.md", "# Report\n")
|
|
write_zip(self.bundle_root / "原始-review-bundle.zip")
|
|
|
|
result, report = self.run_audit()
|
|
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertIn("审查-brief.md", report["files_discovered"])
|
|
self.assertEqual(report["zip_status"]["files"][0], "原始-review-bundle.zip")
|
|
|
|
def test_profile_config_can_define_custom_required_patterns(self) -> None:
|
|
profile_config = pathlib.Path(self.tmp_dir.name) / "profile.json"
|
|
write_text(self.bundle_root / "handoff.txt", "handoff")
|
|
write_json(self.bundle_root / "index.json", {"files": ["handoff.txt"]})
|
|
write_json(self.bundle_root / "checks.json", {"status": "pass"})
|
|
write_json(
|
|
profile_config,
|
|
{
|
|
"profiles": {
|
|
"custom": {
|
|
"required_files": {
|
|
"brief": ["handoff.txt"],
|
|
"manifest": ["index.json"],
|
|
"validation_sidecar": ["checks.json"],
|
|
},
|
|
"optional_files": {"report": ["notes.md"], "zip": ["*.zip"]},
|
|
"allowed_extra_patterns": ["handoff.txt", "index.json", "checks.json"],
|
|
}
|
|
}
|
|
},
|
|
)
|
|
|
|
result, report = self.run_audit("--profile", "custom", "--profile-config", str(profile_config))
|
|
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertEqual(report["profile"], "custom")
|
|
self.assertEqual(report["required_files_missing"], [])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|