201 lines
8.0 KiB
Python
201 lines
8.0 KiB
Python
#!/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())
|