"""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()