claude-dev-env 2.12.0 → 2.13.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/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
- package/_shared/pr-loop/scripts/test_code_rules_gate.py +0 -26
- package/agents/CLAUDE.md +1 -2
- package/agents/pr-description-writer.md +11 -81
- package/bin/install.mjs +48 -9
- package/bin/install.prune.test.mjs +26 -0
- package/bin/install.settings-defaults.test.mjs +60 -33
- package/bin/install.test.mjs +2 -4
- package/commands/sr-loop.md +9 -5
- package/docs/CLAUDE.md +0 -1
- package/docs/references/CLAUDE.md +0 -1
- package/hooks/CLAUDE.md +1 -1
- package/hooks/blocking/CLAUDE.md +2 -27
- package/hooks/blocking/config/CLAUDE.md +1 -13
- package/hooks/blocking/conventional_pr_title_gate.py +1 -2
- package/hooks/blocking/send_user_file_open_locally_blocker.py +1 -1
- package/hooks/blocking/stop_dispatcher.py +4 -4
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -3
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +6 -7
- package/hooks/blocking/test_send_user_file_open_locally_blocker.py +4 -3
- package/hooks/blocking/test_shared_stdin_adoption.py +0 -2
- package/hooks/diagnostic/CLAUDE.md +3 -3
- package/hooks/diagnostic/hook_log_extractor.py +2 -36
- package/hooks/diagnostic/hook_log_stop_wrapper.py +6 -155
- package/hooks/diagnostic/test_hook_log_extractor.py +8 -21
- package/hooks/diagnostic/test_hook_log_stop_wrapper.py +3 -331
- package/hooks/git-hooks/git_hooks_constants/__init__.py +0 -6
- package/hooks/git-hooks/pre_push.py +3 -85
- package/hooks/git-hooks/test_pre_push.py +0 -130
- package/hooks/hooks.json +0 -35
- package/hooks/hooks_constants/CLAUDE.md +0 -9
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -11
- package/hooks/hooks_constants/convergence_branch_constants.py +0 -1
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +3 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -8
- package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +1 -6
- package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -1
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -26
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +0 -5
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -1
- package/hooks/lifecycle/CLAUDE.md +1 -3
- package/package.json +1 -1
- package/rules/CLAUDE.md +0 -2
- package/rules/gh-cli-conventions.md +0 -1
- package/rules/git-workflow.md +1 -3
- package/scripts/CLAUDE.md +0 -1
- package/scripts/_code_review_test_support.py +0 -95
- package/scripts/dev_env_scripts_constants/code_review_constants.py +9 -98
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +1 -44
- package/scripts/invoke_code_review.py +8 -451
- package/scripts/test_invoke_code_review.py +5 -61
- package/scripts/test_invoke_code_review_cli.py +1 -45
- package/scripts/test_invoke_code_review_contract.py +1 -118
- package/settings.json +1 -10
- package/skills/CLAUDE.md +8 -0
- package/skills/autoconverge/reference/CLAUDE.md +1 -1
- package/skills/autoconverge/reference/convergence.md +7 -7
- package/skills/autoconverge/reference/gotchas.md +3 -10
- package/skills/autoconverge/workflow/converge.contract.test.mjs +53 -1388
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +4 -3
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +33 -493
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +15 -97
- package/skills/autoconverge/workflow/converge.mjs +201 -238
- package/skills/autoconverge/workflow/converge.precatch.test.mjs +1 -1
- package/skills/bugteam/CONSTRAINTS.md +1 -1
- package/skills/bugteam/SKILL.md +9 -0
- package/skills/bugteam/reference/audit-and-teammates.md +10 -6
- package/skills/bugteam/test_skill_additions.py +9 -0
- package/skills/comments/SKILL.md +65 -0
- package/skills/descriptions/SKILL.md +51 -0
- package/skills/emergencies/SKILL.md +42 -0
- package/skills/imagegen/SKILL.md +21 -0
- package/skills/imagegen/scripts/config/__init__.py +1 -0
- package/skills/imagegen/scripts/config/constants.py +28 -0
- package/skills/imagegen/scripts/imagegen.py +45 -0
- package/skills/imagegen/scripts/imagegen_core.py +528 -0
- package/skills/imagegen/scripts/test_imagegen.py +204 -0
- package/skills/orchestrator/SKILL.md +2 -4
- package/skills/plan-to-pr/SKILL.md +6 -7
- package/skills/plan-to-pr/reference/final-validation-tasks.md +1 -1
- package/skills/plan-to-pr/reference/process-inventory.md +2 -2
- package/skills/plan-to-pr/reference/review-loop.md +2 -2
- package/skills/plan-to-pr/reference/run-record.schema.json +1 -1
- package/skills/plan-to-pr/reference/task-seeds.md +1 -1
- package/skills/plan-to-pr/reference/task-ticket.md +3 -4
- package/skills/plan-to-pr/scripts/config/constants.py +1 -2
- package/skills/plan-to-pr/scripts/test_validate_protocol.py +1 -1
- package/skills/plan-to-pr/test_skill_contract.py +2 -2
- package/skills/pr-converge/SKILL.md +13 -16
- package/skills/pr-converge/reference/progress-checklist.md +6 -5
- package/skills/pr-converge/reference/state-schema.md +1 -27
- package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
- package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +3 -3
- package/skills/pr-loop-lifecycle/SKILL.md +1 -1
- package/skills/pr-loop-lifecycle/reference/teardown-publish-permissions.md +1 -1
- package/skills/prototype/SKILL.md +4 -4
- package/skills/prototype/reference/honest-limitations.md +4 -4
- package/skills/prototype/reference/promotion-tasks.md +2 -2
- package/skills/prototype/workflows/promotion.md +4 -6
- package/skills/reviews/SKILL.md +85 -0
- package/skills/small-cl/SKILL.md +52 -0
- package/agents/code-verifier.md +0 -74
- package/docs/PR_DESCRIPTION_GUIDE.md +0 -157
- package/docs/references/code-review-enforcement.md +0 -132
- package/hooks/blocking/code_review_enforcement_config_bootstrap.py +0 -53
- package/hooks/blocking/code_review_gate_deny.py +0 -74
- package/hooks/blocking/code_review_pr_create_gate.py +0 -198
- package/hooks/blocking/code_review_push_gate.py +0 -145
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +0 -348
- package/hooks/blocking/code_review_stamp_store.py +0 -233
- package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +0 -7
- package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +0 -15
- package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +0 -212
- package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +0 -138
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +0 -49
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +0 -38
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +0 -483
- package/hooks/blocking/config/code_review_enforcement_constants.py +0 -143
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +0 -166
- package/hooks/blocking/config/verified_commit_constants.py +0 -160
- package/hooks/blocking/config/verified_commit_context_constants.py +0 -21
- package/hooks/blocking/config/verified_commit_gate_output_constants.py +0 -14
- package/hooks/blocking/conftest.py +0 -123
- package/hooks/blocking/convergence_gate_blocker.py +0 -310
- package/hooks/blocking/pr_converge_bugteam_enforcer.py +0 -170
- package/hooks/blocking/pr_description_body_audit.py +0 -148
- package/hooks/blocking/pr_description_command_parser.py +0 -237
- package/hooks/blocking/pr_description_enforcer.py +0 -227
- package/hooks/blocking/pr_description_pr_number.py +0 -155
- package/hooks/blocking/pr_description_proof_of_work.py +0 -385
- package/hooks/blocking/pr_description_readability.py +0 -364
- package/hooks/blocking/reviewer_spawn_gate.py +0 -182
- package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +0 -62
- package/hooks/blocking/test_code_review_gate_deny.py +0 -54
- package/hooks/blocking/test_code_review_pr_create_gate.py +0 -199
- package/hooks/blocking/test_code_review_push_gate.py +0 -205
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +0 -199
- package/hooks/blocking/test_code_review_stamp_store.py +0 -205
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +0 -710
- package/hooks/blocking/test_code_verifier_tools_contract.py +0 -28
- package/hooks/blocking/test_convergence_gate_blocker.py +0 -428
- package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +0 -311
- package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +0 -184
- package/hooks/blocking/test_pr_description_enforcer.py +0 -221
- package/hooks/blocking/test_pr_description_enforcer_body_audit.py +0 -247
- package/hooks/blocking/test_pr_description_enforcer_body_rules.py +0 -493
- package/hooks/blocking/test_pr_description_enforcer_command_parser.py +0 -366
- package/hooks/blocking/test_pr_description_enforcer_pr_number.py +0 -159
- package/hooks/blocking/test_pr_description_enforcer_proof_gate.py +0 -175
- package/hooks/blocking/test_pr_description_enforcer_readability.py +0 -443
- package/hooks/blocking/test_pr_description_proof_of_work.py +0 -313
- package/hooks/blocking/test_reviewer_spawn_gate.py +0 -230
- package/hooks/blocking/test_verdict_directory_write_blocker.py +0 -804
- package/hooks/blocking/test_verification_verdict_store.py +0 -974
- package/hooks/blocking/test_verified_commit_config_bootstrap.py +0 -67
- package/hooks/blocking/test_verified_commit_docs_delta.py +0 -176
- package/hooks/blocking/test_verified_commit_gate.py +0 -581
- package/hooks/blocking/test_verified_commit_gate_additional_context.py +0 -134
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +0 -131
- package/hooks/blocking/test_verifier_verdict_minter.py +0 -299
- package/hooks/blocking/tests/test_verified_commit_gate.py +0 -41
- package/hooks/blocking/verdict_directory_write_blocker.py +0 -687
- package/hooks/blocking/verification_verdict_store.py +0 -1039
- package/hooks/blocking/verified_commit_config_bootstrap.py +0 -63
- package/hooks/blocking/verified_commit_gate.py +0 -113
- package/hooks/blocking/verified_commit_gate_parts/CLAUDE.md +0 -28
- package/hooks/blocking/verified_commit_gate_parts/__init__.py +0 -1
- package/hooks/blocking/verified_commit_gate_parts/command_tokenization.py +0 -174
- package/hooks/blocking/verified_commit_gate_parts/deny_payload.py +0 -53
- package/hooks/blocking/verified_commit_gate_parts/deny_reason.py +0 -80
- package/hooks/blocking/verified_commit_gate_parts/directory_resolution.py +0 -170
- package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +0 -217
- package/hooks/blocking/verified_commit_gate_parts/tests/conftest.py +0 -10
- package/hooks/blocking/verified_commit_gate_parts/tests/test_command_tokenization.py +0 -94
- package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_payload.py +0 -17
- package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_reason.py +0 -38
- package/hooks/blocking/verified_commit_gate_parts/tests/test_directory_resolution.py +0 -71
- package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +0 -96
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +0 -167
- package/hooks/blocking/verifier_verdict_minter.py +0 -280
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +0 -64
- package/hooks/hooks_constants/convergence_gate_blocker_constants.py +0 -53
- package/hooks/hooks_constants/pr_converge_bugteam_enforcer_constants.py +0 -55
- package/hooks/hooks_constants/pr_converge_bugteam_enforcer_state.py +0 -67
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +0 -167
- package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -107
- package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +0 -41
- package/hooks/hooks_constants/test_pr_description_enforcer_constants.py +0 -292
- package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +0 -198
- package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +0 -283
- package/rules/proof-of-work-pr-comments.md +0 -22
- package/rules/verified-commit-gate-skip.md +0 -4
- package/scripts/Show-Asset.ps1 +0 -106
|
@@ -1,710 +0,0 @@
|
|
|
1
|
-
"""Behavior tests for the code_verifier_spawn_preflight_gate PreToolUse hook.
|
|
2
|
-
|
|
3
|
-
Each test builds a real git repository in a temporary directory, writes real
|
|
4
|
-
files (real merge conflicts, real CODE_RULES violations), and runs the hook
|
|
5
|
-
script as a subprocess with a real Agent PreToolUse JSON payload on stdin —
|
|
6
|
-
the exact production invocation path. No mocks. The harness mirrors
|
|
7
|
-
test_precommit_code_rules_gate.py lines 1-70.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
import json
|
|
11
|
-
import shutil
|
|
12
|
-
import subprocess
|
|
13
|
-
import sys
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
|
|
16
|
-
HOOK_PATH = Path(__file__).resolve().parent / "code_verifier_spawn_preflight_gate.py"
|
|
17
|
-
|
|
18
|
-
CLEAN_MODULE_SOURCE = '''"""Increment helper used by the preflight gate tests."""
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def add_one(number: int) -> int:
|
|
22
|
-
"""Return *number* plus one.
|
|
23
|
-
|
|
24
|
-
Args:
|
|
25
|
-
number: The integer to increment.
|
|
26
|
-
|
|
27
|
-
Returns:
|
|
28
|
-
The incremented integer.
|
|
29
|
-
"""
|
|
30
|
-
return number + 1
|
|
31
|
-
'''
|
|
32
|
-
|
|
33
|
-
CLEAN_MODULE_SOURCE_EDITED = '''"""Increment helper used by the preflight gate tests."""
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def add_one(number: int) -> int:
|
|
37
|
-
"""Return *number* plus two.
|
|
38
|
-
|
|
39
|
-
Args:
|
|
40
|
-
number: The integer to increment.
|
|
41
|
-
|
|
42
|
-
Returns:
|
|
43
|
-
The incremented integer.
|
|
44
|
-
"""
|
|
45
|
-
return number + 2
|
|
46
|
-
'''
|
|
47
|
-
|
|
48
|
-
VIOLATING_MODULE_SOURCE = '''"""Module carrying a banned identifier for the preflight gate tests."""
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def compute_total() -> int:
|
|
52
|
-
"""Return a fixed total.
|
|
53
|
-
|
|
54
|
-
Returns:
|
|
55
|
-
The fixed total.
|
|
56
|
-
"""
|
|
57
|
-
result = 1
|
|
58
|
-
return result
|
|
59
|
-
'''
|
|
60
|
-
|
|
61
|
-
CLEAN_TWO_FUNCTION_SOURCE = '''"""Two-function helper used by the preflight gate tests."""
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def add_one(number: int) -> int:
|
|
65
|
-
"""Return *number* plus one.
|
|
66
|
-
|
|
67
|
-
Args:
|
|
68
|
-
number: The integer to increment.
|
|
69
|
-
|
|
70
|
-
Returns:
|
|
71
|
-
The incremented integer.
|
|
72
|
-
"""
|
|
73
|
-
return number + 1
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def add_two(number: int) -> int:
|
|
77
|
-
"""Return *number* plus two.
|
|
78
|
-
|
|
79
|
-
Args:
|
|
80
|
-
number: The integer to increment.
|
|
81
|
-
|
|
82
|
-
Returns:
|
|
83
|
-
The integer plus two.
|
|
84
|
-
"""
|
|
85
|
-
return number + 2
|
|
86
|
-
'''
|
|
87
|
-
|
|
88
|
-
PREEXISTING_VIOLATION_BASE_SOURCE = '''"""Module that already carries a banned identifier at the base commit."""
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def compute_total() -> int:
|
|
92
|
-
"""Return a fixed total.
|
|
93
|
-
|
|
94
|
-
Returns:
|
|
95
|
-
The fixed total.
|
|
96
|
-
"""
|
|
97
|
-
result = 1
|
|
98
|
-
return result
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def add_one(number: int) -> int:
|
|
102
|
-
"""Return *number* plus one.
|
|
103
|
-
|
|
104
|
-
Args:
|
|
105
|
-
number: The integer to increment.
|
|
106
|
-
|
|
107
|
-
Returns:
|
|
108
|
-
The incremented integer.
|
|
109
|
-
"""
|
|
110
|
-
return number + 1
|
|
111
|
-
'''
|
|
112
|
-
|
|
113
|
-
PREEXISTING_VIOLATION_EDITED_SOURCE = '''"""Module that already carries a banned identifier at the base commit."""
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def compute_total() -> int:
|
|
117
|
-
"""Return a fixed total.
|
|
118
|
-
|
|
119
|
-
Returns:
|
|
120
|
-
The fixed total.
|
|
121
|
-
"""
|
|
122
|
-
result = 1
|
|
123
|
-
return result
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def add_one(number: int) -> int:
|
|
127
|
-
"""Return *number* plus two.
|
|
128
|
-
|
|
129
|
-
Args:
|
|
130
|
-
number: The integer to increment.
|
|
131
|
-
|
|
132
|
-
Returns:
|
|
133
|
-
The integer plus two.
|
|
134
|
-
"""
|
|
135
|
-
return number + 2
|
|
136
|
-
'''
|
|
137
|
-
|
|
138
|
-
SHARED_BASE_SOURCE = '''"""Shared module the conflict fixture edits on both sides."""
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def shared_value() -> int:
|
|
142
|
-
"""Return the shared base value.
|
|
143
|
-
|
|
144
|
-
Returns:
|
|
145
|
-
The shared integer.
|
|
146
|
-
"""
|
|
147
|
-
return 100
|
|
148
|
-
'''
|
|
149
|
-
|
|
150
|
-
SHARED_FEATURE_SOURCE = '''"""Shared module the conflict fixture edits on both sides."""
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def shared_value() -> int:
|
|
154
|
-
"""Return the shared base value.
|
|
155
|
-
|
|
156
|
-
Returns:
|
|
157
|
-
The shared integer.
|
|
158
|
-
"""
|
|
159
|
-
return 200
|
|
160
|
-
'''
|
|
161
|
-
|
|
162
|
-
SHARED_DIVERGENT_SOURCE = '''"""Shared module the conflict fixture edits on both sides."""
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
def shared_value() -> int:
|
|
166
|
-
"""Return the shared base value.
|
|
167
|
-
|
|
168
|
-
Returns:
|
|
169
|
-
The shared integer.
|
|
170
|
-
"""
|
|
171
|
-
return 300
|
|
172
|
-
'''
|
|
173
|
-
|
|
174
|
-
OTHER_DIVERGENT_SOURCE = '''"""Unrelated module the divergent base edits for the behind-but-clean case."""
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
def other_value() -> int:
|
|
178
|
-
"""Return the unrelated value.
|
|
179
|
-
|
|
180
|
-
Returns:
|
|
181
|
-
The unrelated integer.
|
|
182
|
-
"""
|
|
183
|
-
return 42
|
|
184
|
-
'''
|
|
185
|
-
|
|
186
|
-
BROKEN_ENFORCER_SOURCE = (
|
|
187
|
-
'"""Enforcer stub that raises a non-import error at load time."""\n'
|
|
188
|
-
"\n"
|
|
189
|
-
"raise RuntimeError('deliberate enforcer load failure')\n"
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
RE_ERROR_ENFORCER_SOURCE = (
|
|
193
|
-
'"""Enforcer stub whose module-scope regex compile raises re.error."""\n'
|
|
194
|
-
"\n"
|
|
195
|
-
"import re\n"
|
|
196
|
-
"\n"
|
|
197
|
-
"re.compile('(')\n"
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def run_git(repository_root: Path, *git_arguments: str) -> str:
|
|
202
|
-
completed = subprocess.run(
|
|
203
|
-
["git", "-C", str(repository_root), *git_arguments],
|
|
204
|
-
check=True,
|
|
205
|
-
capture_output=True,
|
|
206
|
-
text=True,
|
|
207
|
-
)
|
|
208
|
-
return completed.stdout.strip()
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
def disable_native_git_hooks(repository_root: Path) -> None:
|
|
212
|
-
empty_hooks_directory = repository_root.parent / "nohooks"
|
|
213
|
-
empty_hooks_directory.mkdir(exist_ok=True)
|
|
214
|
-
run_git(repository_root, "config", "core.hooksPath", str(empty_hooks_directory))
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
def initialize_repository(repository_root: Path) -> None:
|
|
218
|
-
run_git(repository_root, "init")
|
|
219
|
-
run_git(repository_root, "config", "user.email", "tests@example.com")
|
|
220
|
-
run_git(repository_root, "config", "user.name", "Preflight Tests")
|
|
221
|
-
disable_native_git_hooks(repository_root)
|
|
222
|
-
run_git(repository_root, "checkout", "-b", "main")
|
|
223
|
-
(repository_root / "base.py").write_text(CLEAN_MODULE_SOURCE, encoding="utf-8")
|
|
224
|
-
run_git(repository_root, "add", "base.py")
|
|
225
|
-
run_git(repository_root, "commit", "-m", "initial")
|
|
226
|
-
main_sha = run_git(repository_root, "rev-parse", "HEAD")
|
|
227
|
-
run_git(repository_root, "update-ref", "refs/remotes/origin/main", main_sha)
|
|
228
|
-
resolved_base = run_git(repository_root, "merge-base", "HEAD", "origin/main")
|
|
229
|
-
assert resolved_base, "fixture must resolve a merge base against origin/main"
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
def commit_file(repository_root: Path, relative_name: str, source_text: str, message: str) -> str:
|
|
233
|
-
(repository_root / relative_name).write_text(source_text, encoding="utf-8")
|
|
234
|
-
run_git(repository_root, "add", relative_name)
|
|
235
|
-
run_git(repository_root, "commit", "-m", message)
|
|
236
|
-
return run_git(repository_root, "rev-parse", "HEAD")
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
def write_working_tree_file(repository_root: Path, relative_name: str, source_text: str) -> None:
|
|
240
|
-
(repository_root / relative_name).write_text(source_text, encoding="utf-8")
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
def write_invalid_utf8_file(repository_root: Path, relative_name: str) -> None:
|
|
244
|
-
(repository_root / relative_name).write_bytes(b"\xff\xfe invalid utf-8 bytes\n")
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
def advance_origin_main_divergent(
|
|
248
|
-
repository_root: Path, base_sha: str, relative_name: str, source_text: str
|
|
249
|
-
) -> None:
|
|
250
|
-
run_git(repository_root, "checkout", "-b", "divergent", base_sha)
|
|
251
|
-
commit_file(repository_root, relative_name, source_text, "divergent edit")
|
|
252
|
-
divergent_sha = run_git(repository_root, "rev-parse", "HEAD")
|
|
253
|
-
run_git(repository_root, "update-ref", "refs/remotes/origin/main", divergent_sha)
|
|
254
|
-
run_git(repository_root, "checkout", "feature")
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
def _spawn_payload(tool_name: str, subagent_type: str, prompt: str, cwd: Path) -> str:
|
|
258
|
-
return json.dumps(
|
|
259
|
-
{
|
|
260
|
-
"tool_name": tool_name,
|
|
261
|
-
"tool_input": {"subagent_type": subagent_type, "prompt": prompt},
|
|
262
|
-
"cwd": str(cwd),
|
|
263
|
-
}
|
|
264
|
-
)
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
def write_agent_payload(subagent_type: str, prompt: str, cwd: Path) -> str:
|
|
268
|
-
return _spawn_payload("Agent", subagent_type, prompt, cwd)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
def write_task_payload(subagent_type: str, prompt: str, cwd: Path) -> str:
|
|
272
|
-
return _spawn_payload("Task", subagent_type, prompt, cwd)
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
def run_hook(payload: str, cwd: Path) -> subprocess.CompletedProcess[str]:
|
|
276
|
-
return subprocess.run(
|
|
277
|
-
[sys.executable, str(HOOK_PATH)],
|
|
278
|
-
check=False,
|
|
279
|
-
input=payload,
|
|
280
|
-
capture_output=True,
|
|
281
|
-
text=True,
|
|
282
|
-
cwd=str(cwd),
|
|
283
|
-
timeout=120,
|
|
284
|
-
)
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
def is_allow(result: subprocess.CompletedProcess[str]) -> bool:
|
|
288
|
-
stdout_text = result.stdout.strip()
|
|
289
|
-
if not stdout_text:
|
|
290
|
-
return True
|
|
291
|
-
parsed = json.loads(stdout_text)
|
|
292
|
-
hook_output = parsed.get("hookSpecificOutput", {})
|
|
293
|
-
return hook_output.get("permissionDecision") != "deny"
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
def deny_reason(result: subprocess.CompletedProcess[str]) -> str:
|
|
297
|
-
parsed = json.loads(result.stdout.strip())
|
|
298
|
-
return parsed["hookSpecificOutput"]["permissionDecisionReason"]
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
def make_conflict_repository(tmp_path: Path) -> Path:
|
|
302
|
-
repository_root = tmp_path / "repo"
|
|
303
|
-
repository_root.mkdir()
|
|
304
|
-
initialize_repository(repository_root)
|
|
305
|
-
base_sha = commit_file(repository_root, "shared.py", SHARED_BASE_SOURCE, "add shared")
|
|
306
|
-
run_git(repository_root, "checkout", "-b", "feature")
|
|
307
|
-
commit_file(repository_root, "shared.py", SHARED_FEATURE_SOURCE, "feature edit")
|
|
308
|
-
advance_origin_main_divergent(repository_root, base_sha, "shared.py", SHARED_DIVERGENT_SOURCE)
|
|
309
|
-
return repository_root
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
def make_stale_self_upstream_conflict_repository(tmp_path: Path) -> Path:
|
|
313
|
-
repository_root = make_conflict_repository(tmp_path)
|
|
314
|
-
feature_sha = run_git(repository_root, "rev-parse", "HEAD")
|
|
315
|
-
run_git(repository_root, "remote", "add", "origin", str(repository_root))
|
|
316
|
-
run_git(repository_root, "update-ref", "refs/remotes/origin/feature", feature_sha)
|
|
317
|
-
run_git(repository_root, "config", "branch.feature.remote", "origin")
|
|
318
|
-
run_git(repository_root, "config", "branch.feature.merge", "refs/heads/feature")
|
|
319
|
-
return repository_root
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
def make_merge_in_progress_repository(tmp_path: Path) -> Path:
|
|
323
|
-
repository_root = tmp_path / "repo"
|
|
324
|
-
repository_root.mkdir()
|
|
325
|
-
initialize_repository(repository_root)
|
|
326
|
-
base_sha = commit_file(repository_root, "shared.py", SHARED_BASE_SOURCE, "add shared")
|
|
327
|
-
run_git(repository_root, "update-ref", "refs/remotes/origin/main", base_sha)
|
|
328
|
-
run_git(repository_root, "checkout", "-b", "feature")
|
|
329
|
-
commit_file(repository_root, "shared.py", SHARED_FEATURE_SOURCE, "feature edit")
|
|
330
|
-
advance_origin_main_divergent(repository_root, base_sha, "shared.py", SHARED_DIVERGENT_SOURCE)
|
|
331
|
-
return repository_root
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
def start_conflicting_merge(repository_root: Path) -> None:
|
|
335
|
-
subprocess.run(
|
|
336
|
-
["git", "-C", str(repository_root), "merge", "--no-edit", "origin/main"],
|
|
337
|
-
check=False,
|
|
338
|
-
capture_output=True,
|
|
339
|
-
text=True,
|
|
340
|
-
)
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
def test_non_code_verifier_agent_is_no_op(tmp_path: Path) -> None:
|
|
344
|
-
repository_root = make_conflict_repository(tmp_path)
|
|
345
|
-
write_working_tree_file(repository_root, "violator.py", VIOLATING_MODULE_SOURCE)
|
|
346
|
-
payload = write_agent_payload("clean-coder", "do an audit", repository_root)
|
|
347
|
-
result = run_hook(payload, repository_root)
|
|
348
|
-
assert is_allow(result)
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
def test_non_agent_tool_is_no_op(tmp_path: Path) -> None:
|
|
352
|
-
repository_root = tmp_path / "repo"
|
|
353
|
-
repository_root.mkdir()
|
|
354
|
-
initialize_repository(repository_root)
|
|
355
|
-
payload = json.dumps(
|
|
356
|
-
{"tool_name": "Bash", "tool_input": {"command": "ls"}, "cwd": str(repository_root)}
|
|
357
|
-
)
|
|
358
|
-
result = run_hook(payload, repository_root)
|
|
359
|
-
assert is_allow(result)
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
def test_clean_surface_allows(tmp_path: Path) -> None:
|
|
363
|
-
repository_root = tmp_path / "repo"
|
|
364
|
-
repository_root.mkdir()
|
|
365
|
-
initialize_repository(repository_root)
|
|
366
|
-
run_git(repository_root, "checkout", "-b", "feature")
|
|
367
|
-
write_working_tree_file(repository_root, "feature.py", CLEAN_MODULE_SOURCE)
|
|
368
|
-
payload = write_agent_payload("code-verifier", "verify the change", repository_root)
|
|
369
|
-
result = run_hook(payload, repository_root)
|
|
370
|
-
assert is_allow(result)
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
def test_real_conflict_denies_naming_files(tmp_path: Path) -> None:
|
|
374
|
-
repository_root = make_conflict_repository(tmp_path)
|
|
375
|
-
payload = write_agent_payload("code-verifier", "verify the change", repository_root)
|
|
376
|
-
result = run_hook(payload, repository_root)
|
|
377
|
-
assert not is_allow(result)
|
|
378
|
-
reason = deny_reason(result)
|
|
379
|
-
assert "shared.py" in reason
|
|
380
|
-
assert "Merge conflicts vs" in reason
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
def test_real_code_rules_violation_on_added_line_denies(tmp_path: Path) -> None:
|
|
384
|
-
repository_root = tmp_path / "repo"
|
|
385
|
-
repository_root.mkdir()
|
|
386
|
-
initialize_repository(repository_root)
|
|
387
|
-
run_git(repository_root, "checkout", "-b", "feature")
|
|
388
|
-
commit_file(repository_root, "tracked.py", CLEAN_MODULE_SOURCE, "add tracked")
|
|
389
|
-
write_working_tree_file(repository_root, "tracked.py", VIOLATING_MODULE_SOURCE)
|
|
390
|
-
payload = write_agent_payload("code-verifier", "verify the change", repository_root)
|
|
391
|
-
result = run_hook(payload, repository_root)
|
|
392
|
-
assert not is_allow(result)
|
|
393
|
-
reason = deny_reason(result)
|
|
394
|
-
assert "tracked.py" in reason
|
|
395
|
-
assert "Line " in reason
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
def test_committed_on_branch_violation_with_clean_working_tree_denies(tmp_path: Path) -> None:
|
|
399
|
-
repository_root = tmp_path / "repo"
|
|
400
|
-
repository_root.mkdir()
|
|
401
|
-
initialize_repository(repository_root)
|
|
402
|
-
run_git(repository_root, "checkout", "-b", "feature")
|
|
403
|
-
commit_file(repository_root, "committed.py", VIOLATING_MODULE_SOURCE, "commit violation")
|
|
404
|
-
status_output = run_git(repository_root, "status", "--porcelain")
|
|
405
|
-
assert status_output == "", "working tree must be clean so the deny comes from the committed line"
|
|
406
|
-
payload = write_agent_payload("code-verifier", "verify the change", repository_root)
|
|
407
|
-
result = run_hook(payload, repository_root)
|
|
408
|
-
assert not is_allow(result)
|
|
409
|
-
reason = deny_reason(result)
|
|
410
|
-
assert "committed.py" in reason
|
|
411
|
-
assert "Line " in reason
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
def test_preexisting_violation_on_untouched_line_allows(tmp_path: Path) -> None:
|
|
415
|
-
repository_root = tmp_path / "repo"
|
|
416
|
-
repository_root.mkdir()
|
|
417
|
-
initialize_repository(repository_root)
|
|
418
|
-
carrier_sha = commit_file(
|
|
419
|
-
repository_root, "carrier.py", PREEXISTING_VIOLATION_BASE_SOURCE, "add carrier"
|
|
420
|
-
)
|
|
421
|
-
run_git(repository_root, "update-ref", "refs/remotes/origin/main", carrier_sha)
|
|
422
|
-
run_git(repository_root, "checkout", "-b", "feature")
|
|
423
|
-
write_working_tree_file(repository_root, "carrier.py", PREEXISTING_VIOLATION_EDITED_SOURCE)
|
|
424
|
-
payload = write_agent_payload("code-verifier", "verify the change", repository_root)
|
|
425
|
-
result = run_hook(payload, repository_root)
|
|
426
|
-
assert is_allow(result)
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
def test_untracked_new_violating_file_denies(tmp_path: Path) -> None:
|
|
430
|
-
repository_root = tmp_path / "repo"
|
|
431
|
-
repository_root.mkdir()
|
|
432
|
-
initialize_repository(repository_root)
|
|
433
|
-
run_git(repository_root, "checkout", "-b", "feature")
|
|
434
|
-
write_working_tree_file(repository_root, "fresh.py", VIOLATING_MODULE_SOURCE)
|
|
435
|
-
payload = write_agent_payload("code-verifier", "verify the change", repository_root)
|
|
436
|
-
result = run_hook(payload, repository_root)
|
|
437
|
-
assert not is_allow(result)
|
|
438
|
-
reason = deny_reason(result)
|
|
439
|
-
assert "fresh.py" in reason
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
def test_tooling_scratch_file_is_ignored(tmp_path: Path) -> None:
|
|
443
|
-
repository_root = tmp_path / "repo"
|
|
444
|
-
repository_root.mkdir()
|
|
445
|
-
initialize_repository(repository_root)
|
|
446
|
-
run_git(repository_root, "checkout", "-b", "feature")
|
|
447
|
-
scratch_directory = repository_root / ".claude" / "verification"
|
|
448
|
-
scratch_directory.mkdir(parents=True)
|
|
449
|
-
(scratch_directory / "x.py").write_text(VIOLATING_MODULE_SOURCE, encoding="utf-8")
|
|
450
|
-
payload = write_agent_payload("code-verifier", "verify the change", repository_root)
|
|
451
|
-
result = run_hook(payload, repository_root)
|
|
452
|
-
assert is_allow(result)
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
def test_missing_base_ref_fails_open(tmp_path: Path) -> None:
|
|
456
|
-
repository_root = tmp_path / "repo"
|
|
457
|
-
repository_root.mkdir()
|
|
458
|
-
run_git(repository_root, "init")
|
|
459
|
-
run_git(repository_root, "config", "user.email", "tests@example.com")
|
|
460
|
-
run_git(repository_root, "config", "user.name", "Preflight Tests")
|
|
461
|
-
disable_native_git_hooks(repository_root)
|
|
462
|
-
(repository_root / "base.py").write_text(CLEAN_MODULE_SOURCE, encoding="utf-8")
|
|
463
|
-
run_git(repository_root, "add", "base.py")
|
|
464
|
-
run_git(repository_root, "commit", "-m", "initial")
|
|
465
|
-
payload = write_agent_payload("code-verifier", "verify the change", repository_root)
|
|
466
|
-
result = run_hook(payload, repository_root)
|
|
467
|
-
assert is_allow(result)
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
def test_non_repo_cwd_fails_open(tmp_path: Path) -> None:
|
|
471
|
-
non_repo_directory = tmp_path / "plain"
|
|
472
|
-
non_repo_directory.mkdir()
|
|
473
|
-
payload = write_agent_payload("code-verifier", "verify the change", non_repo_directory)
|
|
474
|
-
result = run_hook(payload, non_repo_directory)
|
|
475
|
-
assert is_allow(result)
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
def test_behind_but_conflict_free_allows(tmp_path: Path) -> None:
|
|
479
|
-
repository_root = tmp_path / "repo"
|
|
480
|
-
repository_root.mkdir()
|
|
481
|
-
initialize_repository(repository_root)
|
|
482
|
-
base_sha = commit_file(repository_root, "shared.py", SHARED_BASE_SOURCE, "add shared")
|
|
483
|
-
run_git(repository_root, "checkout", "-b", "feature")
|
|
484
|
-
commit_file(repository_root, "shared.py", SHARED_FEATURE_SOURCE, "feature edit")
|
|
485
|
-
advance_origin_main_divergent(repository_root, base_sha, "other.py", OTHER_DIVERGENT_SOURCE)
|
|
486
|
-
payload = write_agent_payload("code-verifier", "verify the change", repository_root)
|
|
487
|
-
result = run_hook(payload, repository_root)
|
|
488
|
-
assert is_allow(result)
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
def test_unreadable_changed_file_alone_fails_open(tmp_path: Path) -> None:
|
|
492
|
-
repository_root = tmp_path / "repo"
|
|
493
|
-
repository_root.mkdir()
|
|
494
|
-
initialize_repository(repository_root)
|
|
495
|
-
run_git(repository_root, "checkout", "-b", "feature")
|
|
496
|
-
write_invalid_utf8_file(repository_root, "binary.py")
|
|
497
|
-
payload = write_agent_payload("code-verifier", "verify the change", repository_root)
|
|
498
|
-
result = run_hook(payload, repository_root)
|
|
499
|
-
assert is_allow(result)
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
def test_unreadable_changed_file_does_not_mask_real_violation(tmp_path: Path) -> None:
|
|
503
|
-
repository_root = tmp_path / "repo"
|
|
504
|
-
repository_root.mkdir()
|
|
505
|
-
initialize_repository(repository_root)
|
|
506
|
-
run_git(repository_root, "checkout", "-b", "feature")
|
|
507
|
-
write_invalid_utf8_file(repository_root, "binary.py")
|
|
508
|
-
write_working_tree_file(repository_root, "fresh.py", VIOLATING_MODULE_SOURCE)
|
|
509
|
-
payload = write_agent_payload("code-verifier", "verify the change", repository_root)
|
|
510
|
-
result = run_hook(payload, repository_root)
|
|
511
|
-
assert not is_allow(result)
|
|
512
|
-
reason = deny_reason(result)
|
|
513
|
-
assert "fresh.py" in reason
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
def test_conflict_and_violation_single_deny_names_both(tmp_path: Path) -> None:
|
|
517
|
-
repository_root = make_conflict_repository(tmp_path)
|
|
518
|
-
write_working_tree_file(repository_root, "violator.py", VIOLATING_MODULE_SOURCE)
|
|
519
|
-
payload = write_agent_payload("code-verifier", "verify the change", repository_root)
|
|
520
|
-
result = run_hook(payload, repository_root)
|
|
521
|
-
assert not is_allow(result)
|
|
522
|
-
reason = deny_reason(result)
|
|
523
|
-
assert "Merge conflicts vs" in reason
|
|
524
|
-
assert "shared.py" in reason
|
|
525
|
-
assert "CODE_RULES violations on changed lines:" in reason
|
|
526
|
-
assert "violator.py" in reason
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
def test_stale_self_upstream_still_judges_default_branch(tmp_path: Path) -> None:
|
|
530
|
-
repository_root = make_stale_self_upstream_conflict_repository(tmp_path)
|
|
531
|
-
tracked_upstream = run_git(
|
|
532
|
-
repository_root, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"
|
|
533
|
-
)
|
|
534
|
-
assert tracked_upstream == "origin/feature", "fixture must track the branch's own remote ref"
|
|
535
|
-
payload = write_agent_payload("code-verifier", "verify the change", repository_root)
|
|
536
|
-
result = run_hook(payload, repository_root)
|
|
537
|
-
assert not is_allow(result)
|
|
538
|
-
reason = deny_reason(result)
|
|
539
|
-
assert "shared.py" in reason
|
|
540
|
-
assert "Merge conflicts vs" in reason
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
def test_staged_resolved_merge_allows(tmp_path: Path) -> None:
|
|
544
|
-
repository_root = make_merge_in_progress_repository(tmp_path)
|
|
545
|
-
start_conflicting_merge(repository_root)
|
|
546
|
-
write_working_tree_file(repository_root, "shared.py", SHARED_FEATURE_SOURCE)
|
|
547
|
-
run_git(repository_root, "add", "shared.py")
|
|
548
|
-
remaining_unmerged = run_git(repository_root, "diff", "--name-only", "--diff-filter=U")
|
|
549
|
-
assert remaining_unmerged == "", "the staged resolution must clear every unmerged index entry"
|
|
550
|
-
merge_head_sha = run_git(repository_root, "rev-parse", "--verify", "--quiet", "MERGE_HEAD")
|
|
551
|
-
assert merge_head_sha, "a merge must still be in progress so MERGE_HEAD is present"
|
|
552
|
-
payload = write_agent_payload("code-verifier", "verify the change", repository_root)
|
|
553
|
-
result = run_hook(payload, repository_root)
|
|
554
|
-
assert is_allow(result)
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
def test_unresolved_merge_keeps_denying(tmp_path: Path) -> None:
|
|
558
|
-
repository_root = make_merge_in_progress_repository(tmp_path)
|
|
559
|
-
start_conflicting_merge(repository_root)
|
|
560
|
-
remaining_unmerged = run_git(repository_root, "diff", "--name-only", "--diff-filter=U")
|
|
561
|
-
assert "shared.py" in remaining_unmerged, "the merge must leave shared.py unmerged"
|
|
562
|
-
payload = write_agent_payload("code-verifier", "verify the change", repository_root)
|
|
563
|
-
result = run_hook(payload, repository_root)
|
|
564
|
-
assert not is_allow(result)
|
|
565
|
-
reason = deny_reason(result)
|
|
566
|
-
assert "shared.py" in reason
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
def test_hook_imports_real_config_when_parent_holds_shadowing_config(
|
|
570
|
-
tmp_path: Path,
|
|
571
|
-
) -> None:
|
|
572
|
-
real_hooks_directory = HOOK_PATH.parent.parent
|
|
573
|
-
real_package_directory = real_hooks_directory.parent
|
|
574
|
-
|
|
575
|
-
staged_package_directory = tmp_path / "claude-dev-env"
|
|
576
|
-
shutil.copytree(
|
|
577
|
-
real_hooks_directory,
|
|
578
|
-
staged_package_directory / "hooks",
|
|
579
|
-
)
|
|
580
|
-
shutil.copytree(
|
|
581
|
-
real_package_directory / "_shared",
|
|
582
|
-
staged_package_directory / "_shared",
|
|
583
|
-
)
|
|
584
|
-
|
|
585
|
-
shadowing_config_directory = staged_package_directory / "hooks" / "config"
|
|
586
|
-
shadowing_config_directory.mkdir(parents=True, exist_ok=True)
|
|
587
|
-
(shadowing_config_directory / "__init__.py").write_text("", encoding="utf-8")
|
|
588
|
-
(shadowing_config_directory / "unrelated_constants.py").write_text(
|
|
589
|
-
"UNRELATED_VALUE = 1\n", encoding="utf-8"
|
|
590
|
-
)
|
|
591
|
-
|
|
592
|
-
staged_hook = (
|
|
593
|
-
staged_package_directory
|
|
594
|
-
/ "hooks"
|
|
595
|
-
/ "blocking"
|
|
596
|
-
/ "code_verifier_spawn_preflight_gate.py"
|
|
597
|
-
)
|
|
598
|
-
payload = write_agent_payload("general-purpose", "unrelated work", tmp_path)
|
|
599
|
-
completed = subprocess.run(
|
|
600
|
-
[sys.executable, str(staged_hook)],
|
|
601
|
-
check=False,
|
|
602
|
-
input=payload,
|
|
603
|
-
capture_output=True,
|
|
604
|
-
text=True,
|
|
605
|
-
cwd=str(tmp_path),
|
|
606
|
-
timeout=120,
|
|
607
|
-
)
|
|
608
|
-
|
|
609
|
-
assert "ModuleNotFoundError" not in completed.stderr
|
|
610
|
-
assert completed.returncode == 0
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
def test_task_tool_code_verifier_spawn_is_gated_like_agent(tmp_path: Path) -> None:
|
|
614
|
-
repository_root = tmp_path / "repo"
|
|
615
|
-
repository_root.mkdir()
|
|
616
|
-
initialize_repository(repository_root)
|
|
617
|
-
run_git(repository_root, "checkout", "-b", "feature")
|
|
618
|
-
write_working_tree_file(repository_root, "fresh.py", VIOLATING_MODULE_SOURCE)
|
|
619
|
-
task_payload = write_task_payload("code-verifier", "verify the change", repository_root)
|
|
620
|
-
task_result = run_hook(task_payload, repository_root)
|
|
621
|
-
assert not is_allow(task_result)
|
|
622
|
-
task_reason = deny_reason(task_result)
|
|
623
|
-
assert "fresh.py" in task_reason
|
|
624
|
-
assert "CODE_RULES violations on changed lines:" in task_reason
|
|
625
|
-
clean_coder_payload = write_task_payload("clean-coder", "write code", repository_root)
|
|
626
|
-
clean_coder_result = run_hook(clean_coder_payload, repository_root)
|
|
627
|
-
assert is_allow(clean_coder_result)
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
def stage_package_and_feature_repo(tmp_path: Path) -> tuple[Path, Path]:
|
|
631
|
-
"""Copy the hook package and build a clean feature repo for load-failure tests.
|
|
632
|
-
|
|
633
|
-
Args:
|
|
634
|
-
tmp_path: The pytest temporary directory the staged copy lands under.
|
|
635
|
-
|
|
636
|
-
Returns:
|
|
637
|
-
A ``(staged_enforcer_path, repository_root)`` pair: the copied
|
|
638
|
-
``code_rules_enforcer.py`` a test renders unloadable, and the clean
|
|
639
|
-
feature repository the staged hook runs against.
|
|
640
|
-
"""
|
|
641
|
-
real_hooks_directory = HOOK_PATH.parent.parent
|
|
642
|
-
real_package_directory = real_hooks_directory.parent
|
|
643
|
-
staged_package_directory = tmp_path / "claude-dev-env"
|
|
644
|
-
shutil.copytree(real_hooks_directory, staged_package_directory / "hooks")
|
|
645
|
-
shutil.copytree(
|
|
646
|
-
real_package_directory / "_shared", staged_package_directory / "_shared"
|
|
647
|
-
)
|
|
648
|
-
enforcer_path = staged_package_directory / "hooks" / "blocking" / "code_rules_enforcer.py"
|
|
649
|
-
repository_root = tmp_path / "repo"
|
|
650
|
-
repository_root.mkdir()
|
|
651
|
-
initialize_repository(repository_root)
|
|
652
|
-
run_git(repository_root, "checkout", "-b", "feature")
|
|
653
|
-
write_working_tree_file(repository_root, "feature.py", CLEAN_MODULE_SOURCE)
|
|
654
|
-
return enforcer_path, repository_root
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
def run_staged_preflight_hook(
|
|
658
|
-
enforcer_path: Path, repository_root: Path
|
|
659
|
-
) -> subprocess.CompletedProcess[str]:
|
|
660
|
-
"""Run the staged preflight hook that sits beside *enforcer_path*.
|
|
661
|
-
|
|
662
|
-
Args:
|
|
663
|
-
enforcer_path: The staged ``code_rules_enforcer.py`` path; the staged
|
|
664
|
-
hook is its sibling in the same ``hooks/blocking`` directory.
|
|
665
|
-
repository_root: The feature repository the hook inspects.
|
|
666
|
-
|
|
667
|
-
Returns:
|
|
668
|
-
The completed hook subprocess run.
|
|
669
|
-
"""
|
|
670
|
-
staged_hook = enforcer_path.parent / "code_verifier_spawn_preflight_gate.py"
|
|
671
|
-
payload = write_agent_payload("code-verifier", "verify the change", repository_root)
|
|
672
|
-
return subprocess.run(
|
|
673
|
-
[sys.executable, str(staged_hook)],
|
|
674
|
-
check=False,
|
|
675
|
-
input=payload,
|
|
676
|
-
capture_output=True,
|
|
677
|
-
text=True,
|
|
678
|
-
cwd=str(repository_root),
|
|
679
|
-
timeout=120,
|
|
680
|
-
)
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
def test_engine_load_failure_denies_with_named_reason(tmp_path: Path) -> None:
|
|
684
|
-
enforcer_path, repository_root = stage_package_and_feature_repo(tmp_path)
|
|
685
|
-
enforcer_path.unlink()
|
|
686
|
-
completed = run_staged_preflight_hook(enforcer_path, repository_root)
|
|
687
|
-
assert not is_allow(completed)
|
|
688
|
-
reason = deny_reason(completed)
|
|
689
|
-
assert "CODE_RULES engine failed to load" in reason
|
|
690
|
-
assert "load_validate_content" in reason
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
def test_engine_import_error_denies_with_named_reason(tmp_path: Path) -> None:
|
|
694
|
-
enforcer_path, repository_root = stage_package_and_feature_repo(tmp_path)
|
|
695
|
-
enforcer_path.write_text(BROKEN_ENFORCER_SOURCE, encoding="utf-8")
|
|
696
|
-
completed = run_staged_preflight_hook(enforcer_path, repository_root)
|
|
697
|
-
assert not is_allow(completed)
|
|
698
|
-
reason = deny_reason(completed)
|
|
699
|
-
assert "CODE_RULES engine failed to load" in reason
|
|
700
|
-
assert "load_validate_content" in reason
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
def test_engine_re_error_denies_with_named_reason(tmp_path: Path) -> None:
|
|
704
|
-
enforcer_path, repository_root = stage_package_and_feature_repo(tmp_path)
|
|
705
|
-
enforcer_path.write_text(RE_ERROR_ENFORCER_SOURCE, encoding="utf-8")
|
|
706
|
-
completed = run_staged_preflight_hook(enforcer_path, repository_root)
|
|
707
|
-
assert not is_allow(completed)
|
|
708
|
-
reason = deny_reason(completed)
|
|
709
|
-
assert "CODE_RULES engine failed to load" in reason
|
|
710
|
-
assert "load_validate_content" in reason
|