skills-vault/skills/review-context-builder/tests/test_review_context_builder.py

235 lines
9.0 KiB
Python

from __future__ import annotations
import json
import pathlib
import shutil
import subprocess
import sys
import tempfile
import unittest
SKILL_ROOT = pathlib.Path(__file__).resolve().parents[1]
REPO_ROOT = pathlib.Path(__file__).resolve().parents[3]
SCRIPT = SKILL_ROOT / "scripts" / "review_context_builder.py"
TMP_ROOT = REPO_ROOT / "tmp" / "review-context-builder-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 load_manifest(output_dir: pathlib.Path) -> dict[str, object]:
return json.loads((output_dir / "review-file-manifest.json").read_text(encoding="utf-8"))
class ReviewContextBuilderTest(unittest.TestCase):
def setUp(self) -> None:
TMP_ROOT.mkdir(parents=True, exist_ok=True)
self.tmp_dir = tempfile.TemporaryDirectory(dir=TMP_ROOT)
self.project_root = pathlib.Path(self.tmp_dir.name) / "project"
self.output_dir = pathlib.Path(self.tmp_dir.name) / "outputs"
self.project_root.mkdir()
def tearDown(self) -> None:
self.tmp_dir.cleanup()
@classmethod
def tearDownClass(cls) -> None:
if TMP_ROOT.exists():
shutil.rmtree(TMP_ROOT)
def run_builder(self, *extra_args: str) -> tuple[subprocess.CompletedProcess[str], dict[str, object], str]:
result = run_cli(
"--project-root",
str(self.project_root),
"--review-id",
"round-05-local-pass",
"--output-dir",
str(self.output_dir),
*extra_args,
)
manifest = load_manifest(self.output_dir)
context = (self.output_dir / "review-context.md").read_text(encoding="utf-8")
return result, manifest, context
def test_complete_context_build_indexes_configured_sources(self) -> None:
write_text(self.project_root / "README.md", "# Project\n")
write_text(self.project_root / "reports" / "gate-run-report.md", "# Gate report\n")
write_text(self.project_root / "decisions" / "owner-decision.md", "# Decision\n")
metadata = write_json(
pathlib.Path(self.tmp_dir.name) / "metadata.json",
{
"review_goal": "check readiness",
"non_goals": ["do not decide approval"],
"open_questions": ["is evidence complete?"],
"target_reviewer": "local reviewer",
},
)
result, manifest, context = self.run_builder(
"--source-root",
".",
"--include-pattern",
"**/*.md",
"--metadata",
str(metadata),
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(manifest["review_id"], "round-05-local-pass")
self.assertEqual(manifest["context_profile"], "generic")
self.assertEqual(
manifest["files_included"],
[
{"path": "README.md", "size_bytes": 10, "source_root": "."},
{"path": "decisions/owner-decision.md", "size_bytes": 11, "source_root": "."},
{"path": "reports/gate-run-report.md", "size_bytes": 14, "source_root": "."},
],
)
self.assertEqual(manifest["metadata"]["review_goal"], "check readiness")
self.assertEqual(manifest["known_non_goals"], ["do not decide approval"])
self.assertEqual(manifest["open_questions_for_reviewer_or_agent"], ["is evidence complete?"])
self.assertIn("reports/gate-run-report.md", manifest["important_reports"])
self.assertIn("decisions/owner-decision.md", manifest["prior_decision_references"])
self.assertIn("This context is an index. It does not replace reading source files.", context)
def test_missing_source_root_is_blocking_but_outputs_manifest(self) -> None:
result, manifest, context = self.run_builder("--source-root", "missing")
self.assertEqual(result.returncode, 1)
self.assertEqual(manifest["machine_readable_summary"]["status"], "FAIL")
self.assertEqual(manifest["missing_source_roots"], ["missing"])
self.assertIn("Missing source root", manifest["blocking_errors"][0])
self.assertIn("missing", context)
def test_large_files_are_excluded_with_reason(self) -> None:
write_text(self.project_root / "small.md", "small")
write_text(self.project_root / "large.md", "0123456789")
result, manifest, _ = self.run_builder(
"--source-root",
".",
"--include-pattern",
"**/*.md",
"--max-file-bytes",
"5",
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual([item["path"] for item in manifest["files_included"]], ["small.md"])
self.assertEqual(
manifest["files_excluded"],
[{"path": "large.md", "reason": "size_exceeds_limit", "size_bytes": 10}],
)
def test_manifest_path_normalization_uses_project_relative_posix_paths(self) -> None:
nested = write_text(self.project_root / "docs" / "nested" / "report.md", "report")
result, manifest, _ = self.run_builder("--source-root", str(nested), "--include-pattern", "**/*.md")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(manifest["source_roots"], ["docs/nested/report.md"])
self.assertEqual(manifest["files_included"][0]["path"], "docs/nested/report.md")
self.assertNotIn("\\", manifest["files_included"][0]["path"])
def test_utf8_paths_and_chinese_filenames_are_preserved(self) -> None:
write_text(self.project_root / "材料" / "审查报告.md", "# 报告\n")
result, manifest, context = self.run_builder(
"--source-root",
"材料",
"--include-pattern",
"**/*.md",
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(manifest["files_included"][0]["path"], "材料/审查报告.md")
self.assertIn("材料/审查报告.md", context)
def test_stable_output_ordering_across_repeated_runs(self) -> None:
write_text(self.project_root / "zeta.md", "z")
write_text(self.project_root / "alpha.md", "a")
write_text(self.project_root / "nested" / "beta.md", "b")
first_result, first_manifest, first_context = self.run_builder(
"--source-root",
".",
"--include-pattern",
"**/*.md",
)
second_result, second_manifest, second_context = self.run_builder(
"--source-root",
".",
"--include-pattern",
"**/*.md",
)
self.assertEqual(first_result.returncode, 0, first_result.stderr)
self.assertEqual(second_result.returncode, 0, second_result.stderr)
self.assertEqual(first_manifest, second_manifest)
self.assertEqual(first_context, second_context)
self.assertEqual([item["path"] for item in first_manifest["files_included"]], ["alpha.md", "nested/beta.md", "zeta.md"])
def test_metadata_passthrough_does_not_add_ccra_fields(self) -> None:
metadata = write_json(
pathlib.Path(self.tmp_dir.name) / "metadata.json",
{"custom_key": "custom value", "round_id": "R05"},
)
write_text(self.project_root / "note.md", "note")
result, manifest, _ = self.run_builder(
"--source-root",
".",
"--include-pattern",
"**/*.md",
"--metadata",
str(metadata),
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(manifest["metadata"], {"custom_key": "custom value", "round_id": "R05"})
self.assertNotIn("review_round", manifest)
self.assertNotIn("local_pass", manifest)
def test_exclude_patterns_remove_matching_files(self) -> None:
write_text(self.project_root / "include.md", "include")
write_text(self.project_root / "scratch" / "exclude.md", "exclude")
result, manifest, _ = self.run_builder(
"--source-root",
".",
"--include-pattern",
"**/*.md",
"--exclude-pattern",
"scratch/**",
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual([item["path"] for item in manifest["files_included"]], ["include.md"])
self.assertEqual(
manifest["files_excluded"],
[{"path": "scratch/exclude.md", "reason": "exclude_pattern"}],
)
if __name__ == "__main__":
unittest.main()