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.
- 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-b-selector-engine-compat.md +1 -1
- package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
- package/bin/install.mjs +100 -27
- package/bin/install.test.mjs +133 -1
- 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_dead_module_constant.py +321 -0
- package/hooks/blocking/code_rules_duplicate_body.py +439 -0
- package/hooks/blocking/code_rules_enforcer.py +190 -21
- package/hooks/blocking/code_rules_magic_values.py +98 -0
- package/hooks/blocking/code_rules_shared.py +41 -0
- package/hooks/blocking/code_rules_typeddict_stub.py +172 -0
- package/hooks/blocking/config/__init__.py +5 -0
- package/hooks/blocking/config/verified_commit_constants.py +106 -0
- package/hooks/blocking/destructive_command_blocker.py +1027 -12
- package/hooks/blocking/hook_prose_detector_consistency.py +150 -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_cross_skill_duplicate.py +146 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +467 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +188 -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_code_rules_enforcer_zero_payload_alias.py +415 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +156 -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_subprocess_budget_completeness.py +588 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +720 -0
- package/hooks/blocking/test_verification_verdict_store.py +278 -0
- package/hooks/blocking/test_verified_commit_gate.py +368 -0
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -0
- package/hooks/blocking/test_verifier_verdict_minter.py +214 -0
- package/hooks/blocking/test_workflow_substitution_slot_blocker.py +242 -0
- package/hooks/blocking/verdict_directory_write_blocker.py +667 -0
- package/hooks/blocking/verification_verdict_store.py +446 -0
- package/hooks/blocking/verified_commit_gate.py +523 -0
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +152 -0
- package/hooks/blocking/verifier_verdict_minter.py +299 -0
- package/hooks/blocking/workflow_substitution_slot_blocker.py +159 -0
- package/hooks/diagnostic/test_hook_log_extractor.py +3 -3
- package/hooks/hooks.json +58 -1
- package/hooks/hooks_constants/blocking_check_limits.py +1 -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/dead_module_constant_constants.py +20 -0
- package/hooks/hooks_constants/destructive_command_segment_constants.py +178 -0
- package/hooks/hooks_constants/duplicate_function_body_constants.py +34 -0
- package/hooks/hooks_constants/hook_prose_detector_consistency_constants.py +30 -0
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +1 -1
- package/hooks/hooks_constants/subprocess_budget_completeness_constants.py +5 -0
- package/hooks/hooks_constants/workflow_substitution_slot_blocker_constants.py +22 -0
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +43 -0
- package/rules/file-global-constants.md +7 -1
- package/rules/hook-prose-matches-detector.md +26 -0
- package/rules/no-cross-skill-duplicate-helpers.md +29 -0
- package/rules/no-inline-destructive-literals.md +11 -0
- package/rules/workflow-substitution-slots.md +7 -0
- package/skills/_shared/pr-loop/scripts/preflight_worktree.py +392 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +70 -0
- package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +263 -0
- package/skills/autoconverge/SKILL.md +67 -19
- package/skills/autoconverge/reference/closing-report.md +59 -17
- package/skills/autoconverge/reference/convergence.md +7 -3
- package/skills/autoconverge/reference/stop-conditions.md +7 -2
- package/skills/autoconverge/workflow/aggregate_runs.py +371 -0
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +193 -76
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +76 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +206 -206
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +265 -0
- package/skills/autoconverge/workflow/converge.mjs +234 -42
- package/skills/autoconverge/workflow/convergence_summary.py +110 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +7 -0
- package/skills/autoconverge/workflow/render_report.py +488 -397
- package/skills/autoconverge/workflow/test_aggregate_runs.py +134 -0
- package/skills/autoconverge/workflow/test_convergence_summary.py +132 -0
- package/skills/autoconverge/workflow/test_render_report.py +488 -259
- package/skills/pr-converge/reference/per-tick.md +28 -8
- package/skills/pr-converge/scripts/check_convergence.py +195 -64
- package/skills/pr-converge/scripts/test_check_convergence.py +173 -2
- package/skills/rebase/SKILL.md +2 -4
- package/skills/update/SKILL.md +37 -5
- package/system-prompts/software-engineer.xml +2 -6
- package/hooks/blocking/content_search_to_zoekt_redirector.py +0 -59
- package/hooks/blocking/content_search_zoekt_bash_block_reason.py +0 -25
- package/hooks/blocking/content_search_zoekt_block_payload.py +0 -21
- package/hooks/blocking/content_search_zoekt_indexed_paths.py +0 -24
- package/hooks/blocking/content_search_zoekt_indexed_roots_config.py +0 -131
- package/hooks/blocking/content_search_zoekt_redirect_guidance.py +0 -52
- package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +0 -61
- package/hooks/blocking/test_content_search_to_zoekt_redirector_unit.py +0 -92
- package/hooks/blocking/test_content_search_zoekt_indexed_roots_config.py +0 -102
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
"""Tests for check_zero_payload_function_alias — flag pass-through function aliases.
|
|
2
|
+
|
|
3
|
+
CODE_RULES §9.5 discourages indirection without payload. A function whose entire
|
|
4
|
+
body (after an optional docstring) is a single `return other_function(...)` that
|
|
5
|
+
forwards its own parameters unchanged to another function defined in the same
|
|
6
|
+
module is a second name for one behavior. Callers should invoke the real function
|
|
7
|
+
directly. This check operates at function granularity, complementing the
|
|
8
|
+
module-level thin-wrapper check.
|
|
9
|
+
|
|
10
|
+
Hook infrastructure is NOT exempt: pass-through aliases inside hook modules are
|
|
11
|
+
the original motivating case. Test files and config files remain exempt because
|
|
12
|
+
re-binding aliases are legitimate scaffolding there.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import importlib.util
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from types import ModuleType
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _load_enforcer_module() -> ModuleType:
|
|
23
|
+
module_path = Path(__file__).parent / "code_rules_enforcer.py"
|
|
24
|
+
spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
|
|
25
|
+
assert spec is not None
|
|
26
|
+
assert spec.loader is not None
|
|
27
|
+
module = importlib.util.module_from_spec(spec)
|
|
28
|
+
spec.loader.exec_module(module)
|
|
29
|
+
return module
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
code_rules_enforcer = _load_enforcer_module()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def check_zero_payload_function_alias(content: str, file_path: str) -> list[str]:
|
|
36
|
+
return code_rules_enforcer.check_zero_payload_function_alias(content, file_path)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
PRODUCTION_FILE_PATH = "/project/src/detection.py"
|
|
40
|
+
HOOK_INFRASTRUCTURE_PATH = "/home/user/.claude/hooks/blocking/detection.py"
|
|
41
|
+
TEST_FILE_PATH = "/project/src/test_detection.py"
|
|
42
|
+
CONFIG_FILE_PATH = "/project/config/detection.py"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_should_flag_pass_through_alias_forwarding_same_parameters() -> None:
|
|
46
|
+
source = (
|
|
47
|
+
"def find_bare_path_segments(content: str) -> set[str]:\n"
|
|
48
|
+
" return {part for part in content.split() if part}\n"
|
|
49
|
+
"\n"
|
|
50
|
+
"def find_bare_index_segments(content: str) -> set[str]:\n"
|
|
51
|
+
" return find_bare_path_segments(content)\n"
|
|
52
|
+
)
|
|
53
|
+
issues = check_zero_payload_function_alias(source, PRODUCTION_FILE_PATH)
|
|
54
|
+
assert any("find_bare_index_segments" in each for each in issues), (
|
|
55
|
+
f"Expected pass-through alias flag, got: {issues!r}"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_should_flag_alias_inside_hook_infrastructure() -> None:
|
|
60
|
+
source = (
|
|
61
|
+
"def find_bare_path_segments(content: str) -> set[str]:\n"
|
|
62
|
+
" return {part for part in content.split() if part}\n"
|
|
63
|
+
"\n"
|
|
64
|
+
"def find_bare_index_segments(content: str) -> set[str]:\n"
|
|
65
|
+
" return find_bare_path_segments(content)\n"
|
|
66
|
+
)
|
|
67
|
+
issues = check_zero_payload_function_alias(source, HOOK_INFRASTRUCTURE_PATH)
|
|
68
|
+
assert any("find_bare_index_segments" in each for each in issues), (
|
|
69
|
+
f"Hook infrastructure is the motivating case and must be flagged, got: {issues!r}"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_should_flag_alias_with_docstring_before_return() -> None:
|
|
74
|
+
source = (
|
|
75
|
+
"def compute_total(amount: int) -> int:\n"
|
|
76
|
+
" return amount * 2\n"
|
|
77
|
+
"\n"
|
|
78
|
+
"def calculate_total(amount: int) -> int:\n"
|
|
79
|
+
' """Forward to compute_total."""\n'
|
|
80
|
+
" return compute_total(amount)\n"
|
|
81
|
+
)
|
|
82
|
+
issues = check_zero_payload_function_alias(source, PRODUCTION_FILE_PATH)
|
|
83
|
+
assert any("calculate_total" in each for each in issues), (
|
|
84
|
+
f"A docstring before the single return must not hide the alias, got: {issues!r}"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_should_not_flag_function_that_transforms_arguments() -> None:
|
|
89
|
+
source = (
|
|
90
|
+
"def find_bare_path_segments(content: str) -> set[str]:\n"
|
|
91
|
+
" return {part for part in content.split() if part}\n"
|
|
92
|
+
"\n"
|
|
93
|
+
"def find_stripped_segments(content: str) -> set[str]:\n"
|
|
94
|
+
" return find_bare_path_segments(content.strip())\n"
|
|
95
|
+
)
|
|
96
|
+
issues = check_zero_payload_function_alias(source, PRODUCTION_FILE_PATH)
|
|
97
|
+
assert issues == [], (
|
|
98
|
+
f"Transformed argument adds payload and must not be flagged, got: {issues!r}"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_should_not_flag_function_that_reorders_arguments() -> None:
|
|
103
|
+
source = (
|
|
104
|
+
"def divide(numerator: int, denominator: int) -> float:\n"
|
|
105
|
+
" return numerator / denominator\n"
|
|
106
|
+
"\n"
|
|
107
|
+
"def inverse_divide(denominator: int, numerator: int) -> float:\n"
|
|
108
|
+
" return divide(numerator, denominator)\n"
|
|
109
|
+
)
|
|
110
|
+
issues = check_zero_payload_function_alias(source, PRODUCTION_FILE_PATH)
|
|
111
|
+
assert issues == [], (
|
|
112
|
+
f"Reordered arguments change behavior and must not be flagged, got: {issues!r}"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_should_not_flag_call_to_external_function() -> None:
|
|
117
|
+
source = (
|
|
118
|
+
"from other_module import real_helper\n"
|
|
119
|
+
"\n"
|
|
120
|
+
"def public_helper(value: int) -> int:\n"
|
|
121
|
+
" return real_helper(value)\n"
|
|
122
|
+
)
|
|
123
|
+
issues = check_zero_payload_function_alias(source, PRODUCTION_FILE_PATH)
|
|
124
|
+
assert issues == [], (
|
|
125
|
+
f"A boundary wrapper around an imported function is not a same-module alias, "
|
|
126
|
+
f"got: {issues!r}"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_should_not_flag_method_call() -> None:
|
|
131
|
+
source = (
|
|
132
|
+
"def normalize(text: str) -> str:\n"
|
|
133
|
+
" return text.strip()\n"
|
|
134
|
+
)
|
|
135
|
+
issues = check_zero_payload_function_alias(source, PRODUCTION_FILE_PATH)
|
|
136
|
+
assert issues == [], (
|
|
137
|
+
f"A call to a method on a parameter is real work, got: {issues!r}"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_should_not_flag_function_with_multiple_statements() -> None:
|
|
142
|
+
source = (
|
|
143
|
+
"def find_bare_path_segments(content: str) -> set[str]:\n"
|
|
144
|
+
" return {part for part in content.split() if part}\n"
|
|
145
|
+
"\n"
|
|
146
|
+
"def find_logged_segments(content: str) -> set[str]:\n"
|
|
147
|
+
" all_segments = find_bare_path_segments(content)\n"
|
|
148
|
+
" return all_segments\n"
|
|
149
|
+
)
|
|
150
|
+
issues = check_zero_payload_function_alias(source, PRODUCTION_FILE_PATH)
|
|
151
|
+
assert issues == [], (
|
|
152
|
+
f"A body with an intermediate binding is not a single pass-through, got: {issues!r}"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_should_not_flag_function_that_drops_keyword_only_parameter() -> None:
|
|
157
|
+
source = (
|
|
158
|
+
"def target(first: int) -> int:\n"
|
|
159
|
+
" return first\n"
|
|
160
|
+
"\n"
|
|
161
|
+
"def alias(first: int, *, second: int) -> int:\n"
|
|
162
|
+
" return target(first)\n"
|
|
163
|
+
)
|
|
164
|
+
issues = check_zero_payload_function_alias(source, PRODUCTION_FILE_PATH)
|
|
165
|
+
assert issues == [], (
|
|
166
|
+
f"Dropping a keyword-only parameter changes behavior, not a pure alias, got: {issues!r}"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_should_not_flag_function_that_drops_var_positional_parameter() -> None:
|
|
171
|
+
source = (
|
|
172
|
+
"def target(first: int) -> int:\n"
|
|
173
|
+
" return first\n"
|
|
174
|
+
"\n"
|
|
175
|
+
"def alias(first: int, *rest: int) -> int:\n"
|
|
176
|
+
" return target(first)\n"
|
|
177
|
+
)
|
|
178
|
+
issues = check_zero_payload_function_alias(source, PRODUCTION_FILE_PATH)
|
|
179
|
+
assert issues == [], (
|
|
180
|
+
f"Dropping *args changes behavior, not a pure alias, got: {issues!r}"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_should_not_flag_function_that_drops_var_keyword_parameter() -> None:
|
|
185
|
+
source = (
|
|
186
|
+
"def target(first: int) -> int:\n"
|
|
187
|
+
" return first\n"
|
|
188
|
+
"\n"
|
|
189
|
+
"def alias(first: int, **rest: int) -> int:\n"
|
|
190
|
+
" return target(first)\n"
|
|
191
|
+
)
|
|
192
|
+
issues = check_zero_payload_function_alias(source, PRODUCTION_FILE_PATH)
|
|
193
|
+
assert issues == [], (
|
|
194
|
+
f"Dropping **kwargs changes behavior, not a pure alias, got: {issues!r}"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_should_skip_test_file() -> None:
|
|
199
|
+
source = (
|
|
200
|
+
"def find_bare_path_segments(content: str) -> set[str]:\n"
|
|
201
|
+
" return {part for part in content.split() if part}\n"
|
|
202
|
+
"\n"
|
|
203
|
+
"def find_bare_index_segments(content: str) -> set[str]:\n"
|
|
204
|
+
" return find_bare_path_segments(content)\n"
|
|
205
|
+
)
|
|
206
|
+
issues = check_zero_payload_function_alias(source, TEST_FILE_PATH)
|
|
207
|
+
assert issues == [], f"Test files exempt, got: {issues!r}"
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_should_skip_config_file() -> None:
|
|
211
|
+
source = (
|
|
212
|
+
"def find_bare_path_segments(content: str) -> set[str]:\n"
|
|
213
|
+
" return {part for part in content.split() if part}\n"
|
|
214
|
+
"\n"
|
|
215
|
+
"def find_bare_index_segments(content: str) -> set[str]:\n"
|
|
216
|
+
" return find_bare_path_segments(content)\n"
|
|
217
|
+
)
|
|
218
|
+
issues = check_zero_payload_function_alias(source, CONFIG_FILE_PATH)
|
|
219
|
+
assert issues == [], f"config/ files exempt, got: {issues!r}"
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_should_handle_syntax_error_gracefully() -> None:
|
|
223
|
+
issues = check_zero_payload_function_alias("def broken(\n", PRODUCTION_FILE_PATH)
|
|
224
|
+
assert issues == [], f"Syntax error must yield no issues, got: {issues!r}"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_should_not_flag_empty_file() -> None:
|
|
228
|
+
issues = check_zero_payload_function_alias("", PRODUCTION_FILE_PATH)
|
|
229
|
+
assert issues == [], f"Empty file must not be flagged, got: {issues!r}"
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def test_should_not_flag_recursive_self_call() -> None:
|
|
233
|
+
source = (
|
|
234
|
+
"def walk(node: int) -> int:\n"
|
|
235
|
+
" return walk(node)\n"
|
|
236
|
+
)
|
|
237
|
+
issues = check_zero_payload_function_alias(source, PRODUCTION_FILE_PATH)
|
|
238
|
+
assert issues == [], (
|
|
239
|
+
f"A self-call is recursion, not a zero-payload alias, got: {issues!r}"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def test_should_flag_through_validate_content_for_hook_file() -> None:
|
|
244
|
+
source = (
|
|
245
|
+
"def find_bare_path_segments(content: str) -> set[str]:\n"
|
|
246
|
+
" return {part for part in content.split() if part}\n"
|
|
247
|
+
"\n"
|
|
248
|
+
"def find_bare_index_segments(content: str) -> set[str]:\n"
|
|
249
|
+
" return find_bare_path_segments(content)\n"
|
|
250
|
+
)
|
|
251
|
+
issues = code_rules_enforcer.validate_content(source, HOOK_INFRASTRUCTURE_PATH)
|
|
252
|
+
assert any("find_bare_index_segments" in each for each in issues), (
|
|
253
|
+
f"validate_content must surface the alias for hook files, got: {issues!r}"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def test_should_not_flag_property_decorated_forwarder() -> None:
|
|
258
|
+
source = (
|
|
259
|
+
"def compute_total(amount: int) -> int:\n"
|
|
260
|
+
" return amount * 2\n"
|
|
261
|
+
"\n"
|
|
262
|
+
"class Cart:\n"
|
|
263
|
+
" @property\n"
|
|
264
|
+
" def total(self, amount: int) -> int:\n"
|
|
265
|
+
" return compute_total(amount)\n"
|
|
266
|
+
)
|
|
267
|
+
issues = check_zero_payload_function_alias(source, PRODUCTION_FILE_PATH)
|
|
268
|
+
assert issues == [], (
|
|
269
|
+
f"A @property forwarder gains attribute semantics, not a zero-payload alias, got: {issues!r}"
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def test_should_not_flag_lru_cache_decorated_forwarder() -> None:
|
|
274
|
+
source = (
|
|
275
|
+
"import functools\n"
|
|
276
|
+
"\n"
|
|
277
|
+
"def lookup(key: int) -> int:\n"
|
|
278
|
+
" return key\n"
|
|
279
|
+
"\n"
|
|
280
|
+
"@functools.lru_cache\n"
|
|
281
|
+
"def cached_lookup(key: int) -> int:\n"
|
|
282
|
+
" return lookup(key)\n"
|
|
283
|
+
)
|
|
284
|
+
issues = check_zero_payload_function_alias(source, PRODUCTION_FILE_PATH)
|
|
285
|
+
assert issues == [], (
|
|
286
|
+
f"An @lru_cache forwarder adds memoization the target lacks, got: {issues!r}"
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def test_should_not_flag_forwarder_that_adds_a_default_value() -> None:
|
|
291
|
+
source = (
|
|
292
|
+
"def target(first: int, second: int) -> int:\n"
|
|
293
|
+
" return first + second\n"
|
|
294
|
+
"\n"
|
|
295
|
+
"def alias(first: int, second: int = 5) -> int:\n"
|
|
296
|
+
" return target(first, second)\n"
|
|
297
|
+
)
|
|
298
|
+
issues = check_zero_payload_function_alias(source, PRODUCTION_FILE_PATH)
|
|
299
|
+
assert issues == [], (
|
|
300
|
+
f"A default value makes a call shape valid that the target rejects, got: {issues!r}"
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def test_should_flag_async_pass_through_alias() -> None:
|
|
305
|
+
source = (
|
|
306
|
+
"async def real(value: int) -> int:\n"
|
|
307
|
+
" return value\n"
|
|
308
|
+
"\n"
|
|
309
|
+
"async def alias(value: int) -> int:\n"
|
|
310
|
+
" return real(value)\n"
|
|
311
|
+
)
|
|
312
|
+
issues = check_zero_payload_function_alias(source, PRODUCTION_FILE_PATH)
|
|
313
|
+
assert any("alias" in each for each in issues), (
|
|
314
|
+
f"An async pass-through alias must be flagged like its sync twin, got: {issues!r}"
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def test_should_not_flag_sync_alias_to_async_target() -> None:
|
|
319
|
+
source = (
|
|
320
|
+
"async def target(first: int) -> int:\n"
|
|
321
|
+
" return first\n"
|
|
322
|
+
"\n"
|
|
323
|
+
"def alias(first: int) -> int:\n"
|
|
324
|
+
" return target(first)\n"
|
|
325
|
+
)
|
|
326
|
+
issues = check_zero_payload_function_alias(source, PRODUCTION_FILE_PATH)
|
|
327
|
+
assert issues == [], (
|
|
328
|
+
f"A sync alias to an async target returns a coroutine, changing the contract, got: {issues!r}"
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def test_should_not_flag_async_alias_to_sync_target() -> None:
|
|
333
|
+
source = (
|
|
334
|
+
"def target(first: int) -> int:\n"
|
|
335
|
+
" return first\n"
|
|
336
|
+
"\n"
|
|
337
|
+
"async def alias(first: int) -> int:\n"
|
|
338
|
+
" return target(first)\n"
|
|
339
|
+
)
|
|
340
|
+
issues = check_zero_payload_function_alias(source, PRODUCTION_FILE_PATH)
|
|
341
|
+
assert issues == [], (
|
|
342
|
+
f"An async alias to a sync target changes the awaitability contract, got: {issues!r}"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def test_should_not_flag_forwarder_whose_name_is_a_string_dispatch_target() -> None:
|
|
347
|
+
source = (
|
|
348
|
+
"from typing import Literal\n"
|
|
349
|
+
"\n"
|
|
350
|
+
"TransformName = Literal[\"verbatim\", \"near_verbatim\"]\n"
|
|
351
|
+
"\n"
|
|
352
|
+
"def strip_anthropic_refs(text: str) -> str:\n"
|
|
353
|
+
" return text.replace(\"Anthropic\", \"\")\n"
|
|
354
|
+
"\n"
|
|
355
|
+
"def near_verbatim(text: str) -> str:\n"
|
|
356
|
+
" return strip_anthropic_refs(text)\n"
|
|
357
|
+
"\n"
|
|
358
|
+
"def apply_transform(name: TransformName, text: str) -> str:\n"
|
|
359
|
+
" if name == \"near_verbatim\":\n"
|
|
360
|
+
" return near_verbatim(text)\n"
|
|
361
|
+
" return text\n"
|
|
362
|
+
)
|
|
363
|
+
issues = check_zero_payload_function_alias(source, PRODUCTION_FILE_PATH)
|
|
364
|
+
assert issues == [], (
|
|
365
|
+
"A forwarder dispatched by its own name through a string literal must stay, "
|
|
366
|
+
f"got: {issues!r}"
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def test_should_not_flag_forwarder_calling_keyword_only_target_positionally() -> None:
|
|
371
|
+
source = (
|
|
372
|
+
"def target(first: int, *, second: int) -> int:\n"
|
|
373
|
+
" return first + second\n"
|
|
374
|
+
"\n"
|
|
375
|
+
"def alias(first: int, second: int) -> int:\n"
|
|
376
|
+
" return target(first, second)\n"
|
|
377
|
+
)
|
|
378
|
+
issues = check_zero_payload_function_alias(source, PRODUCTION_FILE_PATH)
|
|
379
|
+
assert issues == [], (
|
|
380
|
+
"The target rejects the forwarded positional call (second is keyword-only), "
|
|
381
|
+
f"so the alias is not interchangeable with a direct call, got: {issues!r}"
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def test_should_not_flag_forwarder_whose_arity_mismatches_redefined_target() -> None:
|
|
386
|
+
source = (
|
|
387
|
+
"def target(first: int) -> int:\n"
|
|
388
|
+
" return first\n"
|
|
389
|
+
"\n"
|
|
390
|
+
"def alias(first: int) -> int:\n"
|
|
391
|
+
" return target(first)\n"
|
|
392
|
+
"\n"
|
|
393
|
+
"def target(first: int, second: int) -> int:\n"
|
|
394
|
+
" return first + second\n"
|
|
395
|
+
)
|
|
396
|
+
issues = check_zero_payload_function_alias(source, PRODUCTION_FILE_PATH)
|
|
397
|
+
assert issues == [], (
|
|
398
|
+
"The live target needs two positional arguments but the forwarder passes one, "
|
|
399
|
+
f"so the forwarded call is invalid against the live target, got: {issues!r}"
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def test_should_flag_forwarder_calling_default_bearing_target_positionally() -> None:
|
|
404
|
+
source = (
|
|
405
|
+
"def target(first: int, second: int = 5) -> int:\n"
|
|
406
|
+
" return first + second\n"
|
|
407
|
+
"\n"
|
|
408
|
+
"def alias(first: int, second: int) -> int:\n"
|
|
409
|
+
" return target(first, second)\n"
|
|
410
|
+
)
|
|
411
|
+
issues = check_zero_payload_function_alias(source, PRODUCTION_FILE_PATH)
|
|
412
|
+
assert any("alias" in each for each in issues), (
|
|
413
|
+
"The target accepts the forwarded positional call, so the alias is a true "
|
|
414
|
+
f"pass-through and must be flagged, got: {issues!r}"
|
|
415
|
+
)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Entry-point tests proving the zero-payload-alias check guards hook-infrastructure files.
|
|
2
|
+
|
|
3
|
+
A pass-through alias inside a hook module is the motivating case for the
|
|
4
|
+
zero-payload-alias check, so the deny must fire on the same PreToolUse path a
|
|
5
|
+
live Write into ``packages/claude-dev-env/hooks/blocking`` would take — not only
|
|
6
|
+
through ``validate_content``, which hook files never reach at PreToolUse. These
|
|
7
|
+
tests drive the real ``main()`` stdin entry point and the pre-check CLI with a
|
|
8
|
+
hook-infrastructure target.
|
|
9
|
+
|
|
10
|
+
Each test builds a temporary tree whose tail mirrors a production hook directory
|
|
11
|
+
(``packages/claude-dev-env/hooks/blocking``) so ``is_hook_infrastructure`` matches
|
|
12
|
+
the target path the same way it would for the real directory.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import contextlib
|
|
18
|
+
import io
|
|
19
|
+
import json
|
|
20
|
+
import pathlib
|
|
21
|
+
import shutil
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
24
|
+
import tempfile
|
|
25
|
+
from collections.abc import Iterator
|
|
26
|
+
from types import SimpleNamespace
|
|
27
|
+
|
|
28
|
+
import pytest
|
|
29
|
+
|
|
30
|
+
_HOOK_DIRECTORY = pathlib.Path(__file__).resolve().parent
|
|
31
|
+
_HOOKS_PARENT = _HOOK_DIRECTORY.parent
|
|
32
|
+
if str(_HOOK_DIRECTORY) not in sys.path:
|
|
33
|
+
sys.path.insert(0, str(_HOOK_DIRECTORY))
|
|
34
|
+
if str(_HOOKS_PARENT) not in sys.path:
|
|
35
|
+
sys.path.insert(0, str(_HOOKS_PARENT))
|
|
36
|
+
|
|
37
|
+
from code_rules_enforcer import main # noqa: E402
|
|
38
|
+
|
|
39
|
+
code_rules_enforcer = SimpleNamespace(main=main, sys=sys)
|
|
40
|
+
|
|
41
|
+
_ENFORCER_SCRIPT_PATH = _HOOK_DIRECTORY / "code_rules_enforcer.py"
|
|
42
|
+
|
|
43
|
+
PASS_THROUGH_ALIAS_SOURCE = (
|
|
44
|
+
"def find_bare_path_segments(content: str) -> set[str]:\n"
|
|
45
|
+
" return {part for part in content.split() if part}\n"
|
|
46
|
+
"\n"
|
|
47
|
+
"def find_bare_index_segments(content: str) -> set[str]:\n"
|
|
48
|
+
" return find_bare_path_segments(content)\n"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
_HOOK_INFRASTRUCTURE_TAIL = pathlib.Path("packages") / "claude-dev-env" / "hooks" / "blocking"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.fixture
|
|
55
|
+
def hook_blocking_dir() -> Iterator[pathlib.Path]:
|
|
56
|
+
base_directory = pathlib.Path(tempfile.mkdtemp())
|
|
57
|
+
blocking_directory = base_directory / _HOOK_INFRASTRUCTURE_TAIL
|
|
58
|
+
blocking_directory.mkdir(parents=True)
|
|
59
|
+
try:
|
|
60
|
+
yield blocking_directory
|
|
61
|
+
finally:
|
|
62
|
+
shutil.rmtree(base_directory, ignore_errors=False)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _run_main_with_write_payload(
|
|
66
|
+
file_path: str,
|
|
67
|
+
content: str,
|
|
68
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
69
|
+
capsys: pytest.CaptureFixture[str],
|
|
70
|
+
) -> str:
|
|
71
|
+
"""Drive ``main()`` through its stdin entry point for a Write and return stdout.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
file_path: The on-disk path the Write targets.
|
|
75
|
+
content: The whole-file body the Write would create.
|
|
76
|
+
monkeypatch: The fixture used to redirect ``sys.stdin``.
|
|
77
|
+
capsys: The fixture used to capture the deny payload on stdout.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
The captured stdout, which holds the deny payload when violations fire.
|
|
81
|
+
"""
|
|
82
|
+
write_payload = json.dumps(
|
|
83
|
+
{
|
|
84
|
+
"tool_name": "Write",
|
|
85
|
+
"tool_input": {"file_path": file_path, "content": content},
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
monkeypatch.setattr(code_rules_enforcer.sys, "stdin", io.StringIO(write_payload))
|
|
89
|
+
with contextlib.suppress(SystemExit):
|
|
90
|
+
code_rules_enforcer.main([])
|
|
91
|
+
return capsys.readouterr().out
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_write_of_pass_through_alias_into_hook_directory_denies(
|
|
95
|
+
hook_blocking_dir: pathlib.Path,
|
|
96
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
97
|
+
capsys: pytest.CaptureFixture[str],
|
|
98
|
+
) -> None:
|
|
99
|
+
"""A Write that introduces a pass-through alias into a hook file is denied.
|
|
100
|
+
|
|
101
|
+
The target lives under a hook-infrastructure path the full code-rules suite
|
|
102
|
+
exempts, so this proves the zero-payload-alias check still fires on the exact
|
|
103
|
+
directory its docstring names as the motivating case — at the PreToolUse Write
|
|
104
|
+
point, not only through ``validate_content``."""
|
|
105
|
+
new_file = hook_blocking_dir / "new_blocker.py"
|
|
106
|
+
stdout = _run_main_with_write_payload(
|
|
107
|
+
str(new_file), PASS_THROUGH_ALIAS_SOURCE, monkeypatch, capsys
|
|
108
|
+
)
|
|
109
|
+
assert stdout != "", (
|
|
110
|
+
"A pass-through alias written into a hook-infrastructure file must produce "
|
|
111
|
+
"a deny payload, got empty stdout"
|
|
112
|
+
)
|
|
113
|
+
deny_payload = json.loads(stdout)
|
|
114
|
+
decision = deny_payload["hookSpecificOutput"]["permissionDecision"]
|
|
115
|
+
reason = deny_payload["hookSpecificOutput"]["permissionDecisionReason"]
|
|
116
|
+
assert decision == "deny", f"expected deny, got: {decision!r}"
|
|
117
|
+
assert "find_bare_index_segments" in reason, (
|
|
118
|
+
f"the deny reason must name the pass-through alias, got: {reason!r}"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_precheck_of_pass_through_alias_at_hook_target_exits_nonzero(
|
|
123
|
+
hook_blocking_dir: pathlib.Path,
|
|
124
|
+
tmp_path_factory: pytest.TempPathFactory,
|
|
125
|
+
) -> None:
|
|
126
|
+
"""The pre-check CLI flags a pass-through alias judged at a hook-infrastructure target.
|
|
127
|
+
|
|
128
|
+
Driving the real ``--check`` argv path proves the gate's pre-check mode also
|
|
129
|
+
routes a hook ``.py`` target through the zero-payload-alias check rather than
|
|
130
|
+
exiting clean on the blanket hook-infrastructure exemption."""
|
|
131
|
+
staging_directory = tmp_path_factory.mktemp("staging")
|
|
132
|
+
candidate_file = staging_directory / "candidate.py"
|
|
133
|
+
candidate_file.write_text(PASS_THROUGH_ALIAS_SOURCE, encoding="utf-8")
|
|
134
|
+
target_path = str(hook_blocking_dir / "new_blocker.py")
|
|
135
|
+
completed = subprocess.run(
|
|
136
|
+
[
|
|
137
|
+
sys.executable,
|
|
138
|
+
str(_ENFORCER_SCRIPT_PATH),
|
|
139
|
+
"--check",
|
|
140
|
+
str(candidate_file),
|
|
141
|
+
"--as",
|
|
142
|
+
target_path,
|
|
143
|
+
],
|
|
144
|
+
input="",
|
|
145
|
+
capture_output=True,
|
|
146
|
+
text=True,
|
|
147
|
+
check=False,
|
|
148
|
+
)
|
|
149
|
+
assert completed.returncode == 1, (
|
|
150
|
+
"a pass-through alias at a hook target must exit nonzero, got: "
|
|
151
|
+
f"{completed.returncode}, stdout: {completed.stdout!r}, "
|
|
152
|
+
f"stderr: {completed.stderr!r}"
|
|
153
|
+
)
|
|
154
|
+
assert "find_bare_index_segments" in completed.stdout, (
|
|
155
|
+
f"the pre-check must name the pass-through alias, got: {completed.stdout!r}"
|
|
156
|
+
)
|