scripts/test_validate_handoff.py
scripts/test_validate_handoff.pyBrowse 4 files
2,488 tokens
11,100 bytes
Token encoding: o200k_base
Snapshot 506f736
← 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(101 ValueError, "Duplicate shipped-path manifest entry"102 ):103 load_shipped_paths(manifest)104 105 manifest.write_text("../outside.txt\n")106 with self.assertRaisesRegex(ValueError, "normalized repository-relative paths"):107 load_shipped_paths(manifest)108 109 @unittest.skipUnless(hasattr(os, "mkfifo"), "Requires POSIX FIFO support.")110 def test_manifest_file_type_is_verified_after_open(self) -> None:111 fifo = self.root / "shipped.paths"112 os.mkfifo(fifo)113 114 with (115 mock.patch.object(Path, "read_text", return_value="src/change.py\n"),116 self.assertRaisesRegex(ValueError, "regular file"),117 ):118 load_shipped_paths(fifo)119 120 def test_body_line_is_not_accepted_as_coauthor_trailer(self) -> None:121 path = self.repo / "src" / "change.py"122 path.parent.mkdir()123 path.write_text("value = 1\n")124 self._git("add", "src/change.py")125 self._git(126 "commit",127 "-qm",128 "change workflow",129 "-m",130 "Co-authored-by: Example User <example@example.com>",131 "-m",132 "This paragraph makes the preceding line part of the body.",133 )134 manifest = self.root / "shipped.paths"135 manifest.write_text("src/change.py\n")136 args = self._args(manifest)137 args.required_trailer_email = ["example@example.com"]138 139 report, failures = validate(args)140 141 self.assertFalse(report["valid"])142 self.assertTrue(143 any("Missing required Co-authored-by trailer" in failure for failure in failures)144 )145 146 def test_terminal_coauthor_trailer_is_accepted(self) -> None:147 path = self.repo / "src" / "change.py"148 path.parent.mkdir()149 path.write_text("value = 1\n")150 self._git("add", "src/change.py")151 self._git(152 "commit",153 "-qm",154 "change workflow",155 "-m",156 "Commit body.",157 "-m",158 "Co-authored-by: Example User <example@example.com>",159 )160 manifest = self.root / "shipped.paths"161 manifest.write_text("src/change.py\n")162 args = self._args(manifest)163 args.required_trailer_email = ["EXAMPLE@example.com"]164 165 report, failures = validate(args)166 167 self.assertEqual(failures, [])168 self.assertEqual(report["coauthor_trailer_emails"], ["example@example.com"])169 170 def test_assume_unchanged_path_is_not_a_clean_handoff(self) -> None:171 self._commit({"src/change.py": "value = 1\n"})172 self._git("update-index", "--assume-unchanged", "README.md")173 (self.repo / "README.md").write_text("hidden change\n")174 manifest = self.root / "shipped.paths"175 manifest.write_text("src/change.py\n")176 177 report, failures = validate(self._args(manifest))178 179 self.assertFalse(report["valid"])180 self.assertTrue(any("assume-unchanged" in failure for failure in failures))181 182 def test_materialized_skip_worktree_path_is_not_a_clean_handoff(self) -> None:183 self._commit({"src/change.py": "value = 1\n"})184 self._git("update-index", "--skip-worktree", "README.md")185 (self.repo / "README.md").write_text("hidden change\n")186 manifest = self.root / "shipped.paths"187 manifest.write_text("src/change.py\n")188 189 report, failures = validate(self._args(manifest))190 191 self.assertFalse(report["valid"])192 self.assertTrue(any("skip-worktree" in failure for failure in failures))193 194 def test_ignored_dirty_submodule_is_not_a_clean_handoff(self) -> None:195 source = self.root / "dependency-source"196 source.mkdir()197 subprocess.run(("git", "init", "-q", str(source)), check=True)198 subprocess.run(199 ("git", "-C", str(source), "config", "user.name", "Submodule Test"),200 check=True,201 )202 subprocess.run(203 ("git", "-C", str(source), "config", "user.email", "submodule@example.test"),204 check=True,205 )206 (source / "tracked.txt").write_text("committed\n")207 subprocess.run(("git", "-C", str(source), "add", "tracked.txt"), check=True)208 subprocess.run(("git", "-C", str(source), "commit", "-qm", "initial"), check=True)209 self._git(210 "-c",211 "protocol.file.allow=always",212 "submodule",213 "add",214 "-q",215 str(source),216 "vendor/dependency",217 )218 self._git(219 "config",220 "-f",221 ".gitmodules",222 "submodule.vendor/dependency.ignore",223 "all",224 )225 self._git("add", ".gitmodules", "vendor/dependency")226 self._git("commit", "-qm", "add dependency")227 manifest = self.root / "shipped.paths"228 manifest.write_text(".gitmodules\nvendor/dependency\n")229 (self.repo / "vendor" / "dependency" / "tracked.txt").write_text("dirty\n")230 231 self.assertEqual(self._git("status", "--porcelain=v1").stdout, "")232 report, failures = validate(self._args(manifest))233 234 self.assertFalse(report["valid"])235 self.assertIn("Worktree is not clean.", failures)236 237 def test_submodule_hidden_index_path_is_not_a_clean_handoff(self) -> None:238 source = self.root / "dependency-source"239 source.mkdir()240 subprocess.run(("git", "init", "-q", str(source)), check=True)241 subprocess.run(242 ("git", "-C", str(source), "config", "user.name", "Submodule Test"),243 check=True,244 )245 subprocess.run(246 ("git", "-C", str(source), "config", "user.email", "submodule@example.test"),247 check=True,248 )249 (source / "tracked.txt").write_text("committed\n")250 subprocess.run(("git", "-C", str(source), "add", "tracked.txt"), check=True)251 subprocess.run(("git", "-C", str(source), "commit", "-qm", "initial"), check=True)252 self._git(253 "-c",254 "protocol.file.allow=always",255 "submodule",256 "add",257 "-q",258 str(source),259 "vendor/dependency",260 )261 self._git("commit", "-qam", "add dependency")262 manifest = self.root / "shipped.paths"263 manifest.write_text(".gitmodules\nvendor/dependency\n")264 self._git(265 "-C",266 "vendor/dependency",267 "update-index",268 "--assume-unchanged",269 "tracked.txt",270 )271 (self.repo / "vendor" / "dependency" / "tracked.txt").write_text("hidden change\n")272 273 self.assertEqual(274 self._git("status", "--porcelain=v1", "--ignore-submodules=none").stdout,275 "",276 )277 report, failures = validate(self._args(manifest))278 279 self.assertFalse(report["valid"])280 self.assertFalse(report["clean"])281 self.assertTrue(282 any("assume-unchanged=vendor/dependency/tracked.txt" in failure for failure in failures)283 )284 285 def test_missing_repository_report_is_explicitly_invalid(self) -> None:286 args = self._args(self.root / "unused.paths")287 args.repo = self.root / "missing"288 289 report, failures = validate(args)290 291 self.assertFalse(report["valid"])292 self.assertEqual(293 failures,294 [f"Repository path does not exist: {args.repo.resolve()}"],295 )296 297 298if __name__ == "__main__":299 unittest.main()300