claude-dev-env 1.58.0 → 1.60.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 +2 -2
- package/_shared/pr-loop/scripts/code_rules_gate.py +36 -3
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +6 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
- package/_shared/pr-loop/scripts/reviews_disabled.py +12 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +265 -0
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +29 -0
- package/audit-rubrics/category_rubrics/category-b-selector-engine-compat.md +1 -1
- package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
- package/bin/install.mjs +100 -27
- package/bin/install.test.mjs +133 -1
- package/docs/CODE_RULES.md +3 -3
- package/hooks/blocking/code_rules_annotations_length.py +153 -0
- package/hooks/blocking/code_rules_dead_dataclass_field.py +319 -0
- package/hooks/blocking/code_rules_dead_module_constant.py +321 -0
- package/hooks/blocking/code_rules_duplicate_body.py +439 -0
- package/hooks/blocking/code_rules_enforcer.py +190 -21
- package/hooks/blocking/code_rules_magic_values.py +98 -0
- package/hooks/blocking/code_rules_shared.py +41 -0
- package/hooks/blocking/code_rules_typeddict_stub.py +172 -0
- package/hooks/blocking/config/__init__.py +5 -0
- package/hooks/blocking/config/verified_commit_constants.py +106 -0
- package/hooks/blocking/destructive_command_blocker.py +1027 -12
- package/hooks/blocking/hook_prose_detector_consistency.py +150 -0
- package/hooks/blocking/subprocess_budget_completeness.py +380 -0
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +225 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
- package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +146 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +467 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +188 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +330 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +179 -0
- package/hooks/blocking/test_code_rules_enforcer_magic_slice_bounds.py +133 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias.py +415 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +156 -0
- package/hooks/blocking/test_destructive_command_blocker.py +622 -3
- package/hooks/blocking/test_hook_prose_detector_consistency.py +265 -0
- package/hooks/blocking/test_subprocess_budget_completeness.py +588 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +720 -0
- package/hooks/blocking/test_verification_verdict_store.py +278 -0
- package/hooks/blocking/test_verified_commit_gate.py +368 -0
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -0
- package/hooks/blocking/test_verifier_verdict_minter.py +214 -0
- package/hooks/blocking/test_workflow_substitution_slot_blocker.py +242 -0
- package/hooks/blocking/verdict_directory_write_blocker.py +667 -0
- package/hooks/blocking/verification_verdict_store.py +446 -0
- package/hooks/blocking/verified_commit_gate.py +523 -0
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +152 -0
- package/hooks/blocking/verifier_verdict_minter.py +299 -0
- package/hooks/blocking/workflow_substitution_slot_blocker.py +159 -0
- package/hooks/diagnostic/test_hook_log_extractor.py +3 -3
- package/hooks/hooks.json +58 -1
- package/hooks/hooks_constants/blocking_check_limits.py +1 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +16 -0
- package/hooks/hooks_constants/dead_dataclass_field_constants.py +25 -0
- package/hooks/hooks_constants/dead_module_constant_constants.py +20 -0
- package/hooks/hooks_constants/destructive_command_segment_constants.py +178 -0
- package/hooks/hooks_constants/duplicate_function_body_constants.py +34 -0
- package/hooks/hooks_constants/hook_prose_detector_consistency_constants.py +30 -0
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +1 -1
- package/hooks/hooks_constants/subprocess_budget_completeness_constants.py +5 -0
- package/hooks/hooks_constants/workflow_substitution_slot_blocker_constants.py +22 -0
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +43 -0
- package/rules/file-global-constants.md +7 -1
- package/rules/hook-prose-matches-detector.md +26 -0
- package/rules/no-cross-skill-duplicate-helpers.md +29 -0
- package/rules/no-inline-destructive-literals.md +11 -0
- package/rules/workflow-substitution-slots.md +7 -0
- package/skills/_shared/pr-loop/scripts/preflight_worktree.py +392 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +70 -0
- package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +263 -0
- package/skills/autoconverge/SKILL.md +67 -19
- package/skills/autoconverge/reference/closing-report.md +59 -17
- package/skills/autoconverge/reference/convergence.md +7 -3
- package/skills/autoconverge/reference/stop-conditions.md +7 -2
- package/skills/autoconverge/workflow/aggregate_runs.py +371 -0
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +193 -76
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +76 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +206 -206
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +265 -0
- package/skills/autoconverge/workflow/converge.mjs +234 -42
- package/skills/autoconverge/workflow/convergence_summary.py +110 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +7 -0
- package/skills/autoconverge/workflow/render_report.py +488 -397
- package/skills/autoconverge/workflow/test_aggregate_runs.py +134 -0
- package/skills/autoconverge/workflow/test_convergence_summary.py +132 -0
- package/skills/autoconverge/workflow/test_render_report.py +488 -259
- package/skills/pr-converge/reference/per-tick.md +28 -8
- package/skills/pr-converge/scripts/check_convergence.py +195 -64
- package/skills/pr-converge/scripts/test_check_convergence.py +173 -2
- package/skills/rebase/SKILL.md +2 -4
- package/skills/update/SKILL.md +37 -5
- package/system-prompts/software-engineer.xml +2 -6
- package/hooks/blocking/content_search_to_zoekt_redirector.py +0 -59
- package/hooks/blocking/content_search_zoekt_bash_block_reason.py +0 -25
- package/hooks/blocking/content_search_zoekt_block_payload.py +0 -21
- package/hooks/blocking/content_search_zoekt_indexed_paths.py +0 -24
- package/hooks/blocking/content_search_zoekt_indexed_roots_config.py +0 -131
- package/hooks/blocking/content_search_zoekt_redirect_guidance.py +0 -52
- package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +0 -61
- package/hooks/blocking/test_content_search_to_zoekt_redirector_unit.py +0 -92
- package/hooks/blocking/test_content_search_zoekt_indexed_roots_config.py +0 -102
|
@@ -95,3 +95,228 @@ def test_should_skip_return_check_in_test_files() -> None:
|
|
|
95
95
|
assert issues == [], f"Test files must be exempt, got: {issues}"
|
|
96
96
|
|
|
97
97
|
|
|
98
|
+
def test_should_flag_unannotated_known_fixture_in_test_file() -> None:
|
|
99
|
+
source = "def test_board(tmp_path):\n assert tmp_path.exists()\n"
|
|
100
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
101
|
+
source, TEST_FILE_PATH
|
|
102
|
+
)
|
|
103
|
+
assert any(
|
|
104
|
+
"tmp_path" in each_issue and "Path" in each_issue for each_issue in issues
|
|
105
|
+
), f"Expected unannotated tmp_path fixture flagged, got: {issues}"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_should_not_flag_annotated_known_fixture_in_test_file() -> None:
|
|
109
|
+
source = (
|
|
110
|
+
"from pathlib import Path\n"
|
|
111
|
+
"def test_board(tmp_path: Path) -> None:\n"
|
|
112
|
+
" assert tmp_path.exists()\n"
|
|
113
|
+
)
|
|
114
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
115
|
+
source, TEST_FILE_PATH
|
|
116
|
+
)
|
|
117
|
+
assert issues == [], f"Annotated tmp_path must not be flagged, got: {issues}"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_should_not_flag_ordinary_test_parameter() -> None:
|
|
121
|
+
source = "def test_thing(some_value):\n assert some_value\n"
|
|
122
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
123
|
+
source, TEST_FILE_PATH
|
|
124
|
+
)
|
|
125
|
+
assert issues == [], (
|
|
126
|
+
f"Ordinary test params stay exempt; only known fixtures are checked, "
|
|
127
|
+
f"got: {issues}"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_should_not_flag_known_fixture_name_outside_test_files() -> None:
|
|
132
|
+
source = "def build(monkeypatch):\n return monkeypatch\n"
|
|
133
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
134
|
+
source, PRODUCTION_FILE_PATH
|
|
135
|
+
)
|
|
136
|
+
assert issues == [], (
|
|
137
|
+
f"Non-test files are covered by the broad parameter check, not this one, "
|
|
138
|
+
f"got: {issues}"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_should_flag_unannotated_monkeypatch_fixture() -> None:
|
|
143
|
+
source = "def test_env(monkeypatch):\n monkeypatch.setenv('A', 'B')\n"
|
|
144
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
145
|
+
source, TEST_FILE_PATH
|
|
146
|
+
)
|
|
147
|
+
assert any(
|
|
148
|
+
"monkeypatch" in each_issue and "MonkeyPatch" in each_issue
|
|
149
|
+
for each_issue in issues
|
|
150
|
+
), f"Expected unannotated monkeypatch fixture flagged, got: {issues}"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_should_not_flag_known_fixture_in_non_test_helper() -> None:
|
|
154
|
+
source = "def render_view(request):\n return request.path\n"
|
|
155
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
156
|
+
source, TEST_FILE_PATH
|
|
157
|
+
)
|
|
158
|
+
assert issues == [], (
|
|
159
|
+
f"Ordinary helper (non-test, non-fixture) must not be flagged, got: {issues}"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_should_flag_unannotated_fixture_in_decorated_fixture() -> None:
|
|
164
|
+
source = (
|
|
165
|
+
"import pytest\n"
|
|
166
|
+
"@pytest.fixture\n"
|
|
167
|
+
"def board(tmp_path):\n"
|
|
168
|
+
" return tmp_path\n"
|
|
169
|
+
)
|
|
170
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
171
|
+
source, TEST_FILE_PATH
|
|
172
|
+
)
|
|
173
|
+
assert any(
|
|
174
|
+
"tmp_path" in each_issue and "Path" in each_issue for each_issue in issues
|
|
175
|
+
), f"Expected unannotated tmp_path in @pytest.fixture-decorated function flagged, got: {issues}"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_should_flag_known_fixture_with_wrong_annotation() -> None:
|
|
179
|
+
source = "def test_board(tmp_path: str):\n assert tmp_path\n"
|
|
180
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
181
|
+
source, TEST_FILE_PATH
|
|
182
|
+
)
|
|
183
|
+
assert any(
|
|
184
|
+
"tmp_path" in each_issue and "Path" in each_issue for each_issue in issues
|
|
185
|
+
), f"Expected wrongly annotated tmp_path: str flagged, got: {issues}"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_should_flag_known_fixture_with_unrelated_annotation() -> None:
|
|
189
|
+
source = "def test_board(tmp_path: int):\n assert tmp_path\n"
|
|
190
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
191
|
+
source, TEST_FILE_PATH
|
|
192
|
+
)
|
|
193
|
+
assert any(
|
|
194
|
+
"tmp_path" in each_issue and "Path" in each_issue for each_issue in issues
|
|
195
|
+
), f"Expected wrongly annotated tmp_path: int flagged, got: {issues}"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_should_not_flag_correctly_annotated_qualified_fixture() -> None:
|
|
199
|
+
source = (
|
|
200
|
+
"import pytest\n"
|
|
201
|
+
"def test_env(monkeypatch: pytest.MonkeyPatch) -> None:\n"
|
|
202
|
+
" monkeypatch.setenv('A', 'B')\n"
|
|
203
|
+
)
|
|
204
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
205
|
+
source, TEST_FILE_PATH
|
|
206
|
+
)
|
|
207
|
+
assert issues == [], (
|
|
208
|
+
f"Correctly annotated monkeypatch must not be flagged, got: {issues}"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def test_should_not_flag_dotted_pathlib_path_fixture_annotation() -> None:
|
|
213
|
+
source = (
|
|
214
|
+
"import pathlib\n"
|
|
215
|
+
"def test_board(tmp_path: pathlib.Path) -> None:\n"
|
|
216
|
+
" assert tmp_path.exists()\n"
|
|
217
|
+
)
|
|
218
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
219
|
+
source, TEST_FILE_PATH
|
|
220
|
+
)
|
|
221
|
+
assert issues == [], (
|
|
222
|
+
f"tmp_path: pathlib.Path is an equally-correct spelling, got: {issues}"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_should_not_flag_bare_tail_of_qualified_fixture_annotation() -> None:
|
|
227
|
+
source = (
|
|
228
|
+
"from pytest import MonkeyPatch\n"
|
|
229
|
+
"def test_env(monkeypatch: MonkeyPatch) -> None:\n"
|
|
230
|
+
" monkeypatch.setenv('A', 'B')\n"
|
|
231
|
+
)
|
|
232
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
233
|
+
source, TEST_FILE_PATH
|
|
234
|
+
)
|
|
235
|
+
assert issues == [], (
|
|
236
|
+
f"monkeypatch: MonkeyPatch matches the qualified expected tail, got: {issues}"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def test_should_not_flag_forward_reference_fixture_annotation() -> None:
|
|
241
|
+
source = 'def test_board(tmp_path: "Path") -> None:\n assert tmp_path\n'
|
|
242
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
243
|
+
source, TEST_FILE_PATH
|
|
244
|
+
)
|
|
245
|
+
assert issues == [], (
|
|
246
|
+
f'A forward-ref "Path" annotation must not be flagged, got: {issues}'
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def test_should_not_flag_star_arg_fixture_name() -> None:
|
|
251
|
+
source = "def test_board(*tmp_path):\n assert tmp_path\n"
|
|
252
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
253
|
+
source, TEST_FILE_PATH
|
|
254
|
+
)
|
|
255
|
+
assert issues == [], (
|
|
256
|
+
f"A *vararg sharing a fixture name is not an injection site, got: {issues}"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def test_should_not_flag_double_star_arg_fixture_name() -> None:
|
|
261
|
+
source = "def test_env(**monkeypatch):\n assert monkeypatch\n"
|
|
262
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
263
|
+
source, TEST_FILE_PATH
|
|
264
|
+
)
|
|
265
|
+
assert issues == [], (
|
|
266
|
+
f"A **kwarg sharing a fixture name is not an injection site, got: {issues}"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def test_should_not_flag_positional_only_fixture_name() -> None:
|
|
271
|
+
source = "def test_board(tmp_path, /):\n pass\n"
|
|
272
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
273
|
+
source, TEST_FILE_PATH
|
|
274
|
+
)
|
|
275
|
+
assert issues == [], (
|
|
276
|
+
f"A positional-only param cannot receive a keyword-injected fixture, got: {issues}"
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def test_should_flag_keyword_only_fixture_name() -> None:
|
|
281
|
+
source = "def test_board(*, tmp_path):\n pass\n"
|
|
282
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
283
|
+
source, TEST_FILE_PATH
|
|
284
|
+
)
|
|
285
|
+
assert any(
|
|
286
|
+
"tmp_path" in each_issue and "Path" in each_issue for each_issue in issues
|
|
287
|
+
), f"A keyword-only fixture is still an injection site, got: {issues}"
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def test_should_not_flag_defaulted_fixture_name() -> None:
|
|
291
|
+
source = "def test_board(tmp_path=None):\n pass\n"
|
|
292
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
293
|
+
source, TEST_FILE_PATH
|
|
294
|
+
)
|
|
295
|
+
assert issues == [], (
|
|
296
|
+
f"A defaulted param is not fixture-injected and must not be flagged, got: {issues}"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def test_should_not_flag_defaulted_keyword_only_fixture_name() -> None:
|
|
301
|
+
source = "def test_board(*, tmp_path=None):\n pass\n"
|
|
302
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
303
|
+
source, TEST_FILE_PATH
|
|
304
|
+
)
|
|
305
|
+
assert issues == [], (
|
|
306
|
+
f"A defaulted keyword-only param is not fixture-injected, got: {issues}"
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def test_should_flag_undefaulted_fixture_before_defaulted_one() -> None:
|
|
311
|
+
source = "def test_board(tmp_path, capsys=None):\n pass\n"
|
|
312
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
313
|
+
source, TEST_FILE_PATH
|
|
314
|
+
)
|
|
315
|
+
assert any(
|
|
316
|
+
"tmp_path" in each_issue and "Path" in each_issue for each_issue in issues
|
|
317
|
+
), f"An undefaulted leading fixture stays an injection site, got: {issues}"
|
|
318
|
+
assert not any("capsys" in each_issue for each_issue in issues), (
|
|
319
|
+
f"The trailing defaulted param must not be flagged, got: {issues}"
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
|
|
@@ -70,6 +70,7 @@ KNOWN_UNCAPPED_CHECKS_PENDING_REVIEW: frozenset[str] = frozenset(
|
|
|
70
70
|
"check_file_global_constants_use_count",
|
|
71
71
|
"check_imports_at_top",
|
|
72
72
|
"check_inline_literal_collections",
|
|
73
|
+
"check_known_pytest_fixture_annotations",
|
|
73
74
|
"check_library_print",
|
|
74
75
|
"check_loop_variable_naming",
|
|
75
76
|
"check_parameter_annotations",
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Tests for the cross-skill duplicate-helper advisory.
|
|
2
|
+
|
|
3
|
+
PR #233 on JonEcho/python-automation copied a Chrome-launch helper from the
|
|
4
|
+
``stp-recolor`` skill's ``scripts`` directory into the ``iconize-and-recolor-stp``
|
|
5
|
+
skill's ``scripts`` directory. The two skills install on their own, so a shared
|
|
6
|
+
module would couple them and break independent install; the copy is a defensible
|
|
7
|
+
skill-isolation tradeoff. ``advise_cross_skill_duplicate_helper`` surfaces that
|
|
8
|
+
copy as a non-blocking ``[CODE_RULES advisory]`` on stderr so a reviewer confirms
|
|
9
|
+
the copy was intentional, without denying the write.
|
|
10
|
+
|
|
11
|
+
The tests build a real ``skills/<name>/scripts`` layout on disk and run the
|
|
12
|
+
advisory against it, so they exercise the on-disk cross-skill scan rather than a
|
|
13
|
+
stubbed view of the filesystem.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import importlib.util
|
|
19
|
+
import pathlib
|
|
20
|
+
import shutil
|
|
21
|
+
import sys
|
|
22
|
+
import tempfile
|
|
23
|
+
from collections.abc import Iterator
|
|
24
|
+
|
|
25
|
+
import pytest
|
|
26
|
+
|
|
27
|
+
_HOOK_DIRECTORY = pathlib.Path(__file__).parent
|
|
28
|
+
if str(_HOOK_DIRECTORY) not in sys.path:
|
|
29
|
+
sys.path.insert(0, str(_HOOK_DIRECTORY))
|
|
30
|
+
|
|
31
|
+
_hook_spec = importlib.util.spec_from_file_location(
|
|
32
|
+
"code_rules_duplicate_body",
|
|
33
|
+
_HOOK_DIRECTORY / "code_rules_duplicate_body.py",
|
|
34
|
+
)
|
|
35
|
+
assert _hook_spec is not None
|
|
36
|
+
assert _hook_spec.loader is not None
|
|
37
|
+
_hook_module = importlib.util.module_from_spec(_hook_spec)
|
|
38
|
+
_hook_spec.loader.exec_module(_hook_module)
|
|
39
|
+
advise_cross_skill_duplicate_helper = _hook_module.advise_cross_skill_duplicate_helper
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
CHROME_HELPER_SOURCE = (
|
|
43
|
+
"import winreg\n"
|
|
44
|
+
"from pathlib import Path\n"
|
|
45
|
+
"\n"
|
|
46
|
+
"chrome_app_paths_key = r'SOFTWARE\\Microsoft\\App Paths\\chrome.exe'\n"
|
|
47
|
+
"chrome_fallback_paths = ('C:/chrome.exe',)\n"
|
|
48
|
+
"\n"
|
|
49
|
+
"def _chrome_executable() -> Path | None:\n"
|
|
50
|
+
" for each_root in (winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE):\n"
|
|
51
|
+
" try:\n"
|
|
52
|
+
" registered = winreg.QueryValue(each_root, chrome_app_paths_key)\n"
|
|
53
|
+
" except OSError:\n"
|
|
54
|
+
" continue\n"
|
|
55
|
+
" registered_path = Path(registered)\n"
|
|
56
|
+
" if registered_path.exists():\n"
|
|
57
|
+
" return registered_path\n"
|
|
58
|
+
" for each_fallback in chrome_fallback_paths:\n"
|
|
59
|
+
" fallback_path = Path(each_fallback)\n"
|
|
60
|
+
" if fallback_path.exists():\n"
|
|
61
|
+
" return fallback_path\n"
|
|
62
|
+
" return None\n"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@pytest.fixture
|
|
67
|
+
def skills_root() -> Iterator[pathlib.Path]:
|
|
68
|
+
base_directory = pathlib.Path(tempfile.mkdtemp())
|
|
69
|
+
skills_directory = base_directory / "skills"
|
|
70
|
+
skills_directory.mkdir()
|
|
71
|
+
try:
|
|
72
|
+
yield skills_directory
|
|
73
|
+
finally:
|
|
74
|
+
shutil.rmtree(base_directory, ignore_errors=False)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _make_skill_scripts(skills_directory: pathlib.Path, skill_name: str) -> pathlib.Path:
|
|
78
|
+
scripts_directory = skills_directory / skill_name / "scripts"
|
|
79
|
+
scripts_directory.mkdir(parents=True)
|
|
80
|
+
return scripts_directory
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_advises_when_helper_copied_from_another_skill(
|
|
84
|
+
skills_root: pathlib.Path, capsys: pytest.CaptureFixture[str]
|
|
85
|
+
) -> None:
|
|
86
|
+
source_scripts = _make_skill_scripts(skills_root, "stp-recolor")
|
|
87
|
+
(source_scripts / "palette_board.py").write_text(CHROME_HELPER_SOURCE, encoding="utf-8")
|
|
88
|
+
target_scripts = _make_skill_scripts(skills_root, "iconize-and-recolor-stp")
|
|
89
|
+
target_file = target_scripts / "combine_report.py"
|
|
90
|
+
|
|
91
|
+
advise_cross_skill_duplicate_helper(CHROME_HELPER_SOURCE, str(target_file))
|
|
92
|
+
|
|
93
|
+
captured = capsys.readouterr()
|
|
94
|
+
assert "[CODE_RULES advisory]" in captured.err, (
|
|
95
|
+
f"Expected a cross-skill advisory on stderr, got: {captured.err!r}"
|
|
96
|
+
)
|
|
97
|
+
assert "_chrome_executable" in captured.err, (
|
|
98
|
+
f"Expected the duplicated function named, got: {captured.err!r}"
|
|
99
|
+
)
|
|
100
|
+
assert "stp-recolor" in captured.err, f"Expected the source skill named, got: {captured.err!r}"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_advisory_does_not_block(
|
|
104
|
+
skills_root: pathlib.Path, capsys: pytest.CaptureFixture[str]
|
|
105
|
+
) -> None:
|
|
106
|
+
source_scripts = _make_skill_scripts(skills_root, "stp-recolor")
|
|
107
|
+
(source_scripts / "palette_board.py").write_text(CHROME_HELPER_SOURCE, encoding="utf-8")
|
|
108
|
+
target_scripts = _make_skill_scripts(skills_root, "iconize-and-recolor-stp")
|
|
109
|
+
target_file = target_scripts / "combine_report.py"
|
|
110
|
+
|
|
111
|
+
returned = advise_cross_skill_duplicate_helper(CHROME_HELPER_SOURCE, str(target_file))
|
|
112
|
+
|
|
113
|
+
assert returned is None, "The advisory returns nothing so it never enters the deny path"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_no_advisory_within_one_skill(
|
|
117
|
+
skills_root: pathlib.Path, capsys: pytest.CaptureFixture[str]
|
|
118
|
+
) -> None:
|
|
119
|
+
scripts_directory = _make_skill_scripts(skills_root, "stp-recolor")
|
|
120
|
+
(scripts_directory / "palette_board.py").write_text(CHROME_HELPER_SOURCE, encoding="utf-8")
|
|
121
|
+
sibling_file = scripts_directory / "combine_report.py"
|
|
122
|
+
|
|
123
|
+
advise_cross_skill_duplicate_helper(CHROME_HELPER_SOURCE, str(sibling_file))
|
|
124
|
+
|
|
125
|
+
captured = capsys.readouterr()
|
|
126
|
+
assert captured.err == "", (
|
|
127
|
+
"Within one skill the blocking gate covers the copy; the cross-skill "
|
|
128
|
+
f"advisory must stay silent, got: {captured.err!r}"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_no_advisory_outside_a_skill_scripts_directory(
|
|
133
|
+
skills_root: pathlib.Path, capsys: pytest.CaptureFixture[str]
|
|
134
|
+
) -> None:
|
|
135
|
+
source_scripts = _make_skill_scripts(skills_root, "stp-recolor")
|
|
136
|
+
(source_scripts / "palette_board.py").write_text(CHROME_HELPER_SOURCE, encoding="utf-8")
|
|
137
|
+
non_skill_directory = skills_root.parent / "elsewhere"
|
|
138
|
+
non_skill_directory.mkdir()
|
|
139
|
+
target_file = non_skill_directory / "combine_report.py"
|
|
140
|
+
|
|
141
|
+
advise_cross_skill_duplicate_helper(CHROME_HELPER_SOURCE, str(target_file))
|
|
142
|
+
|
|
143
|
+
captured = capsys.readouterr()
|
|
144
|
+
assert captured.err == "", (
|
|
145
|
+
f"A file outside a skill scripts directory draws no advisory, got: {captured.err!r}"
|
|
146
|
+
)
|