Files
weichen 8af945a1d5 Make development gates preserve work and verifiable evidence
Why: cleanup, component synchronization, receipt recording and credential handling could contradict the documented development boundaries.

Evidence: SELF 75 isolated governance tests and source checks are captured in .task/feature-governance-reliability.md; combination and CI verification follow.

Impact: hardens local task and CI tooling, aligns policy and component documentation, and adds continuous regression tests without changing product behavior.

Refs: .task/feature-governance-reliability.md; policy/governance.md; policy/platform-admission.md
2026-10-05 12:36:02 +00:00

101 lines
5.0 KiB
Python

"""Exercise the actual hook against disposable repositories; never write on real main."""
import json
from pathlib import Path
import subprocess
import tempfile
import unittest
GUARD = Path(__file__).resolve().parents[1] / "scripts/zcode/write-guard.sh"
class WriteGuardTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.base = Path(self.temp.name)
self.repo = self.base / "repo"
self.repo.mkdir()
self.git("init", "-q", "-b", "main")
self.git("-c", "user.name=Fixture", "-c", "user.email=fixture@example.invalid",
"commit", "-q", "--allow-empty", "-m", "Initial")
def git(self, *args):
return subprocess.run(["git", "-C", str(self.repo), *args], check=True,
capture_output=True, text=True).stdout.strip()
def invoke(self, payload, cwd=None):
return subprocess.run(["bash", str(GUARD)], input=json.dumps(payload),
cwd=cwd or self.repo, capture_output=True, text=True)
def write(self, path, **kw):
return self.invoke({"tool_name": "Write", "tool_input": {"file_path": str(path)}, **kw})
def test_missing_parent_on_main_is_denied(self):
self.assertEqual(self.write(self.repo / "new/deep/file.txt").returncode, 2)
def test_relative_path_uses_hook_cwd_not_process_cwd(self):
self.assertEqual(self.invoke({"tool_name": "Write", "cwd": str(self.repo),
"tool_input": {"file_path": "new/file.txt"}},
cwd=self.base).returncode, 2)
def test_symlink_into_main_with_missing_parents_is_denied(self):
(self.base / "alias").symlink_to(self.repo, target_is_directory=True)
self.assertEqual(self.write(self.base / "alias/new/file.txt").returncode, 2)
def test_deleting_main_symlink_to_outside_is_denied(self):
(self.repo / "link").symlink_to(self.base / "outside")
patch = "*** Begin Patch\n*** Delete File: link\n*** End Patch"
result = self.invoke({"tool_name": "ApplyPatch", "tool_input": {"patch_text": patch}})
self.assertEqual(result.returncode, 2, result.stderr)
def test_feature_missing_parent_is_allowed(self):
self.git("switch", "-q", "-c", "feature/fixture")
self.assertEqual(self.write(self.repo / "new/deep/file.txt").returncode, 0)
def test_detached_and_bad_branches_are_denied(self):
for ref in ("--detach", "-c"):
self.git("switch", "-q", ref, "HEAD" if ref == "--detach" else "bad-branch")
self.assertEqual(self.write(self.repo / "new.txt").returncode, 2)
def test_non_git_destination_is_explicitly_outside_scope(self):
result = self.write(self.base / "notes/new.txt")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("outside-git", result.stderr)
def test_every_patch_file_and_move_destination_is_checked(self):
self.git("switch", "-q", "-c", "feature/fixture")
other = self.base / "protected"
subprocess.run(["git", "init", "-q", "-b", "main", str(other)], check=True)
for operation in (f"*** Add File: {other}/new/deep.txt\n+x",
f"*** Update File: {self.repo}/old.txt\n*** Move to: {other}/new.txt\n@@\n-a\n+b"):
patch = f"*** Begin Patch\n*** Add File: {self.repo}/safe.txt\n+ok\n{operation}\n*** End Patch"
result = self.invoke({"tool_name": "ApplyPatch", "tool_input": {"patch_text": patch}})
self.assertEqual(result.returncode, 2, result.stderr)
def test_valid_multi_file_patch_on_feature_is_allowed(self):
self.git("switch", "-q", "-c", "fix/fixture")
patch = "*** Begin Patch\n*** Add File: new/a.txt\n+a\n*** Delete File: b.txt\n*** End Patch"
self.assertEqual(self.invoke({"tool_name": "ApplyPatch", "tool_input": {"patch_text": patch}}).returncode, 0)
def test_missing_unknown_and_malformed_write_input_are_denied(self):
for payload in ({"tool_name": "Write", "tool_input": {}},
{"tool_name": "ApplyPatch", "tool_input": {"patch_text": "garbage"}},
{"tool_name": "ApplyPatch", "tool_input": {"patch_text": "*** Begin Patch\n*** Add File: a\n+a\n*** Unexpected: b\n*** End Patch"}},
{"tool_name": "Write", "tool_input": {"file_path": 12}},
{"tool_name": "UnknownWriter", "tool_input": {"file_path": "a"}}):
with self.subTest(payload=payload):
self.assertEqual(self.invoke(payload).returncode, 2)
def test_invalid_json_is_denied(self):
result = subprocess.run(["bash", str(GUARD)], input="not-json", cwd=self.repo,
capture_output=True, text=True)
self.assertEqual(result.returncode, 2)
def test_nonwrite_tools_are_outside_scope(self):
self.assertEqual(self.invoke({"tool_name": "Read", "tool_input": {"file_path": "a"}}).returncode, 0)
if __name__ == "__main__":
unittest.main()