#!/usr/bin/env python3 """Behavioral tests for psp_project_guard.py.""" import json import subprocess import tempfile from pathlib import Path import psp_project_guard def git(path: Path, *args: str) -> str: result = subprocess.run( ["git", "-C", str(path), *args], text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, ) return result.stdout.strip() def create_empty_project(psp_root: Path, name: str) -> Path: project = psp_root / name project.mkdir() git(project, "init", "-q") git(project, "remote", "add", "origin", f"ssh://git.example/Psp/{name}.git") return project def expect_guard_error(action, message: str) -> None: try: action() except psp_project_guard.GuardError: return raise AssertionError(message) def main() -> int: with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) psp_root = root / "Psp" psp_root.mkdir() reference = psp_root / "REF" reference.mkdir() (reference / "configs").mkdir() (reference / "configs" / "board.json").write_text( json.dumps({"project": "REF"}), encoding="utf-8" ) (reference / "psp.json").write_text( json.dumps({"project": "REF"}), encoding="utf-8" ) (reference / "build").mkdir() (reference / "build" / "generated.bin").write_bytes(b"generated") git(reference, "init", "-q") git(reference, "add", "configs/board.json", "psp.json") git( reference, "-c", "user.name=PSP Test", "-c", "user.email=psp-test@example.invalid", "commit", "-qm", "reference", ) target = create_empty_project(psp_root, "LY-SM-YT42-AI") original_branch = git(target, "symbolic-ref", "--short", "HEAD") original_remote = git(target, "remote", "get-url", "origin") inspected = psp_project_guard.inspect_project(target, psp_root) assert inspected["mode"] == "precreated-empty-repository" assert inspected["project"] == "LY-SM-YT42-AI" manifest = root / "evidence" / "clone-manifest.json" prepared = psp_project_guard.prepare( reference, target, manifest, ["REF"], include_dirty=False, psp_root=psp_root ) staging = Path(prepared["staging"]) assert prepared["target_mode"] == "precreated-empty-repository" assert prepared["psp_root"] == str(psp_root) assert not (staging / ".git").exists() assert not (staging / "build").exists() (staging / "psp.json").write_text( json.dumps({"project": "NEW"}), encoding="utf-8" ) failing = psp_project_guard.verify(manifest, ["psp.json"], [], None) assert failing["passed"] is False assert failing["residues"] (staging / "configs" / "board.json").write_text( json.dumps({"project": "NEW"}), encoding="utf-8" ) report_path = root / "evidence" / "verify.json" verified = psp_project_guard.verify( manifest, ["psp.json", "configs/board.json"], [], report_path ) assert verified["passed"] is True, verified finalized = psp_project_guard.finalize(manifest, report_path) assert finalized["target"] == str(target) assert finalized["repository_preserved"] is True assert not staging.exists() assert (target / ".git").is_dir() assert git(target, "symbolic-ref", "--short", "HEAD") == original_branch assert git(target, "remote", "get-url", "origin") == original_remote assert not (target / ".git" / "refs" / "heads" / original_branch).exists() audit_path = root / "evidence" / "final-audit.json" audited = psp_project_guard.audit_final( manifest, ["psp.json", "configs/board.json"], [], audit_path ) assert audited["passed"] is True, audited assert audited["repository_unchanged"] is True no_reference = create_empty_project(psp_root, "NO-REFERENCE") empty_manifest = root / "evidence" / "empty-manifest.json" empty_prepared = psp_project_guard.prepare( None, no_reference, empty_manifest, [], False, psp_root ) empty_staging = Path(empty_prepared["staging"]) assert list(empty_staging.iterdir()) == [] assert json.loads(empty_manifest.read_text(encoding="utf-8"))["reference"] is None (empty_staging / "psp.json").write_text('{"project":"NO-REFERENCE"}', encoding="utf-8") empty_report = root / "evidence" / "empty-verify.json" assert psp_project_guard.verify(empty_manifest, ["psp.json"], [], empty_report)["passed"] psp_project_guard.finalize(empty_manifest, empty_report) assert psp_project_guard.audit_final(empty_manifest, ["psp.json"], [], None)["passed"] assert (no_reference / ".git").is_dir() assert (no_reference / "psp.json").is_file() missing = psp_root / "MISSING" expect_guard_error( lambda: psp_project_guard.prepare( reference, missing, root / "missing-manifest.json", [], False, psp_root ), "missing target must be rejected", ) outside = create_empty_project(root, "OUTSIDE") expect_guard_error( lambda: psp_project_guard.prepare( reference, outside, root / "outside-manifest.json", [], False, psp_root ), "target outside the fixed PSP root must be rejected", ) populated = create_empty_project(psp_root, "POPULATED") (populated / "README.md").write_text("existing\n", encoding="utf-8") assert psp_project_guard.inspect_project(populated, psp_root)["mode"] == "existing-project" expect_guard_error( lambda: psp_project_guard.prepare( reference, populated, root / "populated-manifest.json", [], False, psp_root ), "populated target must use the existing-project update flow", ) changed = create_empty_project(psp_root, "CHANGED") changed_manifest = root / "evidence" / "changed-manifest.json" changed_prepared = psp_project_guard.prepare( reference, changed, changed_manifest, [], False, psp_root ) changed_report = root / "evidence" / "changed-verify.json" changed_verified = psp_project_guard.verify(changed_manifest, [], [], changed_report) assert changed_verified["passed"] is True git(changed, "remote", "set-url", "origin", "ssh://git.example/changed.git") expect_guard_error( lambda: psp_project_guard.finalize(changed_manifest, changed_report), "Git metadata drift after prepare must block finalization", ) assert Path(changed_prepared["staging"]).is_dir() assert not (changed / "psp.json").exists() (reference / "psp.json").write_text( json.dumps({"project": "UNCOMMITTED"}), encoding="utf-8" ) head_target = create_empty_project(psp_root, "FROM-HEAD") head_manifest = root / "evidence" / "head-manifest.json" from_head = psp_project_guard.prepare( reference, head_target, head_manifest, ["REF"], False, psp_root ) assert from_head["warnings"] assert json.loads((Path(from_head["staging"]) / "psp.json").read_text())["project"] == "REF" unsafe = root / "unsafe" unsafe.mkdir() (unsafe / "psp.json").write_text("{}", encoding="utf-8") (unsafe / ".env").write_text("TOKEN=secret", encoding="utf-8") unsafe_target = create_empty_project(psp_root, "UNSAFE") expect_guard_error( lambda: psp_project_guard.prepare( unsafe, unsafe_target, root / "unsafe-manifest.json", [], False, psp_root ), "credential-like files must be rejected", ) print("PSP project guard tests passed") return 0 if __name__ == "__main__": raise SystemExit(main())