claude-dev-env 1.79.0 → 1.81.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/_shared/pr-loop/scripts/CLAUDE.md +3 -1
- package/_shared/pr-loop/scripts/code_rules_gate.py +116 -30
- package/_shared/pr-loop/scripts/copilot_quota.py +360 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +2 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +13 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +113 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +467 -0
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +8 -0
- package/_shared/pr-loop/scripts/tests/fixtures/copilot_internal_user_jonecho.json +76 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +339 -0
- package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +242 -0
- package/_shared/pr-loop/scripts/tests/test_copilot_quota_constants.py +63 -0
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +297 -0
- package/audit-rubrics/CLAUDE.md +3 -2
- package/audit-rubrics/category_rubrics/CLAUDE.md +2 -1
- package/audit-rubrics/category_rubrics/category-a-api-contracts.md +2 -1
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +13 -1
- package/audit-rubrics/category_rubrics/category-p-name-vs-behavior-contract.md +19 -0
- package/audit-rubrics/category_rubrics/category-q-cross-surface-claims.md +46 -0
- package/audit-rubrics/prompts/CLAUDE.md +2 -1
- package/audit-rubrics/prompts/category-a-api-contracts.md +1 -0
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +19 -7
- package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +14 -0
- package/audit-rubrics/prompts/category-q-cross-surface-claims.md +51 -0
- package/docs/CODE_RULES.md +2 -2
- package/hooks/blocking/CLAUDE.md +2 -0
- package/hooks/blocking/code_rules_annotations_length.py +59 -11
- package/hooks/blocking/code_rules_banned_identifiers.py +48 -9
- package/hooks/blocking/code_rules_command_dispatch.py +140 -0
- package/hooks/blocking/code_rules_docstrings.py +93 -0
- package/hooks/blocking/code_rules_enforcer.py +58 -4
- package/hooks/blocking/code_rules_imports_logging.py +136 -1
- package/hooks/blocking/code_rules_js_conventions.py +246 -0
- package/hooks/blocking/code_rules_naming_collection.py +5 -0
- package/hooks/blocking/code_rules_test_assertions.py +3 -0
- package/hooks/blocking/code_rules_unused_imports.py +7 -66
- package/hooks/blocking/duplicate_rmtree_helper_blocker.py +7 -0
- package/hooks/blocking/test_code_rules_command_dispatch.py +95 -0
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +20 -2
- package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +10 -2
- package/hooks/blocking/test_code_rules_enforcer_banned_import_alias.py +8 -4
- package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +9 -3
- package/hooks/blocking/test_code_rules_enforcer_docstring_type_checking_gate.py +164 -0
- package/hooks/blocking/test_code_rules_enforcer_js_returns_object.py +72 -0
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +112 -18
- package/hooks/blocking/test_code_rules_js_conventions.py +167 -0
- package/hooks/blocking/test_code_rules_js_returns_object_schemaless.py +167 -0
- package/hooks/hooks_constants/CLAUDE.md +2 -0
- package/hooks/hooks_constants/blocking_check_limits.py +10 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +14 -0
- package/hooks/hooks_constants/command_dispatch_constants.py +28 -0
- package/hooks/hooks_constants/js_conventions_constants.py +54 -0
- package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +93 -0
- package/hooks/hooks_constants/unused_module_import_constants.py +0 -1
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +3 -1
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +43 -1
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +2 -2
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +1 -4
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +100 -4
- package/skills/autoconverge/SKILL.md +28 -7
- package/skills/autoconverge/reference/convergence.md +3 -3
- package/skills/autoconverge/reference/stop-conditions.md +1 -1
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
- package/skills/autoconverge/workflow/converge.contract.test.mjs +58 -122
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +289 -5
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +50 -46
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +6 -6
- package/skills/autoconverge/workflow/converge.mjs +235 -247
- package/skills/autoconverge/workflow/converge.run-input.test.mjs +11 -0
- package/skills/autoconverge/workflow/converge_multi.mjs +23 -7
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +28 -2
- package/skills/pr-converge/SKILL.md +28 -2
- package/skills/pr-converge/reference/convergence-gates.md +13 -1
- package/skills/pr-converge/reference/state-schema.md +11 -0
|
@@ -89,10 +89,28 @@ def test_should_flag_async_function_without_return_annotation() -> None:
|
|
|
89
89
|
)
|
|
90
90
|
|
|
91
91
|
|
|
92
|
-
def
|
|
92
|
+
def test_should_skip_return_check_for_test_file_helper() -> None:
|
|
93
93
|
source = "def fetch(url: str):\n return url\n"
|
|
94
94
|
issues = code_rules_enforcer.check_return_annotations(source, TEST_FILE_PATH)
|
|
95
|
-
assert issues == [], f"
|
|
95
|
+
assert issues == [], f"Non-test helper in a test file must be exempt, got: {issues}"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_should_flag_test_function_missing_return_annotation() -> None:
|
|
99
|
+
source = "def test_loads():\n assert True\n"
|
|
100
|
+
issues = code_rules_enforcer.check_return_annotations(source, TEST_FILE_PATH)
|
|
101
|
+
assert any("test_loads" in each_issue for each_issue in issues), (
|
|
102
|
+
f"Pytest test function must require a return annotation, got: {issues}"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_should_scope_test_function_return_annotation_to_changed_lines() -> None:
|
|
107
|
+
source = "def helper():\n return 1\n\n\ndef test_uses_it():\n assert helper()\n"
|
|
108
|
+
unchanged = code_rules_enforcer.check_return_annotations(source, TEST_FILE_PATH, {2})
|
|
109
|
+
assert unchanged == []
|
|
110
|
+
on_definition = code_rules_enforcer.check_return_annotations(
|
|
111
|
+
source, TEST_FILE_PATH, {5}
|
|
112
|
+
)
|
|
113
|
+
assert any("test_uses_it" in each_issue for each_issue in on_definition)
|
|
96
114
|
|
|
97
115
|
|
|
98
116
|
def test_should_flag_unannotated_known_fixture_in_test_file() -> None:
|
|
@@ -95,10 +95,18 @@ def test_should_not_flag_name_containing_banned_substring() -> None:
|
|
|
95
95
|
assert issues == []
|
|
96
96
|
|
|
97
97
|
|
|
98
|
-
def
|
|
98
|
+
def test_should_scope_test_file_banned_identifier_to_changed_lines() -> None:
|
|
99
|
+
content = "def test_thing():\n result = compute()\n assert result\n"
|
|
100
|
+
unchanged_only = check_banned_identifiers(content, TEST_FILE_PATH, {1, 3})
|
|
101
|
+
assert unchanged_only == []
|
|
102
|
+
on_changed_line = check_banned_identifiers(content, TEST_FILE_PATH, {2})
|
|
103
|
+
assert any("result" in each_issue for each_issue in on_changed_line)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_should_flag_newly_written_test_file_banned_identifier() -> None:
|
|
99
107
|
content = "def test_thing():\n result = compute()\n assert result\n"
|
|
100
108
|
issues = check_banned_identifiers(content, TEST_FILE_PATH)
|
|
101
|
-
assert
|
|
109
|
+
assert any("'result'" in each_issue for each_issue in issues)
|
|
102
110
|
|
|
103
111
|
|
|
104
112
|
def test_should_skip_hook_infrastructure() -> None:
|
|
@@ -117,11 +117,15 @@ def test_should_flag_btn_assignment() -> None:
|
|
|
117
117
|
)
|
|
118
118
|
|
|
119
119
|
|
|
120
|
-
def
|
|
120
|
+
def test_should_scope_import_alias_in_test_file_to_changed_lines() -> None:
|
|
121
121
|
content = "from config import rebase_constants as rc\n"
|
|
122
|
-
|
|
123
|
-
assert
|
|
124
|
-
f"
|
|
122
|
+
unchanged_only = check_banned_identifiers(content, TEST_FILE_PATH, {2})
|
|
123
|
+
assert unchanged_only == [], (
|
|
124
|
+
f"A banned alias on an untouched line must not block, got: {unchanged_only}"
|
|
125
|
+
)
|
|
126
|
+
on_changed_line = check_banned_identifiers(content, TEST_FILE_PATH, {1})
|
|
127
|
+
assert any("'rc'" in each_issue for each_issue in on_changed_line), (
|
|
128
|
+
f"A newly written banned alias in a test file must flag, got: {on_changed_line}"
|
|
125
129
|
)
|
|
126
130
|
|
|
127
131
|
|
|
@@ -10,8 +10,12 @@ orphan CSS class slip past the gate again.
|
|
|
10
10
|
This module reads ``validate_content``'s source and asserts every ``check_*``
|
|
11
11
|
attribute on the enforcer module appears in it. A check that is intentionally
|
|
12
12
|
not wired must be listed in ``KNOWN_UNDISPATCHED_CHECKS`` with a reason in this
|
|
13
|
-
docstring
|
|
14
|
-
|
|
13
|
+
docstring. ``check_unanchored_command_dispatch`` is listed there: it guards a
|
|
14
|
+
``hooks/blocking`` command classifier, and the whole ``validate_content`` verdict
|
|
15
|
+
stays off hook-infrastructure files, so the enforcer dispatches it from
|
|
16
|
+
``_hook_infrastructure_blocking_issues`` instead. The companion
|
|
17
|
+
``test_code_rules_enforcer_cap_meta.py`` guards the payload-cap convention; this
|
|
18
|
+
module guards the wiring.
|
|
15
19
|
"""
|
|
16
20
|
|
|
17
21
|
from __future__ import annotations
|
|
@@ -34,7 +38,9 @@ assert _hook_specification.loader is not None
|
|
|
34
38
|
_hook_module = importlib.util.module_from_spec(_hook_specification)
|
|
35
39
|
_hook_specification.loader.exec_module(_hook_module)
|
|
36
40
|
|
|
37
|
-
KNOWN_UNDISPATCHED_CHECKS: frozenset[str] = frozenset(
|
|
41
|
+
KNOWN_UNDISPATCHED_CHECKS: frozenset[str] = frozenset(
|
|
42
|
+
{"check_unanchored_command_dispatch"}
|
|
43
|
+
)
|
|
38
44
|
|
|
39
45
|
|
|
40
46
|
def _all_check_function_names() -> list[str]:
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Tests for check_docstring_names_absent_type_checking_gate — Category O6.
|
|
2
|
+
|
|
3
|
+
A module or function docstring that names a ``TYPE_CHECKING`` gate-detection step,
|
|
4
|
+
or a ``type-checking-gate`` helper family, drifts once no identifier in the body
|
|
5
|
+
handles TYPE_CHECKING: the prose points a reader at machinery the module does not
|
|
6
|
+
hold. This is the deterministic slice of Category O6 (docstring prose versus
|
|
7
|
+
implementation drift) for a TYPE_CHECKING gate claim. The check covers hook
|
|
8
|
+
infrastructure, where the import-scan gates that carry this drift class live.
|
|
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_type_checking_gate(content: str, file_path: str) -> list[str]:
|
|
32
|
+
return code_rules_enforcer.check_docstring_names_absent_type_checking_gate(content, file_path)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
PRODUCTION_HOOK_PATH = "/home/user/.claude/hooks/blocking/code_rules_unused_imports.py"
|
|
36
|
+
TEST_FILE_PATH = "/home/user/.claude/hooks/blocking/test_code_rules_unused_imports.py"
|
|
37
|
+
MYPY_GATE_PRODUCTION_PATH = "/home/user/project/scripts/run_mypy_gate.py"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_flags_module_docstring_naming_type_checking_gate_helpers() -> None:
|
|
41
|
+
content = (
|
|
42
|
+
'"""Unused module-level import check and its import-range and '
|
|
43
|
+
'type-checking-gate helpers."""\n'
|
|
44
|
+
"\n"
|
|
45
|
+
"import ast\n"
|
|
46
|
+
"\n"
|
|
47
|
+
"\n"
|
|
48
|
+
"def check_unused(content: str) -> list[str]:\n"
|
|
49
|
+
' """Flag unused imports."""\n'
|
|
50
|
+
" return []\n"
|
|
51
|
+
)
|
|
52
|
+
issues = check_type_checking_gate(content, PRODUCTION_HOOK_PATH)
|
|
53
|
+
assert len(issues) == 1
|
|
54
|
+
assert issues[0].startswith("Line 1:")
|
|
55
|
+
assert "type-checking-gate" in issues[0]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_flags_function_docstring_naming_type_checking_gate_detection() -> None:
|
|
59
|
+
content = (
|
|
60
|
+
"import ast\n"
|
|
61
|
+
"\n"
|
|
62
|
+
"\n"
|
|
63
|
+
"def check_unused_module_level_imports(content: str) -> list[str]:\n"
|
|
64
|
+
' """Flag module-level imports never referenced.\n'
|
|
65
|
+
"\n"
|
|
66
|
+
" When ``full_file_content`` is provided, the ``__all__`` /\n"
|
|
67
|
+
" ``TYPE_CHECKING`` gate detection and reference scanning run against\n"
|
|
68
|
+
" ``full_file_content``.\n"
|
|
69
|
+
' """\n'
|
|
70
|
+
" return []\n"
|
|
71
|
+
)
|
|
72
|
+
issues = check_type_checking_gate(content, PRODUCTION_HOOK_PATH)
|
|
73
|
+
assert len(issues) == 1
|
|
74
|
+
assert "type_checking gate" in issues[0]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_flags_both_module_and_function_docstrings_on_the_real_drift() -> None:
|
|
78
|
+
content = (
|
|
79
|
+
'"""Unused module-level import check and its import-range and '
|
|
80
|
+
'type-checking-gate helpers."""\n'
|
|
81
|
+
"\n"
|
|
82
|
+
"import ast\n"
|
|
83
|
+
"\n"
|
|
84
|
+
"\n"
|
|
85
|
+
"def check_unused_module_level_imports(content: str) -> list[str]:\n"
|
|
86
|
+
' """Flag module-level imports never referenced.\n'
|
|
87
|
+
"\n"
|
|
88
|
+
" The ``__all__`` / ``TYPE_CHECKING`` gate detection and reference\n"
|
|
89
|
+
" scanning run against ``full_file_content``.\n"
|
|
90
|
+
' """\n'
|
|
91
|
+
" return []\n"
|
|
92
|
+
)
|
|
93
|
+
issues = check_type_checking_gate(content, PRODUCTION_HOOK_PATH)
|
|
94
|
+
assert len(issues) == 2
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_passes_when_code_declares_a_type_checking_gate_helper() -> None:
|
|
98
|
+
content = (
|
|
99
|
+
'"""Unused module-level import check and its type-checking-gate helpers."""\n'
|
|
100
|
+
"\n"
|
|
101
|
+
"import ast\n"
|
|
102
|
+
"\n"
|
|
103
|
+
"\n"
|
|
104
|
+
"def _module_body_declares_type_checking_gate(tree: ast.Module) -> bool:\n"
|
|
105
|
+
' """Return True when the module guards imports behind TYPE_CHECKING."""\n'
|
|
106
|
+
" return False\n"
|
|
107
|
+
)
|
|
108
|
+
assert check_type_checking_gate(content, PRODUCTION_HOOK_PATH) == []
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_passes_when_body_names_type_checking_guard() -> None:
|
|
112
|
+
content = (
|
|
113
|
+
"import ast\n"
|
|
114
|
+
"from typing import TYPE_CHECKING\n"
|
|
115
|
+
"\n"
|
|
116
|
+
"\n"
|
|
117
|
+
"def check_unused(content: str) -> list[str]:\n"
|
|
118
|
+
' """Skip imports the ``TYPE_CHECKING`` gate detection guards."""\n'
|
|
119
|
+
" if TYPE_CHECKING:\n"
|
|
120
|
+
" return []\n"
|
|
121
|
+
" return []\n"
|
|
122
|
+
)
|
|
123
|
+
assert check_type_checking_gate(content, PRODUCTION_HOOK_PATH) == []
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_passes_when_no_docstring_names_the_gate() -> None:
|
|
127
|
+
content = (
|
|
128
|
+
'"""Unused module-level import check and its import-range helpers."""\n'
|
|
129
|
+
"\n"
|
|
130
|
+
"import ast\n"
|
|
131
|
+
"\n"
|
|
132
|
+
"\n"
|
|
133
|
+
"def check_unused(content: str) -> list[str]:\n"
|
|
134
|
+
' """Flag module-level imports never referenced."""\n'
|
|
135
|
+
" return []\n"
|
|
136
|
+
)
|
|
137
|
+
assert check_type_checking_gate(content, PRODUCTION_HOOK_PATH) == []
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_passes_on_mypy_static_type_checker_gate_docstring() -> None:
|
|
141
|
+
content = (
|
|
142
|
+
"\"\"\"Run the project's type-checking gate and report failures.\"\"\"\n"
|
|
143
|
+
"\n"
|
|
144
|
+
"import subprocess\n"
|
|
145
|
+
"\n"
|
|
146
|
+
"\n"
|
|
147
|
+
"def run_gate() -> int:\n"
|
|
148
|
+
' return subprocess.run(["mypy", "hooks"]).returncode\n'
|
|
149
|
+
)
|
|
150
|
+
assert check_type_checking_gate(content, MYPY_GATE_PRODUCTION_PATH) == []
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_test_files_are_exempt() -> None:
|
|
154
|
+
content = (
|
|
155
|
+
'"""Unused module-level import check and its type-checking-gate helpers."""\n'
|
|
156
|
+
"\n"
|
|
157
|
+
"import ast\n"
|
|
158
|
+
"\n"
|
|
159
|
+
"\n"
|
|
160
|
+
"def check_unused(content: str) -> list[str]:\n"
|
|
161
|
+
' """Flag unused imports."""\n'
|
|
162
|
+
" return []\n"
|
|
163
|
+
)
|
|
164
|
+
assert check_type_checking_gate(content, TEST_FILE_PATH) == []
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Enforcer-dispatch tests for the JS returns-object schema-less branch check.
|
|
2
|
+
|
|
3
|
+
These drive ``validate_content`` end-to-end on a ``.mjs`` payload, so they prove
|
|
4
|
+
the check is wired into the JavaScript branch of the enforcer, not just callable
|
|
5
|
+
in isolation. The drift: a ``function`` whose JSDoc ``@returns {Promise<object>}``
|
|
6
|
+
promises a structured object while one branch returns the agent helper with an
|
|
7
|
+
options object that omits ``schema`` and so resolves to a transcript string.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import importlib.util
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from types import ModuleType
|
|
16
|
+
|
|
17
|
+
_HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
|
|
18
|
+
if _HOOKS_DIRECTORY not in sys.path:
|
|
19
|
+
sys.path.insert(0, _HOOKS_DIRECTORY)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _load_enforcer_module() -> ModuleType:
|
|
23
|
+
module_path = Path(__file__).parent / "code_rules_enforcer.py"
|
|
24
|
+
spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
|
|
25
|
+
assert spec is not None
|
|
26
|
+
assert spec.loader is not None
|
|
27
|
+
module = importlib.util.module_from_spec(spec)
|
|
28
|
+
spec.loader.exec_module(module)
|
|
29
|
+
return module
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
code_rules_enforcer = _load_enforcer_module()
|
|
33
|
+
|
|
34
|
+
_MJS_PATH = "skills/autoconverge/workflow/converge.mjs"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _mixed_schema_mjs() -> str:
|
|
38
|
+
return (
|
|
39
|
+
"/**\n"
|
|
40
|
+
" * Spawn a git-utility agent.\n"
|
|
41
|
+
" * @returns {Promise<object>} the structured output\n"
|
|
42
|
+
" */\n"
|
|
43
|
+
"function runGitTask(task, head) {\n"
|
|
44
|
+
" if (task === 'resolve-head') {\n"
|
|
45
|
+
" return convergeAgent(`head ${x}`, { label: 'g', schema: HEAD_SCHEMA, agentType: 'Explore' })\n"
|
|
46
|
+
" }\n"
|
|
47
|
+
" return convergeAgent(`fetch ${x}`, { label: 'g', agentType: 'Explore' })\n"
|
|
48
|
+
"}\n"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _all_schema_mjs() -> str:
|
|
53
|
+
return _mixed_schema_mjs().replace(
|
|
54
|
+
"{ label: 'g', agentType: 'Explore' })",
|
|
55
|
+
"{ label: 'g', schema: FETCH_SCHEMA, agentType: 'Explore' })",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _has_returns_object_finding(all_issues: list[str]) -> bool:
|
|
60
|
+
return any("runGitTask" in each_issue and "@returns" in each_issue for each_issue in all_issues)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_enforcer_reports_schema_less_branch_under_returns_object() -> None:
|
|
64
|
+
drift_source = _mixed_schema_mjs()
|
|
65
|
+
all_issues = code_rules_enforcer.validate_content(drift_source, _MJS_PATH, drift_source)
|
|
66
|
+
assert _has_returns_object_finding(all_issues)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_enforcer_accepts_every_branch_with_a_schema() -> None:
|
|
70
|
+
clean_source = _all_schema_mjs()
|
|
71
|
+
all_issues = code_rules_enforcer.validate_content(clean_source, _MJS_PATH, clean_source)
|
|
72
|
+
assert not _has_returns_object_finding(all_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
|
|
9
|
-
does not declare `__all__
|
|
10
|
-
|
|
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
|
|
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,20 +117,101 @@ 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
|
-
|
|
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}"
|
|
119
146
|
)
|
|
120
147
|
|
|
121
148
|
|
|
122
|
-
def
|
|
149
|
+
def test_should_flag_dead_import_beside_type_checking_string_annotation() -> None:
|
|
123
150
|
source = (
|
|
124
|
-
"from
|
|
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}"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_should_flag_unused_import_in_test_file() -> None:
|
|
194
|
+
source = (
|
|
195
|
+
"import asyncio\n"
|
|
125
196
|
"\n"
|
|
126
197
|
"def test_thing() -> None:\n"
|
|
127
198
|
" assert True\n"
|
|
128
199
|
)
|
|
129
200
|
issues = check_unused_module_level_imports(source, TEST_FILE_PATH)
|
|
130
|
-
assert
|
|
201
|
+
assert any("asyncio" in each_issue for each_issue in issues), (
|
|
202
|
+
f"Unused import in a test file must be flagged, got: {issues}"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_should_not_flag_used_import_in_test_file() -> None:
|
|
207
|
+
source = (
|
|
208
|
+
"import asyncio\n"
|
|
209
|
+
"\n"
|
|
210
|
+
"def test_thing() -> None:\n"
|
|
211
|
+
" asyncio.run(main())\n"
|
|
212
|
+
)
|
|
213
|
+
issues = check_unused_module_level_imports(source, TEST_FILE_PATH)
|
|
214
|
+
assert issues == [], f"Used import in a test file must not be flagged, got: {issues}"
|
|
131
215
|
|
|
132
216
|
|
|
133
217
|
def test_should_handle_syntax_error_gracefully() -> None:
|
|
@@ -364,7 +448,7 @@ def test_should_skip_when_full_file_declares_dunder_all() -> None:
|
|
|
364
448
|
)
|
|
365
449
|
|
|
366
450
|
|
|
367
|
-
def
|
|
451
|
+
def test_should_flag_dead_fragment_import_when_full_file_uses_type_checking() -> None:
|
|
368
452
|
fragment = "from hooks_constants.constants import NEW_NAME\n"
|
|
369
453
|
full_file = (
|
|
370
454
|
"from typing import TYPE_CHECKING\n"
|
|
@@ -379,9 +463,9 @@ def test_should_skip_when_full_file_uses_type_checking_gate() -> None:
|
|
|
379
463
|
issues = check_unused_module_level_imports(
|
|
380
464
|
fragment, PRODUCTION_FILE_PATH, full_file_content=full_file,
|
|
381
465
|
)
|
|
382
|
-
assert
|
|
383
|
-
"TYPE_CHECKING
|
|
384
|
-
f"got: {issues}"
|
|
466
|
+
assert any("NEW_NAME" in each_issue for each_issue in issues), (
|
|
467
|
+
"A TYPE_CHECKING block in the post-edit file must not exempt a dead "
|
|
468
|
+
f"import added by the edit fragment, got: {issues}"
|
|
385
469
|
)
|
|
386
470
|
|
|
387
471
|
|
|
@@ -472,7 +556,7 @@ def test_should_flag_import_when_only_shadowed_local_name_is_loaded() -> None:
|
|
|
472
556
|
)
|
|
473
557
|
|
|
474
558
|
|
|
475
|
-
def
|
|
559
|
+
def test_should_not_flag_guarded_import_under_imported_type_checking_alias() -> None:
|
|
476
560
|
source = (
|
|
477
561
|
"from typing import TYPE_CHECKING as IS_TYPE_CHECKING\n"
|
|
478
562
|
"from hooks_constants.constants import UNUSED_NAME\n"
|
|
@@ -484,12 +568,17 @@ def test_should_skip_when_type_checking_uses_imported_alias() -> None:
|
|
|
484
568
|
" return None\n"
|
|
485
569
|
)
|
|
486
570
|
issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
|
|
487
|
-
assert
|
|
488
|
-
|
|
571
|
+
assert not any("OtherName" in each_issue for each_issue in issues), (
|
|
572
|
+
"An import guarded under an aliased TYPE_CHECKING is nested in the if "
|
|
573
|
+
f"block and is never a flag candidate, got: {issues}"
|
|
574
|
+
)
|
|
575
|
+
assert any("UNUSED_NAME" in each_issue for each_issue in issues), (
|
|
576
|
+
"A dead top-level import beside an aliased TYPE_CHECKING guard is still "
|
|
577
|
+
f"flagged, got: {issues}"
|
|
489
578
|
)
|
|
490
579
|
|
|
491
580
|
|
|
492
|
-
def
|
|
581
|
+
def test_should_not_flag_guarded_import_under_module_type_checking_alias() -> None:
|
|
493
582
|
source = (
|
|
494
583
|
"import typing as t\n"
|
|
495
584
|
"from hooks_constants.constants import UNUSED_NAME\n"
|
|
@@ -501,8 +590,13 @@ def test_should_skip_when_type_checking_uses_module_alias() -> None:
|
|
|
501
590
|
" return None\n"
|
|
502
591
|
)
|
|
503
592
|
issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
|
|
504
|
-
assert
|
|
505
|
-
|
|
593
|
+
assert not any("OtherName" in each_issue for each_issue in issues), (
|
|
594
|
+
"An import guarded under a module-qualified TYPE_CHECKING is nested in "
|
|
595
|
+
f"the if block and is never a flag candidate, got: {issues}"
|
|
596
|
+
)
|
|
597
|
+
assert any("UNUSED_NAME" in each_issue for each_issue in issues), (
|
|
598
|
+
"A dead top-level import beside a module-qualified TYPE_CHECKING guard "
|
|
599
|
+
f"is still flagged, got: {issues}"
|
|
506
600
|
)
|
|
507
601
|
|
|
508
602
|
|