scripts/test_validate_handoff.py
scripts/test_validate_handoff.pyBrowse 4 files
2,484 tokens
11,078 bytes
Token encoding: o200k_base
Snapshot 1d17ca4
← Back to SKILL.md
1from __future__ import annotations2 3import os4import subprocess5import tempfile6import unittest7from argparse import Namespace8from pathlib import Path9from unittest import mock10 11from validate_handoff import load_shipped_paths, validate12 13 14class ValidateHandoffTests(unittest.TestCase):15 def setUp(self) -> None:16 self.temporary_directory = tempfile.TemporaryDirectory()17 self.addCleanup(self.temporary_directory.cleanup)18 self.root = Path(self.temporary_directory.name)19 self.repo = self.root / "repo"20 self.repo.mkdir()21 self._git("init", "-q", "-b", "main")22 self._git("config", "user.name", "Test User")23 self._git("config", "user.email", "test@example.com")24 (self.repo / "README.md").write_text("base\n")25 self._git("add", "README.md")26 self._git("commit", "-qm", "base")27 self.base = self._git("rev-parse", "HEAD").stdout.strip()28 self._git("checkout", "-qb", "feat/review-workflow")29 30 def _git(self, *args: str) -> subprocess.CompletedProcess[str]:31 return subprocess.run(32 ("git", *args),33 cwd=self.repo,34 check=True,35 capture_output=True,36 text=True,37 )38 39 def _commit(self, paths: dict[str, str]) -> None:40 for relative_path, content in paths.items():41 path = self.repo / relative_path42 path.parent.mkdir(parents=True, exist_ok=True)43 path.write_text(content)44 self._git("add", *paths)45 self._git("commit", "-qm", "change workflow")46 47 def _args(self, manifest: Path) -> Namespace:48 return Namespace(49 repo=self.repo,50 base=self.base,51 expected_branch="feat/review-workflow",52 required_trailer_email=[],53 shipped_path_manifest=manifest,54 )55 56 def test_exact_shipped_manifest_is_valid(self) -> None:57 self._commit({"src/change.py": "value = 1\n"})58 manifest = self.root / "shipped.paths"59 manifest.write_text("src/change.py\n")60 61 report, failures = validate(self._args(manifest))62 63 self.assertEqual(failures, [])64 self.assertTrue(report["valid"])65 self.assertEqual(report["shipped_paths"], ["src/change.py"])66 67 def test_operational_file_not_in_manifest_fails(self) -> None:68 self._commit(69 {70 "src/change.py": "value = 1\n",71 "plans/task.md": "operational plan\n",72 }73 )74 manifest = self.root / "shipped.paths"75 manifest.write_text("src/change.py\n")76 77 report, failures = validate(self._args(manifest))78 79 self.assertFalse(report["valid"])80 self.assertIn("unexpected=['plans/task.md']", failures[0])81 82 def test_explicit_ignored_deliverable_is_valid_after_force_staging(self) -> None:83 (self.repo / ".git" / "info" / "exclude").write_text("fixture.generated\n")84 fixture = self.repo / "fixture.generated"85 fixture.write_text("shipped fixture\n")86 self._git("add", "-f", "fixture.generated")87 self._git("commit", "-qm", "add ignored fixture")88 manifest = self.root / "shipped.paths"89 manifest.write_text("fixture.generated\n")90 91 report, failures = validate(self._args(manifest))92 93 self.assertEqual(failures, [])94 self.assertTrue(report["valid"])95 self.assertEqual(report["shipped_paths"], ["fixture.generated"])96 97 def test_manifest_paths_must_be_normalized_and_unique(self) -> None:98 manifest = self.root / "shipped.paths"99 manifest.write_text("src/change.py\nsrc/change.py\n")100 with self.assertRaisesRegex(ValueError, "Duplicate shipped-path manifest entry"):101 load_shipped_paths(manifest)102 103 manifest.write_text("../outside.txt\n")104 with self.assertRaisesRegex(ValueError, "normalized repository-relative paths"):105 load_shipped_paths(manifest)106 107 @unittest.skipUnless(hasattr(os, "mkfifo"), "Requires POSIX FIFO support.")108 def test_manifest_file_type_is_verified_after_open(self) -> None:109 fifo = self.root / "shipped.paths"110 os.mkfifo(fifo)111 112 with (113 mock.patch.object(Path, "read_text", return_value="src/change.py\n"),114 self.assertRaisesRegex(ValueError, "regular file"),115 ):116 load_shipped_paths(fifo)117 118 def test_body_line_is_not_accepted_as_coauthor_trailer(self) -> None:119 path = self.repo / "src" / "change.py"120 path.parent.mkdir()121 path.write_text("value = 1\n")122 self._git("add", "src/change.py")123 self._git(124 "commit",125 "-qm",126 "change workflow",127 "-m",128 "Co-authored-by: Example User <example@example.com>",129 "-m",130 "This paragraph makes the preceding line part of the body.",131 )132 manifest = self.root / "shipped.paths"133 manifest.write_text("src/change.py\n")134 args = self._args(manifest)135 args.required_trailer_email = ["example@example.com"]136 137 report, failures = validate(args)138 139 self.assertFalse(report["valid"])140 self.assertTrue(141 any("Missing required Co-authored-by trailer" in failure for failure in failures)142 )143 144 def test_terminal_coauthor_trailer_is_accepted(self) -> None:145 path = self.repo / "src" / "change.py"146 path.parent.mkdir()147 path.write_text("value = 1\n")148 self._git("add", "src/change.py")149 self._git(150 "commit",151 "-qm",152 "change workflow",153 "-m",154 "Commit body.",155 "-m",156 "Co-authored-by: Example User <example@example.com>",157 )158 manifest = self.root / "shipped.paths"159 manifest.write_text("src/change.py\n")160 args = self._args(manifest)161 args.required_trailer_email = ["EXAMPLE@example.com"]162 163 report, failures = validate(args)164 165 self.assertEqual(failures, [])166 self.assertEqual(report["coauthor_trailer_emails"], ["example@example.com"])167 168 def test_assume_unchanged_path_is_not_a_clean_handoff(self) -> None:169 self._commit({"src/change.py": "value = 1\n"})170 self._git("update-index", "--assume-unchanged", "README.md")171 (self.repo / "README.md").write_text("hidden change\n")172 manifest = self.root / "shipped.paths"173 manifest.write_text("src/change.py\n")174 175 report, failures = validate(self._args(manifest))176 177 self.assertFalse(report["valid"])178 self.assertTrue(any("assume-unchanged" in failure for failure in failures))179 180 def test_materialized_skip_worktree_path_is_not_a_clean_handoff(self) -> None:181 self._commit({"src/change.py": "value = 1\n"})182 self._git("update-index", "--skip-worktree", "README.md")183 (self.repo / "README.md").write_text("hidden change\n")184 manifest = self.root / "shipped.paths"185 manifest.write_text("src/change.py\n")186 187 report, failures = validate(self._args(manifest))188 189 self.assertFalse(report["valid"])190 self.assertTrue(any("skip-worktree" in failure for failure in failures))191 192 def test_ignored_dirty_submodule_is_not_a_clean_handoff(self) -> None:193 source = self.root / "dependency-source"194 source.mkdir()195 subprocess.run(("git", "init", "-q", str(source)), check=True)196 subprocess.run(197 ("git", "-C", str(source), "config", "user.name", "Submodule Test"),198 check=True,199 )200 subprocess.run(201 ("git", "-C", str(source), "config", "user.email", "submodule@example.test"),202 check=True,203 )204 (source / "tracked.txt").write_text("committed\n")205 subprocess.run(("git", "-C", str(source), "add", "tracked.txt"), check=True)206 subprocess.run(("git", "-C", str(source), "commit", "-qm", "initial"), check=True)207 self._git(208 "-c",209 "protocol.file.allow=always",210 "submodule",211 "add",212 "-q",213 str(source),214 "vendor/dependency",215 )216 self._git(217 "config",218 "-f",219 ".gitmodules",220 "submodule.vendor/dependency.ignore",221 "all",222 )223 self._git("add", ".gitmodules", "vendor/dependency")224 self._git("commit", "-qm", "add dependency")225 manifest = self.root / "shipped.paths"226 manifest.write_text(".gitmodules\nvendor/dependency\n")227 (self.repo / "vendor" / "dependency" / "tracked.txt").write_text("dirty\n")228 229 self.assertEqual(self._git("status", "--porcelain=v1").stdout, "")230 report, failures = validate(self._args(manifest))231 232 self.assertFalse(report["valid"])233 self.assertIn("Worktree is not clean.", failures)234 235 def test_submodule_hidden_index_path_is_not_a_clean_handoff(self) -> None:236 source = self.root / "dependency-source"237 source.mkdir()238 subprocess.run(("git", "init", "-q", str(source)), check=True)239 subprocess.run(240 ("git", "-C", str(source), "config", "user.name", "Submodule Test"),241 check=True,242 )243 subprocess.run(244 ("git", "-C", str(source), "config", "user.email", "submodule@example.test"),245 check=True,246 )247 (source / "tracked.txt").write_text("committed\n")248 subprocess.run(("git", "-C", str(source), "add", "tracked.txt"), check=True)249 subprocess.run(("git", "-C", str(source), "commit", "-qm", "initial"), check=True)250 self._git(251 "-c",252 "protocol.file.allow=always",253 "submodule",254 "add",255 "-q",256 str(source),257 "vendor/dependency",258 )259 self._git("commit", "-qam", "add dependency")260 manifest = self.root / "shipped.paths"261 manifest.write_text(".gitmodules\nvendor/dependency\n")262 self._git(263 "-C",264 "vendor/dependency",265 "update-index",266 "--assume-unchanged",267 "tracked.txt",268 )269 (self.repo / "vendor" / "dependency" / "tracked.txt").write_text("hidden change\n")270 271 self.assertEqual(272 self._git("status", "--porcelain=v1", "--ignore-submodules=none").stdout,273 "",274 )275 report, failures = validate(self._args(manifest))276 277 self.assertFalse(report["valid"])278 self.assertFalse(report["clean"])279 self.assertTrue(280 any("assume-unchanged=vendor/dependency/tracked.txt" in failure for failure in failures)281 )282 283 def test_missing_repository_report_is_explicitly_invalid(self) -> None:284 args = self._args(self.root / "unused.paths")285 args.repo = self.root / "missing"286 287 report, failures = validate(args)288 289 self.assertFalse(report["valid"])290 self.assertEqual(291 failures,292 [f"Repository path does not exist: {args.repo.resolve()}"],293 )294 295 296if __name__ == "__main__":297 unittest.main()298