claude-dev-env 1.71.0 → 1.72.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -27,6 +27,14 @@ When making code changes, make sure you are working in the proper worktree path
27
27
 
28
28
  `Edit` changes existing files; `Write` creates new ones. Default to `Edit` — reach for `Write` only for a genuinely new path. For a true full rewrite, delete the file first, then `Write`.
29
29
 
30
+ ## Showing Files: Open Them, Don't Print the Path
31
+
32
+ When I ask you to "show me", "open", "display", "let me see", or "pull up" a file — an image, PDF, HTML page, document, anything — open it on my screen. Launch the viewer so each image window matches the asset's size:
33
+
34
+ `Start-Process pwsh -WindowStyle Hidden -ArgumentList '-NoProfile','-File',"$HOME\.claude\scripts\Show-Asset.ps1",'<path 1>','<path 2>'`
35
+
36
+ It sizes each image window to the image (scaled down to fit the screen) and opens non-image files in their default app; pass every path I name. Printing a path or attaching the file is not showing it — do that only when the file truly cannot be opened, and say why.
37
+
30
38
  ## Test Philosophy
31
39
 
32
40
  When writing tests, always write tests that actually test the behavior of the function against actual, real data and environments.
@@ -594,11 +594,11 @@ def check_wrapper_plumb_through(content: str, file_path: str) -> list[str]:
594
594
  Args:
595
595
  content: File content as a single string for AST parsing.
596
596
  file_path: Repository-relative POSIX path of the file (used to
597
- skip non-Python code extensions early).
597
+ skip non-Python code extensions and test files early).
598
598
 
599
599
  Returns:
600
- List of violation strings, one per dropped optional kwarg. Returns
601
- an empty list when the file is not Python or has a syntax error.
600
+ List of violation strings, one per dropped optional kwarg. Empty for
601
+ a non-Python file, a test file, or a file with a syntax error.
602
602
  """
603
603
  non_python_code_extensions = ALL_CODE_FILE_EXTENSIONS - {PYTHON_FILE_EXTENSION}
604
604
  lowercase_file_path = file_path.lower()
@@ -607,6 +607,8 @@ def check_wrapper_plumb_through(content: str, file_path: str) -> list[str]:
607
607
  for each_extension in non_python_code_extensions
608
608
  ):
609
609
  return []
610
+ if is_test_path(file_path):
611
+ return []
610
612
  try:
611
613
  tree = ast.parse(content)
612
614
  except SyntaxError:
@@ -430,6 +430,45 @@ def test_check_wrapper_plumb_through_still_flags_attribute_call() -> None:
430
430
  )
431
431
 
432
432
 
433
+ def test_check_wrapper_plumb_through_exempts_test_files() -> None:
434
+ source = (
435
+ "def _helper(name, *, clean_name=None):\n"
436
+ " return (name, clean_name)\n"
437
+ "\n"
438
+ "def test_uses_helper():\n"
439
+ " return _helper('a', clean_name='b')\n"
440
+ )
441
+ shared_issues = gate_module.check_wrapper_plumb_through(source, "pkg/test_thing.py")
442
+ bugteam_gate = _load_bugteam_gate_module()
443
+ bugteam_issues = bugteam_gate.check_wrapper_plumb_through(source, "pkg/test_thing.py")
444
+ assert shared_issues == [], (
445
+ "a test_* function in a test-file path that calls a module-level helper "
446
+ "exposing an optional kwarg is not a wrapper; the shared gate must exempt "
447
+ "test files and emit zero findings"
448
+ )
449
+ assert bugteam_issues == [], (
450
+ "the bugteam gate copy must apply the identical test-file exemption"
451
+ )
452
+
453
+
454
+ def test_check_wrapper_plumb_through_still_flags_non_test_path_with_test_shape() -> None:
455
+ source = (
456
+ "def _helper(name, *, clean_name=None):\n"
457
+ " return (name, clean_name)\n"
458
+ "\n"
459
+ "def test_uses_helper():\n"
460
+ " return _helper('a', clean_name='b')\n"
461
+ )
462
+ issues = gate_module.check_wrapper_plumb_through(source, "pkg/module.py")
463
+ assert any(
464
+ "test_uses_helper" in each_issue and "clean_name" in each_issue
465
+ for each_issue in issues
466
+ ), (
467
+ "the test-file exemption is scoped to test paths only; the same wrapper "
468
+ "shape on a non-test path must still be flagged"
469
+ )
470
+
471
+
433
472
  def test_split_violations_by_scope_accepts_all_added_line_numbers_param_name() -> None:
434
473
  blocking_issues, advisory_issues = gate_module.split_violations_by_scope(
435
474
  ["Line 5: violation"],
@@ -264,6 +264,7 @@ Tests document behavior. The hook layer enforces several constraints on test fil
264
264
  - **No decorators named `skip*` on test functions.** Tests fail with a clear error rather than skip when a system dependency is missing. The hook fires on any decorator (whether `@skip_if_missing_dependency`, `@unittest.skipIf`, `@pytest.mark.skip`, or any custom variant) whose identifier contains the substring `skip`.
265
265
  - **No existence-only tests.** A test whose entire body is `assert callable(x)`, `assert hasattr(module, "name")`, or `assert obj is not None` covers no behavior. Replace with an assertion that exercises the behavior — call the function and assert on its return value or side effect.
266
266
  - **No constant-equality tests.** A test whose sole assertion is `assert CACHE_DIR == "cache"` (or any `UPPER_SNAKE == LITERAL` pattern) just verifies the constant has not changed. Delete it or replace with a behavior assertion.
267
+ - **No stale test names after a rename.** When you rename a function the tests exercise, rename the test functions in the same edit. The `check_stale_test_name_target` hook fires on a `test_*` name that embeds a snake_case run the file never imports, defines, or calls while the body calls a same-shape sibling — the signature of a producer rename that updated the bodies but left the test identifiers naming the deleted function.
267
268
  - **No tautological assertions.** `assert CONSTANT == CONSTANT` and `assert hasattr(module, "name")` pass regardless of the implementation. Replace with assertions that would fail if the implementation regressed.
268
269
  - **Test through the public API.** Do not assert on private state, hook return values, internal class fields, `_protected_field`, `__private_field`, or `component.state.X`. If the test needs visibility the public API does not provide, the public API needs a method, not the test.
269
270
  - **For React components**, query in this priority order: `getByRole > getByLabelText > getByText > getByTestId`. Use `userEvent` over `fireEvent` (more realistic). Mock at API boundaries (network calls, external services), not internal hooks or utilities.
@@ -23,7 +23,7 @@ Compact reference for agents. ⚡ marks rules enforced by `code_rules_enforcer.p
23
23
 
24
24
  `code_rules_enforcer.py` blocks each of these at Write/Edit and explains the specific violation when it fires; exact patterns and exemption lists live in the hook:
25
25
 
26
- no new comments · imports at top · logging format args (`log_*("...", arg)`) · no magic values in production bodies (0, 1, -1 exempt) · UPPER_SNAKE constants only in `config/` (exempt: `config/*`, `/migrations/`, workflow registries `/workflow/` + `_tab.py` + `/states.py` + `/modules.py`, test files) · no hardcoded user home paths · guarded `sys.path.insert` · no unused module-level imports · banned identifiers (`ctx`, `cfg`, `msg`, `btn`, `idx`, `cnt`, `tmp`, `elem`, `val`) · banned function prefixes (`handle_`, `process_`, `manage_`, `do_`) · no type escape hatches (`Any` import, `cast()`, inline `Any`) outside boundary files · no bare/broad `except` · no `Any` in signatures or class attributes · no stub bodies (`pass`/`...`/`raise NotImplementedError`) outside abstract/Protocol · TypedDict `_encode_*`/`_decode_*` companions in the same module · no test-mode branching in production (use dependency injection) · no thin wrapper modules · Google-style docstrings on public functions with `Args:` matching the signature · boolean names prefixed `is_`/`has_`/`should_`/`can_`/`was_`/`did_` (assignments AND bool-typed parameters) · must-check returns (`find_and_click`, `write_outcome`) assigned and checked · known pytest fixture parameters in test files annotated with their single documented type (`tmp_path: Path`, `monkeypatch: pytest.MonkeyPatch`, `capsys`, `caplog`, `request`, …) · known pytest fixture parameters a test function declares but never references (drop the unused parameter — pytest still pays its setup cost)
26
+ no new comments · imports at top · logging format args (`log_*("...", arg)`) · no magic values in production bodies (0, 1, -1 exempt) · UPPER_SNAKE constants only in `config/` (exempt: `config/*`, `/migrations/`, workflow registries `/workflow/` + `_tab.py` + `/states.py` + `/modules.py`, test files) · no hardcoded user home paths · guarded `sys.path.insert` · no unused module-level imports · banned identifiers (`ctx`, `cfg`, `msg`, `btn`, `idx`, `cnt`, `tmp`, `elem`, `val`) · banned function prefixes (`handle_`, `process_`, `manage_`, `do_`) · no type escape hatches (`Any` import, `cast()`, inline `Any`, a parameter typed bare `object` whose body reads `param.attribute`) outside boundary files · no bare/broad `except` · no `Any` in signatures or class attributes · no stub bodies (`pass`/`...`/`raise NotImplementedError`) outside abstract/Protocol · TypedDict `_encode_*`/`_decode_*` companions in the same module · no test-mode branching in production (use dependency injection) · no thin wrapper modules · Google-style docstrings on public functions with `Args:` matching the signature · boolean names prefixed `is_`/`has_`/`should_`/`can_`/`was_`/`did_` (assignments AND bool-typed parameters) · must-check returns (`find_and_click`, `write_outcome`) assigned and checked · known pytest fixture parameters in test files annotated with their single documented type (`tmp_path: Path`, `monkeypatch: pytest.MonkeyPatch`, `capsys`, `caplog`, `request`, …) · known pytest fixture parameters a test function declares but never references (drop the unused parameter — pytest still pays its setup cost)
27
27
 
28
28
  Test files are exempt from most checks. The one annotation the test-file exemption does NOT cover is a known pytest builtin fixture parameter: `tmp_path`, `monkeypatch`, `capsys`, `capfd`, `caplog`, `request`, and `tmp_path_factory` each have a single documented injected type, so the gate requires that annotation (`tmp_path: Path`) even inside a test file. The same set of fixtures is also subject to a use check: a pytest-collected test function that declares one of these parameters and never references it in its body fails the gate, because pytest materializes the fixture's setup (the temp directory, the monkeypatch context, the output capture) on every run whether or not the body reads the value — drop the unused parameter. A parameter counts as referenced when its name is read, augmented-assigned, or deleted anywhere in the body, including inside a nested function or comprehension. Only pytest-collectable functions are inspected — those at module top level or defined directly in a class body; a function nested inside another function's body is a local helper pytest never collects, so its fixture-named parameter is exempt. A `@pytest.fixture`-decorated function is exempt from the use check, since injecting one fixture into another purely to order its setup is intentional. Ordinary test parameters stay exempt from both checks. See also the file-global constants use-count rule: [`rules/file-global-constants.md`](../rules/file-global-constants.md).
29
29
 
@@ -24,12 +24,14 @@ from hooks_constants.blocking_check_limits import ( # noqa: E402
24
24
  ALL_DOCSTRING_EXEMPT_DECORATOR_NAMES,
25
25
  ALL_DOCSTRING_IMPLICIT_INSTANCE_PARAMETER_NAMES,
26
26
  ALL_DOCSTRING_MULTIPLE_CONDITION_JOINING_PHRASES,
27
+ ALL_DOCSTRING_NO_CONSUMER_CLAIM_PHRASES,
27
28
  DOCSTRING_FALLBACK_BRANCH_MINIMUM_ROUTE_COUNT,
28
29
  DOCSTRING_TRIVIAL_FUNCTION_BODY_LINE_LIMIT,
29
30
  MAX_CLASS_DOCSTRING_PUBLIC_METHOD_ISSUES,
30
31
  MAX_DOCSTRING_ARGS_SIGNATURE_ISSUES,
31
32
  MAX_DOCSTRING_FALLBACK_BRANCH_ISSUES,
32
33
  MAX_DOCSTRING_FORMAT_ISSUES,
34
+ MAX_DOCSTRING_NO_CONSUMER_CLAIM_ISSUES,
33
35
  MINIMUM_PUBLIC_METHODS_FOR_CLASS_DOCSTRING_BREADTH,
34
36
  )
35
37
  from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
@@ -559,3 +561,61 @@ def check_class_docstring_names_public_methods(
559
561
  if len(issues) >= MAX_CLASS_DOCSTRING_PUBLIC_METHOD_ISSUES:
560
562
  break
561
563
  return issues[:MAX_CLASS_DOCSTRING_PUBLIC_METHOD_ISSUES]
564
+
565
+
566
+ def _docstring_claims_no_consumer(docstring_text: str) -> str:
567
+ lowered_docstring = docstring_text.lower()
568
+ for each_phrase in ALL_DOCSTRING_NO_CONSUMER_CLAIM_PHRASES:
569
+ if each_phrase in lowered_docstring:
570
+ return each_phrase
571
+ return ""
572
+
573
+
574
+ def check_docstring_no_consumer_claim(content: str, file_path: str) -> list[str]:
575
+ """Flag a docstring that asserts no consumer reads its produced artifact yet.
576
+
577
+ A producer docstring claiming "no consumer reads it yet" (or
578
+ "producer-only artifact") is a transitional statement that drifts the moment
579
+ a consumer lands. Once a submission run, gate, or any reader loads the
580
+ artifact, the claim contradicts both the live behavior and any companion
581
+ SKILL.md that documents the consumer — the Category O8 docstring /
582
+ companion-doc producer-consumer drift. The claim is also a no-historical /
583
+ no-transitional-language violation in its own right: a docstring describes
584
+ the contract that exists, not a not-yet-wired future. Rephrase to state what
585
+ reads the artifact, or drop the no-consumer sentence entirely.
586
+
587
+ Args:
588
+ content: The source text to inspect.
589
+ file_path: The path the source will be written to, used for exemptions.
590
+
591
+ Returns:
592
+ One issue per function whose docstring claims no consumer reads its
593
+ output, capped at the module limit.
594
+ """
595
+ if is_test_file(file_path) or is_hook_infrastructure(file_path):
596
+ return []
597
+ try:
598
+ parsed_tree = ast.parse(content)
599
+ except SyntaxError:
600
+ return []
601
+ issues: list[str] = []
602
+ for each_node in _walk_skipping_type_checking_blocks(parsed_tree):
603
+ if not isinstance(each_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
604
+ continue
605
+ if _function_has_exempt_decorator(each_node):
606
+ continue
607
+ docstring_text = _function_docstring_text(each_node)
608
+ if not docstring_text:
609
+ continue
610
+ matched_phrase = _docstring_claims_no_consumer(docstring_text)
611
+ if not matched_phrase:
612
+ continue
613
+ issues.append(
614
+ f"Line {each_node.lineno}: {each_node.name}() docstring claims "
615
+ f"'{matched_phrase}' — a no-consumer-yet claim drifts the moment a reader "
616
+ "lands and contradicts any companion SKILL.md; state what reads the artifact "
617
+ "or drop the sentence (Category O8 docstring / companion-doc drift)"
618
+ )
619
+ if len(issues) >= MAX_DOCSTRING_NO_CONSUMER_CLAIM_ISSUES:
620
+ break
621
+ return issues[:MAX_DOCSTRING_NO_CONSUMER_CLAIM_ISSUES]
@@ -69,6 +69,7 @@ from code_rules_docstrings import ( # noqa: E402
69
69
  check_docstring_args_match_signature,
70
70
  check_docstring_fallback_branch_coverage,
71
71
  check_docstring_format,
72
+ check_docstring_no_consumer_claim,
72
73
  )
73
74
  from code_rules_duplicate_body import ( # noqa: E402
74
75
  advise_cross_skill_duplicate_helper,
@@ -122,6 +123,7 @@ from code_rules_test_assertions import ( # noqa: E402
122
123
  check_existence_check_tests,
123
124
  check_flag_gated_scenario_test_naming,
124
125
  check_skip_decorators_in_tests,
126
+ check_stale_test_name_target,
125
127
  )
126
128
  from code_rules_test_branching_except import ( # noqa: E402
127
129
  check_bare_except,
@@ -252,6 +254,7 @@ def validate_content(
252
254
  all_issues.extend(check_docstring_format(effective_content, file_path))
253
255
  all_issues.extend(check_docstring_args_match_signature(effective_content, file_path))
254
256
  all_issues.extend(check_docstring_fallback_branch_coverage(effective_content, file_path))
257
+ all_issues.extend(check_docstring_no_consumer_claim(effective_content, file_path))
255
258
  all_issues.extend(
256
259
  check_class_docstring_names_public_methods(effective_content, file_path)
257
260
  )
@@ -282,6 +285,7 @@ def validate_content(
282
285
  )
283
286
  all_issues.extend(check_existence_check_tests(content, file_path))
284
287
  all_issues.extend(check_constant_equality_tests(content, file_path))
288
+ all_issues.extend(check_stale_test_name_target(content, file_path))
285
289
  check_flag_gated_scenario_test_naming(content, file_path)
286
290
  all_issues.extend(check_unused_optional_parameters(content, file_path))
287
291
  all_issues.extend(check_collection_prefix(content, file_path))
@@ -1,4 +1,4 @@
1
- """Skip-decorator, existence-only, constant-equality, and flag-gated scenario test-quality checks."""
1
+ """Skip-decorator, existence-only, constant-equality, stale-test-name, and flag-gated scenario test-quality checks."""
2
2
 
3
3
  import ast
4
4
  import sys
@@ -18,6 +18,10 @@ from code_rules_shared import ( # noqa: E402
18
18
  is_test_file,
19
19
  )
20
20
 
21
+ from hooks_constants.blocking_check_limits import ( # noqa: E402
22
+ MAX_STALE_TEST_NAME_TARGET_ISSUES,
23
+ STALE_TEST_NAME_MINIMUM_SHARED_TOKEN_COUNT,
24
+ )
21
25
  from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
22
26
  UPPER_SNAKE_CONSTANT_PATTERN,
23
27
  )
@@ -346,3 +350,150 @@ def check_flag_gated_scenario_test_naming(content: str, file_path: str) -> list[
346
350
  )
347
351
 
348
352
  return []
353
+
354
+
355
+ def _called_function_names(function_node: ast.FunctionDef | ast.AsyncFunctionDef) -> set[str]:
356
+ """Return the bare names of every function the test body calls."""
357
+ called_names: set[str] = set()
358
+ for each_node in ast.walk(function_node):
359
+ if not isinstance(each_node, ast.Call):
360
+ continue
361
+ callee = each_node.func
362
+ if isinstance(callee, ast.Name):
363
+ called_names.add(callee.id)
364
+ elif isinstance(callee, ast.Attribute):
365
+ called_names.add(callee.attr)
366
+ return called_names
367
+
368
+
369
+ def _module_known_callable_names(syntax_tree: ast.Module) -> set[str]:
370
+ """Return every callable-like name the module imports, defines, or calls.
371
+
372
+ A stale test name embeds a function that has been renamed away, so its name
373
+ appears nowhere as a real symbol. This set is the universe of names that DO
374
+ exist in the file, used to confirm the embedded name is absent.
375
+ """
376
+ known_names: set[str] = set()
377
+ for each_node in ast.walk(syntax_tree):
378
+ if isinstance(each_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
379
+ known_names.add(each_node.name)
380
+ elif isinstance(each_node, ast.ImportFrom):
381
+ for each_alias in each_node.names:
382
+ known_names.add(each_alias.asname or each_alias.name)
383
+ elif isinstance(each_node, ast.Import):
384
+ for each_alias in each_node.names:
385
+ known_names.add((each_alias.asname or each_alias.name).split(".")[0])
386
+ elif isinstance(each_node, ast.Call):
387
+ callee = each_node.func
388
+ if isinstance(callee, ast.Name):
389
+ known_names.add(callee.id)
390
+ elif isinstance(callee, ast.Attribute):
391
+ known_names.add(callee.attr)
392
+ return known_names
393
+
394
+
395
+ def _leading_token_overlap(first_name: str, second_name: str) -> int:
396
+ """Return how many leading underscore-separated tokens two names share."""
397
+ first_tokens = first_name.split("_")
398
+ second_tokens = second_name.split("_")
399
+ shared = 0
400
+ for first_token, second_token in zip(first_tokens, second_tokens):
401
+ if first_token != second_token:
402
+ break
403
+ shared += 1
404
+ return shared
405
+
406
+
407
+ def _renamed_sibling_for_candidate(candidate_name: str, called_names: set[str]) -> str | None:
408
+ """Return a called function that looks like the renamed form of the candidate.
409
+
410
+ A rename keeps the token count and the leading tokens but swaps one or more
411
+ interior or trailing tokens (``collect_skip_theme_names`` to
412
+ ``collect_skip_clean_names``). The match requires an equal token count and a
413
+ shared leading run, which excludes an ordinary descriptive test suffix where
414
+ the called function is a strict shorter prefix of the embedded name.
415
+ """
416
+ candidate_token_count = len(candidate_name.split("_"))
417
+ for each_called in sorted(called_names):
418
+ if each_called == candidate_name:
419
+ continue
420
+ if len(each_called.split("_")) != candidate_token_count:
421
+ continue
422
+ if (
423
+ _leading_token_overlap(candidate_name, each_called)
424
+ >= STALE_TEST_NAME_MINIMUM_SHARED_TOKEN_COUNT
425
+ ):
426
+ return each_called
427
+ return None
428
+
429
+
430
+ def _embedded_target_candidates(test_name: str) -> list[str]:
431
+ """Return the function-name candidates a test name embeds after its test_ prefix.
432
+
433
+ For ``test_collect_skip_theme_names_keeps_only_sorted_at_risk`` the candidates
434
+ are the successive leading runs ``collect_skip_theme_names``,
435
+ ``collect_skip_theme``, ``collect_skip`` — longest first — so the embedded
436
+ function name is matched before its shorter prefixes.
437
+ """
438
+ if not test_name.startswith("test_"):
439
+ return []
440
+ remainder_tokens = test_name[len("test_"):].split("_")
441
+ candidates: list[str] = []
442
+ for token_count in range(len(remainder_tokens), STALE_TEST_NAME_MINIMUM_SHARED_TOKEN_COUNT - 1, -1):
443
+ candidates.append("_".join(remainder_tokens[:token_count]))
444
+ return candidates
445
+
446
+
447
+ def check_stale_test_name_target(content: str, file_path: str) -> list[str]:
448
+ """Flag a test whose name embeds a renamed-away function the body no longer calls.
449
+
450
+ When a producer function is renamed (``collect_skip_theme_names`` to
451
+ ``collect_skip_clean_names``) the test bodies are updated to call the new
452
+ name but the test function identifiers keep the old one. The result is a test
453
+ name advertising a function that exists nowhere in the file. This catches that
454
+ Category N test-name-versus-scenario drift: a ``test_*`` name embeds a
455
+ snake_case run of at least two tokens that names nothing the module imports,
456
+ defines, or calls, while the same test body calls a function sharing the
457
+ embedded run's leading tokens — the renamed sibling. Only applies to test
458
+ files; production files are exempt.
459
+
460
+ Args:
461
+ content: The file body under validation.
462
+ file_path: Path to the file, used for the test-file gate.
463
+
464
+ Returns:
465
+ One issue per test whose name embeds a renamed-away target, capped at the
466
+ module limit.
467
+ """
468
+ if not is_test_file(file_path):
469
+ return []
470
+ try:
471
+ syntax_tree = ast.parse(content)
472
+ except SyntaxError:
473
+ return []
474
+
475
+ known_names = _module_known_callable_names(syntax_tree)
476
+ issues: list[str] = []
477
+ for each_node in ast.walk(syntax_tree):
478
+ if not isinstance(each_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
479
+ continue
480
+ if not each_node.name.startswith("test"):
481
+ continue
482
+ called_names = _called_function_names(each_node)
483
+ for each_candidate in _embedded_target_candidates(each_node.name):
484
+ if each_candidate in known_names:
485
+ break
486
+ renamed_sibling = _renamed_sibling_for_candidate(each_candidate, called_names)
487
+ if renamed_sibling is None:
488
+ continue
489
+ issues.append(
490
+ f"Line {each_node.lineno}: test {each_node.name!r} names "
491
+ f"{each_candidate!r}, which the file never imports, defines, or calls; "
492
+ f"the body calls {renamed_sibling!r} instead — rename the test to match "
493
+ "the function it exercises (Category N test-name-vs-scenario drift)"
494
+ )
495
+ if len(issues) >= MAX_STALE_TEST_NAME_TARGET_ISSUES:
496
+ return issues[:MAX_STALE_TEST_NAME_TARGET_ISSUES]
497
+ break
498
+
499
+ return issues[:MAX_STALE_TEST_NAME_TARGET_ISSUES]