claude-dev-env 1.29.3 → 1.30.1

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 (43) hide show
  1. package/CLAUDE.md +8 -0
  2. package/agents/code-quality-agent.md +279 -24
  3. package/agents/groq-coder.md +111 -0
  4. package/commands/plan.md +4 -5
  5. package/docs/CODE_RULES.md +40 -0
  6. package/hooks/blocking/code_rules_enforcer.py +775 -8
  7. package/hooks/blocking/destructive_command_blocker.py +149 -12
  8. package/hooks/blocking/test_code_rules_enforcer.py +751 -0
  9. package/hooks/blocking/test_code_rules_enforcer_constant_equality.py +130 -0
  10. package/hooks/blocking/test_code_rules_enforcer_existence_checks.py +134 -0
  11. package/hooks/blocking/test_code_rules_enforcer_skip_decorators.py +150 -0
  12. package/hooks/blocking/test_destructive_command_blocker.py +281 -4
  13. package/hooks/git-hooks/test_config.py +9 -3
  14. package/hooks/git-hooks/test_gate_utils.py +9 -3
  15. package/hooks/git-hooks/test_pre_commit.py +9 -3
  16. package/hooks/git-hooks/test_pre_push.py +9 -3
  17. package/hooks/validators/run_all_validators.py +76 -3
  18. package/hooks/validators/test_output_formatter.py +4 -16
  19. package/hooks/validators/test_run_all_validators.py +22 -0
  20. package/hooks/validators/test_run_all_validators_integration.py +2 -11
  21. package/package.json +1 -1
  22. package/scripts/config/groq_bugteam_config.py +104 -0
  23. package/scripts/config/test_groq_bugteam_config.py +11 -0
  24. package/scripts/config/test_spec_implementer_prompt.py +36 -0
  25. package/scripts/groq_bugteam.README.md +2 -0
  26. package/scripts/groq_bugteam.py +74 -15
  27. package/scripts/groq_bugteam_dotenv.py +40 -0
  28. package/scripts/groq_bugteam_spec.py +226 -0
  29. package/scripts/test_groq_bugteam.py +143 -5
  30. package/scripts/test_groq_bugteam_apply_fix_from_spec.py +426 -0
  31. package/scripts/test_groq_bugteam_dotenv.py +66 -0
  32. package/scripts/test_groq_bugteam_spec.py +346 -0
  33. package/skills/bugteam/SKILL.md +4 -0
  34. package/skills/bugteam/reference/README.md +16 -0
  35. package/skills/bugteam/test_skill_additions.py +30 -0
  36. package/skills/monitor-open-prs/SKILL.md +104 -0
  37. package/skills/monitor-open-prs/scripts/discover_open_prs.py +69 -0
  38. package/skills/monitor-open-prs/scripts/test_discover_open_prs.py +149 -0
  39. package/skills/monitor-open-prs/test_skill_contract.py +43 -0
  40. package/skills/pr-review-responder/SKILL.md +10 -8
  41. package/hooks/github-action/pre-push-review.yml +0 -27
  42. package/hooks/github-action/test_workflow.py +0 -33
  43. package/skills/pr-review-responder/update_skill.py +0 -297
@@ -5,10 +5,15 @@ caller so the single-caller rule fires correctly.
5
5
 
6
6
  Loop2-D: module-scope usages must register as a distinct caller bucket so
7
7
  the "zero function references" exemption does not swallow real references.
8
+
9
+ Loop1-1: scope-bounded assertion collection — nested function/class bodies
10
+ inside compound statements must not have their assertions attributed to the
11
+ enclosing test function.
8
12
  """
9
13
 
10
14
  from __future__ import annotations
11
15
 
16
+ import ast
12
17
  import importlib.util
13
18
  import sys
14
19
  from pathlib import Path
@@ -67,6 +72,509 @@ def test_should_flag_constant_used_once_at_module_scope_and_once_in_function() -
67
72
  )
68
73
 
69
74
 
75
+ UNUSED_OPTIONAL_PRODUCTION_FILE_PATH = "packages/app/services/feature.py"
76
+ UNUSED_OPTIONAL_TEST_FILE_PATH = "packages/app/tests/test_feature.py"
77
+ UNUSED_OPTIONAL_CONFIG_FILE_PATH = "packages/app/config/constants.py"
78
+
79
+
80
+ def test_should_flag_optional_param_never_varied_in_file() -> None:
81
+ source = (
82
+ "def build_url(path: str, prefix: str = '/api') -> str:\n"
83
+ " return f'{prefix}{path}'\n"
84
+ "\n"
85
+ "def call_first() -> str:\n"
86
+ " return build_url('/users')\n"
87
+ "\n"
88
+ "def call_second() -> str:\n"
89
+ " return build_url('/items')\n"
90
+ )
91
+ issues = code_rules_enforcer.check_unused_optional_parameters(
92
+ source, UNUSED_OPTIONAL_PRODUCTION_FILE_PATH
93
+ )
94
+ assert any("prefix" in issue for issue in issues), (
95
+ f"Expected 'prefix' flagged as never-varied, got: {issues}"
96
+ )
97
+
98
+
99
+ def test_should_not_flag_when_param_is_varied_at_call_site() -> None:
100
+ source = (
101
+ "def build_url(path: str, prefix: str = '/api') -> str:\n"
102
+ " return f'{prefix}{path}'\n"
103
+ "\n"
104
+ "def call_with_default() -> str:\n"
105
+ " return build_url('/users')\n"
106
+ "\n"
107
+ "def call_with_override() -> str:\n"
108
+ " return build_url('/items', prefix='/v2')\n"
109
+ )
110
+ issues = code_rules_enforcer.check_unused_optional_parameters(
111
+ source, UNUSED_OPTIONAL_PRODUCTION_FILE_PATH
112
+ )
113
+ assert not any("prefix" in issue for issue in issues), (
114
+ f"Expected 'prefix' not flagged when varied, got: {issues}"
115
+ )
116
+
117
+
118
+ def test_should_not_flag_unused_optional_in_test_files() -> None:
119
+ source = (
120
+ "def build_url(path: str, prefix: str = '/api') -> str:\n"
121
+ " return f'{prefix}{path}'\n"
122
+ "\n"
123
+ "def call_first() -> str:\n"
124
+ " return build_url('/users')\n"
125
+ )
126
+ issues = code_rules_enforcer.check_unused_optional_parameters(
127
+ source, UNUSED_OPTIONAL_TEST_FILE_PATH
128
+ )
129
+ assert issues == [], f"Expected no issues in test file, got: {issues}"
130
+
131
+
132
+ def test_should_not_flag_unused_optional_in_config_files() -> None:
133
+ source = (
134
+ "def build_url(path: str, prefix: str = '/api') -> str:\n"
135
+ " return f'{prefix}{path}'\n"
136
+ "\n"
137
+ "def call_first() -> str:\n"
138
+ " return build_url('/users')\n"
139
+ )
140
+ issues = code_rules_enforcer.check_unused_optional_parameters(
141
+ source, UNUSED_OPTIONAL_CONFIG_FILE_PATH
142
+ )
143
+ assert issues == [], f"Expected no issues in config file, got: {issues}"
144
+
145
+
146
+ def test_should_not_flag_when_no_same_file_call_sites_exist() -> None:
147
+ source = (
148
+ "def build_url(path: str, prefix: str = '/api') -> str:\n"
149
+ " return f'{prefix}{path}'\n"
150
+ )
151
+ issues = code_rules_enforcer.check_unused_optional_parameters(
152
+ source, UNUSED_OPTIONAL_PRODUCTION_FILE_PATH
153
+ )
154
+ assert issues == [], (
155
+ f"Expected no issues when no same-file call sites, got: {issues}"
156
+ )
157
+
158
+
159
+ def test_should_include_line_number_and_param_name_in_issue() -> None:
160
+ source = (
161
+ "def fetch(url: str, timeout: int = 30) -> str:\n"
162
+ " return get(url, timeout=timeout)\n"
163
+ "\n"
164
+ "def run_fetch() -> str:\n"
165
+ " return fetch('http://example.com')\n"
166
+ )
167
+ issues = code_rules_enforcer.check_unused_optional_parameters(
168
+ source, UNUSED_OPTIONAL_PRODUCTION_FILE_PATH
169
+ )
170
+ assert any("Line 1" in issue and "timeout" in issue for issue in issues), (
171
+ f"Expected issue with line number and param name, got: {issues}"
172
+ )
173
+
174
+
175
+ def test_should_flag_when_every_call_passes_the_exact_default() -> None:
176
+ source = (
177
+ "def fetch(url: str, timeout: int = 30) -> str:\n"
178
+ " return get(url, timeout=timeout)\n"
179
+ "\n"
180
+ "def run_fetch() -> str:\n"
181
+ " return fetch('http://example.com', timeout=30)\n"
182
+ )
183
+ issues = code_rules_enforcer.check_unused_optional_parameters(
184
+ source, UNUSED_OPTIONAL_PRODUCTION_FILE_PATH
185
+ )
186
+ assert any("timeout" in issue for issue in issues), (
187
+ f"Expected 'timeout' flagged when every call passes the exact default, got: {issues}"
188
+ )
189
+
190
+
191
+ def test_check_unused_optional_parameters_stops_at_max_issues_per_check() -> None:
192
+ source = (
193
+ "def make_url_one(path: str, prefix: str = '/api') -> str:\n"
194
+ " return f'{prefix}{path}'\n"
195
+ "def make_url_two(path: str, prefix: str = '/api') -> str:\n"
196
+ " return f'{prefix}{path}'\n"
197
+ "def make_url_three(path: str, prefix: str = '/api') -> str:\n"
198
+ " return f'{prefix}{path}'\n"
199
+ "def make_url_four(path: str, prefix: str = '/api') -> str:\n"
200
+ " return f'{prefix}{path}'\n"
201
+ "def make_url_five(path: str, prefix: str = '/api') -> str:\n"
202
+ " return f'{prefix}{path}'\n"
203
+ "\n"
204
+ "def call_all() -> None:\n"
205
+ " make_url_one('/a')\n"
206
+ " make_url_two('/b')\n"
207
+ " make_url_three('/c')\n"
208
+ " make_url_four('/d')\n"
209
+ " make_url_five('/e')\n"
210
+ )
211
+ issues = code_rules_enforcer.check_unused_optional_parameters(
212
+ source, UNUSED_OPTIONAL_PRODUCTION_FILE_PATH
213
+ )
214
+ assert len(issues) == code_rules_enforcer.MAX_ISSUES_PER_CHECK, (
215
+ f"Expected exactly MAX_ISSUES_PER_CHECK issues, got {len(issues)}: {issues}"
216
+ )
217
+
218
+
219
+ INCOMPLETE_MOCK_TEST_FILE_PATH = "packages/app/tests/test_orders.py"
220
+ INCOMPLETE_MOCK_PRODUCTION_FILE_PATH = "packages/app/services/orders.py"
221
+
222
+
223
+ def test_should_advise_when_mock_missing_accessed_field(capsys: object) -> None:
224
+ source = (
225
+ "mock_order = {'id': 1}\n"
226
+ "\n"
227
+ "def test_order_total() -> None:\n"
228
+ " total = mock_order['total']\n"
229
+ " assert total > 0\n"
230
+ )
231
+ code_rules_enforcer.check_incomplete_mocks(source, INCOMPLETE_MOCK_TEST_FILE_PATH)
232
+ captured = getattr(capsys, "readouterr")()
233
+ assert "mock_order" in captured.err and "total" in captured.err, (
234
+ f"Expected advisory about missing 'total' field, got: {captured.err!r}"
235
+ )
236
+
237
+
238
+ def test_should_not_advise_when_mock_has_all_accessed_fields(capsys: object) -> None:
239
+ source = (
240
+ "mock_order = {'id': 1, 'total': 50}\n"
241
+ "\n"
242
+ "def test_order_total() -> None:\n"
243
+ " total = mock_order['total']\n"
244
+ " assert total > 0\n"
245
+ )
246
+ code_rules_enforcer.check_incomplete_mocks(source, INCOMPLETE_MOCK_TEST_FILE_PATH)
247
+ captured = getattr(capsys, "readouterr")()
248
+ assert "mock_order" not in captured.err, (
249
+ f"Expected no advisory when all fields present, got: {captured.err!r}"
250
+ )
251
+
252
+
253
+ def test_should_not_advise_for_incomplete_mocks_in_production_files(capsys: object) -> None:
254
+ source = (
255
+ "mock_order = {'id': 1}\n"
256
+ "\n"
257
+ "def run_order() -> None:\n"
258
+ " total = mock_order['total']\n"
259
+ )
260
+ code_rules_enforcer.check_incomplete_mocks(source, INCOMPLETE_MOCK_PRODUCTION_FILE_PATH)
261
+ captured = getattr(capsys, "readouterr")()
262
+ assert "mock_order" not in captured.err, (
263
+ f"Expected no advisory in production file, got: {captured.err!r}"
264
+ )
265
+
266
+
267
+ def test_should_advise_for_attribute_access_on_mock_object(capsys: object) -> None:
268
+ source = (
269
+ "class MockUser:\n"
270
+ " pass\n"
271
+ "\n"
272
+ "mock_user = MockUser()\n"
273
+ "mock_user.name = 'Alice'\n"
274
+ "\n"
275
+ "def test_user_email() -> None:\n"
276
+ " email = mock_user.email\n"
277
+ " assert email\n"
278
+ )
279
+ code_rules_enforcer.check_incomplete_mocks(source, INCOMPLETE_MOCK_TEST_FILE_PATH)
280
+ captured = getattr(capsys, "readouterr")()
281
+ assert "mock_user" in captured.err and "email" in captured.err, (
282
+ f"Expected advisory about missing 'email' attribute, got: {captured.err!r}"
283
+ )
284
+
285
+
286
+ DUPLICATED_FORMAT_PRODUCTION_FILE_PATH = "packages/app/services/api_client.py"
287
+ DUPLICATED_FORMAT_TEST_FILE_PATH = "packages/app/tests/test_api_client.py"
288
+
289
+
290
+ def test_should_advise_when_fstring_skeleton_appears_three_or_more_times(capsys: object) -> None:
291
+ source = (
292
+ "def get_user(user_id: str) -> str:\n"
293
+ " return f'/api/{user_id}'\n"
294
+ "\n"
295
+ "def get_order(order_id: str) -> str:\n"
296
+ " return f'/api/{order_id}'\n"
297
+ "\n"
298
+ "def get_product(product_id: str) -> str:\n"
299
+ " return f'/api/{product_id}'\n"
300
+ )
301
+ code_rules_enforcer.check_duplicated_format_patterns(
302
+ source, DUPLICATED_FORMAT_PRODUCTION_FILE_PATH
303
+ )
304
+ captured = getattr(capsys, "readouterr")()
305
+ assert "/api/" in captured.err and "3" in captured.err, (
306
+ f"Expected advisory for repeated /api/<x> pattern, got: {captured.err!r}"
307
+ )
308
+
309
+
310
+ def test_should_not_advise_when_fstring_skeleton_appears_fewer_than_three_times(capsys: object) -> None:
311
+ source = (
312
+ "def get_user(user_id: str) -> str:\n"
313
+ " return f'/api/{user_id}'\n"
314
+ "\n"
315
+ "def get_order(order_id: str) -> str:\n"
316
+ " return f'/api/{order_id}'\n"
317
+ )
318
+ code_rules_enforcer.check_duplicated_format_patterns(
319
+ source, DUPLICATED_FORMAT_PRODUCTION_FILE_PATH
320
+ )
321
+ captured = getattr(capsys, "readouterr")()
322
+ assert "/api/" not in captured.err, (
323
+ f"Expected no advisory for pattern appearing only twice, got: {captured.err!r}"
324
+ )
325
+
326
+
327
+ def test_should_not_advise_for_duplicated_format_patterns_in_test_files(capsys: object) -> None:
328
+ source = (
329
+ "def test_user() -> None:\n"
330
+ " url_a = f'/api/{1}'\n"
331
+ " url_b = f'/api/{2}'\n"
332
+ " url_c = f'/api/{3}'\n"
333
+ )
334
+ code_rules_enforcer.check_duplicated_format_patterns(
335
+ source, DUPLICATED_FORMAT_TEST_FILE_PATH
336
+ )
337
+ captured = getattr(capsys, "readouterr")()
338
+ assert "/api/" not in captured.err, (
339
+ f"Expected no advisory in test file, got: {captured.err!r}"
340
+ )
341
+
342
+
343
+ def test_should_advise_with_distinct_skeletons(capsys: object) -> None:
344
+ source = (
345
+ "def first(team: str, user: str) -> str:\n"
346
+ " return f'/teams/{team}/users/{user}'\n"
347
+ "\n"
348
+ "def second(team: str, role: str) -> str:\n"
349
+ " return f'/teams/{team}/users/{role}'\n"
350
+ "\n"
351
+ "def third(team: str, admin: str) -> str:\n"
352
+ " return f'/teams/{team}/users/{admin}'\n"
353
+ )
354
+ code_rules_enforcer.check_duplicated_format_patterns(
355
+ source, DUPLICATED_FORMAT_PRODUCTION_FILE_PATH
356
+ )
357
+ captured = getattr(capsys, "readouterr")()
358
+ assert "/teams/" in captured.err, (
359
+ f"Expected advisory for repeated /teams/<x>/users/<x> pattern, got: {captured.err!r}"
360
+ )
361
+
362
+
363
+ def test_build_fstring_skeleton_preserves_literal_interp_substring() -> None:
364
+ joined_str_expression = ast.parse("f'PREFIX INTERP {value} SUFFIX'", mode="eval").body
365
+ assert isinstance(joined_str_expression, ast.JoinedStr)
366
+ skeleton = code_rules_enforcer._build_fstring_skeleton(joined_str_expression)
367
+ assert skeleton == "PREFIX INTERP <x> SUFFIX", (
368
+ "Literal 'INTERP' text inside an f-string must survive skeleton building — "
369
+ f"only interpolation slots should become '<x>'. Got: {skeleton!r}"
370
+ )
371
+
372
+
373
+ CONSTANT_EQUALITY_TEST_FILE_PATH = "packages/app/tests/test_constants.py"
374
+
375
+
376
+ def test_should_not_flag_two_named_constants_compared_to_each_other() -> None:
377
+ source = (
378
+ "FOO = 'a'\n"
379
+ "BAR = 'b'\n"
380
+ "\n"
381
+ "def test_constants_differ() -> None:\n"
382
+ " assert FOO == BAR\n"
383
+ )
384
+ issues = code_rules_enforcer.check_constant_equality_tests(
385
+ source, CONSTANT_EQUALITY_TEST_FILE_PATH
386
+ )
387
+ assert issues == [], (
388
+ f"Expected no flag when both sides are named constants, got: {issues}"
389
+ )
390
+
391
+
392
+ def test_should_flag_named_constant_compared_to_literal() -> None:
393
+ source = (
394
+ "FOO = 'a'\n"
395
+ "\n"
396
+ "def test_foo_value() -> None:\n"
397
+ " assert FOO == 'literal'\n"
398
+ )
399
+ issues = code_rules_enforcer.check_constant_equality_tests(
400
+ source, CONSTANT_EQUALITY_TEST_FILE_PATH
401
+ )
402
+ assert any("constant-value test" in issue for issue in issues), (
403
+ f"Expected flag when UPPER_SNAKE compared to literal, got: {issues}"
404
+ )
405
+
406
+
407
+ NESTED_FUNCTION_PRODUCTION_FILE_PATH = "packages/app/services/nested.py"
408
+
409
+
410
+ def test_should_not_flag_nested_function_optional_param() -> None:
411
+ source = (
412
+ "def outer() -> None:\n"
413
+ " def inner(timeout: int = 30) -> None:\n"
414
+ " pass\n"
415
+ " inner()\n"
416
+ " inner()\n"
417
+ )
418
+ issues = code_rules_enforcer.check_unused_optional_parameters(
419
+ source, NESTED_FUNCTION_PRODUCTION_FILE_PATH
420
+ )
421
+ assert not any("timeout" in issue for issue in issues), (
422
+ f"Expected nested function 'timeout' not flagged, got: {issues}"
423
+ )
424
+
425
+
426
+ def test_should_advise_when_mock_defined_inside_test_function_is_incomplete(
427
+ capsys: object,
428
+ ) -> None:
429
+ source = (
430
+ "def test_thing() -> None:\n"
431
+ " mock_user = {'name': 'x'}\n"
432
+ " assert mock_user['email'] == 'y'\n"
433
+ )
434
+ code_rules_enforcer.check_incomplete_mocks(source, INCOMPLETE_MOCK_TEST_FILE_PATH)
435
+ captured = getattr(capsys, "readouterr")()
436
+ assert "mock_user" in captured.err and "email" in captured.err, (
437
+ f"Expected advisory for mock defined inside test function, got: {captured.err!r}"
438
+ )
439
+
440
+
441
+ def test_should_emit_advisories_for_incomplete_mocks_and_format_patterns_via_validate_content(
442
+ capsys: object,
443
+ ) -> None:
444
+ incomplete_mock_source = (
445
+ "mock_order = {'id': 1}\n"
446
+ "\n"
447
+ "def test_order_total() -> None:\n"
448
+ " total = mock_order['total']\n"
449
+ " assert total > 0\n"
450
+ )
451
+ code_rules_enforcer.validate_content(
452
+ incomplete_mock_source, INCOMPLETE_MOCK_TEST_FILE_PATH
453
+ )
454
+ captured = getattr(capsys, "readouterr")()
455
+ assert "mock_order" in captured.err and "total" in captured.err, (
456
+ f"Expected incomplete-mock advisory from validate_content, got: {captured.err!r}"
457
+ )
458
+
459
+ repeated_pattern_source = (
460
+ "def get_user(user_id: str) -> str:\n"
461
+ " return f'/api/{user_id}'\n"
462
+ "\n"
463
+ "def get_order(order_id: str) -> str:\n"
464
+ " return f'/api/{order_id}'\n"
465
+ "\n"
466
+ "def get_product(product_id: str) -> str:\n"
467
+ " return f'/api/{product_id}'\n"
468
+ )
469
+ code_rules_enforcer.validate_content(
470
+ repeated_pattern_source, DUPLICATED_FORMAT_PRODUCTION_FILE_PATH
471
+ )
472
+ captured = getattr(capsys, "readouterr")()
473
+ assert "/api/" in captured.err and "3" in captured.err, (
474
+ f"Expected duplicated-format advisory from validate_content, got: {captured.err!r}"
475
+ )
476
+
477
+
478
+ SCOPE_KEYED_MOCK_TEST_FILE_PATH = "packages/app/tests/test_scope_mocks.py"
479
+ KWARGS_EXPANSION_PRODUCTION_FILE_PATH = "packages/app/services/fetcher.py"
480
+
481
+
482
+ def test_should_check_each_scope_mock_against_its_own_field_set(capsys: object) -> None:
483
+ """Same mock_user name in two test functions with different field sets.
484
+
485
+ First function defines mock_user with only 'id'; accesses 'email' — should warn.
486
+ Second function defines mock_user with 'id' and 'email'; accesses 'email' — no warn.
487
+ The second definition must NOT overwrite the first scope's tracking.
488
+ """
489
+ source = (
490
+ "def test_first_scope() -> None:\n"
491
+ " mock_user = {'id': 1}\n"
492
+ " email = mock_user['email']\n"
493
+ "\n"
494
+ "def test_second_scope() -> None:\n"
495
+ " mock_user = {'id': 2, 'email': 'b@b.com'}\n"
496
+ " email = mock_user['email']\n"
497
+ )
498
+ code_rules_enforcer.check_incomplete_mocks(source, SCOPE_KEYED_MOCK_TEST_FILE_PATH)
499
+ captured = getattr(capsys, "readouterr")()
500
+ advisory_lines = [
501
+ line for line in captured.err.splitlines() if "mock_user" in line and "email" in line
502
+ ]
503
+ assert len(advisory_lines) == 1, (
504
+ f"Expected exactly 1 advisory (first scope missing email), got: {captured.err!r}"
505
+ )
506
+
507
+
508
+ def test_should_emit_exactly_one_advisory_for_repeated_accesses_to_same_missing_field(
509
+ capsys: object,
510
+ ) -> None:
511
+ """mock_user accessed 5 times for 'email' but email is missing — emit exactly one advisory."""
512
+ source = (
513
+ "def test_repeated_access() -> None:\n"
514
+ " mock_user = {'id': 1}\n"
515
+ " _ = mock_user['email']\n"
516
+ " _ = mock_user['email']\n"
517
+ " _ = mock_user['email']\n"
518
+ " _ = mock_user['email']\n"
519
+ " _ = mock_user['email']\n"
520
+ )
521
+ code_rules_enforcer.check_incomplete_mocks(source, SCOPE_KEYED_MOCK_TEST_FILE_PATH)
522
+ captured = getattr(capsys, "readouterr")()
523
+ advisory_lines = [
524
+ line for line in captured.err.splitlines() if "mock_user" in line and "email" in line
525
+ ]
526
+ assert len(advisory_lines) == 1, (
527
+ f"Expected exactly 1 advisory for 5 repeated accesses to missing 'email', got: {captured.err!r}"
528
+ )
529
+
530
+
531
+ def test_should_not_flag_optional_param_when_only_call_site_uses_kwargs_expansion() -> None:
532
+ """A call using **defaults passes unknown values — the param must NOT be flagged."""
533
+ source = (
534
+ "def fetch(url: str, timeout: int = 30) -> str:\n"
535
+ " return url\n"
536
+ "\n"
537
+ "def run() -> str:\n"
538
+ " defaults = {'timeout': 30}\n"
539
+ " return fetch('http://example.com', **defaults)\n"
540
+ )
541
+ issues = code_rules_enforcer.check_unused_optional_parameters(
542
+ source, KWARGS_EXPANSION_PRODUCTION_FILE_PATH
543
+ )
544
+ assert not any("timeout" in issue for issue in issues), (
545
+ f"Expected 'timeout' NOT flagged when call uses **kwargs expansion, got: {issues}"
546
+ )
547
+
548
+
549
+ MODULE_LEVEL_MOCK_TEST_FILE_PATH = "packages/app/tests/test_module_level.py"
550
+
551
+
552
+ def test_should_emit_exactly_one_advisory_for_module_level_mock_with_missing_field(
553
+ capsys: object,
554
+ ) -> None:
555
+ """Module-level mock_user with one missing field access should produce ONE advisory.
556
+
557
+ Finding 4: ast.walk() already yields the root Module node, so
558
+ [module_tree, *ast.walk(module_tree)] iterates the module twice and
559
+ previously produced two identical advisories for module-level mocks.
560
+ """
561
+ source = (
562
+ "mock_user = {'name': 'Alice'}\n"
563
+ "\n"
564
+ "def test_email_present() -> None:\n"
565
+ " email = mock_user['email']\n"
566
+ " assert email\n"
567
+ )
568
+ code_rules_enforcer.check_incomplete_mocks(source, MODULE_LEVEL_MOCK_TEST_FILE_PATH)
569
+ captured = getattr(capsys, "readouterr")()
570
+ advisory_lines = [
571
+ line for line in captured.err.splitlines() if "mock_user" in line and "email" in line
572
+ ]
573
+ assert len(advisory_lines) == 1, (
574
+ f"Expected exactly 1 advisory for module-level mock missing 'email', got: {captured.err!r}"
575
+ )
576
+
577
+
70
578
  def test_is_config_file_rejects_filename_only_config_pattern() -> None:
71
579
  """Paths where 'config' appears only in the filename (not as a directory segment) must return False."""
72
580
  assert code_rules_enforcer.is_config_file("scripts/db/config.py") is False, (
@@ -207,3 +715,246 @@ def test_advisory_should_flag_outer_constants_after_nested_def() -> None:
207
715
  assert "ANOTHER_OUTER" in flagged_names, (
208
716
  "ANOTHER_OUTER after nested def must be flagged — this is the regression case"
209
717
  )
718
+
719
+
720
+ def test_should_not_leak_shadowed_nested_assignment_into_outer_mock_known_fields(
721
+ capsys: object,
722
+ ) -> None:
723
+ """Assignment collector must skip nested scopes that shadow the mock name.
724
+
725
+ The access collector uses _walk_scope_skipping_shadowed; the assignment
726
+ collector must do the same, otherwise attribute assignments inside a
727
+ nested function that redefines mock_user leak into the outer mock's
728
+ known-fields set and suppress advisories for genuinely missing fields.
729
+ """
730
+ source = (
731
+ "mock_user = {'id': 1}\n"
732
+ "outer_value = mock_user['email']\n"
733
+ "\n"
734
+ "def test_inner() -> None:\n"
735
+ " mock_user = {'id': 2}\n"
736
+ " mock_user.email = 'shadowed@example.com'\n"
737
+ )
738
+ code_rules_enforcer.check_incomplete_mocks(source, MODULE_LEVEL_MOCK_TEST_FILE_PATH)
739
+ captured = getattr(capsys, "readouterr")()
740
+ advisory_lines = [
741
+ line for line in captured.err.splitlines() if "mock_user" in line and "email" in line
742
+ ]
743
+ assert len(advisory_lines) == 1, (
744
+ "Expected outer mock's missing 'email' advisory to fire even when a shadowing "
745
+ f"nested function assigns mock_user.email, got: {captured.err!r}"
746
+ )
747
+
748
+
749
+ def test_should_treat_annotated_assignment_as_shadowing_in_nested_scope(
750
+ capsys: object,
751
+ ) -> None:
752
+ """AnnAssign must shadow just like Assign.
753
+
754
+ When a nested scope re-binds the mock variable via an annotated
755
+ assignment (``mock_user: dict = {...}``), accesses inside that nested
756
+ scope belong to the inner mock, not the outer one. If the shadow
757
+ detector ignores AnnAssign, inner accesses leak out and cause
758
+ spurious advisories against the outer mock for fields it never sees.
759
+ """
760
+ source = (
761
+ "mock_user = {'id': 1, 'name': 'outer'}\n"
762
+ "outer_value = mock_user['name']\n"
763
+ "\n"
764
+ "def test_inner() -> None:\n"
765
+ " mock_user: dict = {'id': 2, 'timezone': 'UTC'}\n"
766
+ " inner_value = mock_user['timezone']\n"
767
+ )
768
+ code_rules_enforcer.check_incomplete_mocks(source, MODULE_LEVEL_MOCK_TEST_FILE_PATH)
769
+ captured = getattr(capsys, "readouterr")()
770
+ leaked_advisories = [
771
+ line
772
+ for line in captured.err.splitlines()
773
+ if "mock_user" in line and "timezone" in line
774
+ ]
775
+ assert leaked_advisories == [], (
776
+ "Expected no advisory on the outer mock for 'timezone' — that field is "
777
+ "accessed only inside a nested scope that re-binds mock_user via an "
778
+ f"annotated assignment, got: {captured.err!r}"
779
+ )
780
+
781
+
782
+ def _assert_inner_field_did_not_leak(
783
+ captured_stderr: str,
784
+ inner_only_field_name: str,
785
+ binding_form_description: str,
786
+ ) -> None:
787
+ leaked_advisories = [
788
+ line
789
+ for line in captured_stderr.splitlines()
790
+ if "mock_user" in line and inner_only_field_name in line
791
+ ]
792
+ assert leaked_advisories == [], (
793
+ f"Expected no advisory on the outer mock for {inner_only_field_name!r} — "
794
+ f"that field is accessed only inside a nested scope that re-binds "
795
+ f"mock_user via {binding_form_description}, got: {captured_stderr!r}"
796
+ )
797
+
798
+
799
+ def test_should_treat_assignment_inside_if_block_as_shadowing(capsys: object) -> None:
800
+ """Binding inside an ``if`` block must shadow the outer mock name.
801
+
802
+ Python binds a name locally when it is assigned *anywhere* in the
803
+ function body, including inside a branch. A shadow detector that only
804
+ inspects the top-level statements misses this form.
805
+ """
806
+ source = (
807
+ "mock_user = {'id': 1, 'name': 'outer'}\n"
808
+ "outer_value = mock_user['name']\n"
809
+ "\n"
810
+ "def test_inner() -> None:\n"
811
+ " if True:\n"
812
+ " mock_user = {'id': 2, 'timezone': 'UTC'}\n"
813
+ " inner_value = mock_user['timezone']\n"
814
+ )
815
+ code_rules_enforcer.check_incomplete_mocks(source, MODULE_LEVEL_MOCK_TEST_FILE_PATH)
816
+ captured = getattr(capsys, "readouterr")()
817
+ _assert_inner_field_did_not_leak(
818
+ captured.err, "timezone", "an assignment nested inside an if-block"
819
+ )
820
+
821
+
822
+ def test_should_treat_for_loop_target_as_shadowing(capsys: object) -> None:
823
+ """A ``for`` loop target binds the name locally and must shadow."""
824
+ source = (
825
+ "mock_user = {'id': 1, 'name': 'outer'}\n"
826
+ "outer_value = mock_user['name']\n"
827
+ "\n"
828
+ "def test_inner() -> None:\n"
829
+ " for mock_user in [{'id': 2, 'timezone': 'UTC'}]:\n"
830
+ " inner_value = mock_user['timezone']\n"
831
+ )
832
+ code_rules_enforcer.check_incomplete_mocks(source, MODULE_LEVEL_MOCK_TEST_FILE_PATH)
833
+ captured = getattr(capsys, "readouterr")()
834
+ _assert_inner_field_did_not_leak(
835
+ captured.err, "timezone", "a for-loop target"
836
+ )
837
+
838
+
839
+ def test_should_treat_try_except_handler_name_as_shadowing(capsys: object) -> None:
840
+ """An ``except ... as mock_user`` handler binds the name locally."""
841
+ source = (
842
+ "mock_user = {'id': 1, 'name': 'outer'}\n"
843
+ "outer_value = mock_user['name']\n"
844
+ "\n"
845
+ "def test_inner() -> None:\n"
846
+ " try:\n"
847
+ " raise ValueError({'id': 2, 'timezone': 'UTC'})\n"
848
+ " except ValueError as mock_user:\n"
849
+ " inner_value = mock_user['timezone']\n"
850
+ )
851
+ code_rules_enforcer.check_incomplete_mocks(source, MODULE_LEVEL_MOCK_TEST_FILE_PATH)
852
+ captured = getattr(capsys, "readouterr")()
853
+ _assert_inner_field_did_not_leak(
854
+ captured.err, "timezone", "an except-handler binding"
855
+ )
856
+
857
+
858
+ def test_should_treat_walrus_expression_as_shadowing(capsys: object) -> None:
859
+ """A named-expression walrus binding inside a condition must shadow."""
860
+ source = (
861
+ "mock_user = {'id': 1, 'name': 'outer'}\n"
862
+ "outer_value = mock_user['name']\n"
863
+ "\n"
864
+ "def test_inner() -> None:\n"
865
+ " if (mock_user := {'id': 2, 'timezone': 'UTC'}):\n"
866
+ " inner_value = mock_user['timezone']\n"
867
+ )
868
+ code_rules_enforcer.check_incomplete_mocks(source, MODULE_LEVEL_MOCK_TEST_FILE_PATH)
869
+ captured = getattr(capsys, "readouterr")()
870
+ _assert_inner_field_did_not_leak(
871
+ captured.err, "timezone", "a walrus named-expression"
872
+ )
873
+
874
+
875
+ def test_should_treat_function_parameter_as_shadowing(capsys: object) -> None:
876
+ """A parameter named like the mock variable must shadow the outer binding."""
877
+ source = (
878
+ "mock_user = {'id': 1, 'name': 'outer'}\n"
879
+ "outer_value = mock_user['name']\n"
880
+ "\n"
881
+ "def test_inner(mock_user: dict) -> None:\n"
882
+ " inner_value = mock_user['timezone']\n"
883
+ )
884
+ code_rules_enforcer.check_incomplete_mocks(source, MODULE_LEVEL_MOCK_TEST_FILE_PATH)
885
+ captured = getattr(capsys, "readouterr")()
886
+ _assert_inner_field_did_not_leak(
887
+ captured.err, "timezone", "a function parameter of the same name"
888
+ )
889
+
890
+
891
+ def test_should_treat_import_asname_as_shadowing(capsys: object) -> None:
892
+ """An ``import ... as mock_user`` must shadow the outer mock name."""
893
+ source = (
894
+ "mock_user = {'id': 1, 'name': 'outer'}\n"
895
+ "outer_value = mock_user['name']\n"
896
+ "\n"
897
+ "def test_inner() -> None:\n"
898
+ " import collections as mock_user\n"
899
+ " inner_value = mock_user['timezone']\n"
900
+ )
901
+ code_rules_enforcer.check_incomplete_mocks(source, MODULE_LEVEL_MOCK_TEST_FILE_PATH)
902
+ captured = getattr(capsys, "readouterr")()
903
+ _assert_inner_field_did_not_leak(
904
+ captured.err, "timezone", "an import-asname binding"
905
+ )
906
+
907
+
908
+ def test_should_not_advise_when_duplicated_fstring_literal_is_short(capsys: object) -> None:
909
+ """Short logger-prefix style f-strings must not emit a duplication advisory.
910
+
911
+ A three-times-repeated ``f"Got {x}"`` has only four characters of literal
912
+ text (``"Got "``). Flagging such short fragments creates noise for common
913
+ logging prefixes. The heuristic requires a minimum amount of structural
914
+ literal text before an advisory fires.
915
+ """
916
+ source = (
917
+ "def first(value: str) -> str:\n"
918
+ " return f'Got {value}'\n"
919
+ "\n"
920
+ "def second(value: str) -> str:\n"
921
+ " return f'Got {value}'\n"
922
+ "\n"
923
+ "def third(value: str) -> str:\n"
924
+ " return f'Got {value}'\n"
925
+ )
926
+ code_rules_enforcer.check_duplicated_format_patterns(
927
+ source, DUPLICATED_FORMAT_PRODUCTION_FILE_PATH
928
+ )
929
+ captured = getattr(capsys, "readouterr")()
930
+ assert "Got" not in captured.err, (
931
+ "Expected no advisory for a short repeated f-string literal fragment, "
932
+ f"got: {captured.err!r}"
933
+ )
934
+
935
+
936
+ def test_should_still_advise_when_duplicated_fstring_literal_is_long(capsys: object) -> None:
937
+ """Longer duplicated f-string skeletons must continue to fire.
938
+
939
+ The short-literal heuristic must not regress the existing
940
+ ``/api/<x>`` and ``/teams/<x>/users/<x>`` advisories — those path
941
+ skeletons carry enough structural literal text to warrant a helper.
942
+ """
943
+ source = (
944
+ "def get_user(user_id: str) -> str:\n"
945
+ " return f'/api/{user_id}'\n"
946
+ "\n"
947
+ "def get_order(order_id: str) -> str:\n"
948
+ " return f'/api/{order_id}'\n"
949
+ "\n"
950
+ "def get_product(product_id: str) -> str:\n"
951
+ " return f'/api/{product_id}'\n"
952
+ )
953
+ code_rules_enforcer.check_duplicated_format_patterns(
954
+ source, DUPLICATED_FORMAT_PRODUCTION_FILE_PATH
955
+ )
956
+ captured = getattr(capsys, "readouterr")()
957
+ assert "/api/" in captured.err, (
958
+ "Expected the existing /api/<x> path-shape advisory to still fire, "
959
+ f"got: {captured.err!r}"
960
+ )