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.
Files changed (91) hide show
  1. package/_shared/pr-loop/audit-contract.md +3 -3
  2. package/audit-rubrics/category_rubrics/category-e-dead-code.md +3 -2
  3. package/audit-rubrics/prompts/category-a-api-contracts.md +1 -1
  4. package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
  5. package/audit-rubrics/prompts/category-c-resource-cleanup.md +2 -2
  6. package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +2 -2
  7. package/audit-rubrics/prompts/category-e-dead-code.md +5 -4
  8. package/audit-rubrics/prompts/category-f-silent-failures.md +2 -2
  9. package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +2 -2
  10. package/audit-rubrics/prompts/category-h-security-boundaries.md +2 -2
  11. package/audit-rubrics/prompts/category-i-concurrency.md +2 -2
  12. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +2 -2
  13. package/audit-rubrics/prompts/category-k-codebase-conflicts.md +2 -2
  14. package/docs/CODE_RULES.md +1 -1
  15. package/hooks/blocking/code_rules_annotations_length.py +167 -0
  16. package/hooks/blocking/code_rules_banned_identifiers.py +385 -0
  17. package/hooks/blocking/code_rules_boolean_mustcheck.py +350 -0
  18. package/hooks/blocking/code_rules_comments.py +337 -0
  19. package/hooks/blocking/code_rules_constants_config.py +252 -0
  20. package/hooks/blocking/code_rules_docstrings.py +308 -0
  21. package/hooks/blocking/code_rules_enforcer.py +98 -5807
  22. package/hooks/blocking/code_rules_imports_logging.py +276 -0
  23. package/hooks/blocking/code_rules_magic_values.py +180 -0
  24. package/hooks/blocking/code_rules_mock_completeness.py +295 -0
  25. package/hooks/blocking/code_rules_naming_collection.py +264 -0
  26. package/hooks/blocking/code_rules_optional_params.py +288 -0
  27. package/hooks/blocking/code_rules_paths_syspath.py +186 -0
  28. package/hooks/blocking/code_rules_probe_chains.py +305 -0
  29. package/hooks/blocking/code_rules_probe_detection.py +257 -0
  30. package/hooks/blocking/code_rules_probe_recording.py +225 -0
  31. package/hooks/blocking/code_rules_scope_binding.py +151 -0
  32. package/hooks/blocking/code_rules_shared.py +301 -0
  33. package/hooks/blocking/code_rules_string_magic.py +207 -0
  34. package/hooks/blocking/code_rules_test_assertions.py +226 -0
  35. package/hooks/blocking/code_rules_test_branching_except.py +181 -0
  36. package/hooks/blocking/code_rules_test_isolation.py +341 -0
  37. package/hooks/blocking/code_rules_type_escape.py +341 -0
  38. package/hooks/blocking/code_rules_typeddict_stub.py +305 -0
  39. package/hooks/blocking/code_rules_unused_imports.py +256 -0
  40. package/hooks/blocking/tdd_enforcer.py +31 -0
  41. package/hooks/blocking/test_code_rules_constants_config.py +26 -0
  42. package/hooks/blocking/test_code_rules_enforcer_banned_noun_word.py +5 -2
  43. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -5
  44. package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +21 -15
  45. package/hooks/blocking/test_code_rules_enforcer_config_path.py +20 -16
  46. package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +4 -2
  47. package/hooks/blocking/test_code_rules_enforcer_function_length.py +18 -13
  48. package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +1 -2
  49. package/hooks/blocking/test_code_rules_enforcer_ignored_must_check_return.py +22 -12
  50. package/hooks/blocking/test_code_rules_enforcer_split_annotations_length.py +55 -0
  51. package/hooks/blocking/test_code_rules_enforcer_split_banned.py +170 -0
  52. package/hooks/blocking/test_code_rules_enforcer_split_comments.py +60 -0
  53. package/hooks/blocking/test_code_rules_enforcer_split_config_path.py +52 -0
  54. package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +236 -0
  55. package/hooks/blocking/test_code_rules_enforcer_split_entry_1.py +296 -0
  56. package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +238 -0
  57. package/hooks/blocking/test_code_rules_enforcer_split_isolation_1.py +271 -0
  58. package/hooks/blocking/test_code_rules_enforcer_split_isolation_2.py +283 -0
  59. package/hooks/blocking/test_code_rules_enforcer_split_isolation_3.py +268 -0
  60. package/hooks/blocking/test_code_rules_enforcer_split_isolation_4.py +85 -0
  61. package/hooks/blocking/test_code_rules_enforcer_split_mocks_1.py +303 -0
  62. package/hooks/blocking/test_code_rules_enforcer_split_mocks_2.py +111 -0
  63. package/hooks/blocking/test_code_rules_enforcer_split_mustcheck.py +87 -0
  64. package/hooks/blocking/test_code_rules_enforcer_split_naming.py +107 -0
  65. package/hooks/blocking/test_code_rules_enforcer_split_optional_params.py +325 -0
  66. package/hooks/blocking/test_code_rules_enforcer_split_paths_syspath.py +110 -0
  67. package/hooks/blocking/test_code_rules_enforcer_split_shared.py +44 -0
  68. package/hooks/blocking/test_code_rules_enforcer_split_string_magic.py +55 -0
  69. package/hooks/blocking/test_code_rules_enforcer_split_test_assertions.py +56 -0
  70. package/hooks/blocking/test_code_rules_enforcer_todo_markers.py +21 -15
  71. package/hooks/blocking/test_code_rules_paths_syspath.py +26 -0
  72. package/hooks/blocking/test_tdd_enforcer.py +116 -0
  73. package/hooks/hooks_constants/blocking_check_limits.py +3 -0
  74. package/hooks/hooks_constants/code_rules_enforcer_constants.py +8 -0
  75. package/hooks/hooks_constants/sys_path_insert_constants.py +1 -0
  76. package/package.json +1 -1
  77. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +13 -7
  78. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +21 -11
  79. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +92 -0
  80. package/skills/bugteam/CONSTRAINTS.md +1 -1
  81. package/skills/bugteam/PROMPTS.md +20 -48
  82. package/skills/bugteam/SKILL.md +5 -5
  83. package/skills/bugteam/reference/audit-and-teammates.md +1 -1
  84. package/skills/bugteam/reference/audit-contract.md +4 -4
  85. package/skills/bugteam/reference/design-rationale.md +1 -1
  86. package/skills/findbugs/SKILL.md +21 -12
  87. package/skills/fixbugs/SKILL.md +1 -1
  88. package/skills/qbug/SKILL.md +5 -5
  89. package/skills/qbug/test_qbug_skill_audit_schema.py +13 -23
  90. package/skills/refine/SKILL.md +1 -1
  91. 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
+ )