claude-dev-env 2.2.1 → 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.
- package/CLAUDE.md +3 -2
- package/_shared/advisor/advisor-protocol.md +2 -2
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -284
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +3 -3
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -0
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +5 -0
- package/_shared/pr-loop/scripts/stale_worktree_rule_sweep.py +107 -0
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +2 -0
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +55 -73
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +9 -15
- package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +91 -1
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +88 -1
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -0
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -0
- package/_shared/pr-loop/worker-spawn.md +1 -1
- package/agents/CLAUDE.md +1 -0
- package/agents/code-verifier.md +4 -4
- package/agents/skill-writer-agent.md +84 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +141 -41
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +29 -13
- package/docs/CLAUDE.md +1 -0
- package/docs/references/CLAUDE.md +1 -0
- package/docs/references/code-review-enforcement.md +97 -0
- package/docs/wsl-docker-cowork-starter-matrix.md +89 -0
- package/hooks/blocking/CLAUDE.md +8 -1
- package/hooks/blocking/code_review_enforcement_config_bootstrap.py +53 -0
- package/hooks/blocking/code_review_gate_deny.py +74 -0
- package/hooks/blocking/code_review_pr_create_gate.py +194 -0
- package/hooks/blocking/code_review_push_gate.py +140 -0
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +340 -0
- package/hooks/blocking/code_review_stamp_store.py +233 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +7 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +15 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +212 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +138 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +49 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +38 -0
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +39 -27
- package/hooks/blocking/config/code_review_enforcement_constants.py +110 -0
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +108 -0
- package/hooks/blocking/config/verified_commit_constants.py +24 -9
- package/hooks/blocking/conftest.py +2 -0
- package/hooks/blocking/convergence_gate_blocker.py +112 -23
- package/hooks/blocking/destructive_command_blocker.py +19 -6
- package/hooks/blocking/pr_description_proof_of_work.py +52 -34
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
- package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +62 -0
- package/hooks/blocking/test_code_review_gate_deny.py +54 -0
- package/hooks/blocking/test_code_review_pr_create_gate.py +185 -0
- package/hooks/blocking/test_code_review_push_gate.py +189 -0
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +180 -0
- package/hooks/blocking/test_code_review_stamp_store.py +205 -0
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +124 -2
- package/hooks/blocking/test_code_verifier_tools_contract.py +28 -0
- package/hooks/blocking/test_convergence_gate_blocker.py +153 -5
- package/hooks/blocking/test_destructive_command_blocker.py +1 -1
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +45 -0
- package/hooks/blocking/test_pr_description_proof_of_work.py +151 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +8 -8
- package/hooks/blocking/test_verification_verdict_store.py +920 -810
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +114 -2
- package/hooks/blocking/verification_verdict_store.py +118 -5
- package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +29 -17
- package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +35 -0
- package/hooks/blocking/volatile_path_in_post_blocker.py +69 -8
- package/hooks/git-hooks/git_hooks_constants/__init__.py +6 -0
- package/hooks/git-hooks/pre_push.py +89 -2
- package/hooks/git-hooks/test_pre_push.py +103 -0
- package/hooks/hooks.json +16 -1
- package/hooks/hooks_constants/CLAUDE.md +1 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +26 -11
- package/hooks/hooks_constants/convergence_gate_blocker_constants.py +20 -3
- package/hooks/hooks_constants/destructive_command_segment_constants.py +3 -1
- package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/pyproject_config_discovery_constants.py +16 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +24 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -0
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +8 -1
- package/hooks/validators/CLAUDE.md +1 -0
- package/hooks/validators/mypy_integration.py +63 -50
- package/hooks/validators/pyproject_config_discovery.py +101 -0
- package/hooks/validators/ruff_integration.py +211 -23
- package/hooks/validators/run_all_validators.py +21 -14
- package/hooks/validators/test_mypy_integration.py +32 -0
- package/hooks/validators/test_pyproject_config_discovery.py +94 -0
- package/hooks/validators/test_ruff_integration.py +68 -1
- package/hooks/validators/test_run_all_validators.py +25 -0
- package/hooks/validators/test_run_all_validators_config_discovery.py +123 -0
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +20 -43
- package/rules/durable-post-artifacts.md +7 -0
- package/scripts/CLAUDE.md +2 -1
- package/scripts/claude-chain.example.json +15 -3
- package/scripts/claude_chain_runner.py +130 -27
- package/scripts/claude_chain_usage.py +346 -0
- package/scripts/codec_forwarding_test_support.py +83 -0
- package/scripts/conftest.py +8 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -3
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +57 -2
- package/scripts/dev_env_scripts_constants/claude_chain_usage_constants.py +58 -0
- package/scripts/dev_env_scripts_constants/code_review_constants.py +129 -12
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +55 -0
- package/scripts/dev_env_scripts_constants/timing.py +1 -1
- package/scripts/invoke_code_review.py +550 -38
- package/scripts/resolve_worker_spawn.py +8 -1
- package/scripts/test_claude_chain_runner.py +412 -6
- package/scripts/test_claude_chain_usage.py +534 -0
- package/scripts/test_invoke_code_review.py +298 -4
- package/scripts/test_invoke_code_review_codec.py +77 -0
- package/scripts/test_resolve_worker_spawn_codec.py +101 -0
- package/skills/autoconverge/SKILL.md +9 -3
- package/skills/autoconverge/reference/convergence.md +2 -1
- package/skills/autoconverge/reference/multi-pr.md +6 -1
- package/skills/autoconverge/reference/stop-conditions.md +16 -10
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +175 -3
- package/skills/autoconverge/workflow/converge.contract.test.mjs +19 -0
- package/skills/autoconverge/workflow/converge.mjs +24 -8
- package/skills/autoconverge/workflow/converge_multi.mjs +7 -3
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +5 -0
- package/skills/fresh-branch/CLAUDE.md +2 -0
- package/skills/fresh-branch/SKILL.md +2 -0
- package/skills/fresh-branch/scripts/create_fresh_branch.py +78 -180
- package/skills/fresh-branch/scripts/fresh_branch_git_commands.py +285 -0
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -0
- package/skills/fresh-branch/scripts/pytest.ini +4 -0
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +98 -0
- package/skills/fresh-branch/scripts/test_fresh_branch_git_commands.py +310 -0
- package/skills/orchestrator/SKILL.md +1 -20
- package/skills/orchestrator-refresh/SKILL.md +1 -1
- package/skills/pr-converge/SKILL.md +3 -3
- package/skills/pr-converge/reference/examples.md +3 -3
- package/skills/pr-converge/reference/fix-protocol.md +1 -1
- package/skills/pr-converge/reference/ground-rules.md +3 -3
- package/skills/pr-converge/reference/per-tick.md +5 -5
- package/skills/pr-converge/reference/progress-checklist.md +1 -1
- package/skills/pr-converge/test_step5_host_branch.py +1 -1
- package/skills/prototype/SKILL.md +86 -0
- package/skills/prototype/reference/honest-limitations.md +23 -0
- package/skills/prototype/reference/promotion-tasks.md +23 -0
- package/skills/prototype/scripts/build_sandbox_settings.py +249 -0
- package/skills/prototype/scripts/conftest.py +15 -0
- package/skills/prototype/scripts/launch_sandbox.py +205 -0
- package/skills/prototype/scripts/probe_sandbox_safety.py +311 -0
- package/skills/prototype/scripts/prototype_scripts_constants/__init__.py +1 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +41 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +23 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +45 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +10 -0
- package/skills/prototype/scripts/test_build_sandbox_settings.py +275 -0
- package/skills/prototype/scripts/test_launch_sandbox.py +303 -0
- package/skills/prototype/scripts/test_probe_sandbox_safety.py +284 -0
- package/skills/prototype/workflows/promotion.md +27 -0
- package/skills/prototype/workflows/sandbox.md +35 -0
- package/skills/skill-builder/CLAUDE.md +3 -3
- package/skills/skill-builder/SKILL.md +5 -5
- package/skills/skill-builder/references/CLAUDE.md +1 -1
- package/skills/skill-builder/references/delegation-map.md +3 -3
- package/skills/skill-builder/references/description-field.md +1 -1
- package/skills/skill-builder/references/skill-modularity.md +2 -3
- package/skills/skill-builder/workflows/CLAUDE.md +1 -1
- package/skills/skill-builder/workflows/improve-skill.md +1 -1
- package/skills/skill-builder/workflows/new-skill.md +2 -2
- package/skills/team-advisor/SKILL.md +5 -4
|
@@ -1,810 +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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
constants_spec.loader
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
_run_git(work_dir, "
|
|
81
|
-
_run_git(work_dir, "config", "
|
|
82
|
-
(work_dir
|
|
83
|
-
(work_dir
|
|
84
|
-
(work_dir / "src"
|
|
85
|
-
(work_dir / "tests"
|
|
86
|
-
(work_dir / "
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
_run_git(work_dir, "
|
|
90
|
-
_run_git(work_dir, "
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
assert "comment
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
assert
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
assert
|
|
258
|
-
|
|
259
|
-
assert
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
"
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
)
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
(
|
|
509
|
-
|
|
510
|
-
)
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
assert
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
str(
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
monkeypatch.
|
|
584
|
-
monkeypatch.
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
_run_git(main_dir, "
|
|
660
|
-
_run_git(main_dir, "config", "
|
|
661
|
-
(main_dir
|
|
662
|
-
_run_git(main_dir, "
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
_run_git(main_dir, "
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
str(
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
assert
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
str(
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
str(
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
"
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
"
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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))
|