claude-dev-env 1.59.0 → 1.61.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 +4 -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-f-silent-failures.md +1 -1
- 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/audit-rubrics/prompts/category-e-dead-code.md +17 -4
- package/audit-rubrics/prompts/category-f-silent-failures.md +1 -0
- package/docs/CODE_RULES.md +2 -2
- package/hooks/blocking/code_rules_annotations_length.py +189 -10
- package/hooks/blocking/code_rules_dead_module_constant.py +321 -0
- package/hooks/blocking/code_rules_duplicate_body.py +152 -0
- package/hooks/blocking/code_rules_enforcer.py +38 -15
- package/hooks/blocking/code_rules_orphan_css_class.py +196 -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 +118 -0
- package/hooks/blocking/destructive_command_blocker.py +483 -61
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +240 -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_module_constant.py +188 -0
- package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +82 -0
- package/hooks/blocking/test_code_rules_enforcer_orphan_css_class.py +196 -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 +213 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +720 -0
- package/hooks/blocking/test_verification_verdict_store.py +490 -0
- package/hooks/blocking/test_verified_commit_gate.py +495 -0
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -0
- package/hooks/blocking/test_verifier_verdict_minter.py +193 -0
- package/hooks/blocking/verdict_directory_write_blocker.py +667 -0
- package/hooks/blocking/verification_verdict_store.py +686 -0
- package/hooks/blocking/verified_commit_gate.py +535 -0
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +152 -0
- package/hooks/blocking/verifier_verdict_minter.py +221 -0
- package/hooks/diagnostic/test_hook_log_extractor.py +3 -3
- package/hooks/hooks.json +43 -1
- package/hooks/hooks_constants/blocking_check_limits.py +1 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +6 -0
- package/hooks/hooks_constants/dead_module_constant_constants.py +20 -0
- package/hooks/hooks_constants/destructive_command_segment_constants.py +15 -0
- package/hooks/hooks_constants/duplicate_function_body_constants.py +22 -5
- package/hooks/hooks_constants/orphan_css_class_constants.py +40 -0
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +1 -1
- package/hooks/validation/mypy_validator.py +59 -7
- package/hooks/validation/test_mypy_validator.py +94 -0
- package/package.json +1 -1
- package/rules/file-global-constants.md +7 -1
- package/rules/no-cross-skill-duplicate-helpers.md +29 -0
- package/rules/orphan-css-class.md +23 -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 +54 -17
- package/skills/autoconverge/reference/closing-report.md +59 -17
- package/skills/autoconverge/workflow/aggregate_runs.py +371 -0
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +192 -76
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +76 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +395 -206
- package/skills/autoconverge/workflow/converge.mjs +520 -57
- 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 +518 -259
- package/skills/pr-converge/reference/per-tick.md +28 -8
- package/skills/rebase/SKILL.md +2 -4
- 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
|
@@ -320,3 +320,243 @@ def test_should_flag_undefaulted_fixture_before_defaulted_one() -> None:
|
|
|
320
320
|
)
|
|
321
321
|
|
|
322
322
|
|
|
323
|
+
def test_should_flag_unused_known_fixture_parameter_in_test_file() -> None:
|
|
324
|
+
source = (
|
|
325
|
+
"from pathlib import Path\n"
|
|
326
|
+
"def test_omits_config(tmp_path: Path) -> None:\n"
|
|
327
|
+
" command = build_command('module.py', None)\n"
|
|
328
|
+
" assert command[-1] == 'module.py'\n"
|
|
329
|
+
)
|
|
330
|
+
issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
|
|
331
|
+
source, TEST_FILE_PATH
|
|
332
|
+
)
|
|
333
|
+
assert any(
|
|
334
|
+
"tmp_path" in each_issue for each_issue in issues
|
|
335
|
+
), f"Expected unused tmp_path fixture parameter flagged, got: {issues}"
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def test_should_not_flag_used_known_fixture_parameter() -> None:
|
|
339
|
+
source = (
|
|
340
|
+
"from pathlib import Path\n"
|
|
341
|
+
"def test_includes_config(tmp_path: Path) -> None:\n"
|
|
342
|
+
" config_file = tmp_path / 'pyproject.toml'\n"
|
|
343
|
+
" assert config_file.name == 'pyproject.toml'\n"
|
|
344
|
+
)
|
|
345
|
+
issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
|
|
346
|
+
source, TEST_FILE_PATH
|
|
347
|
+
)
|
|
348
|
+
assert issues == [], (
|
|
349
|
+
f"A referenced fixture parameter must not be flagged, got: {issues}"
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def test_should_not_flag_unused_ordinary_test_parameter() -> None:
|
|
354
|
+
source = "def test_thing(some_value):\n return 1\n"
|
|
355
|
+
issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
|
|
356
|
+
source, TEST_FILE_PATH
|
|
357
|
+
)
|
|
358
|
+
assert issues == [], (
|
|
359
|
+
f"Only known pytest fixtures are checked, not arbitrary params, got: {issues}"
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def test_should_not_flag_unused_fixture_outside_test_files() -> None:
|
|
364
|
+
source = "def build(tmp_path):\n return 1\n"
|
|
365
|
+
issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
|
|
366
|
+
source, PRODUCTION_FILE_PATH
|
|
367
|
+
)
|
|
368
|
+
assert issues == [], (
|
|
369
|
+
f"This check applies to test files only, got: {issues}"
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def test_should_flag_unused_monkeypatch_fixture_parameter() -> None:
|
|
374
|
+
source = (
|
|
375
|
+
"import pytest\n"
|
|
376
|
+
"def test_env(monkeypatch: pytest.MonkeyPatch) -> None:\n"
|
|
377
|
+
" assert build_command('m.py', None)[-1] == 'm.py'\n"
|
|
378
|
+
)
|
|
379
|
+
issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
|
|
380
|
+
source, TEST_FILE_PATH
|
|
381
|
+
)
|
|
382
|
+
assert any(
|
|
383
|
+
"monkeypatch" in each_issue for each_issue in issues
|
|
384
|
+
), f"Expected unused monkeypatch fixture parameter flagged, got: {issues}"
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def test_should_count_attribute_access_as_fixture_use() -> None:
|
|
388
|
+
source = (
|
|
389
|
+
"import pytest\n"
|
|
390
|
+
"def test_env(monkeypatch: pytest.MonkeyPatch) -> None:\n"
|
|
391
|
+
" monkeypatch.setenv('A', 'B')\n"
|
|
392
|
+
)
|
|
393
|
+
issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
|
|
394
|
+
source, TEST_FILE_PATH
|
|
395
|
+
)
|
|
396
|
+
assert issues == [], (
|
|
397
|
+
f"Attribute access on the fixture counts as a use, got: {issues}"
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def test_should_count_nested_function_reference_as_fixture_use() -> None:
|
|
402
|
+
source = (
|
|
403
|
+
"from pathlib import Path\n"
|
|
404
|
+
"def test_board(tmp_path: Path) -> None:\n"
|
|
405
|
+
" def inner() -> Path:\n"
|
|
406
|
+
" return tmp_path\n"
|
|
407
|
+
" assert inner().name\n"
|
|
408
|
+
)
|
|
409
|
+
issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
|
|
410
|
+
source, TEST_FILE_PATH
|
|
411
|
+
)
|
|
412
|
+
assert issues == [], (
|
|
413
|
+
f"A reference inside a nested function counts as a use, got: {issues}"
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def test_should_not_flag_unused_fixture_in_decorated_fixture_function() -> None:
|
|
418
|
+
source = (
|
|
419
|
+
"import pytest\n"
|
|
420
|
+
"from pathlib import Path\n"
|
|
421
|
+
"@pytest.fixture\n"
|
|
422
|
+
"def board(tmp_path: Path) -> int:\n"
|
|
423
|
+
" return 1\n"
|
|
424
|
+
)
|
|
425
|
+
issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
|
|
426
|
+
source, TEST_FILE_PATH
|
|
427
|
+
)
|
|
428
|
+
assert issues == [], (
|
|
429
|
+
f"A fixture composing another fixture by injection alone is intentional "
|
|
430
|
+
f"and must not be flagged, got: {issues}"
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def test_should_count_comprehension_reference_as_fixture_use() -> None:
|
|
435
|
+
source = (
|
|
436
|
+
"import pytest\n"
|
|
437
|
+
"def test_log_lines(caplog: pytest.LogCaptureFixture) -> None:\n"
|
|
438
|
+
" messages = [each_record.message for each_record in caplog.records]\n"
|
|
439
|
+
" assert messages == []\n"
|
|
440
|
+
)
|
|
441
|
+
issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
|
|
442
|
+
source, TEST_FILE_PATH
|
|
443
|
+
)
|
|
444
|
+
assert issues == [], (
|
|
445
|
+
f"A reference inside a comprehension counts as a use, got: {issues}"
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def test_should_not_flag_unused_fixture_in_decorated_test_named_function() -> None:
|
|
450
|
+
source = (
|
|
451
|
+
"import pytest\n"
|
|
452
|
+
"from pathlib import Path\n"
|
|
453
|
+
"@pytest.fixture\n"
|
|
454
|
+
"def test_board(tmp_path: Path) -> int:\n"
|
|
455
|
+
" return 1\n"
|
|
456
|
+
)
|
|
457
|
+
issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
|
|
458
|
+
source, TEST_FILE_PATH
|
|
459
|
+
)
|
|
460
|
+
assert issues == [], (
|
|
461
|
+
f"A @pytest.fixture-decorated function named test_* is a fixture, not a "
|
|
462
|
+
f"test, and must not be flagged, got: {issues}"
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def test_should_not_flag_fixture_param_on_nested_test_named_helper() -> None:
|
|
467
|
+
source = (
|
|
468
|
+
"from pathlib import Path\n"
|
|
469
|
+
"def test_outer(tmp_path: Path) -> None:\n"
|
|
470
|
+
" assert tmp_path\n"
|
|
471
|
+
" def test_inner(tmp_path) -> None:\n"
|
|
472
|
+
" return None\n"
|
|
473
|
+
" test_inner(tmp_path)\n"
|
|
474
|
+
)
|
|
475
|
+
issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
|
|
476
|
+
source, TEST_FILE_PATH
|
|
477
|
+
)
|
|
478
|
+
assert issues == [], (
|
|
479
|
+
f"A fixture-named parameter on a function nested inside a test body is a "
|
|
480
|
+
f"local helper argument pytest never injects, got: {issues}"
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def test_should_count_augmented_assignment_as_fixture_reference() -> None:
|
|
485
|
+
source = "def test_aug(request) -> None:\n request += 1\n"
|
|
486
|
+
issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
|
|
487
|
+
source, TEST_FILE_PATH
|
|
488
|
+
)
|
|
489
|
+
assert issues == [], (
|
|
490
|
+
f"An augmented assignment to the fixture references it, got: {issues}"
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def test_should_count_del_as_fixture_reference() -> None:
|
|
495
|
+
source = "def test_d(tmp_path: Path) -> None:\n del tmp_path\n"
|
|
496
|
+
issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
|
|
497
|
+
source, TEST_FILE_PATH
|
|
498
|
+
)
|
|
499
|
+
assert issues == [], (
|
|
500
|
+
f"A del of the fixture references it, got: {issues}"
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def test_should_flag_unused_fixture_parameter_on_class_method() -> None:
|
|
505
|
+
source = (
|
|
506
|
+
"from pathlib import Path\n"
|
|
507
|
+
"class TestThing:\n"
|
|
508
|
+
" def test_method(self, tmp_path: Path) -> None:\n"
|
|
509
|
+
" assert 1 == 1\n"
|
|
510
|
+
)
|
|
511
|
+
issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
|
|
512
|
+
source, TEST_FILE_PATH
|
|
513
|
+
)
|
|
514
|
+
assert any(
|
|
515
|
+
"tmp_path" in each_issue for each_issue in issues
|
|
516
|
+
), f"An unused fixture on a class-body test method must be flagged, got: {issues}"
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def test_should_flag_unused_fixture_parameter_on_nested_class_method() -> None:
|
|
520
|
+
source = (
|
|
521
|
+
"from pathlib import Path\n"
|
|
522
|
+
"class TestOuter:\n"
|
|
523
|
+
" class TestInner:\n"
|
|
524
|
+
" def test_method(self, tmp_path: Path) -> None:\n"
|
|
525
|
+
" assert 1 == 1\n"
|
|
526
|
+
)
|
|
527
|
+
issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
|
|
528
|
+
source, TEST_FILE_PATH
|
|
529
|
+
)
|
|
530
|
+
assert any(
|
|
531
|
+
"tmp_path" in each_issue for each_issue in issues
|
|
532
|
+
), f"An unused fixture on a nested-class test method must be flagged, got: {issues}"
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def test_should_flag_fixture_when_body_only_plain_assigns_it() -> None:
|
|
536
|
+
source = "def test_x(tmp_path: Path) -> None:\n tmp_path = 1\n"
|
|
537
|
+
issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
|
|
538
|
+
source, TEST_FILE_PATH
|
|
539
|
+
)
|
|
540
|
+
assert any(
|
|
541
|
+
"tmp_path" in each_issue for each_issue in issues
|
|
542
|
+
), f"A plain Store target is not a reference, so tmp_path is unused, got: {issues}"
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def test_annotation_check_skips_fixture_param_on_function_nested_in_test() -> None:
|
|
546
|
+
source = (
|
|
547
|
+
"from pathlib import Path\n"
|
|
548
|
+
"def test_outer(tmp_path: Path) -> None:\n"
|
|
549
|
+
" def inner(tmp_path) -> None:\n"
|
|
550
|
+
" return None\n"
|
|
551
|
+
" inner(tmp_path)\n"
|
|
552
|
+
)
|
|
553
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
554
|
+
source, TEST_FILE_PATH
|
|
555
|
+
)
|
|
556
|
+
assert issues == [], (
|
|
557
|
+
f"A fixture-named param on a function nested in a test body is not an "
|
|
558
|
+
f"injection site, and the correctly annotated outer param is fine, "
|
|
559
|
+
f"got: {issues}"
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
|
|
@@ -77,6 +77,7 @@ KNOWN_UNCAPPED_CHECKS_PENDING_REVIEW: frozenset[str] = frozenset(
|
|
|
77
77
|
"check_return_annotations",
|
|
78
78
|
"check_skip_decorators_in_tests",
|
|
79
79
|
"check_string_literal_magic",
|
|
80
|
+
"check_unused_known_pytest_fixture_parameters",
|
|
80
81
|
"check_unused_optional_parameters",
|
|
81
82
|
}
|
|
82
83
|
)
|
|
@@ -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
|
+
)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import stat
|
|
7
|
+
import tempfile
|
|
8
|
+
from collections.abc import Callable, Iterator
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
ENFORCER_PATH = Path(__file__).resolve().parent / "code_rules_enforcer.py"
|
|
14
|
+
specification = importlib.util.spec_from_file_location("code_rules_enforcer", ENFORCER_PATH)
|
|
15
|
+
assert specification is not None and specification.loader is not None
|
|
16
|
+
code_rules_enforcer = importlib.util.module_from_spec(specification)
|
|
17
|
+
specification.loader.exec_module(code_rules_enforcer)
|
|
18
|
+
|
|
19
|
+
CONSTANTS_BODY = 'MEDIUM_TERMINAL = "terminal"\nMEDIUM_CODE = "code"\nMEDIUM_TEXT = "text"\n'
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _strip_read_only_and_retry(
|
|
23
|
+
removal_function: Callable[[str], object],
|
|
24
|
+
target_path: str,
|
|
25
|
+
_exc_info: BaseException,
|
|
26
|
+
) -> None:
|
|
27
|
+
try:
|
|
28
|
+
os.chmod(target_path, stat.S_IWRITE)
|
|
29
|
+
removal_function(target_path)
|
|
30
|
+
except OSError:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.fixture
|
|
35
|
+
def neutral_root() -> Iterator[Path]:
|
|
36
|
+
"""Yield a temp directory whose path carries no ``test_`` segment.
|
|
37
|
+
|
|
38
|
+
The enforcer's ``is_test_file`` keys on the full path string, and pytest's
|
|
39
|
+
own ``tmp_path`` directory name embeds the test name, which would make every
|
|
40
|
+
synthetic constants path look like a test file. A neutral ``mkdtemp`` root
|
|
41
|
+
mirrors how a production constants module path looks.
|
|
42
|
+
"""
|
|
43
|
+
neutral_directory = Path(tempfile.mkdtemp(prefix="deadconst-")).resolve()
|
|
44
|
+
try:
|
|
45
|
+
yield neutral_directory
|
|
46
|
+
finally:
|
|
47
|
+
shutil.rmtree(neutral_directory, onexc=_strip_read_only_and_retry)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _check(source: str, file_path: str) -> list[str]:
|
|
51
|
+
return code_rules_enforcer.check_dead_module_constants(source, file_path)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _build_constants_package(
|
|
55
|
+
workflow_directory: Path,
|
|
56
|
+
constants_body: str,
|
|
57
|
+
consumer_body: str,
|
|
58
|
+
) -> Path:
|
|
59
|
+
constants_package = workflow_directory / "report_constants"
|
|
60
|
+
constants_package.mkdir(parents=True)
|
|
61
|
+
(constants_package / "__init__.py").write_text("", encoding="utf-8")
|
|
62
|
+
constants_path = constants_package / "render_report_constants.py"
|
|
63
|
+
constants_path.write_text(constants_body, encoding="utf-8")
|
|
64
|
+
(workflow_directory / "render_report.py").write_text(consumer_body, encoding="utf-8")
|
|
65
|
+
return constants_path
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_flags_constant_imported_by_no_module_in_the_tree(neutral_root: Path) -> None:
|
|
69
|
+
consumer_body = (
|
|
70
|
+
"from report_constants.render_report_constants import (\n"
|
|
71
|
+
" MEDIUM_CODE,\n"
|
|
72
|
+
" MEDIUM_TERMINAL,\n"
|
|
73
|
+
")\n"
|
|
74
|
+
"\n"
|
|
75
|
+
"def panel_class(medium: str) -> str:\n"
|
|
76
|
+
" if medium == MEDIUM_TERMINAL:\n"
|
|
77
|
+
" return 'terminal'\n"
|
|
78
|
+
" return 'code-panel' if medium == MEDIUM_CODE else 'text-panel'\n"
|
|
79
|
+
)
|
|
80
|
+
constants_path = _build_constants_package(
|
|
81
|
+
neutral_root / "workflow", CONSTANTS_BODY, consumer_body
|
|
82
|
+
)
|
|
83
|
+
issues = _check(CONSTANTS_BODY, str(constants_path))
|
|
84
|
+
assert any("MEDIUM_TEXT" in each_issue for each_issue in issues), (
|
|
85
|
+
f"Expected dead MEDIUM_TEXT flagged, got: {issues}"
|
|
86
|
+
)
|
|
87
|
+
assert not any(
|
|
88
|
+
"MEDIUM_TERMINAL" in each_issue or "MEDIUM_CODE" in each_issue for each_issue in issues
|
|
89
|
+
), f"Imported constants must not be flagged, got: {issues}"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_does_not_flag_constant_imported_one_directory_up(neutral_root: Path) -> None:
|
|
93
|
+
consumer_uses_text = (
|
|
94
|
+
"from report_constants.render_report_constants import (\n"
|
|
95
|
+
" MEDIUM_CODE,\n"
|
|
96
|
+
" MEDIUM_TERMINAL,\n"
|
|
97
|
+
" MEDIUM_TEXT,\n"
|
|
98
|
+
")\n"
|
|
99
|
+
"\n"
|
|
100
|
+
"def panel_class(medium: str) -> str:\n"
|
|
101
|
+
" if medium == MEDIUM_TERMINAL:\n"
|
|
102
|
+
" return 'terminal'\n"
|
|
103
|
+
" if medium == MEDIUM_TEXT:\n"
|
|
104
|
+
" return 'text-panel'\n"
|
|
105
|
+
" return 'code-panel' if medium == MEDIUM_CODE else 'text-panel'\n"
|
|
106
|
+
)
|
|
107
|
+
constants_path = _build_constants_package(
|
|
108
|
+
neutral_root / "workflow", CONSTANTS_BODY, consumer_uses_text
|
|
109
|
+
)
|
|
110
|
+
issues = _check(CONSTANTS_BODY, str(constants_path))
|
|
111
|
+
assert issues == [], f"No constant is dead when all are imported, got: {issues}"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_does_not_flag_when_module_declares_dunder_all(neutral_root: Path) -> None:
|
|
115
|
+
constants_body = CONSTANTS_BODY + '__all__ = ["MEDIUM_TERMINAL"]\n'
|
|
116
|
+
consumer_body = (
|
|
117
|
+
"from report_constants.render_report_constants import MEDIUM_TERMINAL\n"
|
|
118
|
+
"\n"
|
|
119
|
+
"def label() -> str:\n"
|
|
120
|
+
" return MEDIUM_TERMINAL\n"
|
|
121
|
+
)
|
|
122
|
+
constants_path = _build_constants_package(
|
|
123
|
+
neutral_root / "workflow", constants_body, consumer_body
|
|
124
|
+
)
|
|
125
|
+
issues = _check(constants_body, str(constants_path))
|
|
126
|
+
assert issues == [], f"__all__ surface suppresses the check, got: {issues}"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_does_not_run_on_ordinary_production_module(neutral_root: Path) -> None:
|
|
130
|
+
workflow_directory = neutral_root / "workflow"
|
|
131
|
+
workflow_directory.mkdir(parents=True)
|
|
132
|
+
ordinary_path = workflow_directory / "render_report.py"
|
|
133
|
+
body = "WIDGET_LIMIT = 5\n\ndef widgets() -> int:\n return WIDGET_LIMIT\n"
|
|
134
|
+
ordinary_path.write_text(body, encoding="utf-8")
|
|
135
|
+
issues = _check(body, str(ordinary_path))
|
|
136
|
+
assert issues == [], (
|
|
137
|
+
f"The dead-constant check runs only on dedicated constants modules, got: {issues}"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_runs_on_config_directory_module(neutral_root: Path) -> None:
|
|
142
|
+
package_directory = neutral_root / "app"
|
|
143
|
+
config_directory = package_directory / "config"
|
|
144
|
+
config_directory.mkdir(parents=True)
|
|
145
|
+
constants_body = "TIMEOUT_SECONDS = 30\nUNUSED_THRESHOLD = 99\n"
|
|
146
|
+
constants_path = config_directory / "timing.py"
|
|
147
|
+
constants_path.write_text(constants_body, encoding="utf-8")
|
|
148
|
+
consumer_body = (
|
|
149
|
+
"from config.timing import TIMEOUT_SECONDS\n"
|
|
150
|
+
"\n"
|
|
151
|
+
"def deadline() -> int:\n"
|
|
152
|
+
" return TIMEOUT_SECONDS\n"
|
|
153
|
+
)
|
|
154
|
+
(package_directory / "service.py").write_text(consumer_body, encoding="utf-8")
|
|
155
|
+
issues = _check(constants_body, str(constants_path))
|
|
156
|
+
assert any("UNUSED_THRESHOLD" in each_issue for each_issue in issues), (
|
|
157
|
+
f"Expected dead UNUSED_THRESHOLD flagged in config module, got: {issues}"
|
|
158
|
+
)
|
|
159
|
+
assert not any("TIMEOUT_SECONDS" in each_issue for each_issue in issues), (
|
|
160
|
+
f"Consumed config constant must not be flagged, got: {issues}"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_counts_a_reference_from_a_test_module(neutral_root: Path) -> None:
|
|
165
|
+
workflow_directory = neutral_root / "workflow"
|
|
166
|
+
constants_body = 'ONLY_TESTS_USE_THIS = "x"\n'
|
|
167
|
+
constants_path = workflow_directory / "render_report_constants.py"
|
|
168
|
+
workflow_directory.mkdir(parents=True)
|
|
169
|
+
constants_path.write_text(constants_body, encoding="utf-8")
|
|
170
|
+
test_body = (
|
|
171
|
+
"from render_report_constants import ONLY_TESTS_USE_THIS\n"
|
|
172
|
+
"\n"
|
|
173
|
+
"def test_value() -> None:\n"
|
|
174
|
+
" assert ONLY_TESTS_USE_THIS == 'x'\n"
|
|
175
|
+
)
|
|
176
|
+
(workflow_directory / "test_render_report.py").write_text(test_body, encoding="utf-8")
|
|
177
|
+
issues = _check(constants_body, str(constants_path))
|
|
178
|
+
assert issues == [], f"A constant used only by a test under the tree stays live, got: {issues}"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_is_skipped_on_a_constants_test_file(neutral_root: Path) -> None:
|
|
182
|
+
workflow_directory = neutral_root / "workflow"
|
|
183
|
+
workflow_directory.mkdir(parents=True)
|
|
184
|
+
test_constants_path = workflow_directory / "test_render_report_constants.py"
|
|
185
|
+
body = 'UNREFERENCED = "y"\n'
|
|
186
|
+
test_constants_path.write_text(body, encoding="utf-8")
|
|
187
|
+
issues = _check(body, str(test_constants_path))
|
|
188
|
+
assert issues == [], f"Test files are exempt, got: {issues}"
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Meta-test asserting every check_* function is dispatched from validate_content.
|
|
2
|
+
|
|
3
|
+
The per-check test modules each prove one ``check_*`` function flags the right
|
|
4
|
+
violation, but none proves the enforcer actually calls that function. A refactor
|
|
5
|
+
that drops a dispatch line from ``validate_content`` leaves every per-check test
|
|
6
|
+
green while the check stops firing at Write/Edit time — the precise failure mode
|
|
7
|
+
that would let a dead module-level constant (the ``MEDIUM_TEXT`` class) or an
|
|
8
|
+
orphan CSS class slip past the gate again.
|
|
9
|
+
|
|
10
|
+
This module reads ``validate_content``'s source and asserts every ``check_*``
|
|
11
|
+
attribute on the enforcer module appears in it. A check that is intentionally
|
|
12
|
+
not wired must be listed in ``KNOWN_UNDISPATCHED_CHECKS`` with a reason in this
|
|
13
|
+
docstring; no such checks exist today. The companion ``test_code_rules_enforcer_
|
|
14
|
+
cap_meta.py`` guards the payload-cap convention; this module guards the wiring.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import importlib.util
|
|
20
|
+
import inspect
|
|
21
|
+
import pathlib
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
_HOOK_DIRECTORY = pathlib.Path(__file__).parent
|
|
25
|
+
if str(_HOOK_DIRECTORY) not in sys.path:
|
|
26
|
+
sys.path.insert(0, str(_HOOK_DIRECTORY))
|
|
27
|
+
|
|
28
|
+
_hook_specification = importlib.util.spec_from_file_location(
|
|
29
|
+
"code_rules_enforcer",
|
|
30
|
+
_HOOK_DIRECTORY / "code_rules_enforcer.py",
|
|
31
|
+
)
|
|
32
|
+
assert _hook_specification is not None
|
|
33
|
+
assert _hook_specification.loader is not None
|
|
34
|
+
_hook_module = importlib.util.module_from_spec(_hook_specification)
|
|
35
|
+
_hook_specification.loader.exec_module(_hook_module)
|
|
36
|
+
|
|
37
|
+
KNOWN_UNDISPATCHED_CHECKS: frozenset[str] = frozenset()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _all_check_function_names() -> list[str]:
|
|
41
|
+
return [
|
|
42
|
+
each_attribute_name
|
|
43
|
+
for each_attribute_name in dir(_hook_module)
|
|
44
|
+
if each_attribute_name.startswith("check_")
|
|
45
|
+
and callable(getattr(_hook_module, each_attribute_name))
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _validate_content_source() -> str:
|
|
50
|
+
return inspect.getsource(_hook_module.validate_content)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_every_check_function_is_called_in_validate_content() -> None:
|
|
54
|
+
all_check_names = set(_all_check_function_names())
|
|
55
|
+
validate_content_source = _validate_content_source()
|
|
56
|
+
undispatched_check_names = {
|
|
57
|
+
each_name for each_name in all_check_names if each_name not in validate_content_source
|
|
58
|
+
}
|
|
59
|
+
unexpected_undispatched = undispatched_check_names - KNOWN_UNDISPATCHED_CHECKS
|
|
60
|
+
assert unexpected_undispatched == set(), (
|
|
61
|
+
f"check_* functions are imported but never called in validate_content: "
|
|
62
|
+
f"{sorted(unexpected_undispatched)}. Wire each into validate_content so the "
|
|
63
|
+
f"check fires at Write/Edit time, or list it in KNOWN_UNDISPATCHED_CHECKS "
|
|
64
|
+
f"with a reason in the test header docstring."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_dead_module_constant_check_stays_wired() -> None:
|
|
69
|
+
validate_content_source = _validate_content_source()
|
|
70
|
+
assert "check_dead_module_constants" in validate_content_source, (
|
|
71
|
+
"check_dead_module_constants must stay dispatched from validate_content so a "
|
|
72
|
+
"dead exported constant (the MEDIUM_TEXT class) is blocked at Write/Edit time."
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_known_undispatched_set_lists_only_existing_checks() -> None:
|
|
77
|
+
all_check_names = set(_all_check_function_names())
|
|
78
|
+
stale_names = KNOWN_UNDISPATCHED_CHECKS - all_check_names
|
|
79
|
+
assert stale_names == set(), (
|
|
80
|
+
f"KNOWN_UNDISPATCHED_CHECKS lists functions that no longer exist: "
|
|
81
|
+
f"{sorted(stale_names)}. Restore the function or remove it from the set."
|
|
82
|
+
)
|