claude-dev-env 1.73.0 → 1.75.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +2 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/hooks/blocking/CLAUDE.md +4 -0
- package/hooks/blocking/block_main_commit.py +14 -0
- package/hooks/blocking/bot_mention_comment_blocker.py +7 -0
- package/hooks/blocking/claude_md_orphan_file_blocker.py +14 -42
- package/hooks/blocking/code_rules_docstrings.py +223 -0
- package/hooks/blocking/code_rules_enforcer.py +16 -0
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +12 -5
- package/hooks/blocking/convergence_gate_blocker.py +17 -3
- package/hooks/blocking/destructive_command_blocker.py +7 -0
- package/hooks/blocking/docstring_rule_gate_count_blocker.py +321 -0
- package/hooks/blocking/duplicate_rmtree_helper_blocker.py +155 -0
- package/hooks/blocking/gh_body_arg_blocker.py +8 -0
- package/hooks/blocking/gh_pr_author_enforcer.py +7 -0
- package/hooks/blocking/hedging_language_blocker.py +17 -23
- package/hooks/blocking/hook_prose_detector_consistency.py +7 -0
- package/hooks/blocking/intent_only_ending_blocker.py +18 -26
- package/hooks/blocking/md_to_html_blocker.py +10 -2
- package/hooks/blocking/open_questions_in_plans_blocker.py +10 -2
- package/hooks/blocking/package_inventory_stale_blocker.py +398 -0
- package/hooks/blocking/plain_language_blocker.py +6 -0
- package/hooks/blocking/pr_converge_bugteam_enforcer.py +6 -0
- package/hooks/blocking/pr_description_enforcer.py +6 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +5 -6
- package/hooks/blocking/precommit_code_rules_gate.py +10 -1
- package/hooks/blocking/pytest_testpaths_orphan_blocker.py +8 -0
- package/hooks/blocking/question_to_user_enforcer.py +19 -23
- package/hooks/blocking/send_user_file_open_locally_blocker.py +70 -0
- package/hooks/blocking/sensitive_file_protector.py +15 -1
- package/hooks/blocking/session_handoff_blocker.py +15 -23
- package/hooks/blocking/state_description_blocker.py +6 -0
- package/hooks/blocking/subprocess_budget_completeness.py +9 -3
- package/hooks/blocking/tdd_enforcer.py +6 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_returns_plural_cardinality.py +207 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_unguarded_payload.py +188 -0
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +61 -0
- package/hooks/blocking/test_docstring_rule_gate_count_blocker.py +203 -0
- package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +328 -0
- package/hooks/blocking/test_hedging_language_blocker.py +6 -0
- package/hooks/blocking/test_hook_block_logger_coverage.py +53 -0
- package/hooks/blocking/test_intent_only_ending_blocker.py +5 -0
- package/hooks/blocking/test_package_inventory_stale_blocker.py +329 -0
- package/hooks/blocking/test_plain_language_blocker.py +36 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +55 -8
- package/hooks/blocking/test_question_to_user_enforcer.py +6 -0
- package/hooks/blocking/test_send_user_file_open_locally_blocker.py +114 -0
- package/hooks/blocking/test_session_handoff_blocker.py +6 -0
- package/hooks/blocking/test_shared_stdin_adoption.py +42 -0
- package/hooks/blocking/test_state_description_blocker.py +41 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +49 -0
- package/hooks/blocking/test_workflow_substitution_slot_blocker.py +4 -19
- package/hooks/blocking/verdict_directory_write_blocker.py +10 -1
- package/hooks/blocking/verified_commit_gate.py +11 -0
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +16 -1
- package/hooks/blocking/windows_rmtree_blocker.py +7 -0
- package/hooks/blocking/workflow_substitution_slot_blocker.py +10 -5
- package/hooks/blocking/write_existing_file_blocker.py +16 -1
- package/hooks/hooks.json +10 -0
- package/hooks/hooks_constants/CLAUDE.md +8 -1
- package/hooks/hooks_constants/blocking_check_limits.py +13 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +3 -0
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +2 -1
- package/hooks/hooks_constants/docstring_rule_gate_count_blocker_constants.py +90 -0
- package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +27 -0
- package/hooks/hooks_constants/hook_block_logger.py +59 -0
- package/hooks/hooks_constants/multi_edit_reconstruction.py +56 -0
- package/hooks/hooks_constants/package_inventory_stale_blocker_constants.py +111 -0
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +3 -2
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +17 -3
- package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +18 -0
- package/hooks/hooks_constants/test_dispatcher_constants_docstrings.py +44 -0
- package/hooks/hooks_constants/test_hook_block_logger.py +159 -0
- package/hooks/hooks_constants/test_post_tool_use_dispatcher_constants.py +43 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +99 -0
- package/hooks/hooks_constants/test_text_stripping.py +39 -0
- package/hooks/hooks_constants/text_stripping.py +36 -0
- package/hooks/lifecycle/config_change_guard.py +12 -0
- package/hooks/lifecycle/test_config_change_guard.py +23 -0
- package/hooks/validation/CLAUDE.md +1 -0
- package/hooks/validation/hook_format_validator.py +13 -0
- package/hooks/validation/mypy_validator.py +30 -1
- package/hooks/validation/post_tool_use_dispatcher.py +2 -2
- package/hooks/validation/test_hook_format_validator.py +64 -0
- package/hooks/validation/test_mypy_validator.py +23 -1
- package/hooks/validation/test_post_tool_use_dispatcher.py +6 -0
- package/hooks/workflow/auto_formatter.py +8 -5
- package/hooks/workflow/test_auto_formatter.py +33 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/docstring-prose-matches-implementation.md +2 -1
- package/rules/package-inventory-stale-entry.md +24 -0
- package/rules/windows-filesystem-safe.md +2 -0
- package/skills/autoconverge/SKILL.md +21 -1
- package/skills/autoconverge/reference/stop-conditions.md +7 -0
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +5 -4
- package/skills/autoconverge/workflow/converge.contract.test.mjs +398 -116
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +16 -16
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +36 -44
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -24
- package/skills/autoconverge/workflow/converge.mjs +599 -606
- package/skills/autoconverge/workflow/convergence_summary.py +1 -1
- package/skills/autoconverge/workflow/render_report.py +2 -6
- package/skills/autoconverge/workflow/test_convergence_summary.py +17 -0
- package/skills/autoconverge/workflow/test_render_report.py +1 -0
|
@@ -10,16 +10,32 @@ const gotchasSource = readFileSync(
|
|
|
10
10
|
join(workflowDirectory, '..', 'reference', 'gotchas.md'),
|
|
11
11
|
'utf8',
|
|
12
12
|
);
|
|
13
|
+
const skillSource = readFileSync(
|
|
14
|
+
join(workflowDirectory, '..', 'SKILL.md'),
|
|
15
|
+
'utf8',
|
|
16
|
+
);
|
|
13
17
|
|
|
14
18
|
function lensPromptBody(builderName) {
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
let builderStart = convergeSource.indexOf(`function ${builderName}(`);
|
|
20
|
+
if (builderStart === -1) {
|
|
21
|
+
builderStart = convergeSource.indexOf(`const ${builderName} =`);
|
|
22
|
+
assert.notEqual(builderStart, -1, `expected ${builderName} to exist as a function or const`);
|
|
23
|
+
}
|
|
17
24
|
const nextBuilderMatch = /\n(?:async )?function /.exec(convergeSource.slice(builderStart + 1));
|
|
18
25
|
const builderEnd =
|
|
19
26
|
nextBuilderMatch === null ? convergeSource.length : builderStart + 1 + nextBuilderMatch.index;
|
|
20
27
|
return convergeSource.slice(builderStart, builderEnd);
|
|
21
28
|
}
|
|
22
29
|
|
|
30
|
+
function functionSource(functionName) {
|
|
31
|
+
const functionStart = convergeSource.indexOf(`function ${functionName}(`);
|
|
32
|
+
assert.notEqual(functionStart, -1, `expected ${functionName} to exist`);
|
|
33
|
+
const nextMatch = /\n(?:async )?function /.exec(convergeSource.slice(functionStart + 1));
|
|
34
|
+
const functionEnd =
|
|
35
|
+
nextMatch === null ? convergeSource.length : functionStart + 1 + nextMatch.index;
|
|
36
|
+
return convergeSource.slice(functionStart, functionEnd);
|
|
37
|
+
}
|
|
38
|
+
|
|
23
39
|
test('code-review lens prompt no longer instructs a per-lens git fetch', () => {
|
|
24
40
|
assert.doesNotMatch(lensPromptBody('runCodeReviewLens'), /git fetch origin main/);
|
|
25
41
|
});
|
|
@@ -29,10 +45,10 @@ test('bug-audit lens prompt no longer instructs a per-lens git fetch', () => {
|
|
|
29
45
|
});
|
|
30
46
|
|
|
31
47
|
test('a single round-level prefetch step fetches origin/main before the parallel lenses', () => {
|
|
32
|
-
assert.
|
|
33
|
-
const prefetchCallIndex = convergeSource.indexOf(
|
|
48
|
+
assert.ok(convergeSource.includes("resumeGitAgent(gitAgentId, 'prefetch-main')"));
|
|
49
|
+
const prefetchCallIndex = convergeSource.indexOf("resumeGitAgent(gitAgentId, 'prefetch-main')");
|
|
34
50
|
const parallelLensIndex = convergeSource.indexOf('const lenses = await parallel(');
|
|
35
|
-
assert.notEqual(prefetchCallIndex, -1, 'expected
|
|
51
|
+
assert.notEqual(prefetchCallIndex, -1, 'expected prefetch to be invoked');
|
|
36
52
|
assert.notEqual(parallelLensIndex, -1, 'expected the parallel lens block to exist');
|
|
37
53
|
assert.ok(
|
|
38
54
|
prefetchCallIndex < parallelLensIndex,
|
|
@@ -69,7 +85,7 @@ test('gotchas doc states parallel lenses must avoid concurrent git operations',
|
|
|
69
85
|
});
|
|
70
86
|
|
|
71
87
|
test('repair-convergence edit step filters unresolved threads to bot authors and skips human threads', () => {
|
|
72
|
-
const repairPrompt =
|
|
88
|
+
const repairPrompt = functionSource('resumeCodeEditorAgent');
|
|
73
89
|
assert.match(
|
|
74
90
|
repairPrompt,
|
|
75
91
|
/cursor.*claude.*copilot|copilot.*cursor.*claude|claude.*cursor.*copilot/is,
|
|
@@ -83,7 +99,7 @@ test('repair-convergence edit step filters unresolved threads to bot authors and
|
|
|
83
99
|
});
|
|
84
100
|
|
|
85
101
|
test('repair-convergence edit step no longer instructs resolving every unresolved thread without an author filter', () => {
|
|
86
|
-
const repairPrompt =
|
|
102
|
+
const repairPrompt = functionSource('resumeCodeEditorAgent');
|
|
87
103
|
assert.doesNotMatch(
|
|
88
104
|
repairPrompt,
|
|
89
105
|
/fetch every thread where isResolved is false/,
|
|
@@ -179,12 +195,12 @@ test('the COPILOT fix branch does not re-assign head from the fix before re-conv
|
|
|
179
195
|
test('the CONVERGE branch re-resolves HEAD from GitHub on every entry', () => {
|
|
180
196
|
const convergeBranchStart = convergeSource.indexOf("if (phase === 'CONVERGE')");
|
|
181
197
|
assert.notEqual(convergeBranchStart, -1, 'expected the CONVERGE branch to exist');
|
|
182
|
-
const
|
|
183
|
-
assert.notEqual(
|
|
198
|
+
const headResolveCallIndex = convergeSource.indexOf("resumeGitAgent(gitAgentId, 'resolve-head')", convergeBranchStart);
|
|
199
|
+
assert.notEqual(headResolveCallIndex, -1, 'expected CONVERGE to re-resolve HEAD via resumeGitAgent');
|
|
184
200
|
});
|
|
185
201
|
|
|
186
202
|
test('fix edit prompt resolves threads by PRRT thread node id looked up from the comment databaseId', () => {
|
|
187
|
-
const editPrompt =
|
|
203
|
+
const editPrompt = functionSource('resumeCodeEditorAgent');
|
|
188
204
|
assert.match(editPrompt, /PRRT/, 'expected the thread node id form (PRRT_...) to be named');
|
|
189
205
|
assert.match(
|
|
190
206
|
editPrompt,
|
|
@@ -200,24 +216,16 @@ test('fix edit prompt resolves threads by PRRT thread node id looked up from the
|
|
|
200
216
|
|
|
201
217
|
test('fix edit prompt does not pass the numeric comment id straight to resolve_thread', () => {
|
|
202
218
|
assert.doesNotMatch(
|
|
203
|
-
|
|
219
|
+
functionSource('resumeCodeEditorAgent'),
|
|
204
220
|
/then resolve that thread \(use the github MCP pull_request_review_write/,
|
|
205
221
|
'resolve_thread and resolveReviewThread require a PRRT_... thread node id, not the comment id',
|
|
206
222
|
);
|
|
207
223
|
});
|
|
208
224
|
|
|
209
|
-
test('the fix flow spawns a
|
|
225
|
+
test('the fix flow spawns a fixer agent and runs fixerWithRecovery after the edit step', () => {
|
|
210
226
|
const applyFixesBody = lensPromptBody('applyFixes');
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const commitIndex = applyFixesBody.indexOf('commitVerifiedFixes(');
|
|
214
|
-
assert.notEqual(editIndex, -1, 'expected applyFixes to call the edit step');
|
|
215
|
-
assert.notEqual(verifyIndex, -1, 'expected applyFixes to call the verify step');
|
|
216
|
-
assert.notEqual(commitIndex, -1, 'expected applyFixes to call the commit step');
|
|
217
|
-
assert.ok(
|
|
218
|
-
editIndex < verifyIndex && verifyIndex < commitIndex,
|
|
219
|
-
'expected the order edit -> verify -> commit so the verifier verdict binds the fixed working tree',
|
|
220
|
-
);
|
|
227
|
+
assert.match(applyFixesBody, /spawnFixerAgent\(/, 'expected applyFixes to call spawnFixerAgent');
|
|
228
|
+
assert.match(applyFixesBody, /fixerWithRecovery\(/, 'expected applyFixes to call fixerWithRecovery');
|
|
221
229
|
});
|
|
222
230
|
|
|
223
231
|
test('the shared verdict-fence builder names the binding-hash command and the verdict fence', () => {
|
|
@@ -261,21 +269,13 @@ test('the verdict-fence binding does not self-resolve a cwd via git rev-parse fo
|
|
|
261
269
|
});
|
|
262
270
|
|
|
263
271
|
test('every verify step calls buildVerdictFenceSteps, uses code-verifier, and forbids edits', () => {
|
|
264
|
-
for (const verifyFunctionName of [
|
|
265
|
-
'verifyFixesInWorkingTree',
|
|
266
|
-
'verifyRepairChanges',
|
|
267
|
-
]) {
|
|
272
|
+
for (const verifyFunctionName of ['resumeVerifierAgent']) {
|
|
268
273
|
const verifyBody = lensPromptBody(verifyFunctionName);
|
|
269
274
|
assert.match(
|
|
270
275
|
verifyBody,
|
|
271
276
|
/buildVerdictFenceSteps\(/,
|
|
272
277
|
`expected ${verifyFunctionName} to call buildVerdictFenceSteps (cwd-immune branch binding)`,
|
|
273
278
|
);
|
|
274
|
-
assert.doesNotMatch(
|
|
275
|
-
verifyBody,
|
|
276
|
-
/VERDICT_FENCE_STEPS(?!\s*\))/,
|
|
277
|
-
`expected ${verifyFunctionName} not to reference the removed VERDICT_FENCE_STEPS constant`,
|
|
278
|
-
);
|
|
279
279
|
assert.match(
|
|
280
280
|
verifyBody,
|
|
281
281
|
/agentType:\s*'code-verifier'/,
|
|
@@ -294,125 +294,91 @@ test('every verify step calls buildVerdictFenceSteps, uses code-verifier, and fo
|
|
|
294
294
|
}
|
|
295
295
|
});
|
|
296
296
|
|
|
297
|
-
test('
|
|
298
|
-
const
|
|
297
|
+
test('resumeFixerAgent verify-commit path uses code-verifier and calls buildVerdictFenceSteps', () => {
|
|
298
|
+
const fixerBody = lensPromptBody('resumeFixerAgent');
|
|
299
|
+
assert.match(fixerBody, /buildVerdictFenceSteps\(/, 'expected resumeFixerAgent to call buildVerdictFenceSteps');
|
|
300
|
+
assert.match(fixerBody, /agentType:\s*'code-verifier'/, 'expected resumeFixerAgent to use code-verifier');
|
|
301
|
+
assert.match(fixerBody, /do no edits|make no edits|not edit|no file edits/i, 'expected resumeFixerAgent to forbid edits');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('resumeVerifierAgent uses --manifest-hash-for-branch with the hardening branch and forbids edits', () => {
|
|
305
|
+
const verifyBody = lensPromptBody('resumeVerifierAgent');
|
|
299
306
|
assert.match(
|
|
300
307
|
verifyBody,
|
|
301
308
|
/--manifest-hash-for-branch/,
|
|
302
|
-
'expected
|
|
303
|
-
);
|
|
304
|
-
assert.doesNotMatch(
|
|
305
|
-
verifyBody,
|
|
306
|
-
/--manifest-hash(?!-for-branch)/,
|
|
307
|
-
'expected verifyHardeningChanges not to use the old --manifest-hash <REPO> form',
|
|
308
|
-
);
|
|
309
|
-
assert.match(
|
|
310
|
-
verifyBody,
|
|
311
|
-
/agentType:\s*'code-verifier'/,
|
|
312
|
-
'expected verifyHardeningChanges to spawn the code-verifier agent type',
|
|
313
|
-
);
|
|
314
|
-
assert.doesNotMatch(
|
|
315
|
-
verifyBody,
|
|
316
|
-
/schema:/,
|
|
317
|
-
'expected verifyHardeningChanges to pass no schema so its verdict fence stays as assistant text',
|
|
309
|
+
'expected the verifier to bind by hardening branch (cwd-immune)',
|
|
318
310
|
);
|
|
319
311
|
assert.match(
|
|
320
312
|
verifyBody,
|
|
321
313
|
/do no edits|make no edits|not edit|no file edits/i,
|
|
322
|
-
'expected
|
|
314
|
+
'expected the verifier to be told to make no edits',
|
|
323
315
|
);
|
|
324
316
|
});
|
|
325
317
|
|
|
326
|
-
test('
|
|
327
|
-
for (const verifyFunctionName of ['
|
|
318
|
+
test('resumeVerifierAgent and resumeFixerAgent pass PR coordinates to buildVerdictFenceSteps', () => {
|
|
319
|
+
for (const verifyFunctionName of ['resumeFixerAgent', 'resumeVerifierAgent']) {
|
|
328
320
|
const verifyBody = lensPromptBody(verifyFunctionName);
|
|
329
321
|
assert.match(
|
|
330
322
|
verifyBody,
|
|
331
|
-
/buildVerdictFenceSteps\(
|
|
332
|
-
`expected ${verifyFunctionName} to
|
|
323
|
+
/buildVerdictFenceSteps\(/,
|
|
324
|
+
`expected ${verifyFunctionName} to call buildVerdictFenceSteps`,
|
|
333
325
|
);
|
|
334
326
|
}
|
|
335
327
|
});
|
|
336
328
|
|
|
337
|
-
test('the commit
|
|
338
|
-
const
|
|
329
|
+
test('the commit path in resumeFixerAgent forbids further edits and uses clean-coder', () => {
|
|
330
|
+
const fixerBody = lensPromptBody('resumeFixerAgent');
|
|
339
331
|
assert.match(
|
|
340
|
-
|
|
332
|
+
fixerBody,
|
|
341
333
|
/no (?:further |additional )?(?:file )?edits|do not edit|make no edits/i,
|
|
342
|
-
'expected the commit
|
|
334
|
+
'expected the commit path to forbid further edits',
|
|
343
335
|
);
|
|
344
336
|
assert.match(
|
|
345
|
-
|
|
337
|
+
fixerBody,
|
|
346
338
|
/agentType:\s*'clean-coder'/,
|
|
347
|
-
'expected the commit
|
|
339
|
+
'expected the commit path to use clean-coder',
|
|
348
340
|
);
|
|
349
341
|
});
|
|
350
342
|
|
|
351
|
-
test('the repair flow
|
|
343
|
+
test('the repair flow uses resume helpers for edit, verify, and commit', () => {
|
|
352
344
|
const repairBody = lensPromptBody('repairConvergence');
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
assert.notEqual(editIndex, -1, 'expected repairConvergence to call the edit step');
|
|
357
|
-
assert.notEqual(verifyIndex, -1, 'expected repairConvergence to call the verify step');
|
|
358
|
-
assert.notEqual(commitIndex, -1, 'expected repairConvergence to call the commit step');
|
|
359
|
-
assert.ok(
|
|
360
|
-
editIndex < verifyIndex && verifyIndex < commitIndex,
|
|
361
|
-
'expected edit -> verify -> commit so the verifier verdict binds the repaired working tree',
|
|
362
|
-
);
|
|
363
|
-
assert.match(
|
|
364
|
-
repairBody,
|
|
365
|
-
/verdictPassed\(/,
|
|
366
|
-
'expected the verify verdict to gate the repair commit step',
|
|
367
|
-
);
|
|
345
|
+
assert.match(repairBody, /spawnCodeEditorAgent\(/, 'expected repairConvergence to spawn a code-editor');
|
|
346
|
+
assert.match(repairBody, /spawnVerifierAgent\(/, 'expected repairConvergence to spawn a verifier');
|
|
347
|
+
assert.match(repairBody, /verdictPassed\(/, 'expected the verify verdict to gate the repair commit step');
|
|
368
348
|
});
|
|
369
349
|
|
|
370
|
-
test('the standards-deferral flow
|
|
350
|
+
test('the standards-deferral flow uses resume helpers for edit, verify, and commit', () => {
|
|
371
351
|
const standardsBody = lensPromptBody('spawnStandardsFollowUp');
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
352
|
+
assert.match(standardsBody, /spawnCodeEditorAgent\(/, 'expected spawnStandardsFollowUp to spawn a code-editor');
|
|
353
|
+
assert.match(standardsBody, /spawnVerifierAgent\(/, 'expected spawnStandardsFollowUp to spawn a verifier');
|
|
354
|
+
assert.match(standardsBody, /verdictPassed\(/, 'expected the verify verdict to gate the hardening commit step');
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test('repair-commit and hardening-commit paths use clean-coder and forbid edits', () => {
|
|
358
|
+
const codeEditorBody = lensPromptBody('resumeCodeEditorAgent');
|
|
359
|
+
assert.match(
|
|
360
|
+
codeEditorBody,
|
|
361
|
+
/no (?:further |additional )?(?:file )?edits|do not edit|make no edits/i,
|
|
362
|
+
'expected the commit paths to forbid further edits',
|
|
381
363
|
);
|
|
382
364
|
assert.match(
|
|
383
|
-
|
|
384
|
-
/
|
|
385
|
-
'expected the
|
|
365
|
+
codeEditorBody,
|
|
366
|
+
/agentType:\s*'clean-coder'/,
|
|
367
|
+
'expected the commit paths to use clean-coder',
|
|
386
368
|
);
|
|
387
369
|
});
|
|
388
370
|
|
|
389
|
-
test('the
|
|
390
|
-
|
|
391
|
-
const commitBody = lensPromptBody(commitFunctionName);
|
|
392
|
-
assert.match(
|
|
393
|
-
commitBody,
|
|
394
|
-
/no (?:further |additional )?(?:file )?edits|do not edit|make no edits/i,
|
|
395
|
-
`expected ${commitFunctionName} to forbid further edits so the verified surface stays bound`,
|
|
396
|
-
);
|
|
397
|
-
assert.match(
|
|
398
|
-
commitBody,
|
|
399
|
-
/agentType:\s*'clean-coder'/,
|
|
400
|
-
`expected ${commitFunctionName} to use clean-coder`,
|
|
401
|
-
);
|
|
402
|
-
}
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
test('the standards-deferral edit step stages the hardening change without committing', () => {
|
|
406
|
-
const editBody = lensPromptBody('standardsFollowUpEdit');
|
|
371
|
+
test('the code-editor standards-edit path stages hardening without committing and uses clean-coder', () => {
|
|
372
|
+
const editBody = lensPromptBody('resumeCodeEditorAgent');
|
|
407
373
|
assert.match(
|
|
408
374
|
editBody,
|
|
409
375
|
/do not commit and do not push|NO commit and NO push|Do NOT commit/i,
|
|
410
|
-
'expected the standards edit
|
|
376
|
+
'expected the standards edit path to leave the hardening change uncommitted',
|
|
411
377
|
);
|
|
412
378
|
assert.match(
|
|
413
379
|
editBody,
|
|
414
380
|
/agentType:\s*'clean-coder'/,
|
|
415
|
-
'expected the
|
|
381
|
+
'expected the edit paths to use clean-coder',
|
|
416
382
|
);
|
|
417
383
|
});
|
|
418
384
|
|
|
@@ -530,20 +496,336 @@ test('the pre-commit gate step is a shared constant that dry-runs the CODE_RULES
|
|
|
530
496
|
);
|
|
531
497
|
});
|
|
532
498
|
|
|
533
|
-
const
|
|
499
|
+
const editStepResumeHelpers = ['resumeCodeEditorAgent', 'resumeFixerAgent'];
|
|
500
|
+
|
|
501
|
+
for (const helperName of editStepResumeHelpers) {
|
|
502
|
+
test(`${helperName} appends the pre-commit gate step to its edit prompts`, () => {
|
|
503
|
+
assert.match(
|
|
504
|
+
functionSource(helperName),
|
|
505
|
+
/\+\s*PRE_COMMIT_GATE_STEP/,
|
|
506
|
+
`expected ${helperName} to append PRE_COMMIT_GATE_STEP to its edit-task prompts`,
|
|
507
|
+
);
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const editStepResumeTasks = [
|
|
512
|
+
['resumeCodeEditorAgent', 'fix-edit'],
|
|
513
|
+
['resumeCodeEditorAgent', 'repair-edit'],
|
|
514
|
+
['resumeCodeEditorAgent', 'standards-edit'],
|
|
515
|
+
['resumeCodeEditorAgent', 'commit-recover'],
|
|
516
|
+
];
|
|
517
|
+
|
|
518
|
+
for (const [helperName, taskName] of editStepResumeTasks) {
|
|
519
|
+
test(`${helperName} routes the ${taskName} task to a pre-commit-gated edit prompt`, () => {
|
|
520
|
+
assert.match(
|
|
521
|
+
functionSource(helperName),
|
|
522
|
+
new RegExp(`task === '${taskName}'`),
|
|
523
|
+
`expected ${helperName} to handle the ${taskName} task`,
|
|
524
|
+
);
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function preambleText() {
|
|
529
|
+
const preambleStart = convergeSource.indexOf('const HEADLESS_SAFETY_PREAMBLE =');
|
|
530
|
+
assert.notEqual(preambleStart, -1, 'expected HEADLESS_SAFETY_PREAMBLE to exist');
|
|
531
|
+
const preambleEnd = convergeSource.indexOf('\n\nlet ', preambleStart);
|
|
532
|
+
return convergeSource.slice(preambleStart, preambleEnd === -1 ? undefined : preambleEnd);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
test('preamble prescribes authoring a Python helper for variable-built or multi-step sandboxes', () => {
|
|
536
|
+
assert.match(
|
|
537
|
+
preambleText(),
|
|
538
|
+
/python\s+<file>\.py|python\s+<.*>\.py|author.*python.*helper|python.*helper.*sandbox|sandbox.*python.*helper/i,
|
|
539
|
+
'expected the preamble to prescribe running a Python helper file for multi-step sandbox teardown',
|
|
540
|
+
);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
test('preamble does not claim the standalone or both rm auto-allow paths fail closed on any $', () => {
|
|
544
|
+
const text = preambleText().replace(/\$\(\.\.\.\)/g, 'SUBSHELL').replace(/\s+/g, ' ');
|
|
545
|
+
const overstatesStandalone =
|
|
546
|
+
/\b(?:both|standalone|neither)\b[^.]*fail closed[^.]*any \$/i.test(text) ||
|
|
547
|
+
/\b(?:both|standalone|neither)\b[^.]*any \$[^.]*fail closed/i.test(text);
|
|
548
|
+
assert.equal(
|
|
549
|
+
overstatesStandalone,
|
|
550
|
+
false,
|
|
551
|
+
'only the compound path fails closed on any $ in the target; the standalone path accepts a $-bearing target whose literal path already sits under an ephemeral root',
|
|
552
|
+
);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
test('preamble does not claim $CLAUDE_JOB_DIR/tmp is blocked', () => {
|
|
556
|
+
assert.doesNotMatch(
|
|
557
|
+
preambleText(),
|
|
558
|
+
/CLAUDE_JOB_DIR\/tmp is NOT auto-allowed/i,
|
|
559
|
+
'under an ephemeral cwd the hook auto-allows rm targeting $CLAUDE_JOB_DIR/tmp',
|
|
560
|
+
);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
test('preamble scopes its rm-shape claim to the narrowest auto-allow path, not the full set', () => {
|
|
564
|
+
assert.doesNotMatch(
|
|
565
|
+
preambleText(),
|
|
566
|
+
/auto-allows rm only when ALL of these hold/i,
|
|
567
|
+
'the hook has three rm auto-allow paths, so the preamble must not assert one narrow shape is the complete set',
|
|
568
|
+
);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
test('SKILL.md does not claim the standalone or both rm auto-allow paths fail closed on any $', () => {
|
|
572
|
+
const text = skillSource.replace(/`/g, '').replace(/\$\(\.\.\.\)/g, 'SUBSHELL').replace(/\s+/g, ' ');
|
|
573
|
+
const overstatesStandalone =
|
|
574
|
+
/\b(?:both|standalone|neither)\b[^.]*fail closed[^.]*any \$/i.test(text) ||
|
|
575
|
+
/\b(?:both|standalone|neither)\b[^.]*any \$[^.]*fail closed/i.test(text);
|
|
576
|
+
assert.equal(
|
|
577
|
+
overstatesStandalone,
|
|
578
|
+
false,
|
|
579
|
+
'only the compound path fails closed on any $ in the target; the standalone path accepts a $-bearing target whose literal path already sits under an ephemeral root',
|
|
580
|
+
);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
test('SKILL.md does not claim it enforces the exact rm shape the hook auto-allows', () => {
|
|
584
|
+
assert.doesNotMatch(
|
|
585
|
+
skillSource,
|
|
586
|
+
/exact rm shape the hook auto-allows/i,
|
|
587
|
+
'the hook has multiple rm auto-allow paths, so SKILL.md must not assert one narrow shape is the exact set',
|
|
588
|
+
);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
test('preamble does not attribute the known-temp-var resolution to the standalone or compound paths', () => {
|
|
592
|
+
assert.doesNotMatch(
|
|
593
|
+
preambleText().replace(/\s+/g, ' '),
|
|
594
|
+
/Across these paths[\s\S]*?CLAUDE_JOB_DIR/i,
|
|
595
|
+
'the temp-var resolution lives only in the broad cwd-scoped path; the standalone and compound paths do not resolve known temp variables',
|
|
596
|
+
);
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
test('preamble attributes the known-temp-var resolution to a third cwd-scoped auto-allow path', () => {
|
|
600
|
+
const text = preambleText().replace(/\s+/g, ' ');
|
|
601
|
+
const tempVarSentenceMatch =
|
|
602
|
+
/[^.]*\bTMPDIR\b[^.]*CLAUDE_JOB_DIR[^.]*\./i.exec(text);
|
|
603
|
+
assert.notEqual(
|
|
604
|
+
tempVarSentenceMatch,
|
|
605
|
+
null,
|
|
606
|
+
'expected a sentence describing the TEMP/TMP/TMPDIR/CLAUDE_JOB_DIR resolution',
|
|
607
|
+
);
|
|
608
|
+
assert.match(
|
|
609
|
+
tempVarSentenceMatch[0],
|
|
610
|
+
/declares? an ephemeral cwd|declared ephemeral cwd|ephemeral-cwd path|third (?:auto-allow )?path|cwd-scoped path/i,
|
|
611
|
+
'expected the temp-var resolution to be tied to the cwd-scoped path that declares an ephemeral working directory, not the standalone or compound paths',
|
|
612
|
+
);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
test('SKILL.md does not attribute the known-temp-var resolution to the standalone or compound paths', () => {
|
|
616
|
+
assert.doesNotMatch(
|
|
617
|
+
skillSource.replace(/\s+/g, ' '),
|
|
618
|
+
/Across those paths[\s\S]*?CLAUDE_JOB_DIR/i,
|
|
619
|
+
'the temp-var resolution lives only in the broad cwd-scoped path; the standalone and compound paths do not resolve known temp variables',
|
|
620
|
+
);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
test('SKILL.md attributes the known-temp-var resolution to the cwd-scoped auto-allow path', () => {
|
|
624
|
+
const tempVarSentenceMatch =
|
|
625
|
+
/[^.]*\bTMPDIR\b[^.]*CLAUDE_JOB_DIR[^.]*\./i.exec(skillSource.replace(/\s+/g, ' '));
|
|
626
|
+
assert.notEqual(
|
|
627
|
+
tempVarSentenceMatch,
|
|
628
|
+
null,
|
|
629
|
+
'expected a sentence describing the TEMP/TMP/TMPDIR/CLAUDE_JOB_DIR resolution',
|
|
630
|
+
);
|
|
631
|
+
assert.match(
|
|
632
|
+
tempVarSentenceMatch[0],
|
|
633
|
+
/declares? an ephemeral cwd|declared ephemeral cwd|ephemeral-cwd path|third (?:auto-allow )?path|cwd-scoped path/i,
|
|
634
|
+
'expected the temp-var resolution to be tied to the cwd-scoped path that declares an ephemeral working directory, not the standalone or compound paths',
|
|
635
|
+
);
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
test('convergeAgent prepends HEADLESS_SAFETY_PREAMBLE and worktree directive on fresh spawn', () => {
|
|
639
|
+
const convergeAgentBody = lensPromptBody('convergeAgent');
|
|
640
|
+
assert.match(
|
|
641
|
+
convergeAgentBody,
|
|
642
|
+
/HEADLESS_SAFETY_PREAMBLE.*worktreeDirective/,
|
|
643
|
+
'expected fresh-spawn path to prepend both preamble and worktree directive',
|
|
644
|
+
);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
test('convergeAgent checks isResume before prepending the preamble', () => {
|
|
648
|
+
const convergeAgentBody = lensPromptBody('convergeAgent');
|
|
649
|
+
assert.match(convergeAgentBody, /isResume/, 'expected an isResume guard in convergeAgent');
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
const newSpawnResumeHelpers = [
|
|
653
|
+
{ name: 'spawnGitAgent', isAsync: true },
|
|
654
|
+
{ name: 'resumeGitAgent', isAsync: false },
|
|
655
|
+
{ name: 'spawnFixerAgent', isAsync: true },
|
|
656
|
+
{ name: 'resumeFixerAgent', isAsync: false },
|
|
657
|
+
{ name: 'fixerWithRecovery', isAsync: true },
|
|
658
|
+
{ name: 'spawnCodeEditorAgent', isAsync: true },
|
|
659
|
+
{ name: 'resumeCodeEditorAgent', isAsync: false },
|
|
660
|
+
{ name: 'spawnVerifierAgent', isAsync: true },
|
|
661
|
+
{ name: 'resumeVerifierAgent', isAsync: false },
|
|
662
|
+
{ name: 'spawnGeneralUtilityAgent', isAsync: true },
|
|
663
|
+
{ name: 'resumeGeneralUtilityAgent', isAsync: false },
|
|
664
|
+
{ name: 'spawnConvergenceCheckAgent', isAsync: true },
|
|
665
|
+
{ name: 'resumeConvergenceCheckAgent', isAsync: false },
|
|
666
|
+
{ name: 'extractVerdict', isAsync: false },
|
|
667
|
+
];
|
|
668
|
+
|
|
669
|
+
for (const { name, isAsync } of newSpawnResumeHelpers) {
|
|
670
|
+
const prefix = isAsync ? 'async ' : '';
|
|
671
|
+
test(`function ${prefix}${name} exists in converge.mjs`, () => {
|
|
672
|
+
const needle = isAsync ? `async function ${name}(` : `function ${name}(`;
|
|
673
|
+
assert.ok(convergeSource.includes(needle), `expected ${name} to exist`);
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const spawnHelperNames = [
|
|
678
|
+
'spawnGitAgent',
|
|
679
|
+
'spawnFixerAgent',
|
|
680
|
+
'spawnCodeEditorAgent',
|
|
681
|
+
'spawnVerifierAgent',
|
|
682
|
+
'spawnGeneralUtilityAgent',
|
|
683
|
+
'spawnConvergenceCheckAgent',
|
|
684
|
+
];
|
|
685
|
+
|
|
686
|
+
for (const spawnName of spawnHelperNames) {
|
|
687
|
+
test(`${spawnName} actually spawns an agent via convergeAgent`, () => {
|
|
688
|
+
const spawnBody = functionSource(spawnName);
|
|
689
|
+
assert.match(
|
|
690
|
+
spawnBody,
|
|
691
|
+
/await convergeAgent\(/,
|
|
692
|
+
`expected ${spawnName} to spawn a real agent through convergeAgent rather than return a hardcoded label`,
|
|
693
|
+
);
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
test(`${spawnName} returns the spawned agent's runtime id`, () => {
|
|
697
|
+
const spawnBody = functionSource(spawnName);
|
|
698
|
+
assert.match(
|
|
699
|
+
spawnBody,
|
|
700
|
+
/return\s+result\?\.agentId/,
|
|
701
|
+
`expected ${spawnName} to return result?.agentId so resume targets the real session`,
|
|
702
|
+
);
|
|
703
|
+
assert.doesNotMatch(
|
|
704
|
+
spawnBody,
|
|
705
|
+
/return\s+['"`]/,
|
|
706
|
+
`expected ${spawnName} not to return a hardcoded label string`,
|
|
707
|
+
);
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
test('spawnGitAgent returns result?.agentId without a tautological ternary', () => {
|
|
712
|
+
const spawnBody = functionSource('spawnGitAgent');
|
|
713
|
+
assert.doesNotMatch(
|
|
714
|
+
spawnBody,
|
|
715
|
+
/\?\s*'git-utility'\s*:\s*'git-utility'/,
|
|
716
|
+
'expected the identical-branch ternary that discards the spawn outcome to be gone',
|
|
717
|
+
);
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
test('resume helpers fall back to a fresh spawn when no agentId is available', () => {
|
|
721
|
+
const convergeAgentBody = lensPromptBody('convergeAgent');
|
|
722
|
+
assert.match(
|
|
723
|
+
convergeAgentBody,
|
|
724
|
+
/options\?\.resume.*length\s*>\s*0|length\s*>\s*0.*resume/s,
|
|
725
|
+
'expected convergeAgent to treat a missing agentId as a fresh spawn (isResume false), restoring the preamble and worktree directive',
|
|
726
|
+
);
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
test('resumeGeneralUtilityAgent only handles the two tasks it is called with', () => {
|
|
730
|
+
const generalBody = functionSource('resumeGeneralUtilityAgent');
|
|
731
|
+
assert.doesNotMatch(
|
|
732
|
+
generalBody,
|
|
733
|
+
/task === 'bugbot-lens'/,
|
|
734
|
+
'the live Bugbot lens is runBugbotLens; the dead bugbot-lens branch must be removed',
|
|
735
|
+
);
|
|
736
|
+
assert.doesNotMatch(
|
|
737
|
+
generalBody,
|
|
738
|
+
/Copilot can run out of usage/,
|
|
739
|
+
'the live Copilot gate is runCopilotGate; the dead copilot-gate branch must be removed',
|
|
740
|
+
);
|
|
741
|
+
assert.doesNotMatch(
|
|
742
|
+
generalBody,
|
|
743
|
+
/convergence summary/,
|
|
744
|
+
'the convergence-summary producer was removed; the dead branch must not return',
|
|
745
|
+
);
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
const orphanedHelperNames = [
|
|
534
749
|
'applyFixesEdit',
|
|
535
750
|
'recoverCommitBlockEdit',
|
|
536
751
|
'recoverVerifyFailEdit',
|
|
752
|
+
'checkConvergence',
|
|
753
|
+
'markReady',
|
|
537
754
|
'repairConvergenceEdit',
|
|
755
|
+
'verifyRepairChanges',
|
|
756
|
+
'commitRepairFixes',
|
|
757
|
+
'resolveConflictsEdit',
|
|
538
758
|
'standardsFollowUpEdit',
|
|
759
|
+
'verifyHardeningChanges',
|
|
760
|
+
'commitHardeningPr',
|
|
761
|
+
'postCleanAudit',
|
|
762
|
+
'spawnConvergenceSummary',
|
|
539
763
|
];
|
|
540
764
|
|
|
541
|
-
for (const
|
|
542
|
-
test(`${
|
|
543
|
-
assert.
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
`expected ${builderName} to append PRE_COMMIT_GATE_STEP`,
|
|
765
|
+
for (const orphanName of orphanedHelperNames) {
|
|
766
|
+
test(`${orphanName} is removed — its behavior lives in a resume helper`, () => {
|
|
767
|
+
assert.ok(
|
|
768
|
+
!convergeSource.includes(`function ${orphanName}(`),
|
|
769
|
+
`expected the orphaned ${orphanName} definition to be deleted (CODE_RULES 9.8)`,
|
|
547
770
|
);
|
|
548
771
|
});
|
|
549
772
|
}
|
|
773
|
+
|
|
774
|
+
test('convergeAgent omits the preamble when options.resume is a non-empty string', () => {
|
|
775
|
+
const convergeAgentBody = lensPromptBody('convergeAgent');
|
|
776
|
+
assert.match(
|
|
777
|
+
convergeAgentBody,
|
|
778
|
+
/isResume\s*\?\s*prompt/,
|
|
779
|
+
'expected the resume path to pass prompt through without the preamble',
|
|
780
|
+
);
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
test('parseLastVerdictFence returns non-null for a verdict fence with valid JSON', () => {
|
|
784
|
+
const parseModule = new Function(
|
|
785
|
+
`${functionSource('parseLastVerdictFence')}\n` +
|
|
786
|
+
'return { parseLastVerdictFence };',
|
|
787
|
+
)();
|
|
788
|
+
const result = parseModule.parseLastVerdictFence('```verdict\n{"all_pass":true,"findings":[],"manifest_sha256":"abc"}\n```');
|
|
789
|
+
assert.notEqual(result, null);
|
|
790
|
+
assert.equal(result.all_pass, true);
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
test('parseLastVerdictFence returns null for non-string input', () => {
|
|
794
|
+
const parseModule = new Function(
|
|
795
|
+
`${functionSource('parseLastVerdictFence')}\n` +
|
|
796
|
+
'return { parseLastVerdictFence };',
|
|
797
|
+
)();
|
|
798
|
+
assert.equal(parseModule.parseLastVerdictFence(null), null);
|
|
799
|
+
assert.equal(parseModule.parseLastVerdictFence(undefined), null);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
test('parseLastVerdictFence returns null when no verdict fence is present', () => {
|
|
803
|
+
const parseModule = new Function(
|
|
804
|
+
`${functionSource('parseLastVerdictFence')}\n` +
|
|
805
|
+
'return { parseLastVerdictFence };',
|
|
806
|
+
)();
|
|
807
|
+
assert.equal(parseModule.parseLastVerdictFence('plain text with no fence'), null);
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
test('parseLastVerdictFence returns null for malformed JSON in the fence', () => {
|
|
811
|
+
const parseModule = new Function(
|
|
812
|
+
`${functionSource('parseLastVerdictFence')}\n` +
|
|
813
|
+
'return { parseLastVerdictFence };',
|
|
814
|
+
)();
|
|
815
|
+
assert.equal(parseModule.parseLastVerdictFence('```verdict\nnot json\n```'), null);
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
test('verdictPassed calls parseLastVerdictFence', () => {
|
|
819
|
+
const verdictBody = lensPromptBody('verdictPassed');
|
|
820
|
+
assert.match(verdictBody, /parseLastVerdictFence\(/, 'expected verdictPassed to call the shared parser');
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
test('extractVerifyObjection calls parseLastVerdictFence', () => {
|
|
824
|
+
const objectionBody = lensPromptBody('extractVerifyObjection');
|
|
825
|
+
assert.match(objectionBody, /parseLastVerdictFence\(/, 'expected extractVerifyObjection to call the shared parser');
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
test('extractVerdict calls parseLastVerdictFence', () => {
|
|
829
|
+
const verdictBody = lensPromptBody('extractVerdict');
|
|
830
|
+
assert.match(verdictBody, /parseLastVerdictFence\(/, 'expected extractVerdict to call the shared parser');
|
|
831
|
+
});
|