claude-dev-env 1.82.0 → 1.83.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 +16 -13
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/README.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviewer_availability_constants.py +12 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +0 -2
- package/_shared/pr-loop/scripts/reviewer_availability.py +182 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +9 -33
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +2 -0
- package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +159 -0
- package/_shared/pr-loop/scripts/tests/test_reviewer_availability_constants.py +36 -0
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +14 -4
- package/hooks/blocking/CLAUDE.md +2 -0
- package/hooks/blocking/code_rules_constants_config.py +159 -1
- package/hooks/blocking/code_rules_docstrings.py +312 -9
- package/hooks/blocking/code_rules_enforcer.py +29 -0
- package/hooks/blocking/code_rules_imports_logging.py +867 -1
- package/hooks/blocking/code_rules_naming_collection.py +141 -0
- package/hooks/blocking/code_rules_string_magic.py +68 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
- package/hooks/blocking/reviewer_spawn_gate.py +182 -0
- package/hooks/blocking/stale_comment_reference_blocker.py +267 -0
- package/hooks/blocking/state_description_blocker.py +96 -5
- package/hooks/blocking/test_code_rules_config_duplicate_path_anchor.py +132 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +2 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +385 -0
- package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +67 -0
- package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +40 -0
- package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +213 -0
- package/hooks/blocking/test_code_rules_enforcer_referenced_underscore_loop.py +169 -0
- package/hooks/blocking/test_code_rules_js_bare_flag_return_directive.py +266 -0
- package/hooks/blocking/test_code_rules_js_sibling_return_object_key_drift.py +490 -0
- package/hooks/blocking/test_code_rules_logging_adjacent_literals.py +171 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +9 -3
- package/hooks/blocking/test_reviewer_spawn_gate.py +230 -0
- package/hooks/blocking/test_stale_comment_reference_blocker.py +236 -0
- package/hooks/blocking/test_state_description_blocker.py +135 -0
- package/hooks/hooks.json +5 -0
- package/hooks/hooks_constants/CLAUDE.md +3 -1
- package/hooks/hooks_constants/blocking_check_limits.py +43 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +41 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +41 -0
- package/hooks/hooks_constants/stale_comment_reference_blocker_constants.py +76 -0
- package/hooks/hooks_constants/state_description_blocker_constants.py +8 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +4 -1
- package/rules/claude-md-orphan-file.md +5 -0
- package/rules/docstring-prose-matches-implementation.md +10 -1
- package/rules/env-var-table-code-drift.md +5 -0
- package/rules/es-exe-file-search.md +17 -0
- package/rules/no-historical-clutter.md +12 -1
- package/rules/orphan-css-class.md +5 -0
- package/rules/package-inventory-stale-entry.md +10 -0
- package/rules/paired-test-coverage.md +5 -0
- package/rules/plain-illustrative-docstrings.md +5 -0
- package/rules/verify-before-asking.md +7 -0
- package/rules/verify-runtime-state.md +40 -0
- package/rules/windows-filesystem-safe.md +8 -0
- package/rules/workers-done-before-complete.md +33 -0
- package/rules/workflow-substitution-slots.md +5 -0
- package/skills/CLAUDE.md +1 -1
- package/skills/autoconverge/SKILL.md +10 -4
- package/skills/autoconverge/workflow/converge.contract.test.mjs +69 -0
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +54 -18
- package/skills/autoconverge/workflow/converge.mjs +97 -33
- package/skills/everything-search/SKILL.md +5 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
"""Tests for check_js_sibling_return_object_key_drift.
|
|
2
|
+
|
|
3
|
+
The check catches the JS/.mjs Category O return-shape drift PR #824 surfaced: a
|
|
4
|
+
workflow body whose success return carries a field (``allDeferredPrs``) while a
|
|
5
|
+
blocker early-return in the same scope omits it. The two return paths disagree on
|
|
6
|
+
shape, so an orchestrator reading the field off the blocker result gets undefined
|
|
7
|
+
where the documented contract and the sibling return both promise it. Early exits
|
|
8
|
+
that omit two or more fields — a genuinely smaller exit shape — pass.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib.util
|
|
14
|
+
import pathlib
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
_HOOK_DIRECTORY = pathlib.Path(__file__).parent
|
|
18
|
+
if str(_HOOK_DIRECTORY) not in sys.path:
|
|
19
|
+
sys.path.insert(0, str(_HOOK_DIRECTORY))
|
|
20
|
+
|
|
21
|
+
_module_spec = importlib.util.spec_from_file_location(
|
|
22
|
+
"code_rules_imports_logging",
|
|
23
|
+
_HOOK_DIRECTORY / "code_rules_imports_logging.py",
|
|
24
|
+
)
|
|
25
|
+
assert _module_spec is not None
|
|
26
|
+
assert _module_spec.loader is not None
|
|
27
|
+
_imports_logging_module = importlib.util.module_from_spec(_module_spec)
|
|
28
|
+
_module_spec.loader.exec_module(_imports_logging_module)
|
|
29
|
+
|
|
30
|
+
check_js_sibling_return_object_key_drift = (
|
|
31
|
+
_imports_logging_module.check_js_sibling_return_object_key_drift
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
_MJS_PATH = "skills/autoconverge/workflow/converge_multi.mjs"
|
|
35
|
+
|
|
36
|
+
_SHIPPED_CONVERGE_MJS = (
|
|
37
|
+
_HOOK_DIRECTORY.parents[1] / "skills" / "autoconverge" / "workflow" / "converge.mjs"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
_SHIPPED_INSTALL_MYPY_INI_MJS = (
|
|
41
|
+
_HOOK_DIRECTORY.parents[1] / "bin" / "install_mypy_ini.mjs"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
_TYPESCRIPT_PATH = "src/example.ts"
|
|
45
|
+
|
|
46
|
+
_TYPESCRIPT_JSX_PATH = "src/example.tsx"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _multi_workflow_source(blocker_return_body: str) -> str:
|
|
50
|
+
return (
|
|
51
|
+
"function classifyMultiInput(rawArgs) {\n"
|
|
52
|
+
" if (rawArgs == null) {\n"
|
|
53
|
+
" return { input: null, blocker: 'bad coordinates' }\n"
|
|
54
|
+
" }\n"
|
|
55
|
+
" return { input: rawArgs, blocker: null }\n"
|
|
56
|
+
"}\n"
|
|
57
|
+
"\n"
|
|
58
|
+
"const multiInput = classifyMultiInput(args)\n"
|
|
59
|
+
"if (multiInput.blocker) {\n"
|
|
60
|
+
f" {blocker_return_body}\n"
|
|
61
|
+
"}\n"
|
|
62
|
+
"const input = multiInput.input\n"
|
|
63
|
+
"\n"
|
|
64
|
+
"const childResults = await parallel(\n"
|
|
65
|
+
" input.prs.map((eachPr) => async () => {\n"
|
|
66
|
+
" return {\n"
|
|
67
|
+
" owner: eachPr.owner,\n"
|
|
68
|
+
" repo: eachPr.repo,\n"
|
|
69
|
+
" prNumber: eachPr.prNumber,\n"
|
|
70
|
+
" converged: true,\n"
|
|
71
|
+
" deferredPrs: [],\n"
|
|
72
|
+
" }\n"
|
|
73
|
+
" }),\n"
|
|
74
|
+
")\n"
|
|
75
|
+
"\n"
|
|
76
|
+
"return {\n"
|
|
77
|
+
" converged: true,\n"
|
|
78
|
+
" prCount: input.prs.length,\n"
|
|
79
|
+
" convergedCount: input.prs.length,\n"
|
|
80
|
+
" results: childResults,\n"
|
|
81
|
+
" allDeferredPrs: [],\n"
|
|
82
|
+
" blocker: null,\n"
|
|
83
|
+
"}\n"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _drifted_blocker_source() -> str:
|
|
88
|
+
return _multi_workflow_source(
|
|
89
|
+
"return { converged: false, prCount: 0, convergedCount: 0, results: [], "
|
|
90
|
+
"blocker: multiInput.blocker }"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _consistent_blocker_source() -> str:
|
|
95
|
+
return _multi_workflow_source(
|
|
96
|
+
"return { converged: false, prCount: 0, convergedCount: 0, results: [], "
|
|
97
|
+
"allDeferredPrs: [], blocker: multiInput.blocker }"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _three_field_gap_source() -> str:
|
|
102
|
+
return (
|
|
103
|
+
"const runInput = classifyRunInput(args)\n"
|
|
104
|
+
"if (runInput.blocker) {\n"
|
|
105
|
+
" return { converged: false, rounds: 0, finalSha: null, blocker: runInput.blocker }\n"
|
|
106
|
+
"}\n"
|
|
107
|
+
"\n"
|
|
108
|
+
"return {\n"
|
|
109
|
+
" converged: true,\n"
|
|
110
|
+
" rounds,\n"
|
|
111
|
+
" finalSha: head,\n"
|
|
112
|
+
" blocker: null,\n"
|
|
113
|
+
" standardsNote,\n"
|
|
114
|
+
" copilotNote,\n"
|
|
115
|
+
" reuseNote,\n"
|
|
116
|
+
"}\n"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _cross_scope_subset_source() -> str:
|
|
121
|
+
return (
|
|
122
|
+
"function buildFull() {\n"
|
|
123
|
+
" return { alpha: 1, beta: 2, gamma: 3 }\n"
|
|
124
|
+
"}\n"
|
|
125
|
+
"\n"
|
|
126
|
+
"function buildPartial() {\n"
|
|
127
|
+
" return { alpha: 1, beta: 2 }\n"
|
|
128
|
+
"}\n"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _discriminated_union_source() -> str:
|
|
133
|
+
return (
|
|
134
|
+
"function classifyOutcome(state) {\n"
|
|
135
|
+
" if (state == null) return { kind: 'retry' }\n"
|
|
136
|
+
" if (state.done === true) return { kind: 'ready', summary: state.summary }\n"
|
|
137
|
+
" return { kind: 'repair', failures: state.failures }\n"
|
|
138
|
+
"}\n"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _action_keyed_union_source() -> str:
|
|
143
|
+
return (
|
|
144
|
+
"function configureTarget(target) {\n"
|
|
145
|
+
" if (alreadyConfigured(target)) {\n"
|
|
146
|
+
" return { action: 'already-configured', path: target }\n"
|
|
147
|
+
" }\n"
|
|
148
|
+
" if (existsSync(target)) {\n"
|
|
149
|
+
" return { action: 'skipped-existing', path: target, expectedLine: expectedPathLine }\n"
|
|
150
|
+
" }\n"
|
|
151
|
+
" writeFileSync(target)\n"
|
|
152
|
+
" return { action: 'created', path: target }\n"
|
|
153
|
+
"}\n"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _two_typescript_functions_with_object_return_type() -> str:
|
|
158
|
+
return (
|
|
159
|
+
"function alpha(): { ok: boolean } {\n"
|
|
160
|
+
" return { a: 1, b: 2 }\n"
|
|
161
|
+
"}\n"
|
|
162
|
+
"function beta(): { ok: boolean } {\n"
|
|
163
|
+
" return { a: 1, b: 2, c: 3 }\n"
|
|
164
|
+
"}\n"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _single_typescript_function_with_drift() -> str:
|
|
169
|
+
return (
|
|
170
|
+
"function classify(state): { converged: boolean } {\n"
|
|
171
|
+
" if (state == null) {\n"
|
|
172
|
+
" return { converged: false, rounds: 0, blocker: 'bad state' }\n"
|
|
173
|
+
" }\n"
|
|
174
|
+
" return { converged: true, rounds: 5, blocker: null, deferred: [] }\n"
|
|
175
|
+
"}\n"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _two_typescript_functions_with_named_return_type() -> str:
|
|
180
|
+
return (
|
|
181
|
+
"function toSummary(): UserSummary {\n"
|
|
182
|
+
" return { count: 1, size: 2 }\n"
|
|
183
|
+
"}\n"
|
|
184
|
+
"function toDetail(): UserDetail {\n"
|
|
185
|
+
" return { count: 3, size: 4, extra: 5 }\n"
|
|
186
|
+
"}\n"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _two_typescript_functions_with_primitive_return_type() -> str:
|
|
191
|
+
return (
|
|
192
|
+
"function toSummary(): void {\n"
|
|
193
|
+
" return { count: 1, size: 2 }\n"
|
|
194
|
+
"}\n"
|
|
195
|
+
"function toDetail(): void {\n"
|
|
196
|
+
" return { count: 3, size: 4, extra: 5 }\n"
|
|
197
|
+
"}\n"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _single_typescript_function_named_return_type_with_drift() -> str:
|
|
202
|
+
return (
|
|
203
|
+
"function classify(state): ClassifyResult {\n"
|
|
204
|
+
" if (state == null) {\n"
|
|
205
|
+
" return { converged: false, rounds: 0, blocker: 'bad state' }\n"
|
|
206
|
+
" }\n"
|
|
207
|
+
" return { converged: true, rounds: 5, blocker: null, deferred: [] }\n"
|
|
208
|
+
"}\n"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _two_typescript_functions_with_function_type_return() -> str:
|
|
213
|
+
return (
|
|
214
|
+
"function toSummary(): (x: number) => void {\n"
|
|
215
|
+
" return { count: 1, size: 2 }\n"
|
|
216
|
+
"}\n"
|
|
217
|
+
"function toDetail(): (x: number) => void {\n"
|
|
218
|
+
" return { count: 3, size: 4, extra: 5 }\n"
|
|
219
|
+
"}\n"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _two_typescript_functions_with_mixed_union_return() -> str:
|
|
224
|
+
return (
|
|
225
|
+
"function alpha(): Promise<Foo> | Bar {\n"
|
|
226
|
+
" return { x: 1, y: 2 }\n"
|
|
227
|
+
"}\n"
|
|
228
|
+
"function beta(): Promise<Foo> | Bar {\n"
|
|
229
|
+
" return { x: 1, y: 2, z: 3 }\n"
|
|
230
|
+
"}\n"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _single_typescript_function_function_type_return_with_drift() -> str:
|
|
235
|
+
return (
|
|
236
|
+
"function classify(state): (x: number) => void {\n"
|
|
237
|
+
" if (state == null) {\n"
|
|
238
|
+
" return { converged: false, rounds: 0, blocker: 'bad state' }\n"
|
|
239
|
+
" }\n"
|
|
240
|
+
" return { converged: true, rounds: 5, blocker: null, deferred: [] }\n"
|
|
241
|
+
"}\n"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _two_typescript_functions_with_conditional_return_type() -> str:
|
|
246
|
+
return (
|
|
247
|
+
"function alpha(state: T): T extends U ? A : B {\n"
|
|
248
|
+
" return { a: 1, b: 2 }\n"
|
|
249
|
+
"}\n"
|
|
250
|
+
"function beta(state: T): T extends U ? A : B {\n"
|
|
251
|
+
" return { a: 1, b: 2, c: 3 }\n"
|
|
252
|
+
"}\n"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _single_typescript_function_conditional_return_type_with_drift() -> str:
|
|
257
|
+
return (
|
|
258
|
+
"function classify(state: T): T extends U ? A : B {\n"
|
|
259
|
+
" if (state == null) {\n"
|
|
260
|
+
" return { converged: false, rounds: 0, blocker: 'bad state' }\n"
|
|
261
|
+
" }\n"
|
|
262
|
+
" return { converged: true, rounds: 5, blocker: null, deferred: [] }\n"
|
|
263
|
+
"}\n"
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _two_concise_body_arrows_in_one_scope_source() -> str:
|
|
268
|
+
return (
|
|
269
|
+
"function build() {\n"
|
|
270
|
+
" const toFull = () => ({ x: 1, y: 2, z: 3 })\n"
|
|
271
|
+
" const toPartial = () => ({ x: 1, y: 2 })\n"
|
|
272
|
+
" return toFull()\n"
|
|
273
|
+
"}\n"
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _for_await_drift_source() -> str:
|
|
278
|
+
return (
|
|
279
|
+
"async function drain(stream) {\n"
|
|
280
|
+
" for await (const eachChunk of stream) {\n"
|
|
281
|
+
" if (eachChunk.bad) {\n"
|
|
282
|
+
" return { converged: false, rounds: 0, blocker: 'bad' }\n"
|
|
283
|
+
" }\n"
|
|
284
|
+
" }\n"
|
|
285
|
+
" return { converged: true, rounds: 1, blocker: null, deferred: [] }\n"
|
|
286
|
+
"}\n"
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _mixed_quote_shared_discriminant_drift_source() -> str:
|
|
291
|
+
return (
|
|
292
|
+
"function classify(state) {\n"
|
|
293
|
+
" if (state == null) {\n"
|
|
294
|
+
' return { action: "created", rounds: 0, blocker: \'bad\' }\n'
|
|
295
|
+
" }\n"
|
|
296
|
+
" return { action: 'created', rounds: 1, blocker: null, deferred: [] }\n"
|
|
297
|
+
"}\n"
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _mixed_quote_differing_discriminant_source() -> str:
|
|
302
|
+
return (
|
|
303
|
+
"function classify(state) {\n"
|
|
304
|
+
" if (state == null) {\n"
|
|
305
|
+
' return { action: "skipped-existing", rounds: 0, blocker: \'bad\' }\n'
|
|
306
|
+
" }\n"
|
|
307
|
+
" return { action: 'created', rounds: 1, blocker: null, deferred: [] }\n"
|
|
308
|
+
"}\n"
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _trailing_comment_shared_discriminant_drift_source() -> str:
|
|
313
|
+
return (
|
|
314
|
+
"function classify(state) {\n"
|
|
315
|
+
" if (state == null) {\n"
|
|
316
|
+
" return { action: 'created' /* legacy tag */, rounds: 0, blocker: 'bad' }\n"
|
|
317
|
+
" }\n"
|
|
318
|
+
" return { action: 'created', rounds: 1, blocker: null, deferred: [] }\n"
|
|
319
|
+
"}\n"
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def test_flags_blocker_return_that_omits_one_documented_field() -> None:
|
|
324
|
+
issues = check_js_sibling_return_object_key_drift(_drifted_blocker_source(), _MJS_PATH)
|
|
325
|
+
assert len(issues) == 1
|
|
326
|
+
assert "allDeferredPrs" in issues[0]
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def test_accepts_when_blocker_return_carries_the_field() -> None:
|
|
330
|
+
issues = check_js_sibling_return_object_key_drift(_consistent_blocker_source(), _MJS_PATH)
|
|
331
|
+
assert issues == []
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def test_accepts_early_exit_that_omits_more_than_one_field() -> None:
|
|
335
|
+
issues = check_js_sibling_return_object_key_drift(_three_field_gap_source(), _MJS_PATH)
|
|
336
|
+
assert issues == []
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def test_scopes_subset_comparison_to_a_single_function() -> None:
|
|
340
|
+
issues = check_js_sibling_return_object_key_drift(_cross_scope_subset_source(), _MJS_PATH)
|
|
341
|
+
assert issues == []
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def test_accepts_discriminated_union_returns() -> None:
|
|
345
|
+
issues = check_js_sibling_return_object_key_drift(_discriminated_union_source(), _MJS_PATH)
|
|
346
|
+
assert issues == []
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def test_accepts_subset_by_one_with_string_discriminant() -> None:
|
|
350
|
+
issues = check_js_sibling_return_object_key_drift(_action_keyed_union_source(), _MJS_PATH)
|
|
351
|
+
assert issues == []
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def test_accepts_candidate_whose_supersets_disagree_on_the_missing_key() -> None:
|
|
355
|
+
source = (
|
|
356
|
+
"function classifyRun(state) {\n"
|
|
357
|
+
" if (state == null) {\n"
|
|
358
|
+
" return { converged: false, rounds: 0, finalSha: null }\n"
|
|
359
|
+
" }\n"
|
|
360
|
+
" if (state.failed) {\n"
|
|
361
|
+
" return { converged: false, rounds: 0, finalSha: null, failureNote: state.note }\n"
|
|
362
|
+
" }\n"
|
|
363
|
+
" return { converged: true, rounds: state.rounds, finalSha: state.sha, "
|
|
364
|
+
"summary: state.summary }\n"
|
|
365
|
+
"}\n"
|
|
366
|
+
)
|
|
367
|
+
assert check_js_sibling_return_object_key_drift(source, _MJS_PATH) == []
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def test_shipped_install_mypy_ini_mjs_passes_its_own_check() -> None:
|
|
371
|
+
shipped_source = _SHIPPED_INSTALL_MYPY_INI_MJS.read_text(encoding="utf-8")
|
|
372
|
+
issues = check_js_sibling_return_object_key_drift(
|
|
373
|
+
shipped_source, "bin/install_mypy_ini.mjs"
|
|
374
|
+
)
|
|
375
|
+
assert issues == []
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def test_typescript_object_return_type_keeps_functions_in_separate_scopes() -> None:
|
|
379
|
+
source = _two_typescript_functions_with_object_return_type()
|
|
380
|
+
assert check_js_sibling_return_object_key_drift(source, _TYPESCRIPT_PATH) == []
|
|
381
|
+
assert check_js_sibling_return_object_key_drift(source, _TYPESCRIPT_JSX_PATH) == []
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def test_typescript_object_return_type_still_flags_intra_function_drift() -> None:
|
|
385
|
+
issues = check_js_sibling_return_object_key_drift(
|
|
386
|
+
_single_typescript_function_with_drift(), _TYPESCRIPT_PATH
|
|
387
|
+
)
|
|
388
|
+
assert len(issues) == 1
|
|
389
|
+
assert "deferred" in issues[0]
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def test_typescript_named_return_type_keeps_functions_in_separate_scopes() -> None:
|
|
393
|
+
source = _two_typescript_functions_with_named_return_type()
|
|
394
|
+
assert check_js_sibling_return_object_key_drift(source, _TYPESCRIPT_PATH) == []
|
|
395
|
+
assert check_js_sibling_return_object_key_drift(source, _TYPESCRIPT_JSX_PATH) == []
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def test_typescript_primitive_return_type_keeps_functions_in_separate_scopes() -> None:
|
|
399
|
+
source = _two_typescript_functions_with_primitive_return_type()
|
|
400
|
+
assert check_js_sibling_return_object_key_drift(source, _TYPESCRIPT_PATH) == []
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def test_typescript_named_return_type_still_flags_intra_function_drift() -> None:
|
|
404
|
+
issues = check_js_sibling_return_object_key_drift(
|
|
405
|
+
_single_typescript_function_named_return_type_with_drift(), _TYPESCRIPT_PATH
|
|
406
|
+
)
|
|
407
|
+
assert len(issues) == 1
|
|
408
|
+
assert "deferred" in issues[0]
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def test_typescript_function_type_return_keeps_functions_in_separate_scopes() -> None:
|
|
412
|
+
source = _two_typescript_functions_with_function_type_return()
|
|
413
|
+
assert check_js_sibling_return_object_key_drift(source, _TYPESCRIPT_PATH) == []
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def test_typescript_mixed_union_return_keeps_functions_in_separate_scopes() -> None:
|
|
417
|
+
source = _two_typescript_functions_with_mixed_union_return()
|
|
418
|
+
assert check_js_sibling_return_object_key_drift(source, _TYPESCRIPT_PATH) == []
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def test_typescript_function_type_return_still_flags_intra_function_drift() -> None:
|
|
422
|
+
issues = check_js_sibling_return_object_key_drift(
|
|
423
|
+
_single_typescript_function_function_type_return_with_drift(), _TYPESCRIPT_PATH
|
|
424
|
+
)
|
|
425
|
+
assert len(issues) == 1
|
|
426
|
+
assert "deferred" in issues[0]
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def test_typescript_conditional_return_type_keeps_functions_in_separate_scopes() -> None:
|
|
430
|
+
source = _two_typescript_functions_with_conditional_return_type()
|
|
431
|
+
assert check_js_sibling_return_object_key_drift(source, _TYPESCRIPT_PATH) == []
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def test_typescript_conditional_return_type_still_flags_intra_function_drift() -> None:
|
|
435
|
+
issues = check_js_sibling_return_object_key_drift(
|
|
436
|
+
_single_typescript_function_conditional_return_type_with_drift(), _TYPESCRIPT_PATH
|
|
437
|
+
)
|
|
438
|
+
assert len(issues) == 1
|
|
439
|
+
assert "deferred" in issues[0]
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def test_distinct_concise_body_arrows_in_one_scope_do_not_drift() -> None:
|
|
443
|
+
issues = check_js_sibling_return_object_key_drift(
|
|
444
|
+
_two_concise_body_arrows_in_one_scope_source(), _MJS_PATH
|
|
445
|
+
)
|
|
446
|
+
assert issues == []
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def test_for_await_block_shares_its_functions_scope() -> None:
|
|
450
|
+
issues = check_js_sibling_return_object_key_drift(_for_await_drift_source(), _MJS_PATH)
|
|
451
|
+
assert len(issues) == 1
|
|
452
|
+
assert "deferred" in issues[0]
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def test_mixed_quote_shared_discriminant_does_not_suppress_drift() -> None:
|
|
456
|
+
issues = check_js_sibling_return_object_key_drift(
|
|
457
|
+
_mixed_quote_shared_discriminant_drift_source(), _MJS_PATH
|
|
458
|
+
)
|
|
459
|
+
assert len(issues) == 1
|
|
460
|
+
assert "deferred" in issues[0]
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def test_mixed_quote_differing_discriminant_still_passes_as_union() -> None:
|
|
464
|
+
issues = check_js_sibling_return_object_key_drift(
|
|
465
|
+
_mixed_quote_differing_discriminant_source(), _MJS_PATH
|
|
466
|
+
)
|
|
467
|
+
assert issues == []
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def test_trailing_comment_after_shared_discriminant_does_not_suppress_drift() -> None:
|
|
471
|
+
issues = check_js_sibling_return_object_key_drift(
|
|
472
|
+
_trailing_comment_shared_discriminant_drift_source(), _MJS_PATH
|
|
473
|
+
)
|
|
474
|
+
assert len(issues) == 1
|
|
475
|
+
assert "deferred" in issues[0]
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def test_skips_python_files() -> None:
|
|
479
|
+
issues = check_js_sibling_return_object_key_drift(
|
|
480
|
+
_drifted_blocker_source(), "workflow/converge_multi.py"
|
|
481
|
+
)
|
|
482
|
+
assert issues == []
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def test_shipped_converge_mjs_passes_its_own_check() -> None:
|
|
486
|
+
shipped_source = _SHIPPED_CONVERGE_MJS.read_text(encoding="utf-8")
|
|
487
|
+
issues = check_js_sibling_return_object_key_drift(
|
|
488
|
+
shipped_source, str(_SHIPPED_CONVERGE_MJS)
|
|
489
|
+
)
|
|
490
|
+
assert issues == []
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Tests for check_logging_adjacent_string_literals."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from types import ModuleType
|
|
8
|
+
|
|
9
|
+
ENFORCER_FILENAME = "code_rules_enforcer.py"
|
|
10
|
+
ENFORCER_MODULE_NAME = "code_rules_enforcer_adjacent_literals_tests"
|
|
11
|
+
PRODUCTION_FILE_PATH = "shared_utils/web_automation/sample.py"
|
|
12
|
+
TEST_FILE_PATH = "shared_utils/web_automation/tests/test_sample.py"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def load_enforcer_module() -> ModuleType:
|
|
16
|
+
loader_path = Path(__file__).parent / ENFORCER_FILENAME
|
|
17
|
+
module_spec = importlib.util.spec_from_file_location(ENFORCER_MODULE_NAME, loader_path)
|
|
18
|
+
assert module_spec is not None
|
|
19
|
+
assert module_spec.loader is not None
|
|
20
|
+
loaded_module = importlib.util.module_from_spec(module_spec)
|
|
21
|
+
module_spec.loader.exec_module(loaded_module)
|
|
22
|
+
return loaded_module
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
enforcer = load_enforcer_module()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_should_flag_adjacent_literals_in_attribute_logger_call() -> None:
|
|
29
|
+
source = 'logger.info("[Batch]" " Failed %s", job_name)\n'
|
|
30
|
+
issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
|
|
31
|
+
assert len(issues) == 1
|
|
32
|
+
assert "adjacent string literals" in issues[0]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_should_flag_adjacent_literals_in_log_helper_call() -> None:
|
|
36
|
+
source = 'log_debug("[Batch]" " Completed {}", job_name)\n'
|
|
37
|
+
issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
|
|
38
|
+
assert len(issues) == 1
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_should_flag_adjacent_literals_on_underscore_prefixed_logger() -> None:
|
|
42
|
+
source = '_logger.info("[Batch]" " Failed %s", job_name)\n'
|
|
43
|
+
issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
|
|
44
|
+
assert len(issues) == 1
|
|
45
|
+
assert "adjacent string literals" in issues[0]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_should_flag_adjacent_single_quoted_literals() -> None:
|
|
49
|
+
source = "logger.warning('[Batch]' ' Skipped %s', job_name)\n"
|
|
50
|
+
issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
|
|
51
|
+
assert len(issues) == 1
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_should_flag_zero_whitespace_adjacent_literals() -> None:
|
|
55
|
+
source = 'log_info("[Batch]""Failed %s", job_name)\n'
|
|
56
|
+
issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
|
|
57
|
+
assert len(issues) == 1
|
|
58
|
+
assert "adjacent string literals" in issues[0]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_should_flag_adjacent_literals_with_prefixed_first_literal_bytes() -> None:
|
|
62
|
+
source = 'log_info(b"[Batch]" b" Failed %s", job_name)\n'
|
|
63
|
+
issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
|
|
64
|
+
assert len(issues) == 1
|
|
65
|
+
assert "adjacent string literals" in issues[0]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_should_flag_zero_whitespace_adjacent_literals_with_prefixed_first_literal() -> None:
|
|
69
|
+
source = 'log_info(r"[Batch]""Failed %s", job_name)\n'
|
|
70
|
+
issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
|
|
71
|
+
assert len(issues) == 1
|
|
72
|
+
assert "adjacent string literals" in issues[0]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_should_allow_single_triple_double_quoted_literal_log_call() -> None:
|
|
76
|
+
source = 'log_info("""message""")\n'
|
|
77
|
+
issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
|
|
78
|
+
assert issues == []
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_should_allow_single_triple_single_quoted_literal_log_call() -> None:
|
|
82
|
+
source = "log_debug('''message''')\n"
|
|
83
|
+
issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
|
|
84
|
+
assert issues == []
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_should_allow_prefixed_triple_quoted_literal_log_call() -> None:
|
|
88
|
+
source = 'log_info(r"""message""")\n'
|
|
89
|
+
issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
|
|
90
|
+
assert issues == []
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_should_allow_single_literal_log_call() -> None:
|
|
94
|
+
source = 'logger.info("[Batch] Starting %s (row %d)", job_name, attempt_index)\n'
|
|
95
|
+
issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
|
|
96
|
+
assert issues == []
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_should_allow_comma_separated_string_arguments() -> None:
|
|
100
|
+
source = 'logger.info("delivered %s %s", message_id, "queued")\n'
|
|
101
|
+
issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
|
|
102
|
+
assert issues == []
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_should_allow_explicit_plus_concatenation() -> None:
|
|
106
|
+
source = 'logger.info("[Batch] " + suffix_pattern, job_name)\n'
|
|
107
|
+
issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
|
|
108
|
+
assert issues == []
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_should_allow_adjacent_literals_outside_log_calls() -> None:
|
|
112
|
+
source = 'banner_text = "[Batch]" " Failed %s"\n'
|
|
113
|
+
issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
|
|
114
|
+
assert issues == []
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_should_exempt_test_files() -> None:
|
|
118
|
+
source = 'logger.info("[Batch]" " Failed %s", job_name)\n'
|
|
119
|
+
issues = enforcer.check_logging_adjacent_string_literals(source, TEST_FILE_PATH)
|
|
120
|
+
assert issues == []
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_should_skip_non_python_files() -> None:
|
|
124
|
+
source = 'logger.info("[Batch]" " Failed %s", job_name)\n'
|
|
125
|
+
issues = enforcer.check_logging_adjacent_string_literals(source, "notes.md")
|
|
126
|
+
assert issues == []
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_should_allow_adjacent_literals_in_second_statement_on_same_line() -> None:
|
|
130
|
+
source = 'logger.info("done"); banner = "a" "b"\n'
|
|
131
|
+
issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
|
|
132
|
+
assert issues == []
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_should_flag_adjacent_literals_in_log_call_preceded_by_other_statement() -> None:
|
|
136
|
+
source = 'attempt_index = 1; logger.info("[Batch]" " Failed %s", job_name)\n'
|
|
137
|
+
issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
|
|
138
|
+
assert len(issues) == 1
|
|
139
|
+
assert "adjacent string literals" in issues[0]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_should_allow_adjacent_literal_example_inside_docstring() -> None:
|
|
143
|
+
source = (
|
|
144
|
+
'def emit_batch_result() -> None:\n'
|
|
145
|
+
' """Example: logger.info("[Batch]" " Failed %s", job_name).\n'
|
|
146
|
+
' """\n'
|
|
147
|
+
" logger.info(\"[Batch] Starting %s\", job_name)\n"
|
|
148
|
+
)
|
|
149
|
+
issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
|
|
150
|
+
assert issues == []
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_should_allow_adjacent_literal_example_inside_comment() -> None:
|
|
154
|
+
source = (
|
|
155
|
+
'# example: logger.info("[Batch]" " Failed %s", job_name)\n'
|
|
156
|
+
'logger.info("[Batch] Starting %s", job_name)\n'
|
|
157
|
+
)
|
|
158
|
+
issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
|
|
159
|
+
assert issues == []
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_should_still_flag_real_call_alongside_docstring_example() -> None:
|
|
163
|
+
source = (
|
|
164
|
+
'def emit_batch_result() -> None:\n'
|
|
165
|
+
' """Example: logger.info("[Batch]" " Failed %s", job_name).\n'
|
|
166
|
+
' """\n'
|
|
167
|
+
' logger.info("[Batch]" " Failed %s", job_name)\n'
|
|
168
|
+
)
|
|
169
|
+
issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
|
|
170
|
+
assert len(issues) == 1
|
|
171
|
+
assert "adjacent string literals" in issues[0]
|
|
@@ -638,10 +638,16 @@ def test_dispatcher_write_applies_both_groups() -> None:
|
|
|
638
638
|
|
|
639
639
|
|
|
640
640
|
def test_dispatcher_edit_applies_both_groups() -> None:
|
|
641
|
-
"""Edit
|
|
641
|
+
"""Edit triggers Group A, Group B, and the Edit-scoped entry through the
|
|
642
|
+
dispatcher.
|
|
643
|
+
"""
|
|
642
644
|
all_edit_entries = _applicable_entries_for_tool(EDIT_TOOL_NAME)
|
|
643
|
-
|
|
644
|
-
|
|
645
|
+
all_edit_script_paths = {each_entry.script_relative_path for each_entry in all_edit_entries}
|
|
646
|
+
assert "blocking/stale_comment_reference_blocker.py" in all_edit_script_paths, (
|
|
647
|
+
"stale_comment_reference_blocker belongs in the Edit applicable set"
|
|
648
|
+
)
|
|
649
|
+
assert len(all_edit_entries) == 20, (
|
|
650
|
+
f"expected 20 Edit entries, got {len(all_edit_entries)}"
|
|
645
651
|
)
|
|
646
652
|
|
|
647
653
|
|