claude-dev-env 1.57.2 → 1.59.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 +2 -2
- package/_shared/pr-loop/scripts/code_rules_gate.py +36 -3
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +6 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
- package/_shared/pr-loop/scripts/reviews_disabled.py +12 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +265 -0
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +29 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/bin/install.mjs +317 -54
- package/bin/install.test.mjs +478 -3
- package/docs/CODE_RULES.md +3 -3
- package/hooks/blocking/code_rules_annotations_length.py +153 -0
- package/hooks/blocking/code_rules_dead_dataclass_field.py +319 -0
- package/hooks/blocking/code_rules_duplicate_body.py +287 -0
- package/hooks/blocking/code_rules_enforcer.py +175 -21
- package/hooks/blocking/code_rules_magic_values.py +98 -0
- package/hooks/blocking/code_rules_shared.py +41 -0
- package/hooks/blocking/destructive_command_blocker.py +1027 -12
- package/hooks/blocking/hook_prose_detector_consistency.py +150 -0
- package/hooks/blocking/intent_only_ending_blocker.py +155 -0
- package/hooks/blocking/session_handoff_blocker.py +190 -0
- package/hooks/blocking/subprocess_budget_completeness.py +380 -0
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +225 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +467 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +330 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +179 -0
- package/hooks/blocking/test_code_rules_enforcer_magic_slice_bounds.py +133 -0
- package/hooks/blocking/test_destructive_command_blocker.py +622 -3
- package/hooks/blocking/test_hook_prose_detector_consistency.py +265 -0
- package/hooks/blocking/test_intent_only_ending_blocker.py +175 -0
- package/hooks/blocking/test_session_handoff_blocker.py +312 -0
- package/hooks/blocking/test_subprocess_budget_completeness.py +588 -0
- package/hooks/blocking/test_workflow_substitution_slot_blocker.py +242 -0
- package/hooks/blocking/workflow_substitution_slot_blocker.py +159 -0
- package/hooks/hooks.json +25 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +16 -0
- package/hooks/hooks_constants/dead_dataclass_field_constants.py +25 -0
- package/hooks/hooks_constants/destructive_command_segment_constants.py +178 -0
- package/hooks/hooks_constants/duplicate_function_body_constants.py +17 -0
- package/hooks/hooks_constants/hook_prose_detector_consistency_constants.py +30 -0
- package/hooks/hooks_constants/messages.py +4 -0
- package/hooks/hooks_constants/session_handoff_blocker_constants.py +10 -0
- package/hooks/hooks_constants/subprocess_budget_completeness_constants.py +5 -0
- package/hooks/hooks_constants/workflow_substitution_slot_blocker_constants.py +22 -0
- package/hooks/workflow/auto_formatter.py +26 -1
- package/hooks/workflow/test_auto_formatter.py +134 -0
- package/package.json +1 -1
- package/rules/conservative-action.md +1 -0
- package/rules/docstring-prose-matches-implementation.md +43 -0
- package/rules/hook-prose-matches-detector.md +26 -0
- package/rules/long-horizon-autonomy.md +43 -0
- package/rules/no-inline-destructive-literals.md +11 -0
- package/rules/workflow-substitution-slots.md +7 -0
- package/skills/autoconverge/SKILL.md +68 -6
- package/skills/autoconverge/reference/closing-report.md +44 -0
- package/skills/autoconverge/reference/convergence.md +7 -3
- package/skills/autoconverge/reference/stop-conditions.md +7 -2
- package/skills/autoconverge/workflow/autoconverge_report_constants/__init__.py +0 -0
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +105 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +30 -1
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +265 -0
- package/skills/autoconverge/workflow/converge.mjs +106 -38
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +259 -0
- package/skills/autoconverge/workflow/render_report.py +903 -0
- package/skills/autoconverge/workflow/test_render_report.py +484 -0
- package/skills/pr-converge/scripts/check_convergence.py +195 -64
- package/skills/pr-converge/scripts/test_check_convergence.py +173 -2
- package/skills/update/SKILL.md +37 -5
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
"""Unit tests for the subprocess_budget_completeness PreToolUse hook."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import json
|
|
5
|
+
import pathlib
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
from collections.abc import Iterator
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
14
|
+
if str(_HOOK_DIR) not in sys.path:
|
|
15
|
+
sys.path.insert(0, str(_HOOK_DIR))
|
|
16
|
+
|
|
17
|
+
hook_spec = importlib.util.spec_from_file_location(
|
|
18
|
+
"subprocess_budget_completeness",
|
|
19
|
+
_HOOK_DIR / "subprocess_budget_completeness.py",
|
|
20
|
+
)
|
|
21
|
+
assert hook_spec is not None
|
|
22
|
+
assert hook_spec.loader is not None
|
|
23
|
+
hook_module = importlib.util.module_from_spec(hook_spec)
|
|
24
|
+
hook_spec.loader.exec_module(hook_module)
|
|
25
|
+
|
|
26
|
+
find_undercounted_budget = hook_module.find_undercounted_budget
|
|
27
|
+
format_block_message = hook_module.format_block_message
|
|
28
|
+
resolved_content = hook_module.resolved_content
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture
|
|
32
|
+
def production_module_path() -> Iterator[pathlib.Path]:
|
|
33
|
+
with tempfile.TemporaryDirectory(prefix="budget_completeness_") as production_dir:
|
|
34
|
+
yield pathlib.Path(production_dir) / "timing_module.py"
|
|
35
|
+
|
|
36
|
+
_BUDGET_FLAGS_GIT_TIMEOUT_OMISSION = """
|
|
37
|
+
import subprocess
|
|
38
|
+
|
|
39
|
+
PYTHON_FORMAT_TIMEOUT_SECONDS = 12
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def worst_case_python_format_seconds() -> int:
|
|
43
|
+
fix_phase_seconds = PYTHON_FORMAT_TIMEOUT_SECONDS
|
|
44
|
+
format_phase_seconds = PYTHON_FORMAT_TIMEOUT_SECONDS
|
|
45
|
+
return fix_phase_seconds + format_phase_seconds
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def is_untracked_in_git(file_path: str) -> bool:
|
|
49
|
+
git_check = subprocess.run(["git", "ls-files", file_path], timeout=5)
|
|
50
|
+
return git_check.returncode != 0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def run_format(file_path: str) -> None:
|
|
54
|
+
subprocess.run(["ruff", "format", file_path], timeout=PYTHON_FORMAT_TIMEOUT_SECONDS)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def main(file_path: str) -> None:
|
|
58
|
+
if is_untracked_in_git(file_path):
|
|
59
|
+
return
|
|
60
|
+
run_format(file_path)
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
_BUDGET_COUNTS_EVERY_TIMEOUT = """
|
|
64
|
+
import subprocess
|
|
65
|
+
|
|
66
|
+
PYTHON_FORMAT_TIMEOUT_SECONDS = 12
|
|
67
|
+
GIT_CHECK_TIMEOUT_SECONDS = 5
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def worst_case_python_format_seconds() -> int:
|
|
71
|
+
git_check_seconds = GIT_CHECK_TIMEOUT_SECONDS
|
|
72
|
+
fix_phase_seconds = PYTHON_FORMAT_TIMEOUT_SECONDS
|
|
73
|
+
format_phase_seconds = PYTHON_FORMAT_TIMEOUT_SECONDS
|
|
74
|
+
return git_check_seconds + fix_phase_seconds + format_phase_seconds
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def is_untracked_in_git(file_path: str) -> bool:
|
|
78
|
+
git_check = subprocess.run(["git", "ls-files", file_path], timeout=GIT_CHECK_TIMEOUT_SECONDS)
|
|
79
|
+
return git_check.returncode != 0
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def run_format(file_path: str) -> None:
|
|
83
|
+
subprocess.run(["ruff", "format", file_path], timeout=PYTHON_FORMAT_TIMEOUT_SECONDS)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def main(file_path: str) -> None:
|
|
87
|
+
if is_untracked_in_git(file_path):
|
|
88
|
+
return
|
|
89
|
+
run_format(file_path)
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
_NO_BUDGET_FUNCTION = """
|
|
93
|
+
import subprocess
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def is_untracked_in_git(file_path: str) -> bool:
|
|
97
|
+
git_check = subprocess.run(["git", "ls-files", file_path], timeout=5)
|
|
98
|
+
return git_check.returncode != 0
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
_BUDGET_OMITS_A_NAMED_CONSTANT_TIMEOUT = """
|
|
102
|
+
import subprocess
|
|
103
|
+
|
|
104
|
+
PYTHON_FORMAT_TIMEOUT_SECONDS = 12
|
|
105
|
+
GIT_CHECK_TIMEOUT_SECONDS = 5
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def worst_case_python_format_seconds() -> int:
|
|
109
|
+
fix_phase_seconds = PYTHON_FORMAT_TIMEOUT_SECONDS
|
|
110
|
+
format_phase_seconds = PYTHON_FORMAT_TIMEOUT_SECONDS
|
|
111
|
+
return fix_phase_seconds + format_phase_seconds
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def is_untracked_in_git(file_path: str) -> bool:
|
|
115
|
+
git_check = subprocess.run(["git", "ls-files", file_path], timeout=GIT_CHECK_TIMEOUT_SECONDS)
|
|
116
|
+
return git_check.returncode != 0
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def run_format(file_path: str) -> None:
|
|
120
|
+
subprocess.run(["ruff", "format", file_path], timeout=PYTHON_FORMAT_TIMEOUT_SECONDS)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def main(file_path: str) -> None:
|
|
124
|
+
if is_untracked_in_git(file_path):
|
|
125
|
+
return
|
|
126
|
+
run_format(file_path)
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
_BUDGET_COUNTS_VIA_ANNOTATED_CONSTANTS = """
|
|
130
|
+
import subprocess
|
|
131
|
+
|
|
132
|
+
PYTHON_FORMAT_TIMEOUT_SECONDS: int = 12
|
|
133
|
+
GIT_CHECK_TIMEOUT_SECONDS: int = 5
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def worst_case_python_format_seconds() -> int:
|
|
137
|
+
git_check_seconds = GIT_CHECK_TIMEOUT_SECONDS
|
|
138
|
+
fix_phase_seconds = PYTHON_FORMAT_TIMEOUT_SECONDS
|
|
139
|
+
format_phase_seconds = PYTHON_FORMAT_TIMEOUT_SECONDS
|
|
140
|
+
return git_check_seconds + fix_phase_seconds + format_phase_seconds
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def is_untracked_in_git(file_path: str) -> bool:
|
|
144
|
+
git_check = subprocess.run(["git", "ls-files", file_path], timeout=5)
|
|
145
|
+
return git_check.returncode != 0
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def run_format(file_path: str) -> None:
|
|
149
|
+
subprocess.run(["ruff", "format", file_path], timeout=PYTHON_FORMAT_TIMEOUT_SECONDS)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def main(file_path: str) -> None:
|
|
153
|
+
if is_untracked_in_git(file_path):
|
|
154
|
+
return
|
|
155
|
+
run_format(file_path)
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
_BUDGET_PLUS_UNREACHABLE_NETWORK_PROBE = """
|
|
159
|
+
import subprocess
|
|
160
|
+
|
|
161
|
+
PYTHON_FORMAT_TIMEOUT_SECONDS = 12
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def worst_case_format_phase_seconds() -> int:
|
|
165
|
+
fix_phase_seconds = PYTHON_FORMAT_TIMEOUT_SECONDS
|
|
166
|
+
format_phase_seconds = PYTHON_FORMAT_TIMEOUT_SECONDS
|
|
167
|
+
return fix_phase_seconds + format_phase_seconds
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def run_format(file_path: str) -> None:
|
|
171
|
+
subprocess.run(["ruff", "format", file_path], timeout=PYTHON_FORMAT_TIMEOUT_SECONDS)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def unrelated_network_probe() -> int:
|
|
175
|
+
completed_probe = subprocess.run(["curl", "https://example.test"], timeout=30)
|
|
176
|
+
return completed_probe.returncode
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def main(file_path: str) -> None:
|
|
180
|
+
run_format(file_path)
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
_INTERIOR_BUDGET_SUBSTRING_NOT_A_TOTAL = """
|
|
184
|
+
import subprocess
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def audit_budget_report() -> int:
|
|
188
|
+
return run_auditor()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def run_auditor() -> int:
|
|
192
|
+
completed_audit = subprocess.run(["auditor"], timeout=30)
|
|
193
|
+
return completed_audit.returncode
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def main() -> int:
|
|
197
|
+
return audit_budget_report()
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
_ASYNC_BUDGET_OMITS_ASYNC_WRAPPER_TIMEOUT = """
|
|
202
|
+
import subprocess
|
|
203
|
+
|
|
204
|
+
PYTHON_FORMAT_TIMEOUT_SECONDS = 12
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def worst_case_python_format_seconds() -> int:
|
|
208
|
+
fix_phase_seconds = PYTHON_FORMAT_TIMEOUT_SECONDS
|
|
209
|
+
format_phase_seconds = PYTHON_FORMAT_TIMEOUT_SECONDS
|
|
210
|
+
return fix_phase_seconds + format_phase_seconds
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
async def is_untracked_in_git(file_path: str) -> bool:
|
|
214
|
+
git_check = subprocess.run(["git", "ls-files", file_path], timeout=5)
|
|
215
|
+
return git_check.returncode != 0
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def run_format(file_path: str) -> None:
|
|
219
|
+
subprocess.run(["ruff", "format", file_path], timeout=PYTHON_FORMAT_TIMEOUT_SECONDS)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
async def main(file_path: str) -> None:
|
|
223
|
+
if await is_untracked_in_git(file_path):
|
|
224
|
+
return
|
|
225
|
+
run_format(file_path)
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
_ASYNC_BUDGET_HELPER_OMITS_A_TIMEOUT = """
|
|
229
|
+
import subprocess
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def run_auditor() -> int:
|
|
233
|
+
completed_audit = subprocess.run(["auditor"], timeout=30)
|
|
234
|
+
return completed_audit.returncode
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
async def worst_case_seconds() -> int:
|
|
238
|
+
return 5
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def main() -> int:
|
|
242
|
+
return run_auditor()
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
_ASYNC_MAIN_NARROWS_REACHABLE_SET = """
|
|
246
|
+
import subprocess
|
|
247
|
+
|
|
248
|
+
PYTHON_FORMAT_TIMEOUT_SECONDS = 12
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def worst_case_format_phase_seconds() -> int:
|
|
252
|
+
fix_phase_seconds = PYTHON_FORMAT_TIMEOUT_SECONDS
|
|
253
|
+
format_phase_seconds = PYTHON_FORMAT_TIMEOUT_SECONDS
|
|
254
|
+
return fix_phase_seconds + format_phase_seconds
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def run_format(file_path: str) -> None:
|
|
258
|
+
subprocess.run(["ruff", "format", file_path], timeout=PYTHON_FORMAT_TIMEOUT_SECONDS)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def unrelated_network_probe() -> int:
|
|
262
|
+
completed_probe = subprocess.run(["curl", "https://example.test"], timeout=30)
|
|
263
|
+
return completed_probe.returncode
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
async def main(file_path: str) -> None:
|
|
267
|
+
run_format(file_path)
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def test_flags_async_subprocess_wrapper_that_omits_a_reachable_timeout() -> None:
|
|
272
|
+
undercounted_budget = find_undercounted_budget(_ASYNC_BUDGET_OMITS_ASYNC_WRAPPER_TIMEOUT)
|
|
273
|
+
assert undercounted_budget is not None
|
|
274
|
+
function_name, omitted_values = undercounted_budget
|
|
275
|
+
assert function_name == "worst_case_python_format_seconds"
|
|
276
|
+
assert omitted_values == {5}
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def test_flags_async_budget_helper_that_omits_a_reachable_timeout() -> None:
|
|
280
|
+
undercounted_budget = find_undercounted_budget(_ASYNC_BUDGET_HELPER_OMITS_A_TIMEOUT)
|
|
281
|
+
assert undercounted_budget is not None
|
|
282
|
+
function_name, omitted_values = undercounted_budget
|
|
283
|
+
assert function_name == "worst_case_seconds"
|
|
284
|
+
assert omitted_values == {30}
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def test_async_main_narrows_the_reachable_set() -> None:
|
|
288
|
+
assert find_undercounted_budget(_ASYNC_MAIN_NARROWS_REACHABLE_SET) is None
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def test_block_message_appends_the_seconds_unit_to_every_omitted_value() -> None:
|
|
292
|
+
block_message = format_block_message("module.py", "worst_case_seconds", {5, 12, 30})
|
|
293
|
+
assert "5s, 12s, 30s" in block_message
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def test_flags_budget_helper_that_omits_a_reachable_subprocess_timeout() -> None:
|
|
297
|
+
undercounted_budget = find_undercounted_budget(_BUDGET_FLAGS_GIT_TIMEOUT_OMISSION)
|
|
298
|
+
assert undercounted_budget is not None
|
|
299
|
+
function_name, omitted_literals = undercounted_budget
|
|
300
|
+
assert function_name == "worst_case_python_format_seconds"
|
|
301
|
+
assert omitted_literals == {5}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def test_passes_budget_helper_that_counts_every_subprocess_timeout() -> None:
|
|
305
|
+
assert find_undercounted_budget(_BUDGET_COUNTS_EVERY_TIMEOUT) is None
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def test_passes_module_without_a_budget_function() -> None:
|
|
309
|
+
assert find_undercounted_budget(_NO_BUDGET_FUNCTION) is None
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def test_passes_module_with_no_subprocess_calls() -> None:
|
|
313
|
+
only_a_budget_function = "def worst_case_seconds() -> int:\n return 5 + 12\n"
|
|
314
|
+
assert find_undercounted_budget(only_a_budget_function) is None
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def test_flags_budget_that_omits_a_named_constant_subprocess_timeout() -> None:
|
|
318
|
+
undercounted_budget = find_undercounted_budget(_BUDGET_OMITS_A_NAMED_CONSTANT_TIMEOUT)
|
|
319
|
+
assert undercounted_budget is not None
|
|
320
|
+
function_name, omitted_values = undercounted_budget
|
|
321
|
+
assert function_name == "worst_case_python_format_seconds"
|
|
322
|
+
assert omitted_values == {5}
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def test_passes_budget_that_accounts_via_annotated_module_constants() -> None:
|
|
326
|
+
assert find_undercounted_budget(_BUDGET_COUNTS_VIA_ANNOTATED_CONSTANTS) is None
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def test_passes_budget_with_unreachable_unrelated_subprocess_probe() -> None:
|
|
330
|
+
assert find_undercounted_budget(_BUDGET_PLUS_UNREACHABLE_NETWORK_PROBE) is None
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def test_ignores_function_whose_name_merely_contains_the_budget_substring() -> None:
|
|
334
|
+
assert find_undercounted_budget(_INTERIOR_BUDGET_SUBSTRING_NOT_A_TOTAL) is None
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
_STRAY_LITERAL_EQUAL_TO_OMITTED_TIMEOUT = """
|
|
338
|
+
import subprocess
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def worst_case_seconds() -> int:
|
|
342
|
+
retry_attempts = 5
|
|
343
|
+
fix_phase_seconds = 12
|
|
344
|
+
format_phase_seconds = 12
|
|
345
|
+
if retry_attempts < 0:
|
|
346
|
+
return 0
|
|
347
|
+
return fix_phase_seconds + format_phase_seconds
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def is_untracked_in_git(file_path: str) -> bool:
|
|
351
|
+
git_check = subprocess.run(["git", "ls-files", file_path], timeout=5)
|
|
352
|
+
return git_check.returncode != 0
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def main(file_path: str) -> bool:
|
|
356
|
+
return is_untracked_in_git(file_path)
|
|
357
|
+
"""
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def test_flags_when_a_stray_literal_equals_the_omitted_subprocess_timeout() -> None:
|
|
361
|
+
undercounted_budget = find_undercounted_budget(_STRAY_LITERAL_EQUAL_TO_OMITTED_TIMEOUT)
|
|
362
|
+
assert undercounted_budget is not None
|
|
363
|
+
function_name, omitted_values = undercounted_budget
|
|
364
|
+
assert function_name == "worst_case_seconds"
|
|
365
|
+
assert omitted_values == {5}
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
_BARE_RUN_IMPORT_OMITS_A_REACHABLE_TIMEOUT = """
|
|
369
|
+
from subprocess import run
|
|
370
|
+
|
|
371
|
+
PYTHON_FORMAT_TIMEOUT_SECONDS = 12
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def worst_case_seconds() -> int:
|
|
375
|
+
return PYTHON_FORMAT_TIMEOUT_SECONDS
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def run_format(file_path: str) -> None:
|
|
379
|
+
run(["ruff", "format", file_path], timeout=PYTHON_FORMAT_TIMEOUT_SECONDS)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def probe() -> int:
|
|
383
|
+
completed_probe = run(["curl", "https://example.test"], timeout=99)
|
|
384
|
+
return completed_probe.returncode
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def main(file_path: str) -> int:
|
|
388
|
+
run_format(file_path)
|
|
389
|
+
return probe()
|
|
390
|
+
"""
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def test_flags_bare_run_call_from_subprocess_import_run() -> None:
|
|
394
|
+
undercounted_budget = find_undercounted_budget(_BARE_RUN_IMPORT_OMITS_A_REACHABLE_TIMEOUT)
|
|
395
|
+
assert undercounted_budget is not None
|
|
396
|
+
function_name, omitted_values = undercounted_budget
|
|
397
|
+
assert function_name == "worst_case_seconds"
|
|
398
|
+
assert omitted_values == {99}
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def test_resolved_content_reconstructs_the_full_file_for_an_edit(
|
|
402
|
+
tmp_path: pathlib.Path,
|
|
403
|
+
) -> None:
|
|
404
|
+
edited_module_path = tmp_path / "timing_module.py"
|
|
405
|
+
old_helper_body = ' git_check = subprocess.run(["git", "ls-files", file_path])\n'
|
|
406
|
+
new_helper_body = ' git_check = subprocess.run(["git", "ls-files", file_path], timeout=45)\n'
|
|
407
|
+
edited_module_path.write_text(
|
|
408
|
+
_BUDGET_COUNTS_EVERY_TIMEOUT.replace(
|
|
409
|
+
' git_check = subprocess.run(["git", "ls-files", file_path],'
|
|
410
|
+
" timeout=GIT_CHECK_TIMEOUT_SECONDS)\n",
|
|
411
|
+
old_helper_body,
|
|
412
|
+
),
|
|
413
|
+
encoding="utf-8",
|
|
414
|
+
)
|
|
415
|
+
reconstructed_content = resolved_content(
|
|
416
|
+
{
|
|
417
|
+
"file_path": str(edited_module_path),
|
|
418
|
+
"old_string": old_helper_body,
|
|
419
|
+
"new_string": new_helper_body,
|
|
420
|
+
}
|
|
421
|
+
)
|
|
422
|
+
assert "timeout=45" in reconstructed_content
|
|
423
|
+
assert "def worst_case_python_format_seconds" in reconstructed_content
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def test_edit_flags_new_timeout_added_to_a_non_budget_helper(tmp_path: pathlib.Path) -> None:
|
|
427
|
+
edited_module_path = tmp_path / "timing_module.py"
|
|
428
|
+
edited_module_path.write_text(_BUDGET_COUNTS_EVERY_TIMEOUT, encoding="utf-8")
|
|
429
|
+
old_helper_line = (
|
|
430
|
+
' git_check = subprocess.run(["git", "ls-files", file_path],'
|
|
431
|
+
" timeout=GIT_CHECK_TIMEOUT_SECONDS)\n"
|
|
432
|
+
)
|
|
433
|
+
new_helper_line = (
|
|
434
|
+
' git_check = subprocess.run(["git", "ls-files", file_path], timeout=45)\n'
|
|
435
|
+
)
|
|
436
|
+
reconstructed_content = resolved_content(
|
|
437
|
+
{
|
|
438
|
+
"file_path": str(edited_module_path),
|
|
439
|
+
"old_string": old_helper_line,
|
|
440
|
+
"new_string": new_helper_line,
|
|
441
|
+
}
|
|
442
|
+
)
|
|
443
|
+
undercounted_budget = find_undercounted_budget(reconstructed_content)
|
|
444
|
+
assert undercounted_budget is not None
|
|
445
|
+
function_name, omitted_values = undercounted_budget
|
|
446
|
+
assert function_name == "worst_case_python_format_seconds"
|
|
447
|
+
assert omitted_values == {45}
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def test_edit_passes_single_helper_when_full_file_budget_is_complete(
|
|
451
|
+
tmp_path: pathlib.Path,
|
|
452
|
+
) -> None:
|
|
453
|
+
edited_module_path = tmp_path / "timing_module.py"
|
|
454
|
+
edited_module_path.write_text(_BUDGET_COUNTS_EVERY_TIMEOUT, encoding="utf-8")
|
|
455
|
+
old_helper_line = ' return git_check.returncode != 0\n'
|
|
456
|
+
new_helper_line = ' return git_check.returncode != 0 # checked\n'
|
|
457
|
+
reconstructed_content = resolved_content(
|
|
458
|
+
{
|
|
459
|
+
"file_path": str(edited_module_path),
|
|
460
|
+
"old_string": old_helper_line,
|
|
461
|
+
"new_string": new_helper_line,
|
|
462
|
+
}
|
|
463
|
+
)
|
|
464
|
+
assert find_undercounted_budget(reconstructed_content) is None
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def test_resolved_content_returns_empty_when_edit_old_string_is_absent(
|
|
468
|
+
tmp_path: pathlib.Path,
|
|
469
|
+
) -> None:
|
|
470
|
+
edited_module_path = tmp_path / "timing_module.py"
|
|
471
|
+
edited_module_path.write_text(_BUDGET_COUNTS_EVERY_TIMEOUT, encoding="utf-8")
|
|
472
|
+
reconstructed_content = resolved_content(
|
|
473
|
+
{
|
|
474
|
+
"file_path": str(edited_module_path),
|
|
475
|
+
"old_string": "no such line in the file\n",
|
|
476
|
+
"new_string": "replacement\n",
|
|
477
|
+
}
|
|
478
|
+
)
|
|
479
|
+
assert reconstructed_content == ""
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _run_hook_on_content(content: str) -> subprocess.CompletedProcess[str]:
|
|
483
|
+
hook_input = json.dumps(
|
|
484
|
+
{
|
|
485
|
+
"tool_name": "Write",
|
|
486
|
+
"tool_input": {"file_path": "packages/example/timing_module.py", "content": content},
|
|
487
|
+
}
|
|
488
|
+
)
|
|
489
|
+
return subprocess.run(
|
|
490
|
+
[sys.executable, str(_HOOK_DIR / "subprocess_budget_completeness.py")],
|
|
491
|
+
input=hook_input,
|
|
492
|
+
capture_output=True,
|
|
493
|
+
text=True,
|
|
494
|
+
timeout=15,
|
|
495
|
+
check=False,
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def test_full_hook_denies_write_with_undercounted_budget() -> None:
|
|
500
|
+
completed_hook = _run_hook_on_content(_BUDGET_FLAGS_GIT_TIMEOUT_OMISSION)
|
|
501
|
+
assert completed_hook.returncode == 0
|
|
502
|
+
hook_output = json.loads(completed_hook.stdout)
|
|
503
|
+
decision = hook_output["hookSpecificOutput"]["permissionDecision"]
|
|
504
|
+
assert decision == "deny"
|
|
505
|
+
assert "5s" in hook_output["hookSpecificOutput"]["permissionDecisionReason"]
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def test_full_hook_allows_write_with_complete_budget() -> None:
|
|
509
|
+
completed_hook = _run_hook_on_content(_BUDGET_COUNTS_EVERY_TIMEOUT)
|
|
510
|
+
assert completed_hook.returncode == 0
|
|
511
|
+
assert completed_hook.stdout.strip() == ""
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def test_full_hook_ignores_a_non_string_file_path() -> None:
|
|
515
|
+
hook_input = json.dumps(
|
|
516
|
+
{
|
|
517
|
+
"tool_name": "Write",
|
|
518
|
+
"tool_input": {"file_path": 5, "content": _BUDGET_FLAGS_GIT_TIMEOUT_OMISSION},
|
|
519
|
+
}
|
|
520
|
+
)
|
|
521
|
+
completed_hook = subprocess.run(
|
|
522
|
+
[sys.executable, str(_HOOK_DIR / "subprocess_budget_completeness.py")],
|
|
523
|
+
input=hook_input,
|
|
524
|
+
capture_output=True,
|
|
525
|
+
text=True,
|
|
526
|
+
timeout=15,
|
|
527
|
+
check=False,
|
|
528
|
+
)
|
|
529
|
+
assert completed_hook.returncode == 0
|
|
530
|
+
assert completed_hook.stdout.strip() == ""
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def test_full_hook_exempts_test_files_from_the_budget_gate() -> None:
|
|
534
|
+
hook_input = json.dumps(
|
|
535
|
+
{
|
|
536
|
+
"tool_name": "Write",
|
|
537
|
+
"tool_input": {
|
|
538
|
+
"file_path": "packages/example/test_timing_module.py",
|
|
539
|
+
"content": _BUDGET_FLAGS_GIT_TIMEOUT_OMISSION,
|
|
540
|
+
},
|
|
541
|
+
}
|
|
542
|
+
)
|
|
543
|
+
completed_hook = subprocess.run(
|
|
544
|
+
[sys.executable, str(_HOOK_DIR / "subprocess_budget_completeness.py")],
|
|
545
|
+
input=hook_input,
|
|
546
|
+
capture_output=True,
|
|
547
|
+
text=True,
|
|
548
|
+
timeout=15,
|
|
549
|
+
check=False,
|
|
550
|
+
)
|
|
551
|
+
assert completed_hook.returncode == 0
|
|
552
|
+
assert completed_hook.stdout.strip() == ""
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def test_full_hook_denies_edit_that_adds_a_timeout_to_a_non_budget_helper(
|
|
556
|
+
production_module_path: pathlib.Path,
|
|
557
|
+
) -> None:
|
|
558
|
+
edited_module_path = production_module_path
|
|
559
|
+
edited_module_path.write_text(_BUDGET_COUNTS_EVERY_TIMEOUT, encoding="utf-8")
|
|
560
|
+
old_helper_line = (
|
|
561
|
+
' git_check = subprocess.run(["git", "ls-files", file_path],'
|
|
562
|
+
" timeout=GIT_CHECK_TIMEOUT_SECONDS)\n"
|
|
563
|
+
)
|
|
564
|
+
new_helper_line = (
|
|
565
|
+
' git_check = subprocess.run(["git", "ls-files", file_path], timeout=45)\n'
|
|
566
|
+
)
|
|
567
|
+
hook_input = json.dumps(
|
|
568
|
+
{
|
|
569
|
+
"tool_name": "Edit",
|
|
570
|
+
"tool_input": {
|
|
571
|
+
"file_path": str(edited_module_path),
|
|
572
|
+
"old_string": old_helper_line,
|
|
573
|
+
"new_string": new_helper_line,
|
|
574
|
+
},
|
|
575
|
+
}
|
|
576
|
+
)
|
|
577
|
+
completed_hook = subprocess.run(
|
|
578
|
+
[sys.executable, str(_HOOK_DIR / "subprocess_budget_completeness.py")],
|
|
579
|
+
input=hook_input,
|
|
580
|
+
capture_output=True,
|
|
581
|
+
text=True,
|
|
582
|
+
timeout=15,
|
|
583
|
+
check=False,
|
|
584
|
+
)
|
|
585
|
+
assert completed_hook.returncode == 0
|
|
586
|
+
hook_output = json.loads(completed_hook.stdout)
|
|
587
|
+
assert hook_output["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
588
|
+
assert "45s" in hook_output["hookSpecificOutput"]["permissionDecisionReason"]
|