claude-dev-env 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/CLAUDE.md +3 -2
  2. package/agents/CLAUDE.md +1 -0
  3. package/agents/code-verifier.md +3 -3
  4. package/agents/skill-writer-agent.md +84 -0
  5. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +141 -41
  6. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +29 -13
  7. package/docs/CLAUDE.md +1 -0
  8. package/docs/references/CLAUDE.md +1 -0
  9. package/docs/references/code-review-enforcement.md +97 -0
  10. package/docs/wsl-docker-cowork-starter-matrix.md +89 -0
  11. package/hooks/blocking/CLAUDE.md +8 -1
  12. package/hooks/blocking/code_review_enforcement_config_bootstrap.py +53 -0
  13. package/hooks/blocking/code_review_gate_deny.py +74 -0
  14. package/hooks/blocking/code_review_pr_create_gate.py +194 -0
  15. package/hooks/blocking/code_review_push_gate.py +140 -0
  16. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +340 -0
  17. package/hooks/blocking/code_review_stamp_store.py +233 -0
  18. package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +7 -0
  19. package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +15 -0
  20. package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +212 -0
  21. package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +138 -0
  22. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +49 -0
  23. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +38 -0
  24. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +39 -27
  25. package/hooks/blocking/config/code_review_enforcement_constants.py +110 -0
  26. package/hooks/blocking/config/test_code_review_enforcement_constants.py +108 -0
  27. package/hooks/blocking/config/verified_commit_constants.py +13 -9
  28. package/hooks/blocking/conftest.py +2 -0
  29. package/hooks/blocking/convergence_gate_blocker.py +112 -23
  30. package/hooks/blocking/destructive_command_blocker.py +19 -6
  31. package/hooks/blocking/pr_description_proof_of_work.py +52 -34
  32. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
  33. package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +62 -0
  34. package/hooks/blocking/test_code_review_gate_deny.py +54 -0
  35. package/hooks/blocking/test_code_review_pr_create_gate.py +185 -0
  36. package/hooks/blocking/test_code_review_push_gate.py +189 -0
  37. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +180 -0
  38. package/hooks/blocking/test_code_review_stamp_store.py +205 -0
  39. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +124 -2
  40. package/hooks/blocking/test_convergence_gate_blocker.py +153 -5
  41. package/hooks/blocking/test_destructive_command_blocker.py +1 -1
  42. package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +45 -0
  43. package/hooks/blocking/test_pr_description_proof_of_work.py +151 -0
  44. package/hooks/blocking/test_pre_tool_use_dispatcher.py +8 -8
  45. package/hooks/blocking/test_verification_verdict_store.py +920 -903
  46. package/hooks/blocking/test_volatile_path_in_post_blocker.py +114 -2
  47. package/hooks/blocking/verification_verdict_store.py +27 -5
  48. package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +29 -17
  49. package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +35 -0
  50. package/hooks/blocking/volatile_path_in_post_blocker.py +69 -8
  51. package/hooks/git-hooks/git_hooks_constants/__init__.py +6 -0
  52. package/hooks/git-hooks/pre_push.py +89 -2
  53. package/hooks/git-hooks/test_pre_push.py +103 -0
  54. package/hooks/hooks.json +16 -1
  55. package/hooks/hooks_constants/CLAUDE.md +1 -0
  56. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
  57. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  58. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +26 -11
  59. package/hooks/hooks_constants/convergence_gate_blocker_constants.py +20 -3
  60. package/hooks/hooks_constants/destructive_command_segment_constants.py +3 -1
  61. package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -4
  62. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  63. package/hooks/hooks_constants/pyproject_config_discovery_constants.py +16 -0
  64. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +24 -0
  65. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -0
  66. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +8 -1
  67. package/hooks/validators/CLAUDE.md +1 -0
  68. package/hooks/validators/mypy_integration.py +63 -50
  69. package/hooks/validators/pyproject_config_discovery.py +101 -0
  70. package/hooks/validators/ruff_integration.py +211 -23
  71. package/hooks/validators/run_all_validators.py +21 -14
  72. package/hooks/validators/test_mypy_integration.py +32 -0
  73. package/hooks/validators/test_pyproject_config_discovery.py +94 -0
  74. package/hooks/validators/test_ruff_integration.py +68 -1
  75. package/hooks/validators/test_run_all_validators.py +25 -0
  76. package/hooks/validators/test_run_all_validators_config_discovery.py +123 -0
  77. package/package.json +1 -1
  78. package/rules/docstring-prose-matches-implementation.md +20 -43
  79. package/rules/durable-post-artifacts.md +7 -0
  80. package/scripts/codec_forwarding_test_support.py +83 -0
  81. package/scripts/conftest.py +8 -0
  82. package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
  83. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +44 -1
  84. package/scripts/dev_env_scripts_constants/code_review_constants.py +129 -12
  85. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +55 -0
  86. package/scripts/invoke_code_review.py +550 -38
  87. package/scripts/resolve_worker_spawn.py +8 -1
  88. package/scripts/test_invoke_code_review.py +298 -4
  89. package/scripts/test_invoke_code_review_codec.py +77 -0
  90. package/scripts/test_resolve_worker_spawn_codec.py +101 -0
  91. package/skills/autoconverge/SKILL.md +9 -3
  92. package/skills/autoconverge/reference/convergence.md +2 -1
  93. package/skills/autoconverge/reference/multi-pr.md +6 -1
  94. package/skills/autoconverge/reference/stop-conditions.md +16 -10
  95. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +1 -1
  96. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +175 -3
  97. package/skills/autoconverge/workflow/converge.contract.test.mjs +19 -0
  98. package/skills/autoconverge/workflow/converge.mjs +24 -8
  99. package/skills/autoconverge/workflow/converge_multi.mjs +7 -3
  100. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +5 -0
  101. package/skills/fresh-branch/CLAUDE.md +2 -0
  102. package/skills/fresh-branch/SKILL.md +2 -0
  103. package/skills/fresh-branch/scripts/create_fresh_branch.py +78 -180
  104. package/skills/fresh-branch/scripts/fresh_branch_git_commands.py +285 -0
  105. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -0
  106. package/skills/fresh-branch/scripts/pytest.ini +4 -0
  107. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +98 -0
  108. package/skills/fresh-branch/scripts/test_fresh_branch_git_commands.py +310 -0
  109. package/skills/prototype/SKILL.md +86 -0
  110. package/skills/prototype/reference/honest-limitations.md +23 -0
  111. package/skills/prototype/reference/promotion-tasks.md +23 -0
  112. package/skills/prototype/scripts/build_sandbox_settings.py +249 -0
  113. package/skills/prototype/scripts/conftest.py +15 -0
  114. package/skills/prototype/scripts/launch_sandbox.py +205 -0
  115. package/skills/prototype/scripts/probe_sandbox_safety.py +311 -0
  116. package/skills/prototype/scripts/prototype_scripts_constants/__init__.py +1 -0
  117. package/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
  118. package/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +41 -0
  119. package/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +23 -0
  120. package/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +45 -0
  121. package/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +10 -0
  122. package/skills/prototype/scripts/test_build_sandbox_settings.py +275 -0
  123. package/skills/prototype/scripts/test_launch_sandbox.py +303 -0
  124. package/skills/prototype/scripts/test_probe_sandbox_safety.py +284 -0
  125. package/skills/prototype/workflows/promotion.md +27 -0
  126. package/skills/prototype/workflows/sandbox.md +35 -0
  127. package/skills/skill-builder/CLAUDE.md +3 -3
  128. package/skills/skill-builder/SKILL.md +5 -5
  129. package/skills/skill-builder/references/CLAUDE.md +1 -1
  130. package/skills/skill-builder/references/delegation-map.md +3 -3
  131. package/skills/skill-builder/references/description-field.md +1 -1
  132. package/skills/skill-builder/references/skill-modularity.md +2 -3
  133. package/skills/skill-builder/workflows/CLAUDE.md +1 -1
  134. package/skills/skill-builder/workflows/improve-skill.md +1 -1
  135. package/skills/skill-builder/workflows/new-skill.md +2 -2
  136. package/skills/team-advisor/SKILL.md +2 -2
@@ -1,903 +1,920 @@
1
- """Tests for the mechanical commit-gate exemption in verification_verdict_store.
2
-
3
- Each test builds a real git repository with a real origin remote and asserts
4
- the exemption decision against the live work tree, exercising the same code
5
- path the verified_commit_gate hook runs.
6
- """
7
-
8
- import importlib.util
9
- import json
10
- import pathlib
11
- import subprocess
12
- import sys
13
- from collections.abc import Callable
14
-
15
- import pytest
16
-
17
- _HOOK_DIR = pathlib.Path(__file__).parent
18
- if str(_HOOK_DIR) not in sys.path:
19
- sys.path.insert(0, str(_HOOK_DIR))
20
-
21
- store_spec = importlib.util.spec_from_file_location(
22
- "verification_verdict_store",
23
- _HOOK_DIR / "verification_verdict_store.py",
24
- )
25
- assert store_spec is not None
26
- assert store_spec.loader is not None
27
- store_module = importlib.util.module_from_spec(store_spec)
28
- store_spec.loader.exec_module(store_module)
29
- is_verification_exempt_diff = store_module.is_verification_exempt_diff
30
- resolve_merge_base = store_module.resolve_merge_base
31
- branch_surface_manifest = store_module.branch_surface_manifest
32
- manifest_sha256 = store_module.manifest_sha256
33
- workflow_verdict_covers_surface = store_module.workflow_verdict_covers_surface
34
- minted_verdict_covers_surface = store_module.minted_verdict_covers_surface
35
- write_verdict = store_module.write_verdict
36
- worktree_path_for_branch = store_module.worktree_path_for_branch
37
- empty_surface_hash = store_module.empty_surface_hash
38
-
39
- constants_spec = importlib.util.spec_from_file_location(
40
- "verified_commit_constants",
41
- _HOOK_DIR / "config" / "verified_commit_constants.py",
42
- )
43
- assert constants_spec is not None
44
- assert constants_spec.loader is not None
45
- constants_module = importlib.util.module_from_spec(constants_spec)
46
- constants_spec.loader.exec_module(constants_module)
47
- CORRECTIVE_MESSAGE = constants_module.CORRECTIVE_MESSAGE
48
- EMPTY_SURFACE_GUARD_MESSAGE = constants_module.EMPTY_SURFACE_GUARD_MESSAGE
49
- BRANCH_WORKTREE_ABSENT_MESSAGE = constants_module.BRANCH_WORKTREE_ABSENT_MESSAGE
50
-
51
- PRODUCTION_SOURCE = "def add(left: int, right: int) -> int:\n return left + right\n"
52
- TEST_SOURCE = "def test_add() -> None:\n assert 1 + 1 == 2\n"
53
-
54
-
55
- def _run_git(repo_dir: pathlib.Path, *git_arguments: str) -> None:
56
- subprocess.run(
57
- ["git", "-C", str(repo_dir), *git_arguments],
58
- check=True,
59
- capture_output=True,
60
- text=True,
61
- )
62
-
63
-
64
- def _make_repo_on_branch(
65
- tmp_path: pathlib.Path, branch_name: str
66
- ) -> pathlib.Path:
67
- origin_dir = tmp_path / "origin.git"
68
- work_dir = tmp_path / "work"
69
- work_dir.mkdir()
70
- subprocess.run(
71
- ["git", "init", "--bare", f"--initial-branch={branch_name}", str(origin_dir)],
72
- check=True,
73
- capture_output=True,
74
- text=True,
75
- )
76
- empty_hooks_dir = tmp_path / "nohooks"
77
- empty_hooks_dir.mkdir()
78
- _run_git(work_dir, "init", f"--initial-branch={branch_name}")
79
- _run_git(work_dir, "config", "user.email", "tests@example.com")
80
- _run_git(work_dir, "config", "user.name", "Verdict Store Tests")
81
- _run_git(work_dir, "config", "core.hooksPath", str(empty_hooks_dir))
82
- (work_dir / "src").mkdir()
83
- (work_dir / "tests").mkdir()
84
- (work_dir / "src" / "app.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
85
- (work_dir / "tests" / "test_app.py").write_text(TEST_SOURCE, encoding="utf-8")
86
- (work_dir / "README.md").write_text("# Fixture repo\n", encoding="utf-8")
87
- _run_git(work_dir, "add", "-A")
88
- _run_git(work_dir, "commit", "-m", "base")
89
- _run_git(work_dir, "remote", "add", "origin", str(origin_dir))
90
- _run_git(work_dir, "push", "-u", "origin", branch_name)
91
- return work_dir
92
-
93
-
94
- def _make_repo_with_origin(tmp_path: pathlib.Path) -> pathlib.Path:
95
- return _make_repo_on_branch(tmp_path, "main")
96
-
97
-
98
- def _exemption_for(work_dir: pathlib.Path) -> bool:
99
- merge_base_sha = resolve_merge_base(str(work_dir))
100
- assert merge_base_sha is not None
101
- return is_verification_exempt_diff(str(work_dir), merge_base_sha)
102
-
103
-
104
- def test_production_change_is_gated(tmp_path: pathlib.Path) -> None:
105
- work_dir = _make_repo_with_origin(tmp_path)
106
- (work_dir / "src" / "app.py").write_text(
107
- "def add(left: int, right: int) -> int:\n return left - right\n",
108
- encoding="utf-8",
109
- )
110
- assert _exemption_for(work_dir) is False
111
-
112
-
113
- def test_docs_only_change_is_exempt(tmp_path: pathlib.Path) -> None:
114
- work_dir = _make_repo_with_origin(tmp_path)
115
- (work_dir / "README.md").write_text(
116
- "# Fixture repo\n\nUpdated.\n", encoding="utf-8"
117
- )
118
- assert _exemption_for(work_dir) is True
119
-
120
-
121
- def test_docstring_only_python_change_is_exempt(tmp_path: pathlib.Path) -> None:
122
- work_dir = _make_repo_with_origin(tmp_path)
123
- (work_dir / "src" / "app.py").write_text(
124
- 'def add(left: int, right: int) -> int:\n """Add two integers."""\n'
125
- " return left + right\n",
126
- encoding="utf-8",
127
- )
128
- assert _exemption_for(work_dir) is True
129
-
130
-
131
- def test_modified_test_file_is_exempt(tmp_path: pathlib.Path) -> None:
132
- work_dir = _make_repo_with_origin(tmp_path)
133
- (work_dir / "tests" / "test_app.py").write_text(
134
- TEST_SOURCE + "\n\ndef test_add_zero() -> None:\n assert 0 + 0 == 0\n",
135
- encoding="utf-8",
136
- )
137
- assert _exemption_for(work_dir) is True
138
-
139
-
140
- def test_untracked_test_prefix_file_is_exempt(tmp_path: pathlib.Path) -> None:
141
- work_dir = _make_repo_with_origin(tmp_path)
142
- (work_dir / "tests" / "test_extra.py").write_text(TEST_SOURCE, encoding="utf-8")
143
- assert _exemption_for(work_dir) is True
144
-
145
-
146
- def test_untracked_test_suffix_file_is_exempt(tmp_path: pathlib.Path) -> None:
147
- work_dir = _make_repo_with_origin(tmp_path)
148
- (work_dir / "tests" / "app_test.py").write_text(TEST_SOURCE, encoding="utf-8")
149
- assert _exemption_for(work_dir) is True
150
-
151
-
152
- def test_modified_conftest_is_exempt(tmp_path: pathlib.Path) -> None:
153
- work_dir = _make_repo_with_origin(tmp_path)
154
- (work_dir / "tests" / "conftest.py").write_text(
155
- "import pytest\n\n\n@pytest.fixture\ndef sample() -> int:\n return 3\n",
156
- encoding="utf-8",
157
- )
158
- assert _exemption_for(work_dir) is True
159
-
160
-
161
- def test_deleted_test_file_is_exempt(tmp_path: pathlib.Path) -> None:
162
- work_dir = _make_repo_with_origin(tmp_path)
163
- (work_dir / "tests" / "test_app.py").unlink()
164
- assert _exemption_for(work_dir) is True
165
-
166
-
167
- def test_mixed_test_and_production_change_is_gated(tmp_path: pathlib.Path) -> None:
168
- work_dir = _make_repo_with_origin(tmp_path)
169
- (work_dir / "tests" / "test_app.py").write_text(
170
- TEST_SOURCE + "\n", encoding="utf-8"
171
- )
172
- (work_dir / "src" / "app.py").write_text(
173
- "def add(left: int, right: int) -> int:\n return left * right\n",
174
- encoding="utf-8",
175
- )
176
- assert _exemption_for(work_dir) is False
177
-
178
-
179
- def test_untracked_production_file_is_gated(tmp_path: pathlib.Path) -> None:
180
- work_dir = _make_repo_with_origin(tmp_path)
181
- (work_dir / "src" / "extra.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
182
- assert _exemption_for(work_dir) is False
183
-
184
-
185
- def test_production_file_named_like_test_outside_python_is_gated(
186
- tmp_path: pathlib.Path,
187
- ) -> None:
188
- work_dir = _make_repo_with_origin(tmp_path)
189
- (work_dir / "src" / "test_data.json").write_text("{}", encoding="utf-8")
190
- assert _exemption_for(work_dir) is False
191
-
192
-
193
- def test_comment_only_change_in_non_python_file_is_gated(
194
- tmp_path: pathlib.Path,
195
- ) -> None:
196
- work_dir = _make_repo_with_origin(tmp_path)
197
- shell_script_path = work_dir / "src" / "deploy.sh"
198
- shell_script_path.write_text("# build the project\nmake build\n", encoding="utf-8")
199
- _run_git(work_dir, "add", "-A")
200
- _run_git(work_dir, "commit", "-m", "add deploy script")
201
- shell_script_path.write_text(
202
- "# build the release artifact\nmake build\n", encoding="utf-8"
203
- )
204
- assert _exemption_for(work_dir) is False
205
-
206
-
207
- def test_corrective_message_scopes_comment_exemption_to_python() -> None:
208
- lowered_message = CORRECTIVE_MESSAGE.lower()
209
- assert "comment" in lowered_message
210
- assert "python" in lowered_message
211
- assert "comment-, and test-only surfaces are exempt" not in lowered_message
212
-
213
-
214
- def test_corrective_message_names_worktree_surface_keying() -> None:
215
- lowered_message = CORRECTIVE_MESSAGE.lower()
216
- assert "work tree" in lowered_message
217
- assert "byte-identical" in lowered_message
218
-
219
-
220
- def test_corrective_message_gives_cross_worktree_remedy() -> None:
221
- lowered_message = CORRECTIVE_MESSAGE.lower()
222
- assert "run this command from the work tree" in lowered_message
223
- assert "verify this surface here" in lowered_message
224
-
225
-
226
- def test_untracked_claude_production_hook_is_gated(tmp_path: pathlib.Path) -> None:
227
- work_dir = _make_repo_with_origin(tmp_path)
228
- new_hook_dir = work_dir / ".claude" / "hooks" / "blocking"
229
- new_hook_dir.mkdir(parents=True)
230
- (new_hook_dir / "evil_new_hook.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
231
- assert _exemption_for(work_dir) is False
232
-
233
-
234
- def test_untracked_claude_production_hook_is_in_surface_manifest(
235
- tmp_path: pathlib.Path,
236
- ) -> None:
237
- work_dir = _make_repo_with_origin(tmp_path)
238
- new_hook_dir = work_dir / ".claude" / "hooks" / "blocking"
239
- new_hook_dir.mkdir(parents=True)
240
- (new_hook_dir / "evil_new_hook.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
241
- merge_base_sha = resolve_merge_base(str(work_dir))
242
- assert merge_base_sha is not None
243
- surface_manifest_text = branch_surface_manifest(str(work_dir), merge_base_sha)
244
- assert surface_manifest_text is not None
245
- assert ".claude/hooks/blocking/evil_new_hook.py" in surface_manifest_text
246
-
247
-
248
- def test_untracked_claude_worktree_scratch_copy_stays_filtered(
249
- tmp_path: pathlib.Path,
250
- ) -> None:
251
- work_dir = _make_repo_with_origin(tmp_path)
252
- scratch_dir = work_dir / ".claude" / "worktrees" / "feature" / "src"
253
- scratch_dir.mkdir(parents=True)
254
- (scratch_dir / "app.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
255
- assert _exemption_for(work_dir) is True
256
- merge_base_sha = resolve_merge_base(str(work_dir))
257
- assert merge_base_sha is not None
258
- surface_manifest_text = branch_surface_manifest(str(work_dir), merge_base_sha)
259
- assert surface_manifest_text == ""
260
-
261
-
262
- def _git_output(work_dir: pathlib.Path, *git_arguments: str) -> str:
263
- completed_process = subprocess.run(
264
- ["git", "-C", str(work_dir), *git_arguments],
265
- check=True,
266
- capture_output=True,
267
- text=True,
268
- )
269
- return completed_process.stdout.strip()
270
-
271
-
272
- def test_resolve_merge_base_finds_nonstandard_default_branch(
273
- tmp_path: pathlib.Path,
274
- ) -> None:
275
- work_dir = _make_repo_on_branch(tmp_path, "develop")
276
- subprocess.run(
277
- ["git", "-C", str(work_dir), "remote", "set-head", "origin", "--delete"],
278
- check=True,
279
- capture_output=True,
280
- text=True,
281
- )
282
- expected_merge_base = _git_output(
283
- work_dir, "merge-base", "HEAD", "origin/develop"
284
- )
285
- assert resolve_merge_base(str(work_dir)) == expected_merge_base
286
-
287
-
288
- def test_production_change_is_gated_on_nonstandard_default_branch(
289
- tmp_path: pathlib.Path,
290
- ) -> None:
291
- work_dir = _make_repo_on_branch(tmp_path, "develop")
292
- subprocess.run(
293
- ["git", "-C", str(work_dir), "remote", "set-head", "origin", "--delete"],
294
- check=True,
295
- capture_output=True,
296
- text=True,
297
- )
298
- (work_dir / "src" / "app.py").write_text(
299
- "def add(left: int, right: int) -> int:\n return left - right\n",
300
- encoding="utf-8",
301
- )
302
- assert _exemption_for(work_dir) is False
303
-
304
-
305
- MATCHING_MANIFEST_SHA256 = "a" * 64
306
- OTHER_MANIFEST_SHA256 = "b" * 64
307
- VERIFIER_AGENT_TYPE = "code-verifier"
308
-
309
-
310
- def _verdict_transcript_text(is_all_pass: bool, bound_manifest_sha256: str) -> str:
311
- verdict_record = {
312
- "all_pass": is_all_pass,
313
- "findings": [],
314
- "manifest_sha256": bound_manifest_sha256,
315
- }
316
- assistant_text = (
317
- "Verification complete.\n\n```verdict\n"
318
- + json.dumps(verdict_record)
319
- + "\n```\n"
320
- )
321
- assistant_entry = {
322
- "type": "assistant",
323
- "message": {"content": [{"type": "text", "text": assistant_text}]},
324
- }
325
- return json.dumps(assistant_entry) + "\n"
326
-
327
-
328
- def _write_agent_transcript(
329
- subagents_dir: pathlib.Path,
330
- agent_id: str,
331
- agent_type: str,
332
- transcript_text: str,
333
- should_write_sidecar: bool,
334
- ) -> None:
335
- workflow_dir = subagents_dir / "workflows" / "wf_x"
336
- workflow_dir.mkdir(parents=True, exist_ok=True)
337
- (workflow_dir / f"agent-{agent_id}.jsonl").write_text(
338
- transcript_text, encoding="utf-8"
339
- )
340
- if should_write_sidecar:
341
- (workflow_dir / f"agent-{agent_id}.meta.json").write_text(
342
- json.dumps({"agentType": agent_type}), encoding="utf-8"
343
- )
344
-
345
-
346
- def _session_transcript_path(tmp_path: pathlib.Path, session_id: str) -> pathlib.Path:
347
- session_root = tmp_path / "projects" / "demo"
348
- session_root.mkdir(parents=True)
349
- transcript_path = session_root / f"{session_id}.jsonl"
350
- transcript_path.write_text("", encoding="utf-8")
351
- return transcript_path
352
-
353
-
354
- def test_workflow_verdict_covers_surface_true_for_matching_passing_verifier(
355
- tmp_path: pathlib.Path,
356
- ) -> None:
357
- transcript_path = _session_transcript_path(tmp_path, "sess1")
358
- subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
359
- _write_agent_transcript(
360
- subagents_dir,
361
- "01",
362
- VERIFIER_AGENT_TYPE,
363
- _verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
364
- should_write_sidecar=True,
365
- )
366
- assert (
367
- workflow_verdict_covers_surface(
368
- str(transcript_path), MATCHING_MANIFEST_SHA256
369
- )
370
- is True
371
- )
372
-
373
-
374
- def test_workflow_verdict_covers_surface_false_for_nonmatching_hash(
375
- tmp_path: pathlib.Path,
376
- ) -> None:
377
- transcript_path = _session_transcript_path(tmp_path, "sess1")
378
- subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
379
- _write_agent_transcript(
380
- subagents_dir,
381
- "01",
382
- VERIFIER_AGENT_TYPE,
383
- _verdict_transcript_text(True, OTHER_MANIFEST_SHA256),
384
- should_write_sidecar=True,
385
- )
386
- assert (
387
- workflow_verdict_covers_surface(
388
- str(transcript_path), MATCHING_MANIFEST_SHA256
389
- )
390
- is False
391
- )
392
-
393
-
394
- def test_workflow_verdict_covers_surface_false_for_all_pass_false(
395
- tmp_path: pathlib.Path,
396
- ) -> None:
397
- transcript_path = _session_transcript_path(tmp_path, "sess1")
398
- subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
399
- _write_agent_transcript(
400
- subagents_dir,
401
- "01",
402
- VERIFIER_AGENT_TYPE,
403
- _verdict_transcript_text(False, MATCHING_MANIFEST_SHA256),
404
- should_write_sidecar=True,
405
- )
406
- assert (
407
- workflow_verdict_covers_surface(
408
- str(transcript_path), MATCHING_MANIFEST_SHA256
409
- )
410
- is False
411
- )
412
-
413
-
414
- def test_workflow_verdict_covers_surface_false_for_non_verifier_sidecar(
415
- tmp_path: pathlib.Path,
416
- ) -> None:
417
- transcript_path = _session_transcript_path(tmp_path, "sess1")
418
- subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
419
- _write_agent_transcript(
420
- subagents_dir,
421
- "01",
422
- "clean-coder",
423
- _verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
424
- should_write_sidecar=True,
425
- )
426
- assert (
427
- workflow_verdict_covers_surface(
428
- str(transcript_path), MATCHING_MANIFEST_SHA256
429
- )
430
- is False
431
- )
432
-
433
-
434
- def test_workflow_verdict_covers_surface_false_for_missing_sidecar_with_verdict(
435
- tmp_path: pathlib.Path,
436
- ) -> None:
437
- transcript_path = _session_transcript_path(tmp_path, "sess1")
438
- subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
439
- _write_agent_transcript(
440
- subagents_dir,
441
- "01",
442
- VERIFIER_AGENT_TYPE,
443
- _verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
444
- should_write_sidecar=False,
445
- )
446
- assert (
447
- workflow_verdict_covers_surface(
448
- str(transcript_path), MATCHING_MANIFEST_SHA256
449
- )
450
- is False
451
- )
452
-
453
-
454
- def test_workflow_verdict_covers_surface_false_for_corrupt_sidecar_with_verdict(
455
- tmp_path: pathlib.Path,
456
- ) -> None:
457
- transcript_path = _session_transcript_path(tmp_path, "sess1")
458
- subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
459
- _write_agent_transcript(
460
- subagents_dir,
461
- "01",
462
- VERIFIER_AGENT_TYPE,
463
- _verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
464
- should_write_sidecar=False,
465
- )
466
- corrupt_sidecar = (
467
- subagents_dir / "workflows" / "wf_x" / "agent-01.meta.json"
468
- )
469
- corrupt_sidecar.write_text("{not valid json", encoding="utf-8")
470
- assert (
471
- workflow_verdict_covers_surface(
472
- str(transcript_path), MATCHING_MANIFEST_SHA256
473
- )
474
- is False
475
- )
476
-
477
-
478
- def test_workflow_verdict_covers_surface_false_for_invalid_utf8_sidecar(
479
- tmp_path: pathlib.Path,
480
- ) -> None:
481
- transcript_path = _session_transcript_path(tmp_path, "sess1")
482
- subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
483
- _write_agent_transcript(
484
- subagents_dir,
485
- "01",
486
- VERIFIER_AGENT_TYPE,
487
- _verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
488
- should_write_sidecar=False,
489
- )
490
- invalid_utf8_sidecar = subagents_dir / "workflows" / "wf_x" / "agent-01.meta.json"
491
- invalid_utf8_sidecar.write_bytes(b'{"agentType": "\xff\xfe bad"}')
492
- assert (
493
- workflow_verdict_covers_surface(
494
- str(transcript_path), MATCHING_MANIFEST_SHA256
495
- )
496
- is False
497
- )
498
-
499
-
500
- def test_workflow_verdict_covers_surface_false_for_invalid_utf8_transcript(
501
- tmp_path: pathlib.Path,
502
- ) -> None:
503
- transcript_path = _session_transcript_path(tmp_path, "sess1")
504
- subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
505
- workflow_dir = subagents_dir / "workflows" / "wf_x"
506
- workflow_dir.mkdir(parents=True, exist_ok=True)
507
- (workflow_dir / "agent-01.jsonl").write_bytes(b'{"type": "assistant"}\xff\xfe\n')
508
- (workflow_dir / "agent-01.meta.json").write_text(
509
- json.dumps({"agentType": VERIFIER_AGENT_TYPE}), encoding="utf-8"
510
- )
511
- assert (
512
- workflow_verdict_covers_surface(
513
- str(transcript_path), MATCHING_MANIFEST_SHA256
514
- )
515
- is False
516
- )
517
-
518
-
519
- def test_workflow_verdict_covers_surface_false_for_missing_subagents_dir(
520
- tmp_path: pathlib.Path,
521
- ) -> None:
522
- transcript_path = _session_transcript_path(tmp_path, "sess1")
523
- assert (
524
- workflow_verdict_covers_surface(
525
- str(transcript_path), MATCHING_MANIFEST_SHA256
526
- )
527
- is False
528
- )
529
-
530
-
531
- def test_workflow_verdict_covers_surface_true_when_transcript_is_under_subagents(
532
- tmp_path: pathlib.Path,
533
- ) -> None:
534
- subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
535
- _write_agent_transcript(
536
- subagents_dir,
537
- "01",
538
- VERIFIER_AGENT_TYPE,
539
- _verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
540
- should_write_sidecar=True,
541
- )
542
- caller_transcript_path = (
543
- subagents_dir / "workflows" / "wf_x" / "agent-00.jsonl"
544
- )
545
- caller_transcript_path.write_text("", encoding="utf-8")
546
- assert (
547
- workflow_verdict_covers_surface(
548
- str(caller_transcript_path), MATCHING_MANIFEST_SHA256
549
- )
550
- is True
551
- )
552
-
553
-
554
- def test_manifest_hash_cli_prints_live_surface_hash(tmp_path: pathlib.Path) -> None:
555
- work_dir = _make_repo_with_origin(tmp_path)
556
- (work_dir / "src" / "app.py").write_text(
557
- "def add(left: int, right: int) -> int:\n return left - right\n",
558
- encoding="utf-8",
559
- )
560
- merge_base_sha = resolve_merge_base(str(work_dir))
561
- assert merge_base_sha is not None
562
- surface_manifest_text = branch_surface_manifest(str(work_dir), merge_base_sha)
563
- assert surface_manifest_text is not None
564
- expected_hash = manifest_sha256(surface_manifest_text)
565
- completed_process = subprocess.run(
566
- [
567
- sys.executable,
568
- str(_HOOK_DIR / "verification_verdict_store.py"),
569
- "--manifest-hash",
570
- str(work_dir),
571
- ],
572
- check=True,
573
- capture_output=True,
574
- text=True,
575
- )
576
- assert completed_process.stdout.strip() == expected_hash
577
-
578
-
579
- def _isolate_home(monkeypatch: pytest.MonkeyPatch, fake_home: pathlib.Path) -> None:
580
- home_text = str(fake_home)
581
- monkeypatch.setenv("HOME", home_text)
582
- monkeypatch.setenv("USERPROFILE", home_text)
583
- monkeypatch.delenv("HOMEDRIVE", raising=False)
584
- monkeypatch.delenv("HOMEPATH", raising=False)
585
-
586
-
587
- def test_minted_verdict_covers_surface_matches_other_worktree_by_hash(
588
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
589
- ) -> None:
590
- fake_home = tmp_path / "home"
591
- fake_home.mkdir()
592
- _isolate_home(monkeypatch, fake_home)
593
- write_verdict(
594
- str(tmp_path / "other" / "worktree"),
595
- MATCHING_MANIFEST_SHA256,
596
- True,
597
- [],
598
- "agent-x",
599
- )
600
- assert minted_verdict_covers_surface(MATCHING_MANIFEST_SHA256) is True
601
-
602
-
603
- def test_minted_verdict_covers_surface_false_for_other_hash(
604
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
605
- ) -> None:
606
- fake_home = tmp_path / "home"
607
- fake_home.mkdir()
608
- _isolate_home(monkeypatch, fake_home)
609
- write_verdict(
610
- str(tmp_path / "other" / "worktree"),
611
- OTHER_MANIFEST_SHA256,
612
- True,
613
- [],
614
- "agent-x",
615
- )
616
- assert minted_verdict_covers_surface(MATCHING_MANIFEST_SHA256) is False
617
-
618
-
619
- def test_minted_verdict_covers_surface_false_for_failing_verdict(
620
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
621
- ) -> None:
622
- fake_home = tmp_path / "home"
623
- fake_home.mkdir()
624
- _isolate_home(monkeypatch, fake_home)
625
- write_verdict(
626
- str(tmp_path / "other" / "worktree"),
627
- MATCHING_MANIFEST_SHA256,
628
- False,
629
- [{"severity": "P0", "summary": "boom"}],
630
- "agent-x",
631
- )
632
- assert minted_verdict_covers_surface(MATCHING_MANIFEST_SHA256) is False
633
-
634
-
635
- def test_minted_verdict_covers_surface_false_when_directory_absent(
636
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
637
- ) -> None:
638
- fake_home = tmp_path / "home"
639
- fake_home.mkdir()
640
- _isolate_home(monkeypatch, fake_home)
641
- assert minted_verdict_covers_surface(MATCHING_MANIFEST_SHA256) is False
642
-
643
-
644
- def _make_repo_with_branch_worktree(
645
- tmp_path: pathlib.Path, branch_name: str
646
- ) -> tuple[pathlib.Path, pathlib.Path]:
647
- """Create a repo with a branch checked out in a separate worktree.
648
-
649
- Returns:
650
- A tuple of (main worktree path, branch worktree path).
651
- """
652
- empty_hooks_dir = tmp_path / "nohooks"
653
- empty_hooks_dir.mkdir()
654
-
655
- main_dir = tmp_path / "main"
656
- main_dir.mkdir()
657
- _run_git(main_dir, "init", "--initial-branch=main")
658
- _run_git(main_dir, "config", "user.email", "tests@example.com")
659
- _run_git(main_dir, "config", "user.name", "Worktree Tests")
660
- _run_git(main_dir, "config", "core.hooksPath", str(empty_hooks_dir))
661
- (main_dir / "app.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
662
- _run_git(main_dir, "add", "-A")
663
- _run_git(main_dir, "commit", "-m", "base")
664
-
665
- origin_dir = tmp_path / "origin.git"
666
- subprocess.run(
667
- ["git", "init", "--bare", "--initial-branch=main", str(origin_dir)],
668
- check=True,
669
- capture_output=True,
670
- text=True,
671
- )
672
- _run_git(main_dir, "remote", "add", "origin", str(origin_dir))
673
- _run_git(main_dir, "push", "-u", "origin", "main")
674
-
675
- _run_git(main_dir, "branch", branch_name)
676
-
677
- branch_worktree_dir = tmp_path / "branch-worktree"
678
- _run_git(main_dir, "worktree", "add", str(branch_worktree_dir), branch_name)
679
-
680
- return main_dir, branch_worktree_dir
681
-
682
-
683
- def test_worktree_path_for_branch_returns_path_when_branch_present(
684
- tmp_path: pathlib.Path,
685
- ) -> None:
686
- _main_dir, branch_worktree_dir = _make_repo_with_branch_worktree(
687
- tmp_path, "feature-x"
688
- )
689
- resolved_path = worktree_path_for_branch(str(branch_worktree_dir), "feature-x")
690
- assert resolved_path is not None
691
- assert pathlib.Path(resolved_path).resolve() == branch_worktree_dir.resolve()
692
-
693
-
694
- def test_worktree_path_for_branch_returns_none_when_branch_absent(
695
- tmp_path: pathlib.Path,
696
- ) -> None:
697
- main_dir, _branch_worktree_dir = _make_repo_with_branch_worktree(
698
- tmp_path, "feature-x"
699
- )
700
- resolved_path = worktree_path_for_branch(str(main_dir), "branch-never-checked-out")
701
- assert resolved_path is None
702
-
703
-
704
- def test_empty_surface_hash_equals_hash_of_empty_string() -> None:
705
- assert empty_surface_hash() == manifest_sha256("")
706
-
707
-
708
- def test_manifest_hash_cli_empty_surface_writes_guard_message_to_stderr(
709
- tmp_path: pathlib.Path,
710
- ) -> None:
711
- work_dir = _make_repo_with_origin(tmp_path)
712
- completed_process = subprocess.run(
713
- [
714
- sys.executable,
715
- str(_HOOK_DIR / "verification_verdict_store.py"),
716
- "--manifest-hash",
717
- str(work_dir),
718
- ],
719
- capture_output=True,
720
- text=True,
721
- )
722
- assert completed_process.returncode != 0
723
- assert completed_process.stdout.strip() == ""
724
- lowered_stderr = completed_process.stderr.lower()
725
- assert "wrong work tree" in lowered_stderr or "empty" in lowered_stderr
726
-
727
-
728
- def test_manifest_hash_cli_empty_surface_prints_nothing_on_stdout(
729
- tmp_path: pathlib.Path,
730
- ) -> None:
731
- work_dir = _make_repo_with_origin(tmp_path)
732
- completed_process = subprocess.run(
733
- [
734
- sys.executable,
735
- str(_HOOK_DIR / "verification_verdict_store.py"),
736
- "--manifest-hash",
737
- str(work_dir),
738
- ],
739
- capture_output=True,
740
- text=True,
741
- )
742
- assert completed_process.stdout.strip() == ""
743
-
744
-
745
- def test_manifest_hash_for_branch_cli_prints_same_hash_as_explicit_dir(
746
- tmp_path: pathlib.Path,
747
- ) -> None:
748
- _main_dir, branch_worktree_dir = _make_repo_with_branch_worktree(
749
- tmp_path, "feature-branch"
750
- )
751
- (branch_worktree_dir / "app.py").write_text(
752
- "def add(left: int, right: int) -> int:\n return left - right\n",
753
- encoding="utf-8",
754
- )
755
- direct_process = subprocess.run(
756
- [
757
- sys.executable,
758
- str(_HOOK_DIR / "verification_verdict_store.py"),
759
- "--manifest-hash",
760
- str(branch_worktree_dir),
761
- ],
762
- capture_output=True,
763
- text=True,
764
- check=True,
765
- )
766
- branch_process = subprocess.run(
767
- [
768
- sys.executable,
769
- str(_HOOK_DIR / "verification_verdict_store.py"),
770
- "--manifest-hash-for-branch",
771
- "feature-branch",
772
- ],
773
- capture_output=True,
774
- text=True,
775
- check=True,
776
- cwd=str(branch_worktree_dir),
777
- )
778
- assert direct_process.stdout.strip() == branch_process.stdout.strip()
779
- assert direct_process.stdout.strip() != ""
780
-
781
-
782
- def test_manifest_hash_for_branch_cli_returns_nonzero_when_branch_absent(
783
- tmp_path: pathlib.Path,
784
- ) -> None:
785
- main_dir, _branch_worktree_dir = _make_repo_with_branch_worktree(
786
- tmp_path, "feature-branch"
787
- )
788
- completed_process = subprocess.run(
789
- [
790
- sys.executable,
791
- str(_HOOK_DIR / "verification_verdict_store.py"),
792
- "--manifest-hash-for-branch",
793
- "branch-never-checked-out",
794
- ],
795
- capture_output=True,
796
- text=True,
797
- cwd=str(main_dir),
798
- )
799
- assert completed_process.returncode != 0
800
- assert completed_process.stdout.strip() == ""
801
-
802
-
803
- def test_store_imports_under_foreign_config_shadow(
804
- run_under_config_shadow: Callable[[str], subprocess.CompletedProcess[str]],
805
- ) -> None:
806
- completed_probe = run_under_config_shadow(
807
- "import verification_verdict_store\nprint('IMPORT_OK')\n"
808
- )
809
- assert completed_probe.returncode == 0, completed_probe.stderr
810
- assert "IMPORT_OK" in completed_probe.stdout
811
-
812
-
813
- def _ledger_path(fake_home: pathlib.Path) -> pathlib.Path:
814
- return (
815
- fake_home
816
- / constants_module.CLAUDE_HOME_DIRECTORY_NAME
817
- / constants_module.LOGS_DIRECTORY_NAME
818
- / constants_module.VERIFIER_VERDICTS_JSONL_FILENAME
819
- )
820
-
821
-
822
- def _read_ledger_rows(fake_home: pathlib.Path) -> list[dict]:
823
- ledger_file = _ledger_path(fake_home)
824
- all_rows: list[dict] = []
825
- for each_line in ledger_file.read_text(encoding="utf-8").splitlines():
826
- if each_line.strip():
827
- all_rows.append(json.loads(each_line))
828
- return all_rows
829
-
830
-
831
- def test_write_verdict_appends_failing_row_that_survives_later_clean_mint(
832
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
833
- ) -> None:
834
- """A failing mint's check names stay readable after a later clean mint."""
835
- fake_home = tmp_path / "home"
836
- fake_home.mkdir()
837
- _isolate_home(monkeypatch, fake_home)
838
- monkeypatch.setattr(pathlib.Path, "home", classmethod(lambda cls: fake_home))
839
- work_dir = _make_repo_with_origin(tmp_path)
840
- failing_findings = [
841
- {"check": "gates.import", "detail": "ImportError: boom"},
842
- {"check": "task.item-2", "detail": "missing hunk"},
843
- ]
844
- write_verdict(
845
- str(work_dir),
846
- MATCHING_MANIFEST_SHA256,
847
- False,
848
- failing_findings,
849
- "agent-fail",
850
- )
851
- write_verdict(
852
- str(work_dir),
853
- MATCHING_MANIFEST_SHA256,
854
- True,
855
- [],
856
- "agent-pass",
857
- )
858
- all_rows = _read_ledger_rows(fake_home)
859
- assert len(all_rows) == 2
860
- failing_row = all_rows[0]
861
- clean_row = all_rows[1]
862
- assert failing_row[constants_module.LEDGER_KEY_ALL_PASS] is False
863
- assert failing_row[constants_module.LEDGER_KEY_FINDING_COUNT] == 2
864
- assert failing_row[constants_module.LEDGER_KEY_FINDING_CHECK_NAMES] == [
865
- "gates.import",
866
- "task.item-2",
867
- ]
868
- assert failing_row[constants_module.LEDGER_KEY_MINTED_FROM_AGENT_ID] == "agent-fail"
869
- assert failing_row[constants_module.LEDGER_KEY_MANIFEST_HASH] == MATCHING_MANIFEST_SHA256
870
- assert failing_row[constants_module.LEDGER_KEY_REPO_ROOT] == str(work_dir)
871
- assert failing_row[constants_module.LEDGER_KEY_BRANCH] == "main"
872
- assert clean_row[constants_module.LEDGER_KEY_ALL_PASS] is True
873
- assert clean_row[constants_module.LEDGER_KEY_FINDING_COUNT] == 0
874
- assert clean_row[constants_module.LEDGER_KEY_FINDING_CHECK_NAMES] == []
875
- assert clean_row[constants_module.LEDGER_KEY_MINTED_FROM_AGENT_ID] == "agent-pass"
876
-
877
-
878
- def test_write_verdict_returns_path_when_ledger_append_fails(
879
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
880
- ) -> None:
881
- """A ledger IO failure never changes the mint path or verdict file write."""
882
- fake_home = tmp_path / "home"
883
- fake_home.mkdir()
884
- _isolate_home(monkeypatch, fake_home)
885
- monkeypatch.setattr(pathlib.Path, "home", classmethod(lambda cls: fake_home))
886
- logs_as_file = (
887
- fake_home
888
- / constants_module.CLAUDE_HOME_DIRECTORY_NAME
889
- / constants_module.LOGS_DIRECTORY_NAME
890
- )
891
- logs_as_file.parent.mkdir(parents=True, exist_ok=True)
892
- logs_as_file.write_text("not-a-directory", encoding="utf-8")
893
- work_dir = _make_repo_with_origin(tmp_path)
894
- verdict_file = write_verdict(
895
- str(work_dir),
896
- MATCHING_MANIFEST_SHA256,
897
- True,
898
- [],
899
- "agent-x",
900
- )
901
- assert verdict_file.is_file()
902
- verdict_record = json.loads(verdict_file.read_text(encoding="utf-8"))
903
- assert verdict_record[constants_module.VERDICT_KEY_ALL_PASS] is True
1
+ """Tests for the mechanical commit-gate exemption in verification_verdict_store.
2
+
3
+ Each test builds a real git repository with a real origin remote and asserts
4
+ the exemption decision against the live work tree, exercising the same code
5
+ path the verified_commit_gate hook runs.
6
+ """
7
+
8
+ import importlib.util
9
+ import json
10
+ import pathlib
11
+ import subprocess
12
+ import sys
13
+ from collections.abc import Callable
14
+
15
+ import pytest
16
+
17
+ _HOOK_DIR = pathlib.Path(__file__).parent
18
+ if str(_HOOK_DIR) not in sys.path:
19
+ sys.path.insert(0, str(_HOOK_DIR))
20
+
21
+ store_spec = importlib.util.spec_from_file_location(
22
+ "verification_verdict_store",
23
+ _HOOK_DIR / "verification_verdict_store.py",
24
+ )
25
+ assert store_spec is not None
26
+ assert store_spec.loader is not None
27
+ store_module = importlib.util.module_from_spec(store_spec)
28
+ store_spec.loader.exec_module(store_module)
29
+ is_verification_exempt_diff = store_module.is_verification_exempt_diff
30
+ resolve_merge_base = store_module.resolve_merge_base
31
+ branch_surface_manifest = store_module.branch_surface_manifest
32
+ manifest_sha256 = store_module.manifest_sha256
33
+ workflow_verdict_covers_surface = store_module.workflow_verdict_covers_surface
34
+ minted_verdict_covers_surface = store_module.minted_verdict_covers_surface
35
+ write_verdict = store_module.write_verdict
36
+ worktree_path_for_branch = store_module.worktree_path_for_branch
37
+ empty_surface_hash = store_module.empty_surface_hash
38
+ root_key_for_repo = store_module.root_key_for_repo
39
+ verdict_path_for_repo = store_module.verdict_path_for_repo
40
+
41
+ constants_spec = importlib.util.spec_from_file_location(
42
+ "verified_commit_constants",
43
+ _HOOK_DIR / "config" / "verified_commit_constants.py",
44
+ )
45
+ assert constants_spec is not None
46
+ assert constants_spec.loader is not None
47
+ constants_module = importlib.util.module_from_spec(constants_spec)
48
+ constants_spec.loader.exec_module(constants_module)
49
+ CORRECTIVE_MESSAGE = constants_module.CORRECTIVE_MESSAGE
50
+ EMPTY_SURFACE_GUARD_MESSAGE = constants_module.EMPTY_SURFACE_GUARD_MESSAGE
51
+ BRANCH_WORKTREE_ABSENT_MESSAGE = constants_module.BRANCH_WORKTREE_ABSENT_MESSAGE
52
+
53
+ PRODUCTION_SOURCE = "def add(left: int, right: int) -> int:\n return left + right\n"
54
+ TEST_SOURCE = "def test_add() -> None:\n assert 1 + 1 == 2\n"
55
+
56
+
57
+ def _run_git(repo_dir: pathlib.Path, *git_arguments: str) -> None:
58
+ subprocess.run(
59
+ ["git", "-C", str(repo_dir), *git_arguments],
60
+ check=True,
61
+ capture_output=True,
62
+ text=True,
63
+ )
64
+
65
+
66
+ def _make_repo_on_branch(
67
+ tmp_path: pathlib.Path, branch_name: str
68
+ ) -> pathlib.Path:
69
+ origin_dir = tmp_path / "origin.git"
70
+ work_dir = tmp_path / "work"
71
+ work_dir.mkdir()
72
+ subprocess.run(
73
+ ["git", "init", "--bare", f"--initial-branch={branch_name}", str(origin_dir)],
74
+ check=True,
75
+ capture_output=True,
76
+ text=True,
77
+ )
78
+ empty_hooks_dir = tmp_path / "nohooks"
79
+ empty_hooks_dir.mkdir()
80
+ _run_git(work_dir, "init", f"--initial-branch={branch_name}")
81
+ _run_git(work_dir, "config", "user.email", "tests@example.com")
82
+ _run_git(work_dir, "config", "user.name", "Verdict Store Tests")
83
+ _run_git(work_dir, "config", "core.hooksPath", str(empty_hooks_dir))
84
+ (work_dir / "src").mkdir()
85
+ (work_dir / "tests").mkdir()
86
+ (work_dir / "src" / "app.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
87
+ (work_dir / "tests" / "test_app.py").write_text(TEST_SOURCE, encoding="utf-8")
88
+ (work_dir / "README.md").write_text("# Fixture repo\n", encoding="utf-8")
89
+ _run_git(work_dir, "add", "-A")
90
+ _run_git(work_dir, "commit", "-m", "base")
91
+ _run_git(work_dir, "remote", "add", "origin", str(origin_dir))
92
+ _run_git(work_dir, "push", "-u", "origin", branch_name)
93
+ return work_dir
94
+
95
+
96
+ def _make_repo_with_origin(tmp_path: pathlib.Path) -> pathlib.Path:
97
+ return _make_repo_on_branch(tmp_path, "main")
98
+
99
+
100
+ def _exemption_for(work_dir: pathlib.Path) -> bool:
101
+ merge_base_sha = resolve_merge_base(str(work_dir))
102
+ assert merge_base_sha is not None
103
+ return is_verification_exempt_diff(str(work_dir), merge_base_sha)
104
+
105
+
106
+ def test_production_change_is_gated(tmp_path: pathlib.Path) -> None:
107
+ work_dir = _make_repo_with_origin(tmp_path)
108
+ (work_dir / "src" / "app.py").write_text(
109
+ "def add(left: int, right: int) -> int:\n return left - right\n",
110
+ encoding="utf-8",
111
+ )
112
+ assert _exemption_for(work_dir) is False
113
+
114
+
115
+ def test_docs_only_change_is_exempt(tmp_path: pathlib.Path) -> None:
116
+ work_dir = _make_repo_with_origin(tmp_path)
117
+ (work_dir / "README.md").write_text(
118
+ "# Fixture repo\n\nUpdated.\n", encoding="utf-8"
119
+ )
120
+ assert _exemption_for(work_dir) is True
121
+
122
+
123
+ def test_docstring_only_python_change_is_exempt(tmp_path: pathlib.Path) -> None:
124
+ work_dir = _make_repo_with_origin(tmp_path)
125
+ (work_dir / "src" / "app.py").write_text(
126
+ 'def add(left: int, right: int) -> int:\n """Add two integers."""\n'
127
+ " return left + right\n",
128
+ encoding="utf-8",
129
+ )
130
+ assert _exemption_for(work_dir) is True
131
+
132
+
133
+ def test_modified_test_file_is_exempt(tmp_path: pathlib.Path) -> None:
134
+ work_dir = _make_repo_with_origin(tmp_path)
135
+ (work_dir / "tests" / "test_app.py").write_text(
136
+ TEST_SOURCE + "\n\ndef test_add_zero() -> None:\n assert 0 + 0 == 0\n",
137
+ encoding="utf-8",
138
+ )
139
+ assert _exemption_for(work_dir) is True
140
+
141
+
142
+ def test_untracked_test_prefix_file_is_exempt(tmp_path: pathlib.Path) -> None:
143
+ work_dir = _make_repo_with_origin(tmp_path)
144
+ (work_dir / "tests" / "test_extra.py").write_text(TEST_SOURCE, encoding="utf-8")
145
+ assert _exemption_for(work_dir) is True
146
+
147
+
148
+ def test_untracked_test_suffix_file_is_exempt(tmp_path: pathlib.Path) -> None:
149
+ work_dir = _make_repo_with_origin(tmp_path)
150
+ (work_dir / "tests" / "app_test.py").write_text(TEST_SOURCE, encoding="utf-8")
151
+ assert _exemption_for(work_dir) is True
152
+
153
+
154
+ def test_modified_conftest_is_exempt(tmp_path: pathlib.Path) -> None:
155
+ work_dir = _make_repo_with_origin(tmp_path)
156
+ (work_dir / "tests" / "conftest.py").write_text(
157
+ "import pytest\n\n\n@pytest.fixture\ndef sample() -> int:\n return 3\n",
158
+ encoding="utf-8",
159
+ )
160
+ assert _exemption_for(work_dir) is True
161
+
162
+
163
+ def test_deleted_test_file_is_exempt(tmp_path: pathlib.Path) -> None:
164
+ work_dir = _make_repo_with_origin(tmp_path)
165
+ (work_dir / "tests" / "test_app.py").unlink()
166
+ assert _exemption_for(work_dir) is True
167
+
168
+
169
+ def test_mixed_test_and_production_change_is_gated(tmp_path: pathlib.Path) -> None:
170
+ work_dir = _make_repo_with_origin(tmp_path)
171
+ (work_dir / "tests" / "test_app.py").write_text(
172
+ TEST_SOURCE + "\n", encoding="utf-8"
173
+ )
174
+ (work_dir / "src" / "app.py").write_text(
175
+ "def add(left: int, right: int) -> int:\n return left * right\n",
176
+ encoding="utf-8",
177
+ )
178
+ assert _exemption_for(work_dir) is False
179
+
180
+
181
+ def test_untracked_production_file_is_gated(tmp_path: pathlib.Path) -> None:
182
+ work_dir = _make_repo_with_origin(tmp_path)
183
+ (work_dir / "src" / "extra.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
184
+ assert _exemption_for(work_dir) is False
185
+
186
+
187
+ def test_production_file_named_like_test_outside_python_is_gated(
188
+ tmp_path: pathlib.Path,
189
+ ) -> None:
190
+ work_dir = _make_repo_with_origin(tmp_path)
191
+ (work_dir / "src" / "test_data.json").write_text("{}", encoding="utf-8")
192
+ assert _exemption_for(work_dir) is False
193
+
194
+
195
+ def test_comment_only_change_in_non_python_file_is_gated(
196
+ tmp_path: pathlib.Path,
197
+ ) -> None:
198
+ work_dir = _make_repo_with_origin(tmp_path)
199
+ shell_script_path = work_dir / "src" / "deploy.sh"
200
+ shell_script_path.write_text("# build the project\nmake build\n", encoding="utf-8")
201
+ _run_git(work_dir, "add", "-A")
202
+ _run_git(work_dir, "commit", "-m", "add deploy script")
203
+ shell_script_path.write_text(
204
+ "# build the release artifact\nmake build\n", encoding="utf-8"
205
+ )
206
+ assert _exemption_for(work_dir) is False
207
+
208
+
209
+ def test_corrective_message_scopes_comment_exemption_to_python() -> None:
210
+ lowered_message = CORRECTIVE_MESSAGE.lower()
211
+ assert "comment" in lowered_message
212
+ assert "python" in lowered_message
213
+ assert "comment-, and test-only surfaces are exempt" not in lowered_message
214
+
215
+
216
+ def test_corrective_message_names_worktree_surface_keying() -> None:
217
+ lowered_message = CORRECTIVE_MESSAGE.lower()
218
+ assert "work tree" in lowered_message
219
+ assert "byte-identical" in lowered_message
220
+
221
+
222
+ def test_corrective_message_gives_cross_worktree_remedy() -> None:
223
+ lowered_message = CORRECTIVE_MESSAGE.lower()
224
+ assert "run this command from the work tree" in lowered_message
225
+ assert "verify this surface here" in lowered_message
226
+
227
+
228
+ def test_untracked_claude_production_hook_is_gated(tmp_path: pathlib.Path) -> None:
229
+ work_dir = _make_repo_with_origin(tmp_path)
230
+ new_hook_dir = work_dir / ".claude" / "hooks" / "blocking"
231
+ new_hook_dir.mkdir(parents=True)
232
+ (new_hook_dir / "evil_new_hook.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
233
+ assert _exemption_for(work_dir) is False
234
+
235
+
236
+ def test_untracked_claude_production_hook_is_in_surface_manifest(
237
+ tmp_path: pathlib.Path,
238
+ ) -> None:
239
+ work_dir = _make_repo_with_origin(tmp_path)
240
+ new_hook_dir = work_dir / ".claude" / "hooks" / "blocking"
241
+ new_hook_dir.mkdir(parents=True)
242
+ (new_hook_dir / "evil_new_hook.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
243
+ merge_base_sha = resolve_merge_base(str(work_dir))
244
+ assert merge_base_sha is not None
245
+ surface_manifest_text = branch_surface_manifest(str(work_dir), merge_base_sha)
246
+ assert surface_manifest_text is not None
247
+ assert ".claude/hooks/blocking/evil_new_hook.py" in surface_manifest_text
248
+
249
+
250
+ def test_untracked_claude_worktree_scratch_copy_stays_filtered(
251
+ tmp_path: pathlib.Path,
252
+ ) -> None:
253
+ work_dir = _make_repo_with_origin(tmp_path)
254
+ scratch_dir = work_dir / ".claude" / "worktrees" / "feature" / "src"
255
+ scratch_dir.mkdir(parents=True)
256
+ (scratch_dir / "app.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
257
+ assert _exemption_for(work_dir) is True
258
+ merge_base_sha = resolve_merge_base(str(work_dir))
259
+ assert merge_base_sha is not None
260
+ surface_manifest_text = branch_surface_manifest(str(work_dir), merge_base_sha)
261
+ assert surface_manifest_text == ""
262
+
263
+
264
+ def _git_output(work_dir: pathlib.Path, *git_arguments: str) -> str:
265
+ completed_process = subprocess.run(
266
+ ["git", "-C", str(work_dir), *git_arguments],
267
+ check=True,
268
+ capture_output=True,
269
+ text=True,
270
+ )
271
+ return completed_process.stdout.strip()
272
+
273
+
274
+ def test_resolve_merge_base_finds_nonstandard_default_branch(
275
+ tmp_path: pathlib.Path,
276
+ ) -> None:
277
+ work_dir = _make_repo_on_branch(tmp_path, "develop")
278
+ subprocess.run(
279
+ ["git", "-C", str(work_dir), "remote", "set-head", "origin", "--delete"],
280
+ check=True,
281
+ capture_output=True,
282
+ text=True,
283
+ )
284
+ expected_merge_base = _git_output(
285
+ work_dir, "merge-base", "HEAD", "origin/develop"
286
+ )
287
+ assert resolve_merge_base(str(work_dir)) == expected_merge_base
288
+
289
+
290
+ def test_production_change_is_gated_on_nonstandard_default_branch(
291
+ tmp_path: pathlib.Path,
292
+ ) -> None:
293
+ work_dir = _make_repo_on_branch(tmp_path, "develop")
294
+ subprocess.run(
295
+ ["git", "-C", str(work_dir), "remote", "set-head", "origin", "--delete"],
296
+ check=True,
297
+ capture_output=True,
298
+ text=True,
299
+ )
300
+ (work_dir / "src" / "app.py").write_text(
301
+ "def add(left: int, right: int) -> int:\n return left - right\n",
302
+ encoding="utf-8",
303
+ )
304
+ assert _exemption_for(work_dir) is False
305
+
306
+
307
+ MATCHING_MANIFEST_SHA256 = "a" * 64
308
+ OTHER_MANIFEST_SHA256 = "b" * 64
309
+ VERIFIER_AGENT_TYPE = "code-verifier"
310
+
311
+
312
+ def _verdict_transcript_text(is_all_pass: bool, bound_manifest_sha256: str) -> str:
313
+ verdict_record = {
314
+ "all_pass": is_all_pass,
315
+ "findings": [],
316
+ "manifest_sha256": bound_manifest_sha256,
317
+ }
318
+ assistant_text = (
319
+ "Verification complete.\n\n```verdict\n"
320
+ + json.dumps(verdict_record)
321
+ + "\n```\n"
322
+ )
323
+ assistant_entry = {
324
+ "type": "assistant",
325
+ "message": {"content": [{"type": "text", "text": assistant_text}]},
326
+ }
327
+ return json.dumps(assistant_entry) + "\n"
328
+
329
+
330
+ def _write_agent_transcript(
331
+ subagents_dir: pathlib.Path,
332
+ agent_id: str,
333
+ agent_type: str,
334
+ transcript_text: str,
335
+ should_write_sidecar: bool,
336
+ ) -> None:
337
+ workflow_dir = subagents_dir / "workflows" / "wf_x"
338
+ workflow_dir.mkdir(parents=True, exist_ok=True)
339
+ (workflow_dir / f"agent-{agent_id}.jsonl").write_text(
340
+ transcript_text, encoding="utf-8"
341
+ )
342
+ if should_write_sidecar:
343
+ (workflow_dir / f"agent-{agent_id}.meta.json").write_text(
344
+ json.dumps({"agentType": agent_type}), encoding="utf-8"
345
+ )
346
+
347
+
348
+ def _session_transcript_path(tmp_path: pathlib.Path, session_id: str) -> pathlib.Path:
349
+ session_root = tmp_path / "projects" / "demo"
350
+ session_root.mkdir(parents=True)
351
+ transcript_path = session_root / f"{session_id}.jsonl"
352
+ transcript_path.write_text("", encoding="utf-8")
353
+ return transcript_path
354
+
355
+
356
+ def test_workflow_verdict_covers_surface_true_for_matching_passing_verifier(
357
+ tmp_path: pathlib.Path,
358
+ ) -> None:
359
+ transcript_path = _session_transcript_path(tmp_path, "sess1")
360
+ subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
361
+ _write_agent_transcript(
362
+ subagents_dir,
363
+ "01",
364
+ VERIFIER_AGENT_TYPE,
365
+ _verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
366
+ should_write_sidecar=True,
367
+ )
368
+ assert (
369
+ workflow_verdict_covers_surface(
370
+ str(transcript_path), MATCHING_MANIFEST_SHA256
371
+ )
372
+ is True
373
+ )
374
+
375
+
376
+ def test_workflow_verdict_covers_surface_false_for_nonmatching_hash(
377
+ tmp_path: pathlib.Path,
378
+ ) -> None:
379
+ transcript_path = _session_transcript_path(tmp_path, "sess1")
380
+ subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
381
+ _write_agent_transcript(
382
+ subagents_dir,
383
+ "01",
384
+ VERIFIER_AGENT_TYPE,
385
+ _verdict_transcript_text(True, OTHER_MANIFEST_SHA256),
386
+ should_write_sidecar=True,
387
+ )
388
+ assert (
389
+ workflow_verdict_covers_surface(
390
+ str(transcript_path), MATCHING_MANIFEST_SHA256
391
+ )
392
+ is False
393
+ )
394
+
395
+
396
+ def test_workflow_verdict_covers_surface_false_for_all_pass_false(
397
+ tmp_path: pathlib.Path,
398
+ ) -> None:
399
+ transcript_path = _session_transcript_path(tmp_path, "sess1")
400
+ subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
401
+ _write_agent_transcript(
402
+ subagents_dir,
403
+ "01",
404
+ VERIFIER_AGENT_TYPE,
405
+ _verdict_transcript_text(False, MATCHING_MANIFEST_SHA256),
406
+ should_write_sidecar=True,
407
+ )
408
+ assert (
409
+ workflow_verdict_covers_surface(
410
+ str(transcript_path), MATCHING_MANIFEST_SHA256
411
+ )
412
+ is False
413
+ )
414
+
415
+
416
+ def test_workflow_verdict_covers_surface_false_for_non_verifier_sidecar(
417
+ tmp_path: pathlib.Path,
418
+ ) -> None:
419
+ transcript_path = _session_transcript_path(tmp_path, "sess1")
420
+ subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
421
+ _write_agent_transcript(
422
+ subagents_dir,
423
+ "01",
424
+ "clean-coder",
425
+ _verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
426
+ should_write_sidecar=True,
427
+ )
428
+ assert (
429
+ workflow_verdict_covers_surface(
430
+ str(transcript_path), MATCHING_MANIFEST_SHA256
431
+ )
432
+ is False
433
+ )
434
+
435
+
436
+ def test_workflow_verdict_covers_surface_false_for_missing_sidecar_with_verdict(
437
+ tmp_path: pathlib.Path,
438
+ ) -> None:
439
+ transcript_path = _session_transcript_path(tmp_path, "sess1")
440
+ subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
441
+ _write_agent_transcript(
442
+ subagents_dir,
443
+ "01",
444
+ VERIFIER_AGENT_TYPE,
445
+ _verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
446
+ should_write_sidecar=False,
447
+ )
448
+ assert (
449
+ workflow_verdict_covers_surface(
450
+ str(transcript_path), MATCHING_MANIFEST_SHA256
451
+ )
452
+ is False
453
+ )
454
+
455
+
456
+ def test_workflow_verdict_covers_surface_false_for_corrupt_sidecar_with_verdict(
457
+ tmp_path: pathlib.Path,
458
+ ) -> None:
459
+ transcript_path = _session_transcript_path(tmp_path, "sess1")
460
+ subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
461
+ _write_agent_transcript(
462
+ subagents_dir,
463
+ "01",
464
+ VERIFIER_AGENT_TYPE,
465
+ _verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
466
+ should_write_sidecar=False,
467
+ )
468
+ corrupt_sidecar = (
469
+ subagents_dir / "workflows" / "wf_x" / "agent-01.meta.json"
470
+ )
471
+ corrupt_sidecar.write_text("{not valid json", encoding="utf-8")
472
+ assert (
473
+ workflow_verdict_covers_surface(
474
+ str(transcript_path), MATCHING_MANIFEST_SHA256
475
+ )
476
+ is False
477
+ )
478
+
479
+
480
+ def test_workflow_verdict_covers_surface_false_for_invalid_utf8_sidecar(
481
+ tmp_path: pathlib.Path,
482
+ ) -> None:
483
+ transcript_path = _session_transcript_path(tmp_path, "sess1")
484
+ subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
485
+ _write_agent_transcript(
486
+ subagents_dir,
487
+ "01",
488
+ VERIFIER_AGENT_TYPE,
489
+ _verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
490
+ should_write_sidecar=False,
491
+ )
492
+ invalid_utf8_sidecar = subagents_dir / "workflows" / "wf_x" / "agent-01.meta.json"
493
+ invalid_utf8_sidecar.write_bytes(b'{"agentType": "\xff\xfe bad"}')
494
+ assert (
495
+ workflow_verdict_covers_surface(
496
+ str(transcript_path), MATCHING_MANIFEST_SHA256
497
+ )
498
+ is False
499
+ )
500
+
501
+
502
+ def test_workflow_verdict_covers_surface_false_for_invalid_utf8_transcript(
503
+ tmp_path: pathlib.Path,
504
+ ) -> None:
505
+ transcript_path = _session_transcript_path(tmp_path, "sess1")
506
+ subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
507
+ workflow_dir = subagents_dir / "workflows" / "wf_x"
508
+ workflow_dir.mkdir(parents=True, exist_ok=True)
509
+ (workflow_dir / "agent-01.jsonl").write_bytes(b'{"type": "assistant"}\xff\xfe\n')
510
+ (workflow_dir / "agent-01.meta.json").write_text(
511
+ json.dumps({"agentType": VERIFIER_AGENT_TYPE}), encoding="utf-8"
512
+ )
513
+ assert (
514
+ workflow_verdict_covers_surface(
515
+ str(transcript_path), MATCHING_MANIFEST_SHA256
516
+ )
517
+ is False
518
+ )
519
+
520
+
521
+ def test_workflow_verdict_covers_surface_false_for_missing_subagents_dir(
522
+ tmp_path: pathlib.Path,
523
+ ) -> None:
524
+ transcript_path = _session_transcript_path(tmp_path, "sess1")
525
+ assert (
526
+ workflow_verdict_covers_surface(
527
+ str(transcript_path), MATCHING_MANIFEST_SHA256
528
+ )
529
+ is False
530
+ )
531
+
532
+
533
+ def test_workflow_verdict_covers_surface_true_when_transcript_is_under_subagents(
534
+ tmp_path: pathlib.Path,
535
+ ) -> None:
536
+ subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
537
+ _write_agent_transcript(
538
+ subagents_dir,
539
+ "01",
540
+ VERIFIER_AGENT_TYPE,
541
+ _verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
542
+ should_write_sidecar=True,
543
+ )
544
+ caller_transcript_path = (
545
+ subagents_dir / "workflows" / "wf_x" / "agent-00.jsonl"
546
+ )
547
+ caller_transcript_path.write_text("", encoding="utf-8")
548
+ assert (
549
+ workflow_verdict_covers_surface(
550
+ str(caller_transcript_path), MATCHING_MANIFEST_SHA256
551
+ )
552
+ is True
553
+ )
554
+
555
+
556
+ def test_manifest_hash_cli_prints_live_surface_hash(tmp_path: pathlib.Path) -> None:
557
+ work_dir = _make_repo_with_origin(tmp_path)
558
+ (work_dir / "src" / "app.py").write_text(
559
+ "def add(left: int, right: int) -> int:\n return left - right\n",
560
+ encoding="utf-8",
561
+ )
562
+ merge_base_sha = resolve_merge_base(str(work_dir))
563
+ assert merge_base_sha is not None
564
+ surface_manifest_text = branch_surface_manifest(str(work_dir), merge_base_sha)
565
+ assert surface_manifest_text is not None
566
+ expected_hash = manifest_sha256(surface_manifest_text)
567
+ completed_process = subprocess.run(
568
+ [
569
+ sys.executable,
570
+ str(_HOOK_DIR / "verification_verdict_store.py"),
571
+ "--manifest-hash",
572
+ str(work_dir),
573
+ ],
574
+ check=True,
575
+ capture_output=True,
576
+ text=True,
577
+ )
578
+ assert completed_process.stdout.strip() == expected_hash
579
+
580
+
581
+ def _isolate_home(monkeypatch: pytest.MonkeyPatch, fake_home: pathlib.Path) -> None:
582
+ home_text = str(fake_home)
583
+ monkeypatch.setenv("HOME", home_text)
584
+ monkeypatch.setenv("USERPROFILE", home_text)
585
+ monkeypatch.delenv("HOMEDRIVE", raising=False)
586
+ monkeypatch.delenv("HOMEPATH", raising=False)
587
+
588
+
589
+ def test_minted_verdict_covers_surface_matches_other_worktree_by_hash(
590
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
591
+ ) -> None:
592
+ fake_home = tmp_path / "home"
593
+ fake_home.mkdir()
594
+ _isolate_home(monkeypatch, fake_home)
595
+ write_verdict(
596
+ str(tmp_path / "other" / "worktree"),
597
+ MATCHING_MANIFEST_SHA256,
598
+ True,
599
+ [],
600
+ "agent-x",
601
+ )
602
+ assert minted_verdict_covers_surface(MATCHING_MANIFEST_SHA256) is True
603
+
604
+
605
+ def test_minted_verdict_covers_surface_false_for_other_hash(
606
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
607
+ ) -> None:
608
+ fake_home = tmp_path / "home"
609
+ fake_home.mkdir()
610
+ _isolate_home(monkeypatch, fake_home)
611
+ write_verdict(
612
+ str(tmp_path / "other" / "worktree"),
613
+ OTHER_MANIFEST_SHA256,
614
+ True,
615
+ [],
616
+ "agent-x",
617
+ )
618
+ assert minted_verdict_covers_surface(MATCHING_MANIFEST_SHA256) is False
619
+
620
+
621
+ def test_minted_verdict_covers_surface_false_for_failing_verdict(
622
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
623
+ ) -> None:
624
+ fake_home = tmp_path / "home"
625
+ fake_home.mkdir()
626
+ _isolate_home(monkeypatch, fake_home)
627
+ write_verdict(
628
+ str(tmp_path / "other" / "worktree"),
629
+ MATCHING_MANIFEST_SHA256,
630
+ False,
631
+ [{"severity": "P0", "summary": "boom"}],
632
+ "agent-x",
633
+ )
634
+ assert minted_verdict_covers_surface(MATCHING_MANIFEST_SHA256) is False
635
+
636
+
637
+ def test_minted_verdict_covers_surface_false_when_directory_absent(
638
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
639
+ ) -> None:
640
+ fake_home = tmp_path / "home"
641
+ fake_home.mkdir()
642
+ _isolate_home(monkeypatch, fake_home)
643
+ assert minted_verdict_covers_surface(MATCHING_MANIFEST_SHA256) is False
644
+
645
+
646
+ def _make_repo_with_branch_worktree(
647
+ tmp_path: pathlib.Path, branch_name: str
648
+ ) -> tuple[pathlib.Path, pathlib.Path]:
649
+ """Create a repo with a branch checked out in a separate worktree.
650
+
651
+ Returns:
652
+ A tuple of (main worktree path, branch worktree path).
653
+ """
654
+ empty_hooks_dir = tmp_path / "nohooks"
655
+ empty_hooks_dir.mkdir()
656
+
657
+ main_dir = tmp_path / "main"
658
+ main_dir.mkdir()
659
+ _run_git(main_dir, "init", "--initial-branch=main")
660
+ _run_git(main_dir, "config", "user.email", "tests@example.com")
661
+ _run_git(main_dir, "config", "user.name", "Worktree Tests")
662
+ _run_git(main_dir, "config", "core.hooksPath", str(empty_hooks_dir))
663
+ (main_dir / "app.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
664
+ _run_git(main_dir, "add", "-A")
665
+ _run_git(main_dir, "commit", "-m", "base")
666
+
667
+ origin_dir = tmp_path / "origin.git"
668
+ subprocess.run(
669
+ ["git", "init", "--bare", "--initial-branch=main", str(origin_dir)],
670
+ check=True,
671
+ capture_output=True,
672
+ text=True,
673
+ )
674
+ _run_git(main_dir, "remote", "add", "origin", str(origin_dir))
675
+ _run_git(main_dir, "push", "-u", "origin", "main")
676
+
677
+ _run_git(main_dir, "branch", branch_name)
678
+
679
+ branch_worktree_dir = tmp_path / "branch-worktree"
680
+ _run_git(main_dir, "worktree", "add", str(branch_worktree_dir), branch_name)
681
+
682
+ return main_dir, branch_worktree_dir
683
+
684
+
685
+ def test_worktree_path_for_branch_returns_path_when_branch_present(
686
+ tmp_path: pathlib.Path,
687
+ ) -> None:
688
+ _main_dir, branch_worktree_dir = _make_repo_with_branch_worktree(
689
+ tmp_path, "feature-x"
690
+ )
691
+ resolved_path = worktree_path_for_branch(str(branch_worktree_dir), "feature-x")
692
+ assert resolved_path is not None
693
+ assert pathlib.Path(resolved_path).resolve() == branch_worktree_dir.resolve()
694
+
695
+
696
+ def test_worktree_path_for_branch_returns_none_when_branch_absent(
697
+ tmp_path: pathlib.Path,
698
+ ) -> None:
699
+ main_dir, _branch_worktree_dir = _make_repo_with_branch_worktree(
700
+ tmp_path, "feature-x"
701
+ )
702
+ resolved_path = worktree_path_for_branch(str(main_dir), "branch-never-checked-out")
703
+ assert resolved_path is None
704
+
705
+
706
+ def test_empty_surface_hash_equals_hash_of_empty_string() -> None:
707
+ assert empty_surface_hash() == manifest_sha256("")
708
+
709
+
710
+ def test_manifest_hash_cli_empty_surface_writes_guard_message_to_stderr(
711
+ tmp_path: pathlib.Path,
712
+ ) -> None:
713
+ work_dir = _make_repo_with_origin(tmp_path)
714
+ completed_process = subprocess.run(
715
+ [
716
+ sys.executable,
717
+ str(_HOOK_DIR / "verification_verdict_store.py"),
718
+ "--manifest-hash",
719
+ str(work_dir),
720
+ ],
721
+ capture_output=True,
722
+ text=True,
723
+ )
724
+ assert completed_process.returncode != 0
725
+ assert completed_process.stdout.strip() == ""
726
+ lowered_stderr = completed_process.stderr.lower()
727
+ assert "wrong work tree" in lowered_stderr or "empty" in lowered_stderr
728
+
729
+
730
+ def test_manifest_hash_cli_empty_surface_prints_nothing_on_stdout(
731
+ tmp_path: pathlib.Path,
732
+ ) -> None:
733
+ work_dir = _make_repo_with_origin(tmp_path)
734
+ completed_process = subprocess.run(
735
+ [
736
+ sys.executable,
737
+ str(_HOOK_DIR / "verification_verdict_store.py"),
738
+ "--manifest-hash",
739
+ str(work_dir),
740
+ ],
741
+ capture_output=True,
742
+ text=True,
743
+ )
744
+ assert completed_process.stdout.strip() == ""
745
+
746
+
747
+ def test_manifest_hash_for_branch_cli_prints_same_hash_as_explicit_dir(
748
+ tmp_path: pathlib.Path,
749
+ ) -> None:
750
+ _main_dir, branch_worktree_dir = _make_repo_with_branch_worktree(
751
+ tmp_path, "feature-branch"
752
+ )
753
+ (branch_worktree_dir / "app.py").write_text(
754
+ "def add(left: int, right: int) -> int:\n return left - right\n",
755
+ encoding="utf-8",
756
+ )
757
+ direct_process = subprocess.run(
758
+ [
759
+ sys.executable,
760
+ str(_HOOK_DIR / "verification_verdict_store.py"),
761
+ "--manifest-hash",
762
+ str(branch_worktree_dir),
763
+ ],
764
+ capture_output=True,
765
+ text=True,
766
+ check=True,
767
+ )
768
+ branch_process = subprocess.run(
769
+ [
770
+ sys.executable,
771
+ str(_HOOK_DIR / "verification_verdict_store.py"),
772
+ "--manifest-hash-for-branch",
773
+ "feature-branch",
774
+ ],
775
+ capture_output=True,
776
+ text=True,
777
+ check=True,
778
+ cwd=str(branch_worktree_dir),
779
+ )
780
+ assert direct_process.stdout.strip() == branch_process.stdout.strip()
781
+ assert direct_process.stdout.strip() != ""
782
+
783
+
784
+ def test_manifest_hash_for_branch_cli_returns_nonzero_when_branch_absent(
785
+ tmp_path: pathlib.Path,
786
+ ) -> None:
787
+ main_dir, _branch_worktree_dir = _make_repo_with_branch_worktree(
788
+ tmp_path, "feature-branch"
789
+ )
790
+ completed_process = subprocess.run(
791
+ [
792
+ sys.executable,
793
+ str(_HOOK_DIR / "verification_verdict_store.py"),
794
+ "--manifest-hash-for-branch",
795
+ "branch-never-checked-out",
796
+ ],
797
+ capture_output=True,
798
+ text=True,
799
+ cwd=str(main_dir),
800
+ )
801
+ assert completed_process.returncode != 0
802
+ assert completed_process.stdout.strip() == ""
803
+
804
+
805
+ def test_store_imports_under_foreign_config_shadow(
806
+ run_under_config_shadow: Callable[[str], subprocess.CompletedProcess[str]],
807
+ ) -> None:
808
+ completed_probe = run_under_config_shadow(
809
+ "import verification_verdict_store\nprint('IMPORT_OK')\n"
810
+ )
811
+ assert completed_probe.returncode == 0, completed_probe.stderr
812
+ assert "IMPORT_OK" in completed_probe.stdout
813
+
814
+
815
+ def _ledger_path(fake_home: pathlib.Path) -> pathlib.Path:
816
+ return (
817
+ fake_home
818
+ / constants_module.CLAUDE_HOME_DIRECTORY_NAME
819
+ / constants_module.LOGS_DIRECTORY_NAME
820
+ / constants_module.VERIFIER_VERDICTS_JSONL_FILENAME
821
+ )
822
+
823
+
824
+ def _read_ledger_rows(fake_home: pathlib.Path) -> list[dict]:
825
+ ledger_file = _ledger_path(fake_home)
826
+ all_rows: list[dict] = []
827
+ for each_line in ledger_file.read_text(encoding="utf-8").splitlines():
828
+ if each_line.strip():
829
+ all_rows.append(json.loads(each_line))
830
+ return all_rows
831
+
832
+
833
+ def test_write_verdict_appends_failing_row_that_survives_later_clean_mint(
834
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
835
+ ) -> None:
836
+ """A failing mint's check names stay readable after a later clean mint."""
837
+ fake_home = tmp_path / "home"
838
+ fake_home.mkdir()
839
+ _isolate_home(monkeypatch, fake_home)
840
+ monkeypatch.setattr(pathlib.Path, "home", classmethod(lambda cls: fake_home))
841
+ work_dir = _make_repo_with_origin(tmp_path)
842
+ failing_findings = [
843
+ {"check": "gates.import", "detail": "ImportError: boom"},
844
+ {"check": "task.item-2", "detail": "missing hunk"},
845
+ ]
846
+ write_verdict(
847
+ str(work_dir),
848
+ MATCHING_MANIFEST_SHA256,
849
+ False,
850
+ failing_findings,
851
+ "agent-fail",
852
+ )
853
+ write_verdict(
854
+ str(work_dir),
855
+ MATCHING_MANIFEST_SHA256,
856
+ True,
857
+ [],
858
+ "agent-pass",
859
+ )
860
+ all_rows = _read_ledger_rows(fake_home)
861
+ assert len(all_rows) == 2
862
+ failing_row = all_rows[0]
863
+ clean_row = all_rows[1]
864
+ assert failing_row[constants_module.LEDGER_KEY_ALL_PASS] is False
865
+ assert failing_row[constants_module.LEDGER_KEY_FINDING_COUNT] == 2
866
+ assert failing_row[constants_module.LEDGER_KEY_FINDING_CHECK_NAMES] == [
867
+ "gates.import",
868
+ "task.item-2",
869
+ ]
870
+ assert failing_row[constants_module.LEDGER_KEY_MINTED_FROM_AGENT_ID] == "agent-fail"
871
+ assert failing_row[constants_module.LEDGER_KEY_MANIFEST_HASH] == MATCHING_MANIFEST_SHA256
872
+ assert failing_row[constants_module.LEDGER_KEY_REPO_ROOT] == str(work_dir)
873
+ assert failing_row[constants_module.LEDGER_KEY_BRANCH] == "main"
874
+ assert clean_row[constants_module.LEDGER_KEY_ALL_PASS] is True
875
+ assert clean_row[constants_module.LEDGER_KEY_FINDING_COUNT] == 0
876
+ assert clean_row[constants_module.LEDGER_KEY_FINDING_CHECK_NAMES] == []
877
+ assert clean_row[constants_module.LEDGER_KEY_MINTED_FROM_AGENT_ID] == "agent-pass"
878
+
879
+
880
+ def test_write_verdict_returns_path_when_ledger_append_fails(
881
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
882
+ ) -> None:
883
+ """A ledger IO failure never changes the mint path or verdict file write."""
884
+ fake_home = tmp_path / "home"
885
+ fake_home.mkdir()
886
+ _isolate_home(monkeypatch, fake_home)
887
+ monkeypatch.setattr(pathlib.Path, "home", classmethod(lambda cls: fake_home))
888
+ logs_as_file = (
889
+ fake_home
890
+ / constants_module.CLAUDE_HOME_DIRECTORY_NAME
891
+ / constants_module.LOGS_DIRECTORY_NAME
892
+ )
893
+ logs_as_file.parent.mkdir(parents=True, exist_ok=True)
894
+ logs_as_file.write_text("not-a-directory", encoding="utf-8")
895
+ work_dir = _make_repo_with_origin(tmp_path)
896
+ verdict_file = write_verdict(
897
+ str(work_dir),
898
+ MATCHING_MANIFEST_SHA256,
899
+ True,
900
+ [],
901
+ "agent-x",
902
+ )
903
+ assert verdict_file.is_file()
904
+ verdict_record = json.loads(verdict_file.read_text(encoding="utf-8"))
905
+ assert verdict_record[constants_module.VERDICT_KEY_ALL_PASS] is True
906
+
907
+
908
+ def test_root_key_for_repo_folds_subdirectory_and_case(tmp_path: pathlib.Path) -> None:
909
+ work_dir = tmp_path / "Repo"
910
+ subdirectory = work_dir / "src"
911
+ subdirectory.mkdir(parents=True)
912
+ root_key = root_key_for_repo(str(work_dir))
913
+ assert root_key_for_repo(str(subdirectory.parent)) == root_key
914
+ assert root_key_for_repo(str(work_dir).lower()) == root_key
915
+
916
+
917
+ def test_verdict_path_for_repo_stem_is_the_shared_root_key(tmp_path: pathlib.Path) -> None:
918
+ work_dir = tmp_path / "repo"
919
+ work_dir.mkdir()
920
+ assert verdict_path_for_repo(str(work_dir)).stem == root_key_for_repo(str(work_dir))