claude-dev-env 1.78.0 → 1.80.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.
Files changed (63) hide show
  1. package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  2. package/_shared/pr-loop/scripts/copilot_quota.py +360 -0
  3. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
  4. package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -0
  5. package/_shared/pr-loop/scripts/tests/CLAUDE.md +7 -0
  6. package/_shared/pr-loop/scripts/tests/fixtures/copilot_internal_user_jonecho.json +76 -0
  7. package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +242 -0
  8. package/_shared/pr-loop/scripts/tests/test_copilot_quota_constants.py +63 -0
  9. package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +1 -0
  10. package/bin/install.mjs +1 -0
  11. package/bin/install.test.mjs +3 -2
  12. package/hooks/blocking/CLAUDE.md +3 -2
  13. package/hooks/blocking/code_rules_docstrings.py +609 -16
  14. package/hooks/blocking/code_rules_enforcer.py +41 -0
  15. package/hooks/blocking/code_rules_imports_logging.py +136 -1
  16. package/hooks/blocking/code_rules_naming_collection.py +76 -1
  17. package/hooks/blocking/code_rules_paired_test.py +240 -22
  18. package/hooks/blocking/code_rules_test_assertions.py +159 -1
  19. package/hooks/blocking/code_rules_unused_imports.py +7 -63
  20. package/hooks/blocking/env_var_table_code_drift_blocker.py +475 -0
  21. package/hooks/blocking/package_inventory_stale_blocker.py +54 -15
  22. package/hooks/blocking/test_code_rules_enforcer_docstring_field_runmode_outcome.py +129 -0
  23. package/hooks/blocking/test_code_rules_enforcer_docstring_length_constant_superlative.py +198 -0
  24. package/hooks/blocking/test_code_rules_enforcer_docstring_no_network.py +115 -0
  25. package/hooks/blocking/test_code_rules_enforcer_docstring_unreferenced_param.py +160 -0
  26. package/hooks/blocking/test_code_rules_enforcer_js_returns_object.py +72 -0
  27. package/hooks/blocking/test_code_rules_enforcer_module_docstring_data_schema_scope.py +82 -0
  28. package/hooks/blocking/test_code_rules_enforcer_paired_test.py +190 -0
  29. package/hooks/blocking/test_code_rules_enforcer_polarity_name_contradiction.py +76 -0
  30. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +96 -15
  31. package/hooks/blocking/test_code_rules_enforcer_vacuous_cleanup_assertion.py +132 -0
  32. package/hooks/blocking/test_code_rules_js_returns_object_schemaless.py +167 -0
  33. package/hooks/blocking/test_env_var_table_code_drift_blocker.py +94 -0
  34. package/hooks/blocking/test_package_inventory_stale_blocker.py +46 -0
  35. package/hooks/blocking/test_pre_tool_use_dispatcher.py +7 -7
  36. package/hooks/hooks_constants/CLAUDE.md +1 -0
  37. package/hooks/hooks_constants/blocking_check_limits.py +79 -0
  38. package/hooks/hooks_constants/code_rules_enforcer_constants.py +28 -0
  39. package/hooks/hooks_constants/env_var_table_code_drift_constants.py +64 -0
  40. package/hooks/hooks_constants/package_inventory_stale_blocker_constants.py +20 -9
  41. package/hooks/hooks_constants/paired_test_coverage_constants.py +11 -3
  42. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  43. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +93 -0
  44. package/hooks/hooks_constants/unused_module_import_constants.py +0 -1
  45. package/package.json +1 -1
  46. package/rules/CLAUDE.md +1 -0
  47. package/rules/docstring-prose-matches-implementation.md +57 -54
  48. package/rules/env-var-table-code-drift.md +24 -0
  49. package/rules/package-inventory-stale-entry.md +4 -4
  50. package/rules/paired-test-coverage.md +12 -5
  51. package/skills/autoconverge/SKILL.md +26 -5
  52. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
  53. package/skills/autoconverge/workflow/converge.contract.test.mjs +56 -120
  54. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +45 -5
  55. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +50 -46
  56. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +6 -6
  57. package/skills/autoconverge/workflow/converge.mjs +110 -228
  58. package/skills/autoconverge/workflow/converge.run-input.test.mjs +11 -0
  59. package/skills/autoconverge/workflow/converge_multi.mjs +23 -7
  60. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +28 -2
  61. package/skills/pr-converge/SKILL.md +28 -2
  62. package/skills/pr-converge/reference/convergence-gates.md +13 -1
  63. package/skills/pr-converge/reference/state-schema.md +11 -0
@@ -0,0 +1,82 @@
1
+ """Tests for check_module_docstring_scope_omits_data_schema_constants (Category O).
2
+
3
+ A module whose one-line docstring scopes its contents to user-facing text while
4
+ the body also defines serialization field keys, run-metadata schema keys, or
5
+ runtime config under-describes the module — the Category O module-responsibility
6
+ drift. The gate fires only when the docstring claims a user-facing-text scope and
7
+ acknowledges no data-schema or runtime-config category, so broadening the summary
8
+ to name the data-schema keys and runtime config clears it.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import importlib.util
14
+ from pathlib import Path
15
+ from types import ModuleType
16
+
17
+
18
+ def _load_enforcer_module() -> ModuleType:
19
+ module_path = Path(__file__).parent / "code_rules_enforcer.py"
20
+ spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
21
+ assert spec is not None
22
+ assert spec.loader is not None
23
+ module = importlib.util.module_from_spec(spec)
24
+ spec.loader.exec_module(module)
25
+ return module
26
+
27
+
28
+ code_rules_enforcer = _load_enforcer_module()
29
+
30
+
31
+ def check_scope(content: str, file_path: str) -> list[str]:
32
+ return code_rules_enforcer.check_module_docstring_scope_omits_data_schema_constants(
33
+ content, file_path
34
+ )
35
+
36
+
37
+ PRODUCTION_FILE_PATH = "/project/stp_contrast_fix/config/messages.py"
38
+ TEST_FILE_PATH = "/project/stp_contrast_fix/config/test_messages.py"
39
+
40
+ USER_FACING_SUMMARY = '"""User-facing strings: CLI flag names, help text, and log messages."""\n'
41
+ DATA_SCHEMA_BODY = (
42
+ "from typing import Final\n"
43
+ 'CLI_FLAG_EXECUTE: Final[str] = "--execute"\n'
44
+ 'JSONL_FIELD_STP_PATH: Final[str] = "stp_path"\n'
45
+ 'RUN_METADATA_CLI_ARG_KEY_LIMIT: Final[str] = "limit"\n'
46
+ 'STDOUT_ENCODING: Final[str] = "utf-8"\n'
47
+ 'MAIN_LOGGING_FORMAT_STRING: Final[str] = "%(message)s"\n'
48
+ )
49
+
50
+
51
+ def test_flags_user_facing_summary_over_data_schema_constants() -> None:
52
+ issues = check_scope(USER_FACING_SUMMARY + DATA_SCHEMA_BODY, PRODUCTION_FILE_PATH)
53
+ assert len(issues) == 1
54
+ assert "JSONL_FIELD_STP_PATH" in issues[0]
55
+ assert "RUN_METADATA_CLI_ARG_KEY_LIMIT" in issues[0]
56
+ assert "module-responsibility drift" in issues[0]
57
+
58
+
59
+ def test_passes_when_summary_acknowledges_data_schema_scope() -> None:
60
+ acknowledging_summary = (
61
+ '"""User-facing strings plus per-theme JSONL field keys, run-metadata '
62
+ 'schema keys, and runtime config."""\n'
63
+ )
64
+ assert check_scope(acknowledging_summary + DATA_SCHEMA_BODY, PRODUCTION_FILE_PATH) == []
65
+
66
+
67
+ def test_passes_when_module_has_no_data_schema_constants() -> None:
68
+ strings_only_body = (
69
+ "from typing import Final\n"
70
+ 'CLI_FLAG_EXECUTE: Final[str] = "--execute"\n'
71
+ 'CLI_FLAG_LIMIT: Final[str] = "--limit"\n'
72
+ )
73
+ assert check_scope(USER_FACING_SUMMARY + strings_only_body, PRODUCTION_FILE_PATH) == []
74
+
75
+
76
+ def test_passes_when_summary_does_not_claim_user_facing_scope() -> None:
77
+ non_user_facing_summary = '"""Theme-database column names and SQL templates."""\n'
78
+ assert check_scope(non_user_facing_summary + DATA_SCHEMA_BODY, PRODUCTION_FILE_PATH) == []
79
+
80
+
81
+ def test_test_files_are_exempt() -> None:
82
+ assert check_scope(USER_FACING_SUMMARY + DATA_SCHEMA_BODY, TEST_FILE_PATH) == []
@@ -21,9 +21,15 @@ if str(_BLOCKING_DIRECTORY) not in sys.path:
21
21
 
22
22
  from code_rules_paired_test import ( # noqa: E402
23
23
  check_public_function_missing_paired_test,
24
+ check_test_file_omits_module_public_function,
24
25
  )
25
26
 
26
27
  _TWO_PUBLIC_FUNCTIONS = "def alpha():\n return 1\n\n\ndef beta():\n return 2\n"
28
+ _THREE_PUBLIC_FUNCTIONS = (
29
+ "def alpha():\n return 1\n\n\n"
30
+ "def beta():\n return 2\n\n\n"
31
+ "def gamma():\n return 3\n"
32
+ )
27
33
 
28
34
 
29
35
  @pytest.fixture
@@ -97,6 +103,29 @@ def test_skips_when_suite_covers_no_public_function(
97
103
  assert all_issues == []
98
104
 
99
105
 
106
+ def test_flags_public_surface_when_suite_exercises_only_private_helper(
107
+ neutral_package_directory: Path,
108
+ ) -> None:
109
+ module_source = (
110
+ "def _aarrggbb_to_css(value):\n return value\n\n\n"
111
+ "def render_table():\n return _aarrggbb_to_css('x')\n\n\n"
112
+ "def render_summary():\n return 2\n"
113
+ )
114
+ _write_test_file(
115
+ neutral_package_directory,
116
+ "test_mod.py",
117
+ "from pkg.mod import _aarrggbb_to_css\n\n"
118
+ "def test_helper():\n assert _aarrggbb_to_css('x') == 'x'\n",
119
+ )
120
+ all_issues = check_public_function_missing_paired_test(
121
+ module_source, str(neutral_package_directory / "mod.py")
122
+ )
123
+ assert len(all_issues) == 2
124
+ flagged_names = " ".join(all_issues)
125
+ assert "render_table" in flagged_names
126
+ assert "render_summary" in flagged_names
127
+
128
+
100
129
  def test_counts_coverage_across_sibling_test_files(
101
130
  neutral_package_directory: Path,
102
131
  ) -> None:
@@ -147,3 +176,164 @@ def test_exempts_hook_infrastructure_and_test_paths(
147
176
  test_path = str(neutral_package_directory / "tests" / "test_mod.py")
148
177
  assert check_public_function_missing_paired_test(_TWO_PUBLIC_FUNCTIONS, hook_path) == []
149
178
  assert check_public_function_missing_paired_test(_TWO_PUBLIC_FUNCTIONS, test_path) == []
179
+
180
+
181
+ def _write_module(package_directory: Path, body: str) -> None:
182
+ (package_directory / "mod.py").write_text(body, encoding="utf-8")
183
+
184
+
185
+ _SUITE_COVERS_ALPHA_BETA = (
186
+ "from pkg.mod import alpha, beta\n\n"
187
+ "def test_pair():\n assert alpha() == 1 and beta() == 2\n"
188
+ )
189
+
190
+
191
+ def test_flags_module_public_function_when_test_suite_omits_it(
192
+ neutral_package_directory: Path,
193
+ ) -> None:
194
+ _write_module(neutral_package_directory, _THREE_PUBLIC_FUNCTIONS)
195
+ test_path = str(neutral_package_directory / "tests" / "test_mod.py")
196
+ all_issues = check_test_file_omits_module_public_function(
197
+ _SUITE_COVERS_ALPHA_BETA, test_path
198
+ )
199
+ assert len(all_issues) == 1
200
+ assert "gamma" in all_issues[0]
201
+ assert "mod.py" in all_issues[0]
202
+ assert "alpha" not in all_issues[0]
203
+ assert "beta" not in all_issues[0]
204
+
205
+
206
+ def test_clean_when_test_suite_covers_every_module_public_function(
207
+ neutral_package_directory: Path,
208
+ ) -> None:
209
+ _write_module(neutral_package_directory, _THREE_PUBLIC_FUNCTIONS)
210
+ test_path = str(neutral_package_directory / "tests" / "test_mod.py")
211
+ suite_covers_all = (
212
+ "from pkg.mod import alpha, beta, gamma\n\n"
213
+ "def test_all():\n assert alpha() and beta() and gamma()\n"
214
+ )
215
+ all_issues = check_test_file_omits_module_public_function(suite_covers_all, test_path)
216
+ assert all_issues == []
217
+
218
+
219
+ def test_skips_when_test_suite_covers_no_module_public_function(
220
+ neutral_package_directory: Path,
221
+ ) -> None:
222
+ _write_module(neutral_package_directory, _THREE_PUBLIC_FUNCTIONS)
223
+ test_path = str(neutral_package_directory / "tests" / "test_mod.py")
224
+ all_issues = check_test_file_omits_module_public_function(
225
+ "def test_unrelated():\n assert 1 + 1 == 2\n", test_path
226
+ )
227
+ assert all_issues == []
228
+
229
+
230
+ def test_skips_test_file_with_no_paired_production_module(
231
+ neutral_package_directory: Path,
232
+ ) -> None:
233
+ test_path = str(neutral_package_directory / "tests" / "test_mod.py")
234
+ all_issues = check_test_file_omits_module_public_function(
235
+ _SUITE_COVERS_ALPHA_BETA, test_path
236
+ )
237
+ assert all_issues == []
238
+
239
+
240
+ def test_ignores_a_non_stem_matched_written_file(
241
+ neutral_package_directory: Path,
242
+ ) -> None:
243
+ _write_module(neutral_package_directory, _THREE_PUBLIC_FUNCTIONS)
244
+ helper_path = str(neutral_package_directory / "helper.py")
245
+ all_issues = check_test_file_omits_module_public_function(
246
+ _SUITE_COVERS_ALPHA_BETA, helper_path
247
+ )
248
+ assert all_issues == []
249
+
250
+
251
+ def test_resolves_production_module_beside_the_test_file(
252
+ neutral_package_directory: Path,
253
+ ) -> None:
254
+ _write_module(neutral_package_directory, _THREE_PUBLIC_FUNCTIONS)
255
+ beside_test_path = str(neutral_package_directory / "test_mod.py")
256
+ all_issues = check_test_file_omits_module_public_function(
257
+ _SUITE_COVERS_ALPHA_BETA, beside_test_path
258
+ )
259
+ assert len(all_issues) == 1
260
+ assert "gamma" in all_issues[0]
261
+
262
+
263
+ def test_judges_post_edit_content_over_stale_on_disk_test(
264
+ neutral_package_directory: Path,
265
+ ) -> None:
266
+ _write_module(neutral_package_directory, _THREE_PUBLIC_FUNCTIONS)
267
+ _write_test_file(
268
+ neutral_package_directory,
269
+ "test_mod.py",
270
+ _SUITE_COVERS_ALPHA_BETA,
271
+ )
272
+ test_path = str(neutral_package_directory / "tests" / "test_mod.py")
273
+ post_edit_covers_all = (
274
+ "from pkg.mod import alpha, beta, gamma\n\n"
275
+ "def test_all():\n assert alpha() and beta() and gamma()\n"
276
+ )
277
+ all_issues = check_test_file_omits_module_public_function(
278
+ post_edit_covers_all, test_path
279
+ )
280
+ assert all_issues == []
281
+
282
+
283
+ def test_counts_coverage_across_sibling_test_files_on_test_write(
284
+ neutral_package_directory: Path,
285
+ ) -> None:
286
+ _write_module(neutral_package_directory, _THREE_PUBLIC_FUNCTIONS)
287
+ _write_test_file(
288
+ neutral_package_directory,
289
+ "test_extra.py",
290
+ "from pkg.mod import gamma\n\ndef test_gamma():\n assert gamma() == 3\n",
291
+ )
292
+ test_path = str(neutral_package_directory / "tests" / "test_mod.py")
293
+ all_issues = check_test_file_omits_module_public_function(
294
+ _SUITE_COVERS_ALPHA_BETA, test_path
295
+ )
296
+ assert all_issues == []
297
+
298
+
299
+ def test_test_side_message_names_module_and_function_without_line_prefix(
300
+ neutral_package_directory: Path,
301
+ ) -> None:
302
+ _write_module(neutral_package_directory, _THREE_PUBLIC_FUNCTIONS)
303
+ test_path = str(neutral_package_directory / "tests" / "test_mod.py")
304
+ all_issues = check_test_file_omits_module_public_function(
305
+ _SUITE_COVERS_ALPHA_BETA, test_path
306
+ )
307
+ assert len(all_issues) == 1
308
+ assert not all_issues[0].startswith("Line ")
309
+ assert "mod.py" in all_issues[0]
310
+ assert "gamma" in all_issues[0]
311
+
312
+
313
+ def test_test_side_omission_blocks_on_any_test_file_edit(
314
+ neutral_package_directory: Path,
315
+ ) -> None:
316
+ _write_module(neutral_package_directory, _THREE_PUBLIC_FUNCTIONS)
317
+ test_path = str(neutral_package_directory / "tests" / "test_mod.py")
318
+ all_issues = check_test_file_omits_module_public_function(
319
+ _SUITE_COVERS_ALPHA_BETA, test_path
320
+ )
321
+ assert len(all_issues) == 1
322
+ assert "gamma" in all_issues[0]
323
+
324
+
325
+ def test_skips_when_paired_production_module_is_exempt(
326
+ neutral_package_directory: Path,
327
+ ) -> None:
328
+ (neutral_package_directory / "__init__.py").write_text(
329
+ _THREE_PUBLIC_FUNCTIONS, encoding="utf-8"
330
+ )
331
+ beside_test_path = str(neutral_package_directory / "test___init__.py")
332
+ suite_covers_all = (
333
+ "from pkg import alpha, beta, gamma\n\n"
334
+ "def test_all():\n assert alpha() and beta() and gamma()\n"
335
+ )
336
+ all_issues = check_test_file_omits_module_public_function(
337
+ suite_covers_all, beside_test_path
338
+ )
339
+ assert all_issues == []
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib.util
4
+ from pathlib import Path
5
+
6
+ ENFORCER_PATH = Path(__file__).resolve().parent / "code_rules_enforcer.py"
7
+ specification = importlib.util.spec_from_file_location("code_rules_enforcer", ENFORCER_PATH)
8
+ code_rules_enforcer = importlib.util.module_from_spec(specification)
9
+ specification.loader.exec_module(code_rules_enforcer)
10
+
11
+ PRODUCTION_FILE_PATH = "packages/app/services/foo.py"
12
+ TEST_FILE_PATH = "packages/app/tests/test_foo.py"
13
+
14
+
15
+ def test_should_flag_allowed_target_assigned_from_forbidden_callee() -> None:
16
+ source = (
17
+ "def offset() -> None:\n"
18
+ " is_inside_allowed = _point_hits_any_forbidden(px, py, rects)\n"
19
+ " return is_inside_allowed\n"
20
+ )
21
+ issues = code_rules_enforcer.check_polarity_name_contradiction(source, PRODUCTION_FILE_PATH)
22
+ assert any("is_inside_allowed" in each_issue for each_issue in issues), (
23
+ f"Expected the allowed/forbidden contradiction flagged, got: {issues}"
24
+ )
25
+
26
+
27
+ def test_should_flag_attribute_callee_with_antonym() -> None:
28
+ source = (
29
+ "def gate() -> None:\n is_enabled = self._is_disabled(state)\n return is_enabled\n"
30
+ )
31
+ issues = code_rules_enforcer.check_polarity_name_contradiction(source, PRODUCTION_FILE_PATH)
32
+ assert any("is_enabled" in each_issue for each_issue in issues), (
33
+ f"Expected the enabled/disabled contradiction flagged, got: {issues}"
34
+ )
35
+
36
+
37
+ def test_should_not_flag_when_callee_polarity_matches_target() -> None:
38
+ source = (
39
+ "def gate() -> None:\n"
40
+ " is_allowed = _point_inside_any_allowed_rect(px, py, rects)\n"
41
+ " return is_allowed\n"
42
+ )
43
+ issues = code_rules_enforcer.check_polarity_name_contradiction(source, PRODUCTION_FILE_PATH)
44
+ assert issues == [], f"Matching polarity must pass, got: {issues}"
45
+
46
+
47
+ def test_should_not_flag_neutral_callee() -> None:
48
+ source = (
49
+ "def gate() -> None:\n"
50
+ " is_allowed = _point_inside_any_rect(px, py, rects)\n"
51
+ " return is_allowed\n"
52
+ )
53
+ issues = code_rules_enforcer.check_polarity_name_contradiction(source, PRODUCTION_FILE_PATH)
54
+ assert issues == [], f"Neutral callee must pass, got: {issues}"
55
+
56
+
57
+ def test_should_not_flag_substring_only_token_match() -> None:
58
+ source = (
59
+ "def gate() -> None:\n"
60
+ " is_disallowed_count = compute_blocked_total(rects)\n"
61
+ " return is_disallowed_count\n"
62
+ )
63
+ issues = code_rules_enforcer.check_polarity_name_contradiction(source, PRODUCTION_FILE_PATH)
64
+ assert issues == [], (
65
+ f"'disallowed' embeds 'allowed' as a substring, not a whole token; must not flag, got: {issues}"
66
+ )
67
+
68
+
69
+ def test_should_skip_in_test_files() -> None:
70
+ source = (
71
+ "def offset() -> None:\n"
72
+ " is_inside_allowed = _point_hits_any_forbidden(px, py, rects)\n"
73
+ " return is_inside_allowed\n"
74
+ )
75
+ issues = code_rules_enforcer.check_polarity_name_contradiction(source, TEST_FILE_PATH)
76
+ assert issues == [], f"Test files exempt, got: {issues}"
@@ -5,9 +5,12 @@ ALL_REPOSITORY_ROOT_MARKER_FILENAMES, VENV_DIRECTORY_NAME and other
5
5
  imports that survived into a PR without ever being referenced.
6
6
 
7
7
  The detector is intentionally narrow: it only flags `from X import Y`
8
- or `import X` where Y/X is never referenced in the file body, the file
9
- does not declare `__all__`, and the file does not use TYPE_CHECKING
10
- conditional imports. The narrow scope keeps false positives low.
8
+ or `import X` where Y/X is never referenced in the file body and the
9
+ file does not declare `__all__`. A `TYPE_CHECKING` guard block no longer
10
+ exempts the whole file its guarded imports live nested inside the
11
+ `if TYPE_CHECKING:` node so they are never flag candidates, while a
12
+ genuinely dead top-level runtime import in the same file is still
13
+ flagged. The narrow scope keeps false positives low.
11
14
  """
12
15
 
13
16
  from __future__ import annotations
@@ -102,7 +105,7 @@ def test_should_skip_file_with_dunder_all_annotated_assignment() -> None:
102
105
  )
103
106
 
104
107
 
105
- def test_should_skip_file_using_type_checking_block() -> None:
108
+ def test_should_flag_dead_top_level_import_in_type_checking_file() -> None:
106
109
  source = (
107
110
  "from typing import TYPE_CHECKING\n"
108
111
  "from hooks_constants.constants import UNUSED_NAME\n"
@@ -114,8 +117,76 @@ def test_should_skip_file_using_type_checking_block() -> None:
114
117
  " return None\n"
115
118
  )
116
119
  issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
120
+ assert any("UNUSED_NAME" in each_issue for each_issue in issues), (
121
+ "A dead top-level runtime import must be flagged even when the file "
122
+ f"also declares a TYPE_CHECKING block, got: {issues}"
123
+ )
124
+ assert not any("OtherName" in each_issue for each_issue in issues), (
125
+ "A TYPE_CHECKING-guarded import is nested inside the if block and is "
126
+ f"never a flag candidate, got: {issues}"
127
+ )
128
+
129
+
130
+ def test_should_not_flag_standard_type_checking_idiom() -> None:
131
+ source = (
132
+ "from typing import TYPE_CHECKING\n"
133
+ "from hooks_constants.constants import RUNTIME_NAME\n"
134
+ "\n"
135
+ "if TYPE_CHECKING:\n"
136
+ " from somewhere import TypeOnly\n"
137
+ "\n"
138
+ "def run(node: TypeOnly) -> None:\n"
139
+ " RUNTIME_NAME()\n"
140
+ )
141
+ issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
117
142
  assert issues == [], (
118
- f"TYPE_CHECKING-using files have annotation-only imports skip, got: {issues}"
143
+ "The standard TYPE_CHECKING idiom TYPE_CHECKING referenced by the "
144
+ "guard, the runtime import used in the body, and the guarded import "
145
+ f"used in an annotation — must produce no findings, got: {issues}"
146
+ )
147
+
148
+
149
+ def test_should_flag_dead_import_beside_type_checking_string_annotation() -> None:
150
+ source = (
151
+ "from typing import TYPE_CHECKING\n"
152
+ "from shared_utils.config import portal_specific_timeouts\n"
153
+ "\n"
154
+ "if TYPE_CHECKING:\n"
155
+ " from shared_utils.base import BaseAutomation\n"
156
+ "\n"
157
+ 'async def run(automation: "BaseAutomation") -> None:\n'
158
+ " return None\n"
159
+ )
160
+ issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
161
+ assert any("portal_specific_timeouts" in each_issue for each_issue in issues), (
162
+ "A dead top-level import in a module that guards its annotation-only "
163
+ f"import under TYPE_CHECKING must still be flagged, got: {issues}"
164
+ )
165
+ assert not any("BaseAutomation" in each_issue for each_issue in issues), (
166
+ "The TYPE_CHECKING-guarded import used in a string annotation must not "
167
+ f"be flagged, got: {issues}"
168
+ )
169
+
170
+
171
+ def test_should_flag_dead_import_in_workflow_handler_file() -> None:
172
+ workflow_handler_path = (
173
+ "shared_utils/web_automation/samsung_portal/workflow/binary/delete_handlers.py"
174
+ )
175
+ source = (
176
+ "from typing import TYPE_CHECKING\n"
177
+ "from shared_utils.config import portal_specific_timeouts\n"
178
+ "\n"
179
+ "if TYPE_CHECKING:\n"
180
+ " from shared_utils.base import BaseAutomation\n"
181
+ "\n"
182
+ 'async def run(automation: "BaseAutomation") -> None:\n'
183
+ " return None\n"
184
+ )
185
+ issues = check_unused_module_level_imports(source, workflow_handler_path)
186
+ assert any("portal_specific_timeouts" in each_issue for each_issue in issues), (
187
+ "A handler file under a workflow/ directory is not a state/module "
188
+ "registry; a dead import in it must be flagged, not exempted by the "
189
+ f"workflow path, got: {issues}"
119
190
  )
120
191
 
121
192
 
@@ -364,7 +435,7 @@ def test_should_skip_when_full_file_declares_dunder_all() -> None:
364
435
  )
365
436
 
366
437
 
367
- def test_should_skip_when_full_file_uses_type_checking_gate() -> None:
438
+ def test_should_flag_dead_fragment_import_when_full_file_uses_type_checking() -> None:
368
439
  fragment = "from hooks_constants.constants import NEW_NAME\n"
369
440
  full_file = (
370
441
  "from typing import TYPE_CHECKING\n"
@@ -379,9 +450,9 @@ def test_should_skip_when_full_file_uses_type_checking_gate() -> None:
379
450
  issues = check_unused_module_level_imports(
380
451
  fragment, PRODUCTION_FILE_PATH, full_file_content=full_file,
381
452
  )
382
- assert issues == [], (
383
- "TYPE_CHECKING gate in the post-edit file must skip the scan, "
384
- f"got: {issues}"
453
+ assert any("NEW_NAME" in each_issue for each_issue in issues), (
454
+ "A TYPE_CHECKING block in the post-edit file must not exempt a dead "
455
+ f"import added by the edit fragment, got: {issues}"
385
456
  )
386
457
 
387
458
 
@@ -472,7 +543,7 @@ def test_should_flag_import_when_only_shadowed_local_name_is_loaded() -> None:
472
543
  )
473
544
 
474
545
 
475
- def test_should_skip_when_type_checking_uses_imported_alias() -> None:
546
+ def test_should_not_flag_guarded_import_under_imported_type_checking_alias() -> None:
476
547
  source = (
477
548
  "from typing import TYPE_CHECKING as IS_TYPE_CHECKING\n"
478
549
  "from hooks_constants.constants import UNUSED_NAME\n"
@@ -484,12 +555,17 @@ def test_should_skip_when_type_checking_uses_imported_alias() -> None:
484
555
  " return None\n"
485
556
  )
486
557
  issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
487
- assert issues == [], (
488
- f"TYPE_CHECKING imported aliases must skip annotation-only files, got: {issues}"
558
+ assert not any("OtherName" in each_issue for each_issue in issues), (
559
+ "An import guarded under an aliased TYPE_CHECKING is nested in the if "
560
+ f"block and is never a flag candidate, got: {issues}"
561
+ )
562
+ assert any("UNUSED_NAME" in each_issue for each_issue in issues), (
563
+ "A dead top-level import beside an aliased TYPE_CHECKING guard is still "
564
+ f"flagged, got: {issues}"
489
565
  )
490
566
 
491
567
 
492
- def test_should_skip_when_type_checking_uses_module_alias() -> None:
568
+ def test_should_not_flag_guarded_import_under_module_type_checking_alias() -> None:
493
569
  source = (
494
570
  "import typing as t\n"
495
571
  "from hooks_constants.constants import UNUSED_NAME\n"
@@ -501,8 +577,13 @@ def test_should_skip_when_type_checking_uses_module_alias() -> None:
501
577
  " return None\n"
502
578
  )
503
579
  issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
504
- assert issues == [], (
505
- f"TYPE_CHECKING module aliases must skip annotation-only files, got: {issues}"
580
+ assert not any("OtherName" in each_issue for each_issue in issues), (
581
+ "An import guarded under a module-qualified TYPE_CHECKING is nested in "
582
+ f"the if block and is never a flag candidate, got: {issues}"
583
+ )
584
+ assert any("UNUSED_NAME" in each_issue for each_issue in issues), (
585
+ "A dead top-level import beside a module-qualified TYPE_CHECKING guard "
586
+ f"is still flagged, got: {issues}"
506
587
  )
507
588
 
508
589
 
@@ -0,0 +1,132 @@
1
+ """Tests for vacuous cleanup-on-failure assertion detection.
2
+
3
+ A cleanup-on-failure test that asserts no leftover temp file is left behind,
4
+ yet never proves the temp file was created, passes vacuously: the assertion
5
+ holds even when the on-failure cleanup is entirely broken. The gate flags that
6
+ shape so the author arranges a post-creation failure and asserts real removal.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import importlib.util
12
+ from pathlib import Path
13
+ from types import ModuleType
14
+
15
+
16
+ def _load_enforcer_module() -> ModuleType:
17
+ module_path = Path(__file__).parent / "code_rules_enforcer.py"
18
+ spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
19
+ assert spec is not None
20
+ assert spec.loader is not None
21
+ module = importlib.util.module_from_spec(spec)
22
+ spec.loader.exec_module(module)
23
+ return module
24
+
25
+
26
+ code_rules_enforcer = _load_enforcer_module()
27
+
28
+ TEST_FILE_PATH = "packages/app/tests/test_archive_rewrite.py"
29
+ PRODUCTION_FILE_PATH = "packages/app/services/archive_rewrite.py"
30
+
31
+
32
+ def test_should_flag_glob_emptiness_cleanup_test_without_temp_creation() -> None:
33
+ source = (
34
+ "def test_rewrite_removes_tmp_file_on_failure(tmp_path: Path) -> None:\n"
35
+ " stp_path = build_corrupt_stp(stp_path=tmp_path / 'corrupt.stp')\n"
36
+ " rewrite_properties_xml_atomically(stp_path=stp_path, patched='<x/>')\n"
37
+ " all_tmp_siblings = list(tmp_path.glob('*.tmp'))\n"
38
+ " assert len(all_tmp_siblings) == 0\n"
39
+ )
40
+ issues = code_rules_enforcer.check_vacuous_cleanup_assertion_tests(source, TEST_FILE_PATH)
41
+ assert any("vacuous" in issue.lower() for issue in issues), (
42
+ f"Expected a vacuous-cleanup issue, got: {issues}"
43
+ )
44
+
45
+
46
+ def test_should_not_flag_when_post_creation_failure_is_arranged() -> None:
47
+ source = (
48
+ "def test_rewrite_removes_tmp_file_on_replace_failure(tmp_path, monkeypatch)"
49
+ " -> None:\n"
50
+ " stp_path = build_valid_stp(stp_path=tmp_path / 'theme.stp')\n"
51
+ " monkeypatch.setattr(os, 'replace', _raise_oserror)\n"
52
+ " rewrite_properties_xml_atomically(stp_path=stp_path, patched='<x/>')\n"
53
+ " all_tmp_siblings = list(tmp_path.glob('*.tmp'))\n"
54
+ " assert len(all_tmp_siblings) == 0\n"
55
+ )
56
+ issues = code_rules_enforcer.check_vacuous_cleanup_assertion_tests(source, TEST_FILE_PATH)
57
+ assert issues == [], f"Expected no issue when failure is arranged, got: {issues}"
58
+
59
+
60
+ def test_should_not_flag_when_temp_existence_is_proven_first() -> None:
61
+ source = (
62
+ "def test_cleanup_removes_temp_on_failure(tmp_path: Path) -> None:\n"
63
+ " temporary_path = tmp_path / 'theme.stp.tmp'\n"
64
+ " temporary_path.write_bytes(b'partial')\n"
65
+ " assert temporary_path.exists()\n"
66
+ " run_cleanup_after_failure(temporary_path)\n"
67
+ " assert not list(tmp_path.glob('*.tmp'))\n"
68
+ )
69
+ issues = code_rules_enforcer.check_vacuous_cleanup_assertion_tests(source, TEST_FILE_PATH)
70
+ assert issues == [], f"Expected no issue when temp existence proven, got: {issues}"
71
+
72
+
73
+ def test_should_not_flag_success_named_cleanup_assertion() -> None:
74
+ source = (
75
+ "def test_rewrite_leaves_no_tmp_sibling_on_success(tmp_path: Path) -> None:\n"
76
+ " stp_path = build_valid_stp(stp_path=tmp_path / 'theme.stp')\n"
77
+ " rewrite_properties_xml_atomically(stp_path=stp_path, patched='<x/>')\n"
78
+ " all_tmp_siblings = list(tmp_path.glob('*.tmp'))\n"
79
+ " assert len(all_tmp_siblings) == 0\n"
80
+ )
81
+ issues = code_rules_enforcer.check_vacuous_cleanup_assertion_tests(source, TEST_FILE_PATH)
82
+ assert issues == [], f"Expected no issue for a success-named test, got: {issues}"
83
+
84
+
85
+ def test_should_not_flag_failure_test_asserting_real_behavior() -> None:
86
+ source = (
87
+ "def test_rewrite_reports_cleanup_failure_on_directory_tmp("
88
+ "tmp_path: Path) -> None:\n"
89
+ " temporary_path = tmp_path / 'theme.stp.tmp'\n"
90
+ " temporary_path.mkdir()\n"
91
+ " write_outcome = rewrite_properties_xml_atomically("
92
+ "stp_path=tmp_path / 't.stp', patched='<x/>')\n"
93
+ " assert write_outcome.temporary_cleanup_error_message is not None\n"
94
+ )
95
+ issues = code_rules_enforcer.check_vacuous_cleanup_assertion_tests(source, TEST_FILE_PATH)
96
+ assert issues == [], f"Expected no issue for a behavior assertion, got: {issues}"
97
+
98
+
99
+ def test_should_not_flag_mixed_assertion_cleanup_test() -> None:
100
+ source = (
101
+ "def test_rewrite_removes_tmp_and_reports_failure(tmp_path: Path) -> None:\n"
102
+ " stp_path = build_corrupt_stp(stp_path=tmp_path / 'corrupt.stp')\n"
103
+ " write_outcome = rewrite_properties_xml_atomically("
104
+ "stp_path=stp_path, patched='<x/>')\n"
105
+ " assert write_outcome.is_write_successful is False\n"
106
+ " all_tmp_siblings = list(tmp_path.glob('*.tmp'))\n"
107
+ " assert len(all_tmp_siblings) == 0\n"
108
+ )
109
+ issues = code_rules_enforcer.check_vacuous_cleanup_assertion_tests(source, TEST_FILE_PATH)
110
+ assert issues == [], f"Expected no issue for a mixed-assertion test, got: {issues}"
111
+
112
+
113
+ def test_should_not_flag_in_production_files() -> None:
114
+ source = (
115
+ "def test_rewrite_removes_tmp_file_on_failure(tmp_path: Path) -> None:\n"
116
+ " all_tmp_siblings = list(tmp_path.glob('*.tmp'))\n"
117
+ " assert len(all_tmp_siblings) == 0\n"
118
+ )
119
+ issues = code_rules_enforcer.check_vacuous_cleanup_assertion_tests(source, PRODUCTION_FILE_PATH)
120
+ assert issues == [], f"Expected no issue in a production file, got: {issues}"
121
+
122
+
123
+ def test_should_include_line_number_in_issue() -> None:
124
+ source = (
125
+ "def test_rewrite_removes_tmp_file_on_failure(tmp_path: Path) -> None:\n"
126
+ " all_tmp_siblings = list(tmp_path.glob('*.tmp'))\n"
127
+ " assert all_tmp_siblings == []\n"
128
+ )
129
+ issues = code_rules_enforcer.check_vacuous_cleanup_assertion_tests(source, TEST_FILE_PATH)
130
+ assert any("Line 1" in issue for issue in issues), (
131
+ f"Expected a line number in the issue, got: {issues}"
132
+ )