claude-dev-env 1.72.0 → 1.73.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/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +2 -2
- package/bin/install.mjs +73 -5
- package/bin/install.test.mjs +360 -4
- package/hooks/blocking/CLAUDE.md +3 -1
- package/hooks/blocking/claude_md_orphan_file_blocker.py +5 -6
- package/hooks/blocking/code_rules_dead_config_field.py +69 -56
- package/hooks/blocking/code_rules_docstrings.py +616 -0
- package/hooks/blocking/code_rules_enforcer.py +22 -0
- package/hooks/blocking/code_rules_shared.py +19 -0
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +420 -0
- package/hooks/blocking/md_to_html_blocker.py +7 -8
- package/hooks/blocking/open_questions_in_plans_blocker.py +5 -6
- package/hooks/blocking/plain_language_blocker.py +51 -16
- package/hooks/blocking/pr_converge_bugteam_enforcer.py +5 -5
- package/hooks/blocking/pre_tool_use_dispatcher.py +545 -0
- package/hooks/blocking/pytest_testpaths_orphan_blocker.py +358 -0
- package/hooks/blocking/state_description_blocker.py +75 -36
- package/hooks/blocking/test_code_rules_enforcer_dead_config_field.py +81 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_inline_literal_claim.py +93 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_step_dispatch.py +262 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_undefined_constant.py +253 -0
- package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +279 -0
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +456 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +816 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +341 -0
- package/hooks/blocking/test_pytest_testpaths_orphan_blocker.py +247 -0
- package/hooks/blocking/test_shared_stdin_adoption.py +166 -0
- package/hooks/blocking/verdict_directory_write_blocker.py +12 -7
- package/hooks/hooks.json +9 -79
- package/hooks/hooks_constants/CLAUDE.md +3 -1
- package/hooks/hooks_constants/blocking_check_limits.py +61 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +6 -0
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +45 -0
- package/hooks/hooks_constants/dead_config_field_constants.py +5 -5
- package/hooks/hooks_constants/mypy_validator_cache_constants.py +36 -0
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +69 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +135 -0
- package/hooks/hooks_constants/pytest_testpaths_orphan_blocker_constants.py +79 -0
- package/hooks/validation/mypy_validator.py +215 -17
- package/hooks/validation/post_tool_use_dispatcher.py +344 -0
- package/hooks/validation/test_mypy_validator.py +184 -1
- package/hooks/validation/test_post_tool_use_dispatcher.py +610 -0
- package/hooks/workflow/test_auto_formatter.py +10 -9
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +2 -1
- package/skills/autoconverge/SKILL.md +93 -0
- package/skills/autoconverge/workflow/converge.mjs +27 -2
- package/skills/autoconverge/workflow/converge.path-aware.test.mjs +47 -0
- package/skills/autoconverge/workflow/converge_multi.mjs +161 -0
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +100 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""Tests for check_docstring_step_enumeration_dispatch_coverage — O4 step drift.
|
|
2
|
+
|
|
3
|
+
A function whose docstring enumerates a linear step sequence matching the body's
|
|
4
|
+
top-level calls, while the body also routes to a corrective action inside an
|
|
5
|
+
``if``/``elif`` branch the prose never names, hides that conditional path from the
|
|
6
|
+
reader. This is the deterministic slice of Category O4 (step-ordering narrative).
|
|
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
|
+
|
|
29
|
+
def check_docstring_step_enumeration_dispatch_coverage(content: str, file_path: str) -> list[str]:
|
|
30
|
+
return code_rules_enforcer.check_docstring_step_enumeration_dispatch_coverage(
|
|
31
|
+
content, file_path
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def validate_content(content: str, file_path: str, old_content: str) -> list[str]:
|
|
36
|
+
return code_rules_enforcer.validate_content(content, file_path, old_content)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
PRODUCTION_FILE_PATH = "/project/src/theme_update_listing_edits.py"
|
|
40
|
+
TEST_FILE_PATH = "/project/src/test_theme_update_listing_edits.py"
|
|
41
|
+
HOOK_INFRASTRUCTURE_PATH = "/home/user/.claude/hooks/blocking/example.py"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _drifted_update_binary_function() -> str:
|
|
45
|
+
return (
|
|
46
|
+
"async def _open_update_and_upload_binary(\n"
|
|
47
|
+
" self, content_id: str, apk_path: Path\n"
|
|
48
|
+
") -> ClickedUpdateButton:\n"
|
|
49
|
+
' """Open the theme\'s update form, upload the new binary, and exclude'
|
|
50
|
+
" Fold devices.\n"
|
|
51
|
+
"\n"
|
|
52
|
+
" Navigates to the content list, searches for the content ID, clicks\n"
|
|
53
|
+
" whichever update-action button is present, navigates to the Binary tab,\n"
|
|
54
|
+
" and uploads the APK.\n"
|
|
55
|
+
' """\n'
|
|
56
|
+
" if not await self.navigate_to_content_list_start():\n"
|
|
57
|
+
" return ClickedUpdateButton.NONE\n"
|
|
58
|
+
" if not await self.search_for_content_id(content_id):\n"
|
|
59
|
+
" return ClickedUpdateButton.NONE\n"
|
|
60
|
+
" clicked = await self.click_update_button()\n"
|
|
61
|
+
" if not await self.navigate_to_binary_tab():\n"
|
|
62
|
+
" return ClickedUpdateButton.NONE\n"
|
|
63
|
+
" decision = await classify_binary_state(self.automation.cdp, self.oneui_number)\n"
|
|
64
|
+
" if decision.path is BinaryUpdatePath.PATH_B_CANCEL_UPDATE:\n"
|
|
65
|
+
" if not await cancel_and_reinitiate_update(self, content_id):\n"
|
|
66
|
+
" return ClickedUpdateButton.NONE\n"
|
|
67
|
+
" elif decision.path is BinaryUpdatePath.PATH_A_REPLACE_BINARY:\n"
|
|
68
|
+
" if not await replace_target_binary_row(self.automation, apk_path):\n"
|
|
69
|
+
" return ClickedUpdateButton.NONE\n"
|
|
70
|
+
" if not await upload_binary_to_open_theme(self.automation, [apk_path]):\n"
|
|
71
|
+
" return ClickedUpdateButton.NONE\n"
|
|
72
|
+
" return clicked\n"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _enumerated_update_binary_function() -> str:
|
|
77
|
+
return (
|
|
78
|
+
"async def _open_update_and_upload_binary(\n"
|
|
79
|
+
" self, content_id: str, apk_path: Path\n"
|
|
80
|
+
") -> ClickedUpdateButton:\n"
|
|
81
|
+
' """Open the theme\'s update form, upload the new binary, and exclude'
|
|
82
|
+
" Fold devices.\n"
|
|
83
|
+
"\n"
|
|
84
|
+
" Navigates to the content list, searches for the content ID, clicks\n"
|
|
85
|
+
" whichever update-action button is present, navigates to the Binary tab,\n"
|
|
86
|
+
" classifies the existing binary rows and, when needed, cancels and\n"
|
|
87
|
+
" reinitiates the update or replaces the target binary row, then uploads\n"
|
|
88
|
+
" the APK.\n"
|
|
89
|
+
' """\n'
|
|
90
|
+
" if not await self.navigate_to_content_list_start():\n"
|
|
91
|
+
" return ClickedUpdateButton.NONE\n"
|
|
92
|
+
" if not await self.search_for_content_id(content_id):\n"
|
|
93
|
+
" return ClickedUpdateButton.NONE\n"
|
|
94
|
+
" clicked = await self.click_update_button()\n"
|
|
95
|
+
" if not await self.navigate_to_binary_tab():\n"
|
|
96
|
+
" return ClickedUpdateButton.NONE\n"
|
|
97
|
+
" decision = await classify_binary_state(self.automation.cdp, self.oneui_number)\n"
|
|
98
|
+
" if decision.path is BinaryUpdatePath.PATH_B_CANCEL_UPDATE:\n"
|
|
99
|
+
" if not await cancel_and_reinitiate_update(self, content_id):\n"
|
|
100
|
+
" return ClickedUpdateButton.NONE\n"
|
|
101
|
+
" elif decision.path is BinaryUpdatePath.PATH_A_REPLACE_BINARY:\n"
|
|
102
|
+
" if not await replace_target_binary_row(self.automation, apk_path):\n"
|
|
103
|
+
" return ClickedUpdateButton.NONE\n"
|
|
104
|
+
" if not await upload_binary_to_open_theme(self.automation, [apk_path]):\n"
|
|
105
|
+
" return ClickedUpdateButton.NONE\n"
|
|
106
|
+
" return clicked\n"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_should_flag_branch_only_dispatch_call_the_prose_omits() -> None:
|
|
111
|
+
issues = check_docstring_step_enumeration_dispatch_coverage(
|
|
112
|
+
_drifted_update_binary_function(), PRODUCTION_FILE_PATH
|
|
113
|
+
)
|
|
114
|
+
assert any("cancel_and_reinitiate_update" in each for each in issues), (
|
|
115
|
+
f"The Path B cancel-and-reinitiate dispatch must be flagged, got: {issues!r}"
|
|
116
|
+
)
|
|
117
|
+
assert any("replace_target_binary_row" in each for each in issues), (
|
|
118
|
+
f"The Path A replace-target-row dispatch must be flagged, got: {issues!r}"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_should_report_category_o4_in_the_message() -> None:
|
|
123
|
+
issues = check_docstring_step_enumeration_dispatch_coverage(
|
|
124
|
+
_drifted_update_binary_function(), PRODUCTION_FILE_PATH
|
|
125
|
+
)
|
|
126
|
+
assert any("O4" in each for each in issues), (
|
|
127
|
+
f"Expected the Category O4 label in the message, got: {issues!r}"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_should_not_flag_when_dispatch_steps_are_enumerated() -> None:
|
|
132
|
+
issues = check_docstring_step_enumeration_dispatch_coverage(
|
|
133
|
+
_enumerated_update_binary_function(), PRODUCTION_FILE_PATH
|
|
134
|
+
)
|
|
135
|
+
assert issues == [], (
|
|
136
|
+
f"A docstring that names the corrective-path steps must not be flagged, got: {issues!r}"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_should_not_flag_when_fewer_than_two_linear_steps_are_named() -> None:
|
|
141
|
+
source = (
|
|
142
|
+
"def run(target: object) -> None:\n"
|
|
143
|
+
' """Compose the report from the target."""\n'
|
|
144
|
+
" compose_report(target)\n"
|
|
145
|
+
" flush_pending_writes(target)\n"
|
|
146
|
+
" if target.is_stale:\n"
|
|
147
|
+
" purge_expired_cache_entries(target)\n"
|
|
148
|
+
)
|
|
149
|
+
issues = check_docstring_step_enumeration_dispatch_coverage(source, PRODUCTION_FILE_PATH)
|
|
150
|
+
assert issues == [], (
|
|
151
|
+
"Only one linear step (compose_report) is named in the prose, below the bind "
|
|
152
|
+
f"threshold, so no dispatch is flagged, got: {issues!r}"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_should_not_flag_guarded_callee_that_is_also_a_linear_step() -> None:
|
|
157
|
+
source = (
|
|
158
|
+
"def run(target: object) -> None:\n"
|
|
159
|
+
' """Validate target, then dispatch event to queue."""\n'
|
|
160
|
+
" if not validate_target(target):\n"
|
|
161
|
+
" return\n"
|
|
162
|
+
" if not dispatch_event_to_queue(target):\n"
|
|
163
|
+
" return\n"
|
|
164
|
+
" if target.is_retry:\n"
|
|
165
|
+
" if not dispatch_event_to_queue(target):\n"
|
|
166
|
+
" return\n"
|
|
167
|
+
)
|
|
168
|
+
issues = check_docstring_step_enumeration_dispatch_coverage(source, PRODUCTION_FILE_PATH)
|
|
169
|
+
assert issues == [], (
|
|
170
|
+
"A guarded callee the body also guards as a linear step is covered by the "
|
|
171
|
+
f"enumeration and must not be flagged, got: {issues!r}"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_should_not_flag_plain_branch_logging_call() -> None:
|
|
176
|
+
source = (
|
|
177
|
+
"def run(target: object) -> None:\n"
|
|
178
|
+
' """Validate target, then dispatch event to queue."""\n'
|
|
179
|
+
" if not validate_target(target):\n"
|
|
180
|
+
" return\n"
|
|
181
|
+
" if not dispatch_event_to_queue(target):\n"
|
|
182
|
+
" return\n"
|
|
183
|
+
" if target.is_error:\n"
|
|
184
|
+
" capture_error_screenshot(target)\n"
|
|
185
|
+
" log_error_details(target)\n"
|
|
186
|
+
)
|
|
187
|
+
issues = check_docstring_step_enumeration_dispatch_coverage(source, PRODUCTION_FILE_PATH)
|
|
188
|
+
assert issues == [], (
|
|
189
|
+
"Plain (unguarded) branch logging and screenshot calls are not control-flow "
|
|
190
|
+
f"dispatch steps and must not be flagged, got: {issues!r}"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_should_not_flag_single_token_guarded_branch_callee() -> None:
|
|
195
|
+
source = (
|
|
196
|
+
"def run(target: object) -> None:\n"
|
|
197
|
+
' """Open the form, then submit the form."""\n'
|
|
198
|
+
" if not open_the_form(target):\n"
|
|
199
|
+
" return\n"
|
|
200
|
+
" if not submit_the_form(target):\n"
|
|
201
|
+
" return\n"
|
|
202
|
+
" if target.is_dirty:\n"
|
|
203
|
+
" if not rollback(target):\n"
|
|
204
|
+
" return\n"
|
|
205
|
+
)
|
|
206
|
+
issues = check_docstring_step_enumeration_dispatch_coverage(source, PRODUCTION_FILE_PATH)
|
|
207
|
+
assert issues == [], (
|
|
208
|
+
"A single-token guarded branch callee is below the token threshold and must "
|
|
209
|
+
f"not be flagged, got: {issues!r}"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_should_flag_guarded_branch_dispatch_step_the_prose_omits() -> None:
|
|
214
|
+
source = (
|
|
215
|
+
"def run(target: object) -> None:\n"
|
|
216
|
+
' """Validate target, then dispatch event to queue."""\n'
|
|
217
|
+
" if not validate_target(target):\n"
|
|
218
|
+
" return\n"
|
|
219
|
+
" if not dispatch_event_to_queue(target):\n"
|
|
220
|
+
" return\n"
|
|
221
|
+
" if target.is_retry:\n"
|
|
222
|
+
" if not reissue_pending_credential(target):\n"
|
|
223
|
+
" return\n"
|
|
224
|
+
)
|
|
225
|
+
issues = check_docstring_step_enumeration_dispatch_coverage(source, PRODUCTION_FILE_PATH)
|
|
226
|
+
assert any("reissue_pending_credential" in each for each in issues), (
|
|
227
|
+
"A guarded multi-token branch dispatch step the prose omits must be flagged, "
|
|
228
|
+
f"got: {issues!r}"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def test_should_skip_test_file() -> None:
|
|
233
|
+
issues = check_docstring_step_enumeration_dispatch_coverage(
|
|
234
|
+
_drifted_update_binary_function(), TEST_FILE_PATH
|
|
235
|
+
)
|
|
236
|
+
assert issues == [], f"Test files exempt, got: {issues!r}"
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def test_should_skip_hook_infrastructure() -> None:
|
|
240
|
+
issues = check_docstring_step_enumeration_dispatch_coverage(
|
|
241
|
+
_drifted_update_binary_function(), HOOK_INFRASTRUCTURE_PATH
|
|
242
|
+
)
|
|
243
|
+
assert issues == [], f"Hook infrastructure exempt, got: {issues!r}"
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def test_should_handle_syntax_error_gracefully() -> None:
|
|
247
|
+
issues = check_docstring_step_enumeration_dispatch_coverage(
|
|
248
|
+
"def fetch(\n", PRODUCTION_FILE_PATH
|
|
249
|
+
)
|
|
250
|
+
assert issues == [], f"Syntax error must yield no issues, got: {issues!r}"
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def test_validate_content_surfaces_step_dispatch_drift() -> None:
|
|
254
|
+
issues = validate_content(
|
|
255
|
+
_drifted_update_binary_function(), PRODUCTION_FILE_PATH, old_content=""
|
|
256
|
+
)
|
|
257
|
+
matching_issues = [
|
|
258
|
+
each for each in issues if "cancel_and_reinitiate_update" in each and "O4" in each
|
|
259
|
+
]
|
|
260
|
+
assert matching_issues, (
|
|
261
|
+
f"Expected validate_content to surface the O4 step-dispatch drift, got: {issues!r}"
|
|
262
|
+
)
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""Tests for check_docstring_names_undefined_constant — docstring-vs-impl drift.
|
|
2
|
+
|
|
3
|
+
A docstring that names an UPPER_SNAKE_CASE symbol as a contract identifier while
|
|
4
|
+
the enclosing module neither defines nor imports that name is docstring drift:
|
|
5
|
+
a reader who trusts the docstring to name a real constant finds nothing. This is
|
|
6
|
+
the deterministic slice of Category O6 where the named symbol is structurally a
|
|
7
|
+
constant (all-caps, underscore-joined) and resolvable against the module's
|
|
8
|
+
defined-and-imported name set.
|
|
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_docstring_names_undefined_constant(content: str, file_path: str) -> list[str]:
|
|
32
|
+
return code_rules_enforcer.check_docstring_names_undefined_constant(content, file_path)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
PRODUCTION_FILE_PATH = "/project/scripts/dispatch_registry.py"
|
|
36
|
+
TEST_FILE_PATH = "/project/scripts/test_dispatch_registry.py"
|
|
37
|
+
HOOK_INFRASTRUCTURE_PATH = "/home/user/.claude/hooks/blocking/example.py"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_flags_docstring_naming_constant_the_module_never_defines() -> None:
|
|
41
|
+
content = (
|
|
42
|
+
"class HostedHookEntry:\n"
|
|
43
|
+
' """A hosted hook entry.\n'
|
|
44
|
+
"\n"
|
|
45
|
+
" Attributes:\n"
|
|
46
|
+
" native_module_name: The module exposes a function named\n"
|
|
47
|
+
" NATIVE_EVALUATE_FUNCTION_NAME taking the payload and returning a\n"
|
|
48
|
+
" deny-reason string or None.\n"
|
|
49
|
+
' """\n'
|
|
50
|
+
"\n"
|
|
51
|
+
" native_module_name: str\n"
|
|
52
|
+
)
|
|
53
|
+
issues = check_docstring_names_undefined_constant(content, PRODUCTION_FILE_PATH)
|
|
54
|
+
assert len(issues) == 1
|
|
55
|
+
assert "NATIVE_EVALUATE_FUNCTION_NAME" in issues[0]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_passes_when_the_named_constant_is_defined_at_module_scope() -> None:
|
|
59
|
+
content = (
|
|
60
|
+
"MAXIMUM_RETRIES = 3\n"
|
|
61
|
+
"\n"
|
|
62
|
+
"def fetch_with_retries(url: str) -> str:\n"
|
|
63
|
+
' """Retry the fetch.\n'
|
|
64
|
+
"\n"
|
|
65
|
+
" The loop runs at most MAXIMUM_RETRIES times before giving up.\n"
|
|
66
|
+
' """\n'
|
|
67
|
+
" return url\n"
|
|
68
|
+
)
|
|
69
|
+
assert check_docstring_names_undefined_constant(content, PRODUCTION_FILE_PATH) == []
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_passes_when_the_named_constant_is_imported() -> None:
|
|
73
|
+
content = (
|
|
74
|
+
"from config.timing import MAXIMUM_RETRIES\n"
|
|
75
|
+
"\n"
|
|
76
|
+
"def fetch_with_retries(url: str) -> str:\n"
|
|
77
|
+
' """Retry the fetch up to MAXIMUM_RETRIES times."""\n'
|
|
78
|
+
" return url\n"
|
|
79
|
+
)
|
|
80
|
+
assert check_docstring_names_undefined_constant(content, PRODUCTION_FILE_PATH) == []
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_passes_when_the_named_constant_is_an_aliased_import() -> None:
|
|
84
|
+
content = (
|
|
85
|
+
"from config.timing import RETRY_BUDGET as MAXIMUM_RETRIES\n"
|
|
86
|
+
"\n"
|
|
87
|
+
"def fetch_with_retries(url: str) -> str:\n"
|
|
88
|
+
' """Retry the fetch up to MAXIMUM_RETRIES times."""\n'
|
|
89
|
+
" return url\n"
|
|
90
|
+
)
|
|
91
|
+
assert check_docstring_names_undefined_constant(content, PRODUCTION_FILE_PATH) == []
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_passes_when_module_docstring_names_a_defined_constant() -> None:
|
|
95
|
+
content = '"""Module that runs at most MAXIMUM_RETRIES attempts."""\n\nMAXIMUM_RETRIES = 3\n'
|
|
96
|
+
assert check_docstring_names_undefined_constant(content, PRODUCTION_FILE_PATH) == []
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_flags_module_docstring_naming_undefined_constant() -> None:
|
|
100
|
+
content = '"""Module that runs at most MAXIMUM_RETRIES attempts."""\n\nvalue_in_use = 3\n'
|
|
101
|
+
issues = check_docstring_names_undefined_constant(content, PRODUCTION_FILE_PATH)
|
|
102
|
+
assert len(issues) == 1
|
|
103
|
+
assert "MAXIMUM_RETRIES" in issues[0]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_ignores_http_acronym_and_short_all_caps_words() -> None:
|
|
107
|
+
content = (
|
|
108
|
+
"def send_request(url: str) -> str:\n"
|
|
109
|
+
' """Send an HTTP GET request and return the body as JSON."""\n'
|
|
110
|
+
" return url\n"
|
|
111
|
+
)
|
|
112
|
+
assert check_docstring_names_undefined_constant(content, PRODUCTION_FILE_PATH) == []
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_ignores_dunder_names() -> None:
|
|
116
|
+
content = (
|
|
117
|
+
"def export_surface() -> None:\n"
|
|
118
|
+
' """Names listed in __all__ form the export surface."""\n'
|
|
119
|
+
" return None\n"
|
|
120
|
+
)
|
|
121
|
+
assert check_docstring_names_undefined_constant(content, PRODUCTION_FILE_PATH) == []
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_test_files_are_exempt() -> None:
|
|
125
|
+
content = (
|
|
126
|
+
"class HostedHookEntry:\n"
|
|
127
|
+
' """The module exposes NATIVE_EVALUATE_FUNCTION_NAME."""\n'
|
|
128
|
+
"\n"
|
|
129
|
+
" native_module_name: str\n"
|
|
130
|
+
)
|
|
131
|
+
assert check_docstring_names_undefined_constant(content, TEST_FILE_PATH) == []
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_hook_infrastructure_is_exempt() -> None:
|
|
135
|
+
content = (
|
|
136
|
+
"class HostedHookEntry:\n"
|
|
137
|
+
' """The module exposes NATIVE_EVALUATE_FUNCTION_NAME."""\n'
|
|
138
|
+
"\n"
|
|
139
|
+
" native_module_name: str\n"
|
|
140
|
+
)
|
|
141
|
+
assert check_docstring_names_undefined_constant(content, HOOK_INFRASTRUCTURE_PATH) == []
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_passes_when_token_is_attribute_on_an_imported_stdlib_module() -> None:
|
|
145
|
+
content = (
|
|
146
|
+
"import os\n"
|
|
147
|
+
"def open_no_follow(target: str) -> int:\n"
|
|
148
|
+
' """Open target with O_NOFOLLOW so a symlink at target raises."""\n'
|
|
149
|
+
" return os.open(target, os.O_NOFOLLOW)\n"
|
|
150
|
+
)
|
|
151
|
+
assert check_docstring_names_undefined_constant(content, PRODUCTION_FILE_PATH) == []
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_passes_when_token_is_attribute_on_a_dotted_imported_module() -> None:
|
|
155
|
+
content = (
|
|
156
|
+
"import config.timing\n"
|
|
157
|
+
"def delay() -> int:\n"
|
|
158
|
+
' """Sleep up to MAX_DELAY seconds."""\n'
|
|
159
|
+
" return config.timing.MAX_DELAY\n"
|
|
160
|
+
)
|
|
161
|
+
assert check_docstring_names_undefined_constant(content, PRODUCTION_FILE_PATH) == []
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_passes_when_token_is_an_environment_variable_string_key() -> None:
|
|
165
|
+
content = (
|
|
166
|
+
"import os\n"
|
|
167
|
+
"def read_token() -> str:\n"
|
|
168
|
+
' """Read the secret from BWS_ACCESS_TOKEN in the environment."""\n'
|
|
169
|
+
" return os.environ['BWS_ACCESS_TOKEN']\n"
|
|
170
|
+
)
|
|
171
|
+
assert check_docstring_names_undefined_constant(content, PRODUCTION_FILE_PATH) == []
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_passes_when_token_is_a_naming_convention_descriptor() -> None:
|
|
175
|
+
content = (
|
|
176
|
+
"def is_styled(name: str) -> bool:\n"
|
|
177
|
+
' """Return True when name is UPPER_SNAKE_CASE styled."""\n'
|
|
178
|
+
" return name.isupper()\n"
|
|
179
|
+
)
|
|
180
|
+
assert check_docstring_names_undefined_constant(content, PRODUCTION_FILE_PATH) == []
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_passes_when_token_is_an_api_enum_string_literal() -> None:
|
|
184
|
+
content = (
|
|
185
|
+
"def submit_review(blocking: bool) -> dict[str, str]:\n"
|
|
186
|
+
' """Submit with event REQUEST_CHANGES when blocking."""\n'
|
|
187
|
+
" return {'event': 'REQUEST_CHANGES'} if blocking else {}\n"
|
|
188
|
+
)
|
|
189
|
+
assert check_docstring_names_undefined_constant(content, PRODUCTION_FILE_PATH) == []
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_passes_when_token_is_a_word_run_of_a_defined_or_imported_name() -> None:
|
|
193
|
+
content = (
|
|
194
|
+
"from github_constants import GITHUB_REVIEW_EVENT_REQUEST_CHANGES\n"
|
|
195
|
+
"def submit_review() -> str:\n"
|
|
196
|
+
' """Submit with the REQUEST_CHANGES event."""\n'
|
|
197
|
+
" return GITHUB_REVIEW_EVENT_REQUEST_CHANGES\n"
|
|
198
|
+
)
|
|
199
|
+
assert check_docstring_names_undefined_constant(content, PRODUCTION_FILE_PATH) == []
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_passes_when_token_is_an_enum_family_sibling_of_an_imported_constant() -> None:
|
|
203
|
+
content = (
|
|
204
|
+
"from preflight_constants import MODE_STRICT\n"
|
|
205
|
+
"def run(mode: str) -> None:\n"
|
|
206
|
+
' """mode: MODE_STRICT (autoconverge) or MODE_CLASSIFY (pr-converge)."""\n'
|
|
207
|
+
" if mode == MODE_STRICT:\n"
|
|
208
|
+
" return None\n"
|
|
209
|
+
" return None\n"
|
|
210
|
+
)
|
|
211
|
+
assert check_docstring_names_undefined_constant(content, PRODUCTION_FILE_PATH) == []
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_passes_when_docstring_frames_token_as_a_doc_file_reference() -> None:
|
|
215
|
+
content = '"""Constants for code_rules_gate.py per CODE_RULES centralized-config rule."""\n\nMAX_VIOLATIONS = 3\n'
|
|
216
|
+
assert check_docstring_names_undefined_constant(content, PRODUCTION_FILE_PATH) == []
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_passes_when_docstring_frames_token_with_a_file_suffix() -> None:
|
|
220
|
+
content = (
|
|
221
|
+
'"""Byte-copies CODE_RULES.md and TEST_QUALITY.md into the rules tree."""\n'
|
|
222
|
+
"\n"
|
|
223
|
+
"value_in_use = 3\n"
|
|
224
|
+
)
|
|
225
|
+
assert check_docstring_names_undefined_constant(content, PRODUCTION_FILE_PATH) == []
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_passes_when_docstring_frames_token_as_an_env_variable_set() -> None:
|
|
229
|
+
content = (
|
|
230
|
+
'"""Resolve the layout root.\n'
|
|
231
|
+
"\n"
|
|
232
|
+
" If LLM_SETTINGS_ROOT is set to the repo root, uses that root.\n"
|
|
233
|
+
' """\n'
|
|
234
|
+
"value_in_use = 3\n"
|
|
235
|
+
)
|
|
236
|
+
assert check_docstring_names_undefined_constant(content, PRODUCTION_FILE_PATH) == []
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def test_still_flags_the_genuine_miss_with_no_supporting_reference() -> None:
|
|
240
|
+
content = (
|
|
241
|
+
"class HostedHookEntry:\n"
|
|
242
|
+
' """A hosted hook entry.\n'
|
|
243
|
+
"\n"
|
|
244
|
+
" Attributes:\n"
|
|
245
|
+
" native_module_name: The module exposes a function named\n"
|
|
246
|
+
" NATIVE_EVALUATE_FUNCTION_NAME taking the payload.\n"
|
|
247
|
+
' """\n'
|
|
248
|
+
"\n"
|
|
249
|
+
" native_module_name: str\n"
|
|
250
|
+
)
|
|
251
|
+
issues = check_docstring_names_undefined_constant(content, PRODUCTION_FILE_PATH)
|
|
252
|
+
assert len(issues) == 1
|
|
253
|
+
assert "NATIVE_EVALUATE_FUNCTION_NAME" in issues[0]
|