claude-dev-env 1.78.0 → 1.79.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 (35) hide show
  1. package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +1 -0
  2. package/bin/install.mjs +1 -0
  3. package/bin/install.test.mjs +3 -2
  4. package/hooks/blocking/CLAUDE.md +3 -2
  5. package/hooks/blocking/code_rules_docstrings.py +609 -16
  6. package/hooks/blocking/code_rules_enforcer.py +37 -0
  7. package/hooks/blocking/code_rules_naming_collection.py +76 -1
  8. package/hooks/blocking/code_rules_paired_test.py +240 -22
  9. package/hooks/blocking/code_rules_test_assertions.py +159 -1
  10. package/hooks/blocking/env_var_table_code_drift_blocker.py +475 -0
  11. package/hooks/blocking/package_inventory_stale_blocker.py +54 -15
  12. package/hooks/blocking/test_code_rules_enforcer_docstring_field_runmode_outcome.py +129 -0
  13. package/hooks/blocking/test_code_rules_enforcer_docstring_length_constant_superlative.py +198 -0
  14. package/hooks/blocking/test_code_rules_enforcer_docstring_no_network.py +115 -0
  15. package/hooks/blocking/test_code_rules_enforcer_docstring_unreferenced_param.py +160 -0
  16. package/hooks/blocking/test_code_rules_enforcer_module_docstring_data_schema_scope.py +82 -0
  17. package/hooks/blocking/test_code_rules_enforcer_paired_test.py +190 -0
  18. package/hooks/blocking/test_code_rules_enforcer_polarity_name_contradiction.py +76 -0
  19. package/hooks/blocking/test_code_rules_enforcer_vacuous_cleanup_assertion.py +132 -0
  20. package/hooks/blocking/test_env_var_table_code_drift_blocker.py +94 -0
  21. package/hooks/blocking/test_package_inventory_stale_blocker.py +46 -0
  22. package/hooks/blocking/test_pre_tool_use_dispatcher.py +7 -7
  23. package/hooks/hooks_constants/CLAUDE.md +1 -0
  24. package/hooks/hooks_constants/blocking_check_limits.py +78 -0
  25. package/hooks/hooks_constants/code_rules_enforcer_constants.py +14 -0
  26. package/hooks/hooks_constants/env_var_table_code_drift_constants.py +64 -0
  27. package/hooks/hooks_constants/package_inventory_stale_blocker_constants.py +20 -9
  28. package/hooks/hooks_constants/paired_test_coverage_constants.py +11 -3
  29. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  30. package/package.json +1 -1
  31. package/rules/CLAUDE.md +1 -0
  32. package/rules/docstring-prose-matches-implementation.md +56 -54
  33. package/rules/env-var-table-code-drift.md +24 -0
  34. package/rules/package-inventory-stale-entry.md +4 -4
  35. package/rules/paired-test-coverage.md +12 -5
@@ -0,0 +1,160 @@
1
+ """Tests for check_docstring_documents_unreferenced_parameter.
2
+
3
+ A parameter named in the ``Args:`` block but referenced nowhere in the body is
4
+ dead: the function does not read it, yet the docstring describes behavior keyed
5
+ to it. The common shape is a flag a caller wired in before the real logic moved
6
+ up a level, leaving the parameter and its Args line claiming a behavior the body
7
+ does not implement.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import importlib.util
13
+ from pathlib import Path
14
+ from types import ModuleType
15
+
16
+
17
+ def _load_enforcer_module() -> ModuleType:
18
+ module_path = Path(__file__).parent / "code_rules_enforcer.py"
19
+ spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
20
+ assert spec is not None
21
+ assert spec.loader is not None
22
+ module = importlib.util.module_from_spec(spec)
23
+ spec.loader.exec_module(module)
24
+ return module
25
+
26
+
27
+ code_rules_enforcer = _load_enforcer_module()
28
+
29
+
30
+ def check_documents_unreferenced_parameter(content: str, file_path: str) -> list[str]:
31
+ return code_rules_enforcer.check_docstring_documents_unreferenced_parameter(content, file_path)
32
+
33
+
34
+ def validate_content(content: str, file_path: str, old_content: str) -> list[str]:
35
+ return code_rules_enforcer.validate_content(content, file_path, old_content)
36
+
37
+
38
+ PRODUCTION_FILE_PATH = "/project/src/per_theme_loop.py"
39
+ TEST_FILE_PATH = "/project/src/test_per_theme_loop.py"
40
+ HOOK_INFRASTRUCTURE_PATH = "/home/user/.claude/hooks/blocking/example.py"
41
+
42
+
43
+ def _function_with_dead_documented_flag() -> str:
44
+ return (
45
+ "def run_per_theme_loop_and_finalize(themes: list, is_no_notify: bool) -> int:\n"
46
+ ' """Run the per-theme loop and finalize the report.\n'
47
+ "\n"
48
+ " Args:\n"
49
+ " themes: The themes to process.\n"
50
+ " is_no_notify: When True, suppresses opening the HTML report.\n"
51
+ "\n"
52
+ " Returns:\n"
53
+ " The resolved exit code.\n"
54
+ ' """\n'
55
+ " exit_code = 0\n"
56
+ " for each_theme in themes:\n"
57
+ " exit_code = max(exit_code, each_theme.run())\n"
58
+ " render_and_write_html_report(themes)\n"
59
+ " return exit_code\n"
60
+ )
61
+
62
+
63
+ def test_should_flag_documented_parameter_never_referenced() -> None:
64
+ issues = check_documents_unreferenced_parameter(
65
+ _function_with_dead_documented_flag(), PRODUCTION_FILE_PATH
66
+ )
67
+ assert any("is_no_notify" in each for each in issues), (
68
+ f"Expected dead 'is_no_notify' flag, got: {issues!r}"
69
+ )
70
+ assert len(issues) == 1
71
+
72
+
73
+ def test_should_not_flag_parameter_referenced_in_body() -> None:
74
+ source = (
75
+ "def run_per_theme_loop_and_finalize(themes: list, is_no_notify: bool) -> int:\n"
76
+ ' """Run the per-theme loop and finalize the report.\n'
77
+ "\n"
78
+ " Args:\n"
79
+ " themes: The themes to process.\n"
80
+ " is_no_notify: When True, suppresses opening the HTML report.\n"
81
+ "\n"
82
+ " Returns:\n"
83
+ " The resolved exit code.\n"
84
+ ' """\n'
85
+ " exit_code = 0\n"
86
+ " for each_theme in themes:\n"
87
+ " exit_code = max(exit_code, each_theme.run())\n"
88
+ " if not is_no_notify:\n"
89
+ " open_html_report(themes)\n"
90
+ " return exit_code\n"
91
+ )
92
+ issues = check_documents_unreferenced_parameter(source, PRODUCTION_FILE_PATH)
93
+ assert issues == [], f"Referenced parameter must not be flagged, got: {issues!r}"
94
+
95
+
96
+ def test_should_not_flag_when_kwargs_present() -> None:
97
+ source = (
98
+ "def render(themes: list, is_no_notify: bool, **overrides) -> int:\n"
99
+ ' """Render the report.\n'
100
+ "\n"
101
+ " Args:\n"
102
+ " themes: The themes to process.\n"
103
+ " is_no_notify: When True, suppresses opening the report.\n"
104
+ ' """\n'
105
+ " settings = dict(overrides)\n"
106
+ " for each_theme in themes:\n"
107
+ " each_theme.render(settings)\n"
108
+ " return 0\n"
109
+ )
110
+ issues = check_documents_unreferenced_parameter(source, PRODUCTION_FILE_PATH)
111
+ assert issues == [], f"**kwargs functions must be skipped, got: {issues!r}"
112
+
113
+
114
+ def test_should_skip_private_function() -> None:
115
+ source = (
116
+ "def _drive(themes: list, is_no_notify: bool) -> int:\n"
117
+ ' """Drive internally.\n'
118
+ "\n"
119
+ " Args:\n"
120
+ " themes: The themes to process.\n"
121
+ " is_no_notify: When True, suppresses the report.\n"
122
+ ' """\n'
123
+ " exit_code = 0\n"
124
+ " for each_theme in themes:\n"
125
+ " exit_code = max(exit_code, each_theme.run())\n"
126
+ " return exit_code\n"
127
+ )
128
+ issues = check_documents_unreferenced_parameter(source, PRODUCTION_FILE_PATH)
129
+ assert issues == [], f"Private functions exempt, got: {issues!r}"
130
+
131
+
132
+ def test_should_skip_test_file() -> None:
133
+ issues = check_documents_unreferenced_parameter(
134
+ _function_with_dead_documented_flag(), TEST_FILE_PATH
135
+ )
136
+ assert issues == [], f"Test files exempt, got: {issues!r}"
137
+
138
+
139
+ def test_should_skip_hook_infrastructure() -> None:
140
+ issues = check_documents_unreferenced_parameter(
141
+ _function_with_dead_documented_flag(), HOOK_INFRASTRUCTURE_PATH
142
+ )
143
+ assert issues == [], f"Hook infrastructure exempt, got: {issues!r}"
144
+
145
+
146
+ def test_should_handle_syntax_error_gracefully() -> None:
147
+ issues = check_documents_unreferenced_parameter("def fetch(\n", PRODUCTION_FILE_PATH)
148
+ assert issues == [], f"Syntax error must yield no issues, got: {issues!r}"
149
+
150
+
151
+ def test_validate_content_surfaces_unreferenced_parameter() -> None:
152
+ issues = validate_content(
153
+ _function_with_dead_documented_flag(), PRODUCTION_FILE_PATH, old_content=""
154
+ )
155
+ matching_issues = [
156
+ each for each in issues if "is_no_notify" in each and "never references" in each
157
+ ]
158
+ assert matching_issues, (
159
+ f"Expected validate_content to surface the dead-parameter issue, got: {issues!r}"
160
+ )
@@ -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}"
@@ -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
+ )