claude-dev-env 1.58.0 → 1.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/CLAUDE.md +2 -2
  2. package/_shared/pr-loop/scripts/code_rules_gate.py +36 -3
  3. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +6 -0
  4. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
  5. package/_shared/pr-loop/scripts/reviews_disabled.py +12 -0
  6. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +265 -0
  7. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +29 -0
  8. package/audit-rubrics/category_rubrics/category-b-selector-engine-compat.md +1 -1
  9. package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -0
  10. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
  11. package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
  12. package/bin/install.mjs +100 -27
  13. package/bin/install.test.mjs +133 -1
  14. package/docs/CODE_RULES.md +3 -3
  15. package/hooks/blocking/code_rules_annotations_length.py +153 -0
  16. package/hooks/blocking/code_rules_dead_dataclass_field.py +319 -0
  17. package/hooks/blocking/code_rules_dead_module_constant.py +321 -0
  18. package/hooks/blocking/code_rules_duplicate_body.py +439 -0
  19. package/hooks/blocking/code_rules_enforcer.py +190 -21
  20. package/hooks/blocking/code_rules_magic_values.py +98 -0
  21. package/hooks/blocking/code_rules_shared.py +41 -0
  22. package/hooks/blocking/code_rules_typeddict_stub.py +172 -0
  23. package/hooks/blocking/config/__init__.py +5 -0
  24. package/hooks/blocking/config/verified_commit_constants.py +106 -0
  25. package/hooks/blocking/destructive_command_blocker.py +1027 -12
  26. package/hooks/blocking/hook_prose_detector_consistency.py +150 -0
  27. package/hooks/blocking/subprocess_budget_completeness.py +380 -0
  28. package/hooks/blocking/test_code_rules_enforcer_annotations.py +225 -0
  29. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
  30. package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +146 -0
  31. package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +467 -0
  32. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +188 -0
  33. package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +330 -0
  34. package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +179 -0
  35. package/hooks/blocking/test_code_rules_enforcer_magic_slice_bounds.py +133 -0
  36. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias.py +415 -0
  37. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +156 -0
  38. package/hooks/blocking/test_destructive_command_blocker.py +622 -3
  39. package/hooks/blocking/test_hook_prose_detector_consistency.py +265 -0
  40. package/hooks/blocking/test_subprocess_budget_completeness.py +588 -0
  41. package/hooks/blocking/test_verdict_directory_write_blocker.py +720 -0
  42. package/hooks/blocking/test_verification_verdict_store.py +278 -0
  43. package/hooks/blocking/test_verified_commit_gate.py +368 -0
  44. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -0
  45. package/hooks/blocking/test_verifier_verdict_minter.py +214 -0
  46. package/hooks/blocking/test_workflow_substitution_slot_blocker.py +242 -0
  47. package/hooks/blocking/verdict_directory_write_blocker.py +667 -0
  48. package/hooks/blocking/verification_verdict_store.py +446 -0
  49. package/hooks/blocking/verified_commit_gate.py +523 -0
  50. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +152 -0
  51. package/hooks/blocking/verifier_verdict_minter.py +299 -0
  52. package/hooks/blocking/workflow_substitution_slot_blocker.py +159 -0
  53. package/hooks/diagnostic/test_hook_log_extractor.py +3 -3
  54. package/hooks/hooks.json +58 -1
  55. package/hooks/hooks_constants/blocking_check_limits.py +1 -0
  56. package/hooks/hooks_constants/code_rules_enforcer_constants.py +16 -0
  57. package/hooks/hooks_constants/dead_dataclass_field_constants.py +25 -0
  58. package/hooks/hooks_constants/dead_module_constant_constants.py +20 -0
  59. package/hooks/hooks_constants/destructive_command_segment_constants.py +178 -0
  60. package/hooks/hooks_constants/duplicate_function_body_constants.py +34 -0
  61. package/hooks/hooks_constants/hook_prose_detector_consistency_constants.py +30 -0
  62. package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +1 -1
  63. package/hooks/hooks_constants/subprocess_budget_completeness_constants.py +5 -0
  64. package/hooks/hooks_constants/workflow_substitution_slot_blocker_constants.py +22 -0
  65. package/package.json +1 -1
  66. package/rules/docstring-prose-matches-implementation.md +43 -0
  67. package/rules/file-global-constants.md +7 -1
  68. package/rules/hook-prose-matches-detector.md +26 -0
  69. package/rules/no-cross-skill-duplicate-helpers.md +29 -0
  70. package/rules/no-inline-destructive-literals.md +11 -0
  71. package/rules/workflow-substitution-slots.md +7 -0
  72. package/skills/_shared/pr-loop/scripts/preflight_worktree.py +392 -0
  73. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +70 -0
  74. package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +263 -0
  75. package/skills/autoconverge/SKILL.md +67 -19
  76. package/skills/autoconverge/reference/closing-report.md +59 -17
  77. package/skills/autoconverge/reference/convergence.md +7 -3
  78. package/skills/autoconverge/reference/stop-conditions.md +7 -2
  79. package/skills/autoconverge/workflow/aggregate_runs.py +371 -0
  80. package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +193 -76
  81. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +76 -0
  82. package/skills/autoconverge/workflow/converge.contract.test.mjs +206 -206
  83. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +265 -0
  84. package/skills/autoconverge/workflow/converge.mjs +234 -42
  85. package/skills/autoconverge/workflow/convergence_summary.py +110 -0
  86. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +2 -0
  87. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +7 -0
  88. package/skills/autoconverge/workflow/render_report.py +488 -397
  89. package/skills/autoconverge/workflow/test_aggregate_runs.py +134 -0
  90. package/skills/autoconverge/workflow/test_convergence_summary.py +132 -0
  91. package/skills/autoconverge/workflow/test_render_report.py +488 -259
  92. package/skills/pr-converge/reference/per-tick.md +28 -8
  93. package/skills/pr-converge/scripts/check_convergence.py +195 -64
  94. package/skills/pr-converge/scripts/test_check_convergence.py +173 -2
  95. package/skills/rebase/SKILL.md +2 -4
  96. package/skills/update/SKILL.md +37 -5
  97. package/system-prompts/software-engineer.xml +2 -6
  98. package/hooks/blocking/content_search_to_zoekt_redirector.py +0 -59
  99. package/hooks/blocking/content_search_zoekt_bash_block_reason.py +0 -25
  100. package/hooks/blocking/content_search_zoekt_block_payload.py +0 -21
  101. package/hooks/blocking/content_search_zoekt_indexed_paths.py +0 -24
  102. package/hooks/blocking/content_search_zoekt_indexed_roots_config.py +0 -131
  103. package/hooks/blocking/content_search_zoekt_redirect_guidance.py +0 -52
  104. package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +0 -61
  105. package/hooks/blocking/test_content_search_to_zoekt_redirector_unit.py +0 -92
  106. package/hooks/blocking/test_content_search_zoekt_indexed_roots_config.py +0 -102
@@ -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"]