claude-dev-env 1.50.1 → 1.50.3
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/audit-contract.md +3 -3
- package/audit-rubrics/category_rubrics/category-e-dead-code.md +3 -2
- package/audit-rubrics/prompts/category-a-api-contracts.md +1 -1
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
- package/audit-rubrics/prompts/category-c-resource-cleanup.md +2 -2
- package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +2 -2
- package/audit-rubrics/prompts/category-e-dead-code.md +5 -4
- package/audit-rubrics/prompts/category-f-silent-failures.md +2 -2
- package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +2 -2
- package/audit-rubrics/prompts/category-h-security-boundaries.md +2 -2
- package/audit-rubrics/prompts/category-i-concurrency.md +2 -2
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +2 -2
- package/audit-rubrics/prompts/category-k-codebase-conflicts.md +2 -2
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/code_rules_annotations_length.py +167 -0
- package/hooks/blocking/code_rules_banned_identifiers.py +385 -0
- package/hooks/blocking/code_rules_boolean_mustcheck.py +350 -0
- package/hooks/blocking/code_rules_comments.py +337 -0
- package/hooks/blocking/code_rules_constants_config.py +252 -0
- package/hooks/blocking/code_rules_docstrings.py +308 -0
- package/hooks/blocking/code_rules_enforcer.py +98 -5807
- package/hooks/blocking/code_rules_imports_logging.py +276 -0
- package/hooks/blocking/code_rules_magic_values.py +180 -0
- package/hooks/blocking/code_rules_mock_completeness.py +295 -0
- package/hooks/blocking/code_rules_naming_collection.py +264 -0
- package/hooks/blocking/code_rules_optional_params.py +288 -0
- package/hooks/blocking/code_rules_paths_syspath.py +186 -0
- package/hooks/blocking/code_rules_probe_chains.py +305 -0
- package/hooks/blocking/code_rules_probe_detection.py +257 -0
- package/hooks/blocking/code_rules_probe_recording.py +225 -0
- package/hooks/blocking/code_rules_scope_binding.py +151 -0
- package/hooks/blocking/code_rules_shared.py +301 -0
- package/hooks/blocking/code_rules_string_magic.py +207 -0
- package/hooks/blocking/code_rules_test_assertions.py +226 -0
- package/hooks/blocking/code_rules_test_branching_except.py +181 -0
- package/hooks/blocking/code_rules_test_isolation.py +341 -0
- package/hooks/blocking/code_rules_type_escape.py +341 -0
- package/hooks/blocking/code_rules_typeddict_stub.py +305 -0
- package/hooks/blocking/code_rules_unused_imports.py +256 -0
- package/hooks/blocking/tdd_enforcer.py +31 -0
- package/hooks/blocking/test_code_rules_constants_config.py +26 -0
- package/hooks/blocking/test_code_rules_enforcer_banned_noun_word.py +5 -2
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -5
- package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +21 -15
- package/hooks/blocking/test_code_rules_enforcer_config_path.py +20 -16
- package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +4 -2
- package/hooks/blocking/test_code_rules_enforcer_function_length.py +18 -13
- package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +1 -2
- package/hooks/blocking/test_code_rules_enforcer_ignored_must_check_return.py +22 -12
- package/hooks/blocking/test_code_rules_enforcer_split_annotations_length.py +55 -0
- package/hooks/blocking/test_code_rules_enforcer_split_banned.py +170 -0
- package/hooks/blocking/test_code_rules_enforcer_split_comments.py +60 -0
- package/hooks/blocking/test_code_rules_enforcer_split_config_path.py +52 -0
- package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +236 -0
- package/hooks/blocking/test_code_rules_enforcer_split_entry_1.py +296 -0
- package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +238 -0
- package/hooks/blocking/test_code_rules_enforcer_split_isolation_1.py +271 -0
- package/hooks/blocking/test_code_rules_enforcer_split_isolation_2.py +283 -0
- package/hooks/blocking/test_code_rules_enforcer_split_isolation_3.py +268 -0
- package/hooks/blocking/test_code_rules_enforcer_split_isolation_4.py +85 -0
- package/hooks/blocking/test_code_rules_enforcer_split_mocks_1.py +303 -0
- package/hooks/blocking/test_code_rules_enforcer_split_mocks_2.py +111 -0
- package/hooks/blocking/test_code_rules_enforcer_split_mustcheck.py +87 -0
- package/hooks/blocking/test_code_rules_enforcer_split_naming.py +107 -0
- package/hooks/blocking/test_code_rules_enforcer_split_optional_params.py +325 -0
- package/hooks/blocking/test_code_rules_enforcer_split_paths_syspath.py +110 -0
- package/hooks/blocking/test_code_rules_enforcer_split_shared.py +44 -0
- package/hooks/blocking/test_code_rules_enforcer_split_string_magic.py +55 -0
- package/hooks/blocking/test_code_rules_enforcer_split_test_assertions.py +56 -0
- package/hooks/blocking/test_code_rules_enforcer_todo_markers.py +21 -15
- package/hooks/blocking/test_code_rules_paths_syspath.py +26 -0
- package/hooks/blocking/test_tdd_enforcer.py +116 -0
- package/hooks/hooks_constants/blocking_check_limits.py +3 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +8 -0
- package/hooks/hooks_constants/sys_path_insert_constants.py +1 -0
- package/package.json +1 -1
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +13 -7
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +21 -11
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +92 -0
- package/skills/bugteam/CONSTRAINTS.md +1 -1
- package/skills/bugteam/PROMPTS.md +20 -48
- package/skills/bugteam/SKILL.md +5 -5
- package/skills/bugteam/reference/audit-and-teammates.md +1 -1
- package/skills/bugteam/reference/audit-contract.md +4 -4
- package/skills/bugteam/reference/design-rationale.md +1 -1
- package/skills/findbugs/SKILL.md +21 -12
- package/skills/fixbugs/SKILL.md +1 -1
- package/skills/qbug/SKILL.md +5 -5
- package/skills/qbug/test_qbug_skill_audit_schema.py +13 -23
- package/skills/refine/SKILL.md +1 -1
- package/hooks/blocking/test_code_rules_enforcer.py +0 -2669
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Behavior tests for the code_rules_constants_config code-rules check module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from types import SimpleNamespace
|
|
8
|
+
|
|
9
|
+
_BLOCKING_DIRECTORY = str(Path(__file__).resolve().parent)
|
|
10
|
+
_HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
|
|
11
|
+
if _BLOCKING_DIRECTORY not in sys.path:
|
|
12
|
+
sys.path.insert(0, _BLOCKING_DIRECTORY)
|
|
13
|
+
if _HOOKS_DIRECTORY not in sys.path:
|
|
14
|
+
sys.path.insert(0, _HOOKS_DIRECTORY)
|
|
15
|
+
|
|
16
|
+
from code_rules_constants_config import ( # noqa: E402
|
|
17
|
+
_is_exempt_for_advisory_scan,
|
|
18
|
+
_scan_function_body_constants,
|
|
19
|
+
check_constants_outside_config,
|
|
20
|
+
check_constants_outside_config_advisory,
|
|
21
|
+
check_file_global_constants_use_count,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
code_rules_enforcer = SimpleNamespace(
|
|
25
|
+
_is_exempt_for_advisory_scan=_is_exempt_for_advisory_scan,
|
|
26
|
+
_scan_function_body_constants=_scan_function_body_constants,
|
|
27
|
+
check_constants_outside_config=check_constants_outside_config,
|
|
28
|
+
check_constants_outside_config_advisory=check_constants_outside_config_advisory,
|
|
29
|
+
check_file_global_constants_use_count=check_file_global_constants_use_count,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
CONSTANTS_OUTSIDE_CONFIG_PRODUCTION_FILE_PATH = "packages/app/services/encoding.py"
|
|
34
|
+
|
|
35
|
+
PRODUCTION_FILE_PATH = "packages/claude-dev-env/hooks/blocking/example_production.py"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_should_flag_constant_used_only_in_class_level_decorator() -> None:
|
|
39
|
+
source = (
|
|
40
|
+
"TIMEOUT = 5\n"
|
|
41
|
+
"\n"
|
|
42
|
+
"def register(value):\n"
|
|
43
|
+
" def wrap(cls):\n"
|
|
44
|
+
" return cls\n"
|
|
45
|
+
" return wrap\n"
|
|
46
|
+
"\n"
|
|
47
|
+
"@register(TIMEOUT)\n"
|
|
48
|
+
"class Foo:\n"
|
|
49
|
+
" pass\n"
|
|
50
|
+
)
|
|
51
|
+
issues = code_rules_enforcer.check_file_global_constants_use_count(
|
|
52
|
+
source, PRODUCTION_FILE_PATH
|
|
53
|
+
)
|
|
54
|
+
assert any(
|
|
55
|
+
"TIMEOUT" in issue and "only 1 function/method" in issue for issue in issues
|
|
56
|
+
), f"Expected class-decorator usage to register as a caller, got: {issues}"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_should_flag_constant_used_once_at_module_scope_and_once_in_function() -> None:
|
|
60
|
+
source = "UPPER = 1\nSHADOW = UPPER\n\ndef lonely_caller():\n return UPPER\n"
|
|
61
|
+
issues = code_rules_enforcer.check_file_global_constants_use_count(
|
|
62
|
+
source, PRODUCTION_FILE_PATH
|
|
63
|
+
)
|
|
64
|
+
assert issues == [], (
|
|
65
|
+
f"Expected module-scope + function usage to count as 2 distinct callers, got: {issues}"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_is_exempt_for_advisory_scan_returns_true_for_config_file() -> None:
|
|
70
|
+
assert code_rules_enforcer._is_exempt_for_advisory_scan("project/config/constants.py") is True
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_is_exempt_for_advisory_scan_returns_true_for_test_file() -> None:
|
|
74
|
+
assert code_rules_enforcer._is_exempt_for_advisory_scan("test_example.py") is True
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_is_exempt_for_advisory_scan_returns_true_for_workflow_registry() -> None:
|
|
78
|
+
assert code_rules_enforcer._is_exempt_for_advisory_scan("app/workflow/states.py") is True
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_is_exempt_for_advisory_scan_returns_true_for_migration() -> None:
|
|
82
|
+
assert code_rules_enforcer._is_exempt_for_advisory_scan("app/migrations/0001_initial.py") is True
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_is_exempt_for_advisory_scan_returns_false_for_production_file() -> None:
|
|
86
|
+
assert code_rules_enforcer._is_exempt_for_advisory_scan("packages/myapp/some_module.py") is False
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_scan_function_body_constants_finds_upper_snake_in_function() -> None:
|
|
90
|
+
source = (
|
|
91
|
+
"def fetch():\n"
|
|
92
|
+
" MAX_RETRIES = 3\n"
|
|
93
|
+
" for attempt in range(MAX_RETRIES):\n"
|
|
94
|
+
" pass\n"
|
|
95
|
+
)
|
|
96
|
+
advisory_issues = code_rules_enforcer._scan_function_body_constants(source)
|
|
97
|
+
assert any("MAX_RETRIES" in issue for issue in advisory_issues)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_scan_function_body_constants_does_not_flag_module_level() -> None:
|
|
101
|
+
source = "MAX_RETRIES = 3\n\ndef fetch():\n pass\n"
|
|
102
|
+
advisory_issues = code_rules_enforcer._scan_function_body_constants(source)
|
|
103
|
+
assert advisory_issues == []
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_advisory_should_not_flag_class_attribute_after_method_def() -> None:
|
|
107
|
+
source_with_class_attribute_after_method = (
|
|
108
|
+
"class ExampleModel:\n"
|
|
109
|
+
" def method_a(self) -> None:\n"
|
|
110
|
+
" pass\n"
|
|
111
|
+
"\n"
|
|
112
|
+
" TABLE_NAME = \"example\"\n"
|
|
113
|
+
)
|
|
114
|
+
advisory_issues = code_rules_enforcer.check_constants_outside_config_advisory(
|
|
115
|
+
source_with_class_attribute_after_method,
|
|
116
|
+
"example_module.py",
|
|
117
|
+
)
|
|
118
|
+
assert advisory_issues == [], (
|
|
119
|
+
"Class-level TABLE_NAME attribute must not be flagged as function-local"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_advisory_should_still_flag_actual_method_body_constant() -> None:
|
|
124
|
+
source_with_method_body_constant = (
|
|
125
|
+
"class ExampleModel:\n"
|
|
126
|
+
" def method_a(self) -> None:\n"
|
|
127
|
+
" MAXIMUM_RETRIES = 3\n"
|
|
128
|
+
" return None\n"
|
|
129
|
+
)
|
|
130
|
+
advisory_issues = code_rules_enforcer.check_constants_outside_config_advisory(
|
|
131
|
+
source_with_method_body_constant,
|
|
132
|
+
"example_module.py",
|
|
133
|
+
)
|
|
134
|
+
assert len(advisory_issues) == 1, (
|
|
135
|
+
"Method-body UPPER_SNAKE constant must still surface as advisory"
|
|
136
|
+
)
|
|
137
|
+
assert "MAXIMUM_RETRIES" in advisory_issues[0]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_advisory_should_flag_annotated_function_body_constant() -> None:
|
|
141
|
+
source_with_annotated_function_body_constant = (
|
|
142
|
+
"def example_function() -> None:\n"
|
|
143
|
+
" MAXIMUM_RETRIES: int = 3\n"
|
|
144
|
+
" return None\n"
|
|
145
|
+
)
|
|
146
|
+
advisory_issues = code_rules_enforcer.check_constants_outside_config_advisory(
|
|
147
|
+
source_with_annotated_function_body_constant,
|
|
148
|
+
"example_module.py",
|
|
149
|
+
)
|
|
150
|
+
assert len(advisory_issues) == 1, (
|
|
151
|
+
"Annotated function-body UPPER_SNAKE constant (PEP 526) must surface as advisory"
|
|
152
|
+
)
|
|
153
|
+
assert "MAXIMUM_RETRIES" in advisory_issues[0]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_advisory_should_flag_outer_constants_after_nested_def() -> None:
|
|
157
|
+
source_with_nested_def = (
|
|
158
|
+
"def outer():\n"
|
|
159
|
+
" OUTER_CONST = 1\n"
|
|
160
|
+
" def inner():\n"
|
|
161
|
+
" INNER_CONST = 2\n"
|
|
162
|
+
" ANOTHER_OUTER = 3\n"
|
|
163
|
+
)
|
|
164
|
+
advisory_issues = code_rules_enforcer.check_constants_outside_config_advisory(
|
|
165
|
+
source_with_nested_def,
|
|
166
|
+
"example_module.py",
|
|
167
|
+
)
|
|
168
|
+
flagged_names = " ".join(advisory_issues)
|
|
169
|
+
assert "OUTER_CONST" in flagged_names, (
|
|
170
|
+
"OUTER_CONST before nested def must be flagged"
|
|
171
|
+
)
|
|
172
|
+
assert "INNER_CONST" in flagged_names, (
|
|
173
|
+
"INNER_CONST inside nested def must be flagged"
|
|
174
|
+
)
|
|
175
|
+
assert "ANOTHER_OUTER" in flagged_names, (
|
|
176
|
+
"ANOTHER_OUTER after nested def must be flagged — this is the regression case"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_check_constants_outside_config_flags_annotated_assignment() -> None:
|
|
181
|
+
source = "TEXT_FILE_ENCODING: str = 'utf-8'\n"
|
|
182
|
+
issues = code_rules_enforcer.check_constants_outside_config(
|
|
183
|
+
source, CONSTANTS_OUTSIDE_CONFIG_PRODUCTION_FILE_PATH
|
|
184
|
+
)
|
|
185
|
+
assert any("TEXT_FILE_ENCODING" in each_issue for each_issue in issues), (
|
|
186
|
+
f"Expected annotated UPPER_SNAKE assignment flagged, got: {issues}"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_check_constants_outside_config_reports_more_than_three_constants() -> None:
|
|
191
|
+
source = (
|
|
192
|
+
"ALPHA_VALUE = 1\n"
|
|
193
|
+
"BETA_VALUE = 2\n"
|
|
194
|
+
"GAMMA_VALUE = 3\n"
|
|
195
|
+
"DELTA_VALUE = 4\n"
|
|
196
|
+
"EPSILON_VALUE = 5\n"
|
|
197
|
+
"\n"
|
|
198
|
+
"def consumer() -> int:\n"
|
|
199
|
+
" return ALPHA_VALUE + BETA_VALUE\n"
|
|
200
|
+
)
|
|
201
|
+
issues = code_rules_enforcer.check_constants_outside_config(
|
|
202
|
+
source, CONSTANTS_OUTSIDE_CONFIG_PRODUCTION_FILE_PATH
|
|
203
|
+
)
|
|
204
|
+
expected_constant_count = 5
|
|
205
|
+
assert len(issues) == expected_constant_count, (
|
|
206
|
+
f"Expected all {expected_constant_count} constants reported, got {len(issues)}: {issues}"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
_SINGLE_CALLER_CONSTANT_SOURCE = (
|
|
211
|
+
"TIMEOUT = 5\n"
|
|
212
|
+
"\n"
|
|
213
|
+
"def lonely_caller() -> int:\n"
|
|
214
|
+
" return TIMEOUT\n"
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
_ENFORCER_ENTRY_FILE_PATH = "packages/claude-dev-env/hooks/blocking/code_rules_enforcer.py"
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_use_count_flags_single_caller_constant_for_ordinary_production_path() -> None:
|
|
221
|
+
issues = code_rules_enforcer.check_file_global_constants_use_count(
|
|
222
|
+
_SINGLE_CALLER_CONSTANT_SOURCE, PRODUCTION_FILE_PATH
|
|
223
|
+
)
|
|
224
|
+
assert any(
|
|
225
|
+
"TIMEOUT" in issue and "only 1 function/method" in issue for issue in issues
|
|
226
|
+
), f"Expected single-caller constant flagged on an ordinary production path, got: {issues}"
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def test_use_count_exempts_enforcer_entry_module_path() -> None:
|
|
230
|
+
issues = code_rules_enforcer.check_file_global_constants_use_count(
|
|
231
|
+
_SINGLE_CALLER_CONSTANT_SOURCE, _ENFORCER_ENTRY_FILE_PATH
|
|
232
|
+
)
|
|
233
|
+
assert issues == [], (
|
|
234
|
+
"The enforcer entry module must be exempt to avoid self-blocking, "
|
|
235
|
+
f"got: {issues}"
|
|
236
|
+
)
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""Behavior tests for the code_rules_enforcer code-rules check module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from types import SimpleNamespace
|
|
8
|
+
|
|
9
|
+
_BLOCKING_DIRECTORY = str(Path(__file__).resolve().parent)
|
|
10
|
+
_HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
|
|
11
|
+
if _BLOCKING_DIRECTORY not in sys.path:
|
|
12
|
+
sys.path.insert(0, _BLOCKING_DIRECTORY)
|
|
13
|
+
if _HOOKS_DIRECTORY not in sys.path:
|
|
14
|
+
sys.path.insert(0, _HOOKS_DIRECTORY)
|
|
15
|
+
|
|
16
|
+
from code_rules_annotations_length import ( # noqa: E402
|
|
17
|
+
FUNCTION_LENGTH_BLOCKING_THRESHOLD,
|
|
18
|
+
)
|
|
19
|
+
from code_rules_enforcer import ( # noqa: E402
|
|
20
|
+
validate_content,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
code_rules_enforcer = SimpleNamespace(
|
|
24
|
+
FUNCTION_LENGTH_BLOCKING_THRESHOLD=FUNCTION_LENGTH_BLOCKING_THRESHOLD,
|
|
25
|
+
validate_content=validate_content,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
DUPLICATED_FORMAT_PRODUCTION_FILE_PATH = "packages/app/services/api_client.py"
|
|
30
|
+
|
|
31
|
+
INCOMPLETE_MOCK_TEST_FILE_PATH = "packages/app/tests/test_orders.py"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _oversized_function_source(name: str) -> str:
|
|
35
|
+
body_line_count = code_rules_enforcer.FUNCTION_LENGTH_BLOCKING_THRESHOLD - 1
|
|
36
|
+
body_lines = [
|
|
37
|
+
f" bound_{each_index} = {each_index}" for each_index in range(body_line_count)
|
|
38
|
+
]
|
|
39
|
+
return f"def {name}() -> None:\n" + "\n".join(body_lines) + "\n"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_should_emit_advisories_for_incomplete_mocks_and_format_patterns_via_validate_content(
|
|
43
|
+
capsys: object,
|
|
44
|
+
) -> None:
|
|
45
|
+
incomplete_mock_source = (
|
|
46
|
+
"mock_order = {'id': 1}\n"
|
|
47
|
+
"\n"
|
|
48
|
+
"def test_order_total() -> None:\n"
|
|
49
|
+
" total = mock_order['total']\n"
|
|
50
|
+
" assert total > 0\n"
|
|
51
|
+
)
|
|
52
|
+
code_rules_enforcer.validate_content(
|
|
53
|
+
incomplete_mock_source, INCOMPLETE_MOCK_TEST_FILE_PATH
|
|
54
|
+
)
|
|
55
|
+
captured = getattr(capsys, "readouterr")()
|
|
56
|
+
assert "mock_order" in captured.err and "total" in captured.err, (
|
|
57
|
+
f"Expected incomplete-mock advisory from validate_content, got: {captured.err!r}"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
repeated_pattern_source = (
|
|
61
|
+
"def get_user(user_id: str) -> str:\n"
|
|
62
|
+
" return f'/api/{user_id}'\n"
|
|
63
|
+
"\n"
|
|
64
|
+
"def get_order(order_id: str) -> str:\n"
|
|
65
|
+
" return f'/api/{order_id}'\n"
|
|
66
|
+
"\n"
|
|
67
|
+
"def get_product(product_id: str) -> str:\n"
|
|
68
|
+
" return f'/api/{product_id}'\n"
|
|
69
|
+
)
|
|
70
|
+
code_rules_enforcer.validate_content(
|
|
71
|
+
repeated_pattern_source, DUPLICATED_FORMAT_PRODUCTION_FILE_PATH
|
|
72
|
+
)
|
|
73
|
+
captured = getattr(capsys, "readouterr")()
|
|
74
|
+
assert "/api/" in captured.err and "3" in captured.err, (
|
|
75
|
+
f"Expected duplicated-format advisory from validate_content, got: {captured.err!r}"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_validate_content_honors_empty_full_file_content_for_thin_wrapper_check() -> None:
|
|
80
|
+
"""An empty `full_file_content` must not be silently replaced with the pre-edit fragment.
|
|
81
|
+
|
|
82
|
+
Regression for loop1-8: the `or` short-circuit at the thin-wrapper call
|
|
83
|
+
site treated `""` identically to `None`, so an Edit collapsing a file to
|
|
84
|
+
empty was scanned against the pre-edit fragment instead of the empty
|
|
85
|
+
post-edit content. Mirror the canonical idiom at line 3438.
|
|
86
|
+
"""
|
|
87
|
+
pre_edit_fragment_with_imports_only = (
|
|
88
|
+
"from real_module import do_thing\n__all__ = ['do_thing']\n"
|
|
89
|
+
)
|
|
90
|
+
issues = code_rules_enforcer.validate_content(
|
|
91
|
+
pre_edit_fragment_with_imports_only,
|
|
92
|
+
"/project/src/aliases.py",
|
|
93
|
+
full_file_content="",
|
|
94
|
+
)
|
|
95
|
+
assert not any("thin wrapper" in each.lower() for each in issues), (
|
|
96
|
+
f"empty post-edit file must not be flagged as a thin wrapper, got: {issues!r}"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_function_length_edit_does_not_block_untouched_long_function() -> None:
|
|
101
|
+
"""loop5-1: editing a short region of a file that already contains an
|
|
102
|
+
untouched oversized function must not produce a blocking function-length
|
|
103
|
+
violation at the PreToolUse layer."""
|
|
104
|
+
untouched_long_function = _oversized_function_source("untouched_long")
|
|
105
|
+
short_helper_before = "def short_helper() -> int:\n return 1\n"
|
|
106
|
+
short_helper_after = "def short_helper() -> int:\n return 2\n"
|
|
107
|
+
prior_full_file = untouched_long_function + "\n" + short_helper_before
|
|
108
|
+
post_edit_full_file = untouched_long_function + "\n" + short_helper_after
|
|
109
|
+
issues = code_rules_enforcer.validate_content(
|
|
110
|
+
short_helper_after,
|
|
111
|
+
"/project/src/edited_module.py",
|
|
112
|
+
old_content=short_helper_before,
|
|
113
|
+
full_file_content=post_edit_full_file,
|
|
114
|
+
prior_full_file_content=prior_full_file,
|
|
115
|
+
)
|
|
116
|
+
assert not any(
|
|
117
|
+
"untouched_long" in each_issue for each_issue in issues
|
|
118
|
+
), f"untouched long function must not block on an unrelated edit, got: {issues!r}"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_function_length_edit_blocks_function_grown_on_changed_lines() -> None:
|
|
122
|
+
"""loop5-1: when the edit itself grows a function past the threshold, the
|
|
123
|
+
function-length violation must still block at the PreToolUse layer."""
|
|
124
|
+
short_function_before = "def grows_now() -> int:\n return 1\n"
|
|
125
|
+
grown_function_after = _oversized_function_source("grows_now")
|
|
126
|
+
prior_full_file = short_function_before
|
|
127
|
+
post_edit_full_file = grown_function_after
|
|
128
|
+
issues = code_rules_enforcer.validate_content(
|
|
129
|
+
grown_function_after,
|
|
130
|
+
"/project/src/edited_module.py",
|
|
131
|
+
old_content=short_function_before,
|
|
132
|
+
full_file_content=post_edit_full_file,
|
|
133
|
+
prior_full_file_content=prior_full_file,
|
|
134
|
+
)
|
|
135
|
+
assert any(
|
|
136
|
+
"grows_now" in each_issue for each_issue in issues
|
|
137
|
+
), f"function grown past threshold on changed lines must block, got: {issues!r}"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_isolation_edit_does_not_block_untouched_probe() -> None:
|
|
141
|
+
"""loop5-3: editing a short region of a test file that already contains an
|
|
142
|
+
untouched HOME probe must not block at the PreToolUse layer."""
|
|
143
|
+
untouched_probe_function = (
|
|
144
|
+
"def test_reads_home() -> None:\n"
|
|
145
|
+
" target_path = Path.home()\n"
|
|
146
|
+
" assert target_path\n"
|
|
147
|
+
)
|
|
148
|
+
short_test_before = "def test_addition() -> None:\n assert 1 + 1 == 2\n"
|
|
149
|
+
short_test_after = "def test_addition() -> None:\n assert 2 + 2 == 4\n"
|
|
150
|
+
header = "from pathlib import Path\n"
|
|
151
|
+
prior_full_file = header + untouched_probe_function + "\n" + short_test_before
|
|
152
|
+
post_edit_full_file = header + untouched_probe_function + "\n" + short_test_after
|
|
153
|
+
issues = code_rules_enforcer.validate_content(
|
|
154
|
+
short_test_after,
|
|
155
|
+
"/project/src/test_edited_module.py",
|
|
156
|
+
old_content=short_test_before,
|
|
157
|
+
full_file_content=post_edit_full_file,
|
|
158
|
+
prior_full_file_content=prior_full_file,
|
|
159
|
+
)
|
|
160
|
+
assert not any(
|
|
161
|
+
"test_reads_home" in each_issue for each_issue in issues
|
|
162
|
+
), f"untouched isolation probe must not block on an unrelated edit, got: {issues!r}"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_isolation_edit_blocks_probe_added_on_changed_lines() -> None:
|
|
166
|
+
"""loop5-3: when the edit introduces a HOME probe, the isolation violation
|
|
167
|
+
must still block at the PreToolUse layer."""
|
|
168
|
+
test_before = "def test_writes() -> None:\n assert True\n"
|
|
169
|
+
test_after = (
|
|
170
|
+
"def test_writes() -> None:\n"
|
|
171
|
+
" target_path = Path.home()\n"
|
|
172
|
+
" assert target_path\n"
|
|
173
|
+
)
|
|
174
|
+
header = "from pathlib import Path\n"
|
|
175
|
+
prior_full_file = header + test_before
|
|
176
|
+
post_edit_full_file = header + test_after
|
|
177
|
+
issues = code_rules_enforcer.validate_content(
|
|
178
|
+
test_after,
|
|
179
|
+
"/project/src/test_edited_module.py",
|
|
180
|
+
old_content=test_before,
|
|
181
|
+
full_file_content=post_edit_full_file,
|
|
182
|
+
prior_full_file_content=prior_full_file,
|
|
183
|
+
)
|
|
184
|
+
assert any(
|
|
185
|
+
"test_writes" in each_issue and "Path.home" in each_issue
|
|
186
|
+
for each_issue in issues
|
|
187
|
+
), f"isolation probe added on changed lines must block, got: {issues!r}"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_isolation_edit_blocks_probe_unisolated_by_signature_line_change() -> None:
|
|
191
|
+
"""Removing the ``monkeypatch`` fixture from a test's signature line
|
|
192
|
+
un-isolates a HOME probe in its unchanged body; the violation must block
|
|
193
|
+
because the enclosing function's span covers the changed signature line."""
|
|
194
|
+
test_before = (
|
|
195
|
+
"def test_reads_home(monkeypatch) -> None:\n"
|
|
196
|
+
" target_path = Path.home()\n"
|
|
197
|
+
" assert target_path\n"
|
|
198
|
+
)
|
|
199
|
+
test_after = (
|
|
200
|
+
"def test_reads_home() -> None:\n"
|
|
201
|
+
" target_path = Path.home()\n"
|
|
202
|
+
" assert target_path\n"
|
|
203
|
+
)
|
|
204
|
+
header = "from pathlib import Path\n"
|
|
205
|
+
prior_full_file = header + test_before
|
|
206
|
+
post_edit_full_file = header + test_after
|
|
207
|
+
issues = code_rules_enforcer.validate_content(
|
|
208
|
+
test_after,
|
|
209
|
+
"/project/src/test_edited_module.py",
|
|
210
|
+
old_content=test_before,
|
|
211
|
+
full_file_content=post_edit_full_file,
|
|
212
|
+
prior_full_file_content=prior_full_file,
|
|
213
|
+
)
|
|
214
|
+
assert any(
|
|
215
|
+
"test_reads_home" in each_issue and "Path.home" in each_issue
|
|
216
|
+
for each_issue in issues
|
|
217
|
+
), f"signature-line change that un-isolates a probe must block, got: {issues!r}"
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_function_length_reports_only_in_scope_violation_on_terminal_edit() -> None:
|
|
221
|
+
"""A terminal diff-scoped Edit reports only the function whose changed-line
|
|
222
|
+
span grew past the threshold; untouched oversized functions earlier in the
|
|
223
|
+
file are out of scope and dropped, regardless of how many precede it."""
|
|
224
|
+
leading_function_count = 6
|
|
225
|
+
leading_functions = "\n".join(
|
|
226
|
+
_oversized_function_source(f"leading_long_{each_index}")
|
|
227
|
+
for each_index in range(leading_function_count)
|
|
228
|
+
)
|
|
229
|
+
short_target_before = "def target_function() -> int:\n return 1\n"
|
|
230
|
+
grown_target_after = _oversized_function_source("target_function")
|
|
231
|
+
prior_full_file = leading_functions + "\n" + short_target_before
|
|
232
|
+
post_edit_full_file = leading_functions + "\n" + grown_target_after
|
|
233
|
+
issues = code_rules_enforcer.validate_content(
|
|
234
|
+
grown_target_after,
|
|
235
|
+
"/project/src/many_functions.py",
|
|
236
|
+
old_content=short_target_before,
|
|
237
|
+
full_file_content=post_edit_full_file,
|
|
238
|
+
prior_full_file_content=prior_full_file,
|
|
239
|
+
)
|
|
240
|
+
function_length_issues = [
|
|
241
|
+
each_issue for each_issue in issues if "defined at line" in each_issue
|
|
242
|
+
]
|
|
243
|
+
assert any(
|
|
244
|
+
"target_function" in each_issue for each_issue in function_length_issues
|
|
245
|
+
), f"in-scope grown function must still block, got: {issues!r}"
|
|
246
|
+
assert not any(
|
|
247
|
+
"leading_long_" in each_issue for each_issue in function_length_issues
|
|
248
|
+
), f"untouched functions must stay out of scope, got: {function_length_issues!r}"
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def test_new_file_write_reports_every_in_scope_long_function_uncapped() -> None:
|
|
252
|
+
"""loop7-bugbot: a new-file Write passes ``all_changed_lines is None``; every
|
|
253
|
+
line was just authored and is in scope, so every long function is reported
|
|
254
|
+
with no ceiling on the count."""
|
|
255
|
+
function_count = 6
|
|
256
|
+
all_functions = "\n".join(
|
|
257
|
+
_oversized_function_source(f"new_long_{each_index}")
|
|
258
|
+
for each_index in range(function_count)
|
|
259
|
+
)
|
|
260
|
+
issues = code_rules_enforcer.validate_content(
|
|
261
|
+
all_functions,
|
|
262
|
+
"/project/src/freshly_written_module.py",
|
|
263
|
+
old_content="",
|
|
264
|
+
)
|
|
265
|
+
function_length_issues = [
|
|
266
|
+
each_issue for each_issue in issues if "defined at line" in each_issue
|
|
267
|
+
]
|
|
268
|
+
assert len(function_length_issues) == function_count, (
|
|
269
|
+
"every long function in a new file is in scope and must be reported, "
|
|
270
|
+
f"got: {function_length_issues!r}"
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def test_new_file_write_reports_every_in_scope_isolation_probe_uncapped() -> None:
|
|
275
|
+
"""loop7-bugbot: a new test file Write passes ``all_changed_lines is None``;
|
|
276
|
+
every HOME probe is in scope, so each one is reported with no count ceiling."""
|
|
277
|
+
probe_count = 6
|
|
278
|
+
probing_tests = "".join(
|
|
279
|
+
f"def test_probe_{each_index}() -> None:\n"
|
|
280
|
+
f" home_dir_{each_index} = Path.home()\n"
|
|
281
|
+
f" assert home_dir_{each_index}\n"
|
|
282
|
+
for each_index in range(probe_count)
|
|
283
|
+
)
|
|
284
|
+
source = "from pathlib import Path\n" + probing_tests
|
|
285
|
+
issues = code_rules_enforcer.validate_content(
|
|
286
|
+
source,
|
|
287
|
+
"/project/src/test_freshly_written_module.py",
|
|
288
|
+
old_content="",
|
|
289
|
+
)
|
|
290
|
+
home_probe_issues = [
|
|
291
|
+
each_issue for each_issue in issues if "Path.home" in each_issue
|
|
292
|
+
]
|
|
293
|
+
assert len(home_probe_issues) == probe_count, (
|
|
294
|
+
"every HOME probe in a new test file is in scope and must be reported, "
|
|
295
|
+
f"got: {home_probe_issues!r}"
|
|
296
|
+
)
|