claude-dev-env 1.50.0 → 1.50.2

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 (82) hide show
  1. package/hooks/blocking/_gh_body_arg_utils.py +67 -11
  2. package/hooks/blocking/_md_to_html_blocker_test_support.py +65 -0
  3. package/hooks/blocking/code_rules_annotations_length.py +167 -0
  4. package/hooks/blocking/code_rules_banned_identifiers.py +385 -0
  5. package/hooks/blocking/code_rules_boolean_mustcheck.py +350 -0
  6. package/hooks/blocking/code_rules_comments.py +337 -0
  7. package/hooks/blocking/code_rules_constants_config.py +252 -0
  8. package/hooks/blocking/code_rules_docstrings.py +308 -0
  9. package/hooks/blocking/code_rules_enforcer.py +98 -5765
  10. package/hooks/blocking/code_rules_imports_logging.py +276 -0
  11. package/hooks/blocking/code_rules_magic_values.py +180 -0
  12. package/hooks/blocking/code_rules_mock_completeness.py +295 -0
  13. package/hooks/blocking/code_rules_naming_collection.py +264 -0
  14. package/hooks/blocking/code_rules_optional_params.py +288 -0
  15. package/hooks/blocking/code_rules_paths_syspath.py +186 -0
  16. package/hooks/blocking/code_rules_probe_chains.py +305 -0
  17. package/hooks/blocking/code_rules_probe_detection.py +257 -0
  18. package/hooks/blocking/code_rules_probe_recording.py +225 -0
  19. package/hooks/blocking/code_rules_scope_binding.py +151 -0
  20. package/hooks/blocking/code_rules_shared.py +301 -0
  21. package/hooks/blocking/code_rules_string_magic.py +207 -0
  22. package/hooks/blocking/code_rules_test_assertions.py +226 -0
  23. package/hooks/blocking/code_rules_test_branching_except.py +181 -0
  24. package/hooks/blocking/code_rules_test_isolation.py +341 -0
  25. package/hooks/blocking/code_rules_type_escape.py +341 -0
  26. package/hooks/blocking/code_rules_typeddict_stub.py +305 -0
  27. package/hooks/blocking/code_rules_unused_imports.py +256 -0
  28. package/hooks/blocking/conftest.py +30 -0
  29. package/hooks/blocking/pr_description_body_audit.py +148 -0
  30. package/hooks/blocking/pr_description_command_parser.py +233 -0
  31. package/hooks/blocking/pr_description_enforcer.py +36 -825
  32. package/hooks/blocking/pr_description_pr_number.py +153 -0
  33. package/hooks/blocking/pr_description_readability.py +366 -0
  34. package/hooks/blocking/tdd_enforcer.py +31 -0
  35. package/hooks/blocking/test_code_rules_constants_config.py +26 -0
  36. package/hooks/blocking/test_code_rules_enforcer_banned_noun_word.py +5 -2
  37. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -5
  38. package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +21 -15
  39. package/hooks/blocking/test_code_rules_enforcer_config_path.py +20 -16
  40. package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +4 -2
  41. package/hooks/blocking/test_code_rules_enforcer_function_length.py +154 -18
  42. package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +1 -2
  43. package/hooks/blocking/test_code_rules_enforcer_ignored_must_check_return.py +22 -12
  44. package/hooks/blocking/test_code_rules_enforcer_split_annotations_length.py +55 -0
  45. package/hooks/blocking/test_code_rules_enforcer_split_banned.py +170 -0
  46. package/hooks/blocking/test_code_rules_enforcer_split_comments.py +60 -0
  47. package/hooks/blocking/test_code_rules_enforcer_split_config_path.py +52 -0
  48. package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +236 -0
  49. package/hooks/blocking/test_code_rules_enforcer_split_entry_1.py +296 -0
  50. package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +238 -0
  51. package/hooks/blocking/test_code_rules_enforcer_split_isolation_1.py +271 -0
  52. package/hooks/blocking/test_code_rules_enforcer_split_isolation_2.py +283 -0
  53. package/hooks/blocking/test_code_rules_enforcer_split_isolation_3.py +268 -0
  54. package/hooks/blocking/test_code_rules_enforcer_split_isolation_4.py +85 -0
  55. package/hooks/blocking/test_code_rules_enforcer_split_mocks_1.py +303 -0
  56. package/hooks/blocking/test_code_rules_enforcer_split_mocks_2.py +111 -0
  57. package/hooks/blocking/test_code_rules_enforcer_split_mustcheck.py +87 -0
  58. package/hooks/blocking/test_code_rules_enforcer_split_naming.py +107 -0
  59. package/hooks/blocking/test_code_rules_enforcer_split_optional_params.py +325 -0
  60. package/hooks/blocking/test_code_rules_enforcer_split_paths_syspath.py +110 -0
  61. package/hooks/blocking/test_code_rules_enforcer_split_shared.py +44 -0
  62. package/hooks/blocking/test_code_rules_enforcer_split_string_magic.py +55 -0
  63. package/hooks/blocking/test_code_rules_enforcer_split_test_assertions.py +56 -0
  64. package/hooks/blocking/test_code_rules_enforcer_todo_markers.py +21 -15
  65. package/hooks/blocking/test_code_rules_paths_syspath.py +26 -0
  66. package/hooks/blocking/test_md_to_html_blocker_exemptions.py +368 -0
  67. package/hooks/blocking/test_md_to_html_blocker_extensions.py +157 -0
  68. package/hooks/blocking/test_md_to_html_blocker_path_resolution.py +336 -0
  69. package/hooks/blocking/test_pr_description_enforcer.py +13 -1499
  70. package/hooks/blocking/test_pr_description_enforcer_body_audit.py +247 -0
  71. package/hooks/blocking/test_pr_description_enforcer_body_rules.py +493 -0
  72. package/hooks/blocking/test_pr_description_enforcer_command_parser.py +366 -0
  73. package/hooks/blocking/test_pr_description_enforcer_pr_number.py +159 -0
  74. package/hooks/blocking/test_pr_description_enforcer_readability.py +443 -0
  75. package/hooks/blocking/test_tdd_enforcer.py +116 -0
  76. package/hooks/hooks_constants/blocking_check_limits.py +3 -0
  77. package/hooks/hooks_constants/code_rules_enforcer_constants.py +8 -0
  78. package/hooks/hooks_constants/pr_description_enforcer_constants.py +7 -0
  79. package/hooks/hooks_constants/sys_path_insert_constants.py +1 -0
  80. package/package.json +1 -1
  81. package/hooks/blocking/test_code_rules_enforcer.py +0 -2669
  82. package/hooks/blocking/test_md_to_html_blocker.py +0 -810
@@ -0,0 +1,303 @@
1
+ """Behavior tests for the code_rules_mock_completeness 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_mock_completeness import ( # noqa: E402
17
+ check_incomplete_mocks,
18
+ )
19
+
20
+ code_rules_enforcer = SimpleNamespace(
21
+ check_incomplete_mocks=check_incomplete_mocks,
22
+ )
23
+
24
+
25
+ INCOMPLETE_MOCK_PRODUCTION_FILE_PATH = "packages/app/services/orders.py"
26
+
27
+ INCOMPLETE_MOCK_TEST_FILE_PATH = "packages/app/tests/test_orders.py"
28
+
29
+ MODULE_LEVEL_MOCK_TEST_FILE_PATH = "packages/app/tests/test_module_level.py"
30
+
31
+ SCOPE_KEYED_MOCK_TEST_FILE_PATH = "packages/app/tests/test_scope_mocks.py"
32
+
33
+
34
+ def _assert_inner_field_did_not_leak(
35
+ captured_stderr: str,
36
+ inner_only_field_name: str,
37
+ binding_form_description: str,
38
+ ) -> None:
39
+ leaked_advisories = [
40
+ line
41
+ for line in captured_stderr.splitlines()
42
+ if "mock_user" in line and inner_only_field_name in line
43
+ ]
44
+ assert leaked_advisories == [], (
45
+ f"Expected no advisory on the outer mock for {inner_only_field_name!r} — "
46
+ f"that field is accessed only inside a nested scope that re-binds "
47
+ f"mock_user via {binding_form_description}, got: {captured_stderr!r}"
48
+ )
49
+
50
+
51
+ def test_should_advise_when_mock_missing_accessed_field(capsys: object) -> None:
52
+ source = (
53
+ "mock_order = {'id': 1}\n"
54
+ "\n"
55
+ "def test_order_total() -> None:\n"
56
+ " total = mock_order['total']\n"
57
+ " assert total > 0\n"
58
+ )
59
+ code_rules_enforcer.check_incomplete_mocks(source, INCOMPLETE_MOCK_TEST_FILE_PATH)
60
+ captured = getattr(capsys, "readouterr")()
61
+ assert "mock_order" in captured.err and "total" in captured.err, (
62
+ f"Expected advisory about missing 'total' field, got: {captured.err!r}"
63
+ )
64
+
65
+
66
+ def test_should_not_advise_when_mock_has_all_accessed_fields(capsys: object) -> None:
67
+ source = (
68
+ "mock_order = {'id': 1, 'total': 50}\n"
69
+ "\n"
70
+ "def test_order_total() -> None:\n"
71
+ " total = mock_order['total']\n"
72
+ " assert total > 0\n"
73
+ )
74
+ code_rules_enforcer.check_incomplete_mocks(source, INCOMPLETE_MOCK_TEST_FILE_PATH)
75
+ captured = getattr(capsys, "readouterr")()
76
+ assert "mock_order" not in captured.err, (
77
+ f"Expected no advisory when all fields present, got: {captured.err!r}"
78
+ )
79
+
80
+
81
+ def test_should_not_advise_for_incomplete_mocks_in_production_files(capsys: object) -> None:
82
+ source = (
83
+ "mock_order = {'id': 1}\n"
84
+ "\n"
85
+ "def run_order() -> None:\n"
86
+ " total = mock_order['total']\n"
87
+ )
88
+ code_rules_enforcer.check_incomplete_mocks(source, INCOMPLETE_MOCK_PRODUCTION_FILE_PATH)
89
+ captured = getattr(capsys, "readouterr")()
90
+ assert "mock_order" not in captured.err, (
91
+ f"Expected no advisory in production file, got: {captured.err!r}"
92
+ )
93
+
94
+
95
+ def test_should_advise_for_attribute_access_on_mock_object(capsys: object) -> None:
96
+ source = (
97
+ "class MockUser:\n"
98
+ " pass\n"
99
+ "\n"
100
+ "mock_user = MockUser()\n"
101
+ "mock_user.name = 'Alice'\n"
102
+ "\n"
103
+ "def test_user_email() -> None:\n"
104
+ " email = mock_user.email\n"
105
+ " assert email\n"
106
+ )
107
+ code_rules_enforcer.check_incomplete_mocks(source, INCOMPLETE_MOCK_TEST_FILE_PATH)
108
+ captured = getattr(capsys, "readouterr")()
109
+ assert "mock_user" in captured.err and "email" in captured.err, (
110
+ f"Expected advisory about missing 'email' attribute, got: {captured.err!r}"
111
+ )
112
+
113
+
114
+ def test_should_advise_when_mock_defined_inside_test_function_is_incomplete(
115
+ capsys: object,
116
+ ) -> None:
117
+ source = (
118
+ "def test_thing() -> None:\n"
119
+ " mock_user = {'name': 'x'}\n"
120
+ " assert mock_user['email'] == 'y'\n"
121
+ )
122
+ code_rules_enforcer.check_incomplete_mocks(source, INCOMPLETE_MOCK_TEST_FILE_PATH)
123
+ captured = getattr(capsys, "readouterr")()
124
+ assert "mock_user" in captured.err and "email" in captured.err, (
125
+ f"Expected advisory for mock defined inside test function, got: {captured.err!r}"
126
+ )
127
+
128
+
129
+ def test_should_check_each_scope_mock_against_its_own_field_set(capsys: object) -> None:
130
+ """Same mock_user name in two test functions with different field sets.
131
+
132
+ First function defines mock_user with only 'id'; accesses 'email' — should warn.
133
+ Second function defines mock_user with 'id' and 'email'; accesses 'email' — no warn.
134
+ The second definition must NOT overwrite the first scope's tracking.
135
+ """
136
+ source = (
137
+ "def test_first_scope() -> None:\n"
138
+ " mock_user = {'id': 1}\n"
139
+ " email = mock_user['email']\n"
140
+ "\n"
141
+ "def test_second_scope() -> None:\n"
142
+ " mock_user = {'id': 2, 'email': 'b@b.com'}\n"
143
+ " email = mock_user['email']\n"
144
+ )
145
+ code_rules_enforcer.check_incomplete_mocks(source, SCOPE_KEYED_MOCK_TEST_FILE_PATH)
146
+ captured = getattr(capsys, "readouterr")()
147
+ advisory_lines = [
148
+ line for line in captured.err.splitlines() if "mock_user" in line and "email" in line
149
+ ]
150
+ assert len(advisory_lines) == 1, (
151
+ f"Expected exactly 1 advisory (first scope missing email), got: {captured.err!r}"
152
+ )
153
+
154
+
155
+ def test_should_emit_exactly_one_advisory_for_repeated_accesses_to_same_missing_field(
156
+ capsys: object,
157
+ ) -> None:
158
+ """mock_user accessed 5 times for 'email' but email is missing — emit exactly one advisory."""
159
+ source = (
160
+ "def test_repeated_access() -> None:\n"
161
+ " mock_user = {'id': 1}\n"
162
+ " _ = mock_user['email']\n"
163
+ " _ = mock_user['email']\n"
164
+ " _ = mock_user['email']\n"
165
+ " _ = mock_user['email']\n"
166
+ " _ = mock_user['email']\n"
167
+ )
168
+ code_rules_enforcer.check_incomplete_mocks(source, SCOPE_KEYED_MOCK_TEST_FILE_PATH)
169
+ captured = getattr(capsys, "readouterr")()
170
+ advisory_lines = [
171
+ line for line in captured.err.splitlines() if "mock_user" in line and "email" in line
172
+ ]
173
+ assert len(advisory_lines) == 1, (
174
+ f"Expected exactly 1 advisory for 5 repeated accesses to missing 'email', got: {captured.err!r}"
175
+ )
176
+
177
+
178
+ def test_should_emit_exactly_one_advisory_for_module_level_mock_with_missing_field(
179
+ capsys: object,
180
+ ) -> None:
181
+ """Module-level mock_user with one missing field access should produce ONE advisory.
182
+
183
+ Finding 4: ast.walk() already yields the root Module node, so
184
+ [module_tree, *ast.walk(module_tree)] iterates the module twice and
185
+ previously produced two identical advisories for module-level mocks.
186
+ """
187
+ source = (
188
+ "mock_user = {'name': 'Alice'}\n"
189
+ "\n"
190
+ "def test_email_present() -> None:\n"
191
+ " email = mock_user['email']\n"
192
+ " assert email\n"
193
+ )
194
+ code_rules_enforcer.check_incomplete_mocks(source, MODULE_LEVEL_MOCK_TEST_FILE_PATH)
195
+ captured = getattr(capsys, "readouterr")()
196
+ advisory_lines = [
197
+ line for line in captured.err.splitlines() if "mock_user" in line and "email" in line
198
+ ]
199
+ assert len(advisory_lines) == 1, (
200
+ f"Expected exactly 1 advisory for module-level mock missing 'email', got: {captured.err!r}"
201
+ )
202
+
203
+
204
+ def test_should_not_leak_shadowed_nested_assignment_into_outer_mock_known_fields(
205
+ capsys: object,
206
+ ) -> None:
207
+ """Assignment collector must skip nested scopes that shadow the mock name.
208
+
209
+ The access collector uses _walk_scope_skipping_shadowed; the assignment
210
+ collector must do the same, otherwise attribute assignments inside a
211
+ nested function that redefines mock_user leak into the outer mock's
212
+ known-fields set and suppress advisories for genuinely missing fields.
213
+ """
214
+ source = (
215
+ "mock_user = {'id': 1}\n"
216
+ "outer_value = mock_user['email']\n"
217
+ "\n"
218
+ "def test_inner() -> None:\n"
219
+ " mock_user = {'id': 2}\n"
220
+ " mock_user.email = 'shadowed@example.com'\n"
221
+ )
222
+ code_rules_enforcer.check_incomplete_mocks(source, MODULE_LEVEL_MOCK_TEST_FILE_PATH)
223
+ captured = getattr(capsys, "readouterr")()
224
+ advisory_lines = [
225
+ line for line in captured.err.splitlines() if "mock_user" in line and "email" in line
226
+ ]
227
+ assert len(advisory_lines) == 1, (
228
+ "Expected outer mock's missing 'email' advisory to fire even when a shadowing "
229
+ f"nested function assigns mock_user.email, got: {captured.err!r}"
230
+ )
231
+
232
+
233
+ def test_should_treat_annotated_assignment_as_shadowing_in_nested_scope(
234
+ capsys: object,
235
+ ) -> None:
236
+ """AnnAssign must shadow just like Assign.
237
+
238
+ When a nested scope re-binds the mock variable via an annotated
239
+ assignment (``mock_user: dict = {...}``), accesses inside that nested
240
+ scope belong to the inner mock, not the outer one. If the shadow
241
+ detector ignores AnnAssign, inner accesses leak out and cause
242
+ spurious advisories against the outer mock for fields it never sees.
243
+ """
244
+ source = (
245
+ "mock_user = {'id': 1, 'name': 'outer'}\n"
246
+ "outer_value = mock_user['name']\n"
247
+ "\n"
248
+ "def test_inner() -> None:\n"
249
+ " mock_user: dict = {'id': 2, 'timezone': 'UTC'}\n"
250
+ " inner_value = mock_user['timezone']\n"
251
+ )
252
+ code_rules_enforcer.check_incomplete_mocks(source, MODULE_LEVEL_MOCK_TEST_FILE_PATH)
253
+ captured = getattr(capsys, "readouterr")()
254
+ leaked_advisories = [
255
+ line
256
+ for line in captured.err.splitlines()
257
+ if "mock_user" in line and "timezone" in line
258
+ ]
259
+ assert leaked_advisories == [], (
260
+ "Expected no advisory on the outer mock for 'timezone' — that field is "
261
+ "accessed only inside a nested scope that re-binds mock_user via an "
262
+ f"annotated assignment, got: {captured.err!r}"
263
+ )
264
+
265
+
266
+ def test_should_treat_assignment_inside_if_block_as_shadowing(capsys: object) -> None:
267
+ """Binding inside an ``if`` block must shadow the outer mock name.
268
+
269
+ Python binds a name locally when it is assigned *anywhere* in the
270
+ function body, including inside a branch. A shadow detector that only
271
+ inspects the top-level statements misses this form.
272
+ """
273
+ source = (
274
+ "mock_user = {'id': 1, 'name': 'outer'}\n"
275
+ "outer_value = mock_user['name']\n"
276
+ "\n"
277
+ "def test_inner() -> None:\n"
278
+ " if True:\n"
279
+ " mock_user = {'id': 2, 'timezone': 'UTC'}\n"
280
+ " inner_value = mock_user['timezone']\n"
281
+ )
282
+ code_rules_enforcer.check_incomplete_mocks(source, MODULE_LEVEL_MOCK_TEST_FILE_PATH)
283
+ captured = getattr(capsys, "readouterr")()
284
+ _assert_inner_field_did_not_leak(
285
+ captured.err, "timezone", "an assignment nested inside an if-block"
286
+ )
287
+
288
+
289
+ def test_should_treat_for_loop_target_as_shadowing(capsys: object) -> None:
290
+ """A ``for`` loop target binds the name locally and must shadow."""
291
+ source = (
292
+ "mock_user = {'id': 1, 'name': 'outer'}\n"
293
+ "outer_value = mock_user['name']\n"
294
+ "\n"
295
+ "def test_inner() -> None:\n"
296
+ " for mock_user in [{'id': 2, 'timezone': 'UTC'}]:\n"
297
+ " inner_value = mock_user['timezone']\n"
298
+ )
299
+ code_rules_enforcer.check_incomplete_mocks(source, MODULE_LEVEL_MOCK_TEST_FILE_PATH)
300
+ captured = getattr(capsys, "readouterr")()
301
+ _assert_inner_field_did_not_leak(
302
+ captured.err, "timezone", "a for-loop target"
303
+ )
@@ -0,0 +1,111 @@
1
+ """Behavior tests for the code_rules_mock_completeness 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_mock_completeness import ( # noqa: E402
17
+ check_incomplete_mocks,
18
+ )
19
+
20
+ code_rules_enforcer = SimpleNamespace(
21
+ check_incomplete_mocks=check_incomplete_mocks,
22
+ )
23
+
24
+
25
+ MODULE_LEVEL_MOCK_TEST_FILE_PATH = "packages/app/tests/test_module_level.py"
26
+
27
+
28
+ def _assert_inner_field_did_not_leak(
29
+ captured_stderr: str,
30
+ inner_only_field_name: str,
31
+ binding_form_description: str,
32
+ ) -> None:
33
+ leaked_advisories = [
34
+ line
35
+ for line in captured_stderr.splitlines()
36
+ if "mock_user" in line and inner_only_field_name in line
37
+ ]
38
+ assert leaked_advisories == [], (
39
+ f"Expected no advisory on the outer mock for {inner_only_field_name!r} — "
40
+ f"that field is accessed only inside a nested scope that re-binds "
41
+ f"mock_user via {binding_form_description}, got: {captured_stderr!r}"
42
+ )
43
+
44
+
45
+ def test_should_treat_try_except_handler_name_as_shadowing(capsys: object) -> None:
46
+ """An ``except ... as mock_user`` handler binds the name locally."""
47
+ source = (
48
+ "mock_user = {'id': 1, 'name': 'outer'}\n"
49
+ "outer_value = mock_user['name']\n"
50
+ "\n"
51
+ "def test_inner() -> None:\n"
52
+ " try:\n"
53
+ " raise ValueError({'id': 2, 'timezone': 'UTC'})\n"
54
+ " except ValueError as mock_user:\n"
55
+ " inner_value = mock_user['timezone']\n"
56
+ )
57
+ code_rules_enforcer.check_incomplete_mocks(source, MODULE_LEVEL_MOCK_TEST_FILE_PATH)
58
+ captured = getattr(capsys, "readouterr")()
59
+ _assert_inner_field_did_not_leak(
60
+ captured.err, "timezone", "an except-handler binding"
61
+ )
62
+
63
+
64
+ def test_should_treat_walrus_expression_as_shadowing(capsys: object) -> None:
65
+ """A named-expression walrus binding inside a condition must shadow."""
66
+ source = (
67
+ "mock_user = {'id': 1, 'name': 'outer'}\n"
68
+ "outer_value = mock_user['name']\n"
69
+ "\n"
70
+ "def test_inner() -> None:\n"
71
+ " if (mock_user := {'id': 2, 'timezone': 'UTC'}):\n"
72
+ " inner_value = mock_user['timezone']\n"
73
+ )
74
+ code_rules_enforcer.check_incomplete_mocks(source, MODULE_LEVEL_MOCK_TEST_FILE_PATH)
75
+ captured = getattr(capsys, "readouterr")()
76
+ _assert_inner_field_did_not_leak(
77
+ captured.err, "timezone", "a walrus named-expression"
78
+ )
79
+
80
+
81
+ def test_should_treat_function_parameter_as_shadowing(capsys: object) -> None:
82
+ """A parameter named like the mock variable must shadow the outer binding."""
83
+ source = (
84
+ "mock_user = {'id': 1, 'name': 'outer'}\n"
85
+ "outer_value = mock_user['name']\n"
86
+ "\n"
87
+ "def test_inner(mock_user: dict) -> None:\n"
88
+ " inner_value = mock_user['timezone']\n"
89
+ )
90
+ code_rules_enforcer.check_incomplete_mocks(source, MODULE_LEVEL_MOCK_TEST_FILE_PATH)
91
+ captured = getattr(capsys, "readouterr")()
92
+ _assert_inner_field_did_not_leak(
93
+ captured.err, "timezone", "a function parameter of the same name"
94
+ )
95
+
96
+
97
+ def test_should_treat_import_asname_as_shadowing(capsys: object) -> None:
98
+ """An ``import ... as mock_user`` must shadow the outer mock name."""
99
+ source = (
100
+ "mock_user = {'id': 1, 'name': 'outer'}\n"
101
+ "outer_value = mock_user['name']\n"
102
+ "\n"
103
+ "def test_inner() -> None:\n"
104
+ " import collections as mock_user\n"
105
+ " inner_value = mock_user['timezone']\n"
106
+ )
107
+ code_rules_enforcer.check_incomplete_mocks(source, MODULE_LEVEL_MOCK_TEST_FILE_PATH)
108
+ captured = getattr(capsys, "readouterr")()
109
+ _assert_inner_field_did_not_leak(
110
+ captured.err, "timezone", "an import-asname binding"
111
+ )
@@ -0,0 +1,87 @@
1
+ """Behavior tests for the code_rules_boolean_mustcheck 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_boolean_mustcheck import ( # noqa: E402
17
+ check_ignored_must_check_return,
18
+ )
19
+
20
+ code_rules_enforcer = SimpleNamespace(
21
+ check_ignored_must_check_return=check_ignored_must_check_return,
22
+ )
23
+
24
+
25
+ def test_ignored_must_check_return_flags_bare_awaited_call() -> None:
26
+ """A bare ``await find_and_click(...)`` statement discards its only failure signal.
27
+
28
+ The curated must-check functions are async, so the common real call site is a
29
+ bare ``await``-wrapped call. Unwrapping ``ast.Await`` before the Call check is
30
+ required for this case to be flagged.
31
+ """
32
+ source = "async def step() -> None:\n await find_and_click('#x')\n"
33
+ issues = code_rules_enforcer.check_ignored_must_check_return(
34
+ source, "/project/src/clicker.py"
35
+ )
36
+ assert any("find_and_click" in each_issue for each_issue in issues), (
37
+ f"a bare awaited must-check call must be flagged; got: {issues!r}"
38
+ )
39
+ assert len(issues) == 1
40
+
41
+
42
+ def test_ignored_must_check_return_exempts_consumed_awaited_call() -> None:
43
+ """An assigned or branched-on awaited must-check call consumes its outcome."""
44
+ assigned = "async def step() -> None:\n clicked = await find_and_click('#x')\n print(clicked)\n"
45
+ branched = "async def step() -> None:\n if await find_and_click('#x'):\n pass\n"
46
+ assert (
47
+ code_rules_enforcer.check_ignored_must_check_return(assigned, "/project/src/clicker.py")
48
+ == []
49
+ )
50
+ assert (
51
+ code_rules_enforcer.check_ignored_must_check_return(branched, "/project/src/clicker.py")
52
+ == []
53
+ )
54
+
55
+
56
+ def test_ignored_must_check_return_flags_edited_line_past_a_cap_of_earlier_violations() -> None:
57
+ """The cap must apply after scoping so the edited-line violation is never dropped.
58
+
59
+ Collecting only a cap's worth of violations in ``ast.walk`` order, then scoping,
60
+ fills the cap with earlier out-of-scope calls and discards the edited-line one —
61
+ the very violation the scoped enforcer exists to block. Every violation must be
62
+ collected before scoping so the edited line survives the diff filter.
63
+ """
64
+ pre_existing_call_count = 5
65
+ edited_call_line_number = pre_existing_call_count + 2
66
+ all_pre_existing_call_lines = [
67
+ f" await find_and_click('#x{each_index}')"
68
+ for each_index in range(pre_existing_call_count)
69
+ ]
70
+ all_lines = (
71
+ ["async def step() -> None:"]
72
+ + all_pre_existing_call_lines
73
+ + [" await find_and_click('#edited')"]
74
+ )
75
+ source = "\n".join(all_lines) + "\n"
76
+ issues = code_rules_enforcer.check_ignored_must_check_return(
77
+ source,
78
+ "/project/src/clicker.py",
79
+ {edited_call_line_number},
80
+ False,
81
+ )
82
+ assert len(issues) == 1, (
83
+ f"the edited-line violation must survive a cap's worth of earlier calls; got: {issues!r}"
84
+ )
85
+ assert f"Line {edited_call_line_number}:" in issues[0], (
86
+ f"the single issue must name the edited line {edited_call_line_number}; got: {issues!r}"
87
+ )
@@ -0,0 +1,107 @@
1
+ """Behavior tests for the code_rules_naming_collection 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_naming_collection import ( # noqa: E402
17
+ MAX_STUTTERING_PREFIX_ISSUES,
18
+ STUTTERING_ALL_PREFIX_PATTERN,
19
+ check_loop_variable_naming,
20
+ check_stuttering_collection_prefix,
21
+ )
22
+
23
+ from hooks_constants.stuttering_check_config import ( # noqa: E402
24
+ MAX_STUTTERING_PREFIX_ISSUES as config_max_stuttering_prefix_issues,
25
+ )
26
+ from hooks_constants.stuttering_check_config import ( # noqa: E402
27
+ STUTTERING_ALL_PREFIX_PATTERN as config_stuttering_all_prefix_pattern,
28
+ )
29
+
30
+ code_rules_enforcer = SimpleNamespace(
31
+ MAX_STUTTERING_PREFIX_ISSUES=MAX_STUTTERING_PREFIX_ISSUES,
32
+ STUTTERING_ALL_PREFIX_PATTERN=STUTTERING_ALL_PREFIX_PATTERN,
33
+ check_loop_variable_naming=check_loop_variable_naming,
34
+ check_stuttering_collection_prefix=check_stuttering_collection_prefix,
35
+ )
36
+
37
+
38
+ LOOP_NAMING_PRODUCTION_FILE_PATH = "packages/app/services/loop_naming.py"
39
+
40
+
41
+ def test_check_loop_variable_naming_flags_missing_each_prefix() -> None:
42
+ source = (
43
+ "def consume() -> None:\n"
44
+ " for marker in []:\n"
45
+ " return None\n"
46
+ )
47
+ issues = code_rules_enforcer.check_loop_variable_naming(
48
+ source, LOOP_NAMING_PRODUCTION_FILE_PATH
49
+ )
50
+ assert any("marker" in each_issue for each_issue in issues), (
51
+ f"Expected 'marker' loop variable flagged, got: {issues}"
52
+ )
53
+
54
+
55
+ def test_stuttering_collection_prefix_flags_function_name_loop1_1() -> None:
56
+ source = "def all_all_process() -> None:\n return None\n"
57
+ issues = code_rules_enforcer.check_stuttering_collection_prefix(
58
+ source, "packages/app/services/foo.py"
59
+ )
60
+ assert any("all_all_process" in each_issue for each_issue in issues), (
61
+ f"loop1-1: stuttering function name must be flagged, got: {issues}"
62
+ )
63
+
64
+
65
+ def test_stuttering_collection_prefix_flags_with_as_binding_loop3_1() -> None:
66
+ source = "def f() -> None:\n with open('x') as all_all_context:\n pass\n"
67
+ issues = code_rules_enforcer.check_stuttering_collection_prefix(
68
+ source, "packages/app/services/foo.py"
69
+ )
70
+ assert any("all_all_context" in each_issue for each_issue in issues), (
71
+ f"loop3-1: stuttering with-as binding must be flagged, got: {issues}"
72
+ )
73
+
74
+
75
+ def test_stuttering_collection_prefix_flags_except_as_binding_loop3_1() -> None:
76
+ source = (
77
+ "def f() -> None:\n"
78
+ " try:\n"
79
+ " pass\n"
80
+ " except Exception as all_all_error:\n"
81
+ " pass\n"
82
+ )
83
+ issues = code_rules_enforcer.check_stuttering_collection_prefix(
84
+ source, "packages/app/services/foo.py"
85
+ )
86
+ assert any("all_all_error" in each_issue for each_issue in issues), (
87
+ f"loop3-1: stuttering except-as binding must be flagged, got: {issues}"
88
+ )
89
+
90
+
91
+ def test_stuttering_constants_live_under_config_subpackage() -> None:
92
+ """Stuttering-prefix constants must be sourced from the hooks-tree config package.
93
+
94
+ Per CODE_RULES, module-level UPPER_SNAKE constants must live under a
95
+ directory segment named ``config``. This test pins the move so the
96
+ constants cannot regress to inline definition at the enforcer module's
97
+ top level. The enforcer's own bootstrap inserts the hooks tree onto
98
+ ``sys.path`` so ``config.stuttering_check_config`` resolves at runtime.
99
+ """
100
+ assert (
101
+ code_rules_enforcer.STUTTERING_ALL_PREFIX_PATTERN
102
+ is config_stuttering_all_prefix_pattern
103
+ ), "Enforcer must reuse the hooks-tree config STUTTERING_ALL_PREFIX_PATTERN object"
104
+ assert (
105
+ code_rules_enforcer.MAX_STUTTERING_PREFIX_ISSUES
106
+ == config_max_stuttering_prefix_issues
107
+ ), "Enforcer must reuse the hooks-tree config MAX_STUTTERING_PREFIX_ISSUES value"