claude-dev-env 1.71.0 → 1.72.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 +8 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +5 -3
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +39 -0
- package/agents/clean-coder.md +1 -0
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/code_rules_docstrings.py +60 -0
- package/hooks/blocking/code_rules_enforcer.py +4 -0
- package/hooks/blocking/code_rules_test_assertions.py +152 -1
- package/hooks/blocking/code_rules_type_escape.py +447 -2
- package/hooks/blocking/test_code_rules_enforcer_docstring_no_consumer.py +93 -0
- package/hooks/blocking/test_code_rules_enforcer_object_parameter.py +499 -0
- package/hooks/blocking/test_code_rules_enforcer_stale_test_name.py +103 -0
- package/hooks/hooks_constants/blocking_check_limits.py +14 -0
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +1 -1
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +1 -1
- package/scripts/CLAUDE.md +1 -0
- package/scripts/Show-Asset.ps1 +106 -0
- package/skills/autoconverge/SKILL.md +30 -3
- package/skills/autoconverge/reference/convergence.md +41 -1
- package/skills/autoconverge/workflow/converge.contract.test.mjs +90 -0
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +98 -0
- package/skills/autoconverge/workflow/converge.mjs +176 -6
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +47 -3
- package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +34 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Opens files on screen, sizing each image window to the image's own dimensions.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
For every path given, an image opens in a window whose client area matches the
|
|
7
|
+
image's pixel size, scaled down to fit the primary screen's working area when the
|
|
8
|
+
image is larger than the screen. A small image gets a usable minimum window with
|
|
9
|
+
the picture centered at native size. Non-image files open in their registered
|
|
10
|
+
default application, and any file that cannot be loaded as an image falls back to
|
|
11
|
+
that default application too. Escape or the close button dismisses a window; the
|
|
12
|
+
process exits once every window is closed.
|
|
13
|
+
|
|
14
|
+
.PARAMETER Paths
|
|
15
|
+
One or more file paths to open.
|
|
16
|
+
#>
|
|
17
|
+
param(
|
|
18
|
+
[Parameter(ValueFromRemainingArguments = $true)]
|
|
19
|
+
[string[]]$Paths
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
23
|
+
Add-Type -AssemblyName System.Drawing
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
[System.Windows.Forms.Application]::SetHighDpiMode([System.Windows.Forms.HighDpiMode]::PerMonitorV2) | Out-Null
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
$null = $_
|
|
30
|
+
}
|
|
31
|
+
[System.Windows.Forms.Application]::EnableVisualStyles()
|
|
32
|
+
|
|
33
|
+
$imageExtensions = @('.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.tif', '.tiff', '.ico')
|
|
34
|
+
$screenMargin = 80
|
|
35
|
+
$minimumClientWidth = 220
|
|
36
|
+
$minimumClientHeight = 160
|
|
37
|
+
$openWindowCount = 0
|
|
38
|
+
|
|
39
|
+
foreach ($path in $Paths) {
|
|
40
|
+
if (-not (Test-Path -LiteralPath $path)) { continue }
|
|
41
|
+
$fullPath = (Resolve-Path -LiteralPath $path).Path
|
|
42
|
+
$extension = [System.IO.Path]::GetExtension($fullPath).ToLowerInvariant()
|
|
43
|
+
|
|
44
|
+
if ($imageExtensions -notcontains $extension) {
|
|
45
|
+
Invoke-Item -LiteralPath $fullPath
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
$imageBytes = [System.IO.File]::ReadAllBytes($fullPath)
|
|
51
|
+
$imageStream = New-Object System.IO.MemoryStream(, $imageBytes)
|
|
52
|
+
$loadedImage = [System.Drawing.Image]::FromStream($imageStream)
|
|
53
|
+
$image = New-Object System.Drawing.Bitmap($loadedImage)
|
|
54
|
+
$loadedImage.Dispose()
|
|
55
|
+
$imageStream.Dispose()
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
Invoke-Item -LiteralPath $fullPath
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
$workingArea = [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea
|
|
63
|
+
$maximumWidth = $workingArea.Width - $screenMargin
|
|
64
|
+
$maximumHeight = $workingArea.Height - $screenMargin
|
|
65
|
+
$scale = [Math]::Min(1.0, [Math]::Min($maximumWidth / $image.Width, $maximumHeight / $image.Height))
|
|
66
|
+
|
|
67
|
+
$pictureBox = New-Object System.Windows.Forms.PictureBox
|
|
68
|
+
$pictureBox.Dock = [System.Windows.Forms.DockStyle]::Fill
|
|
69
|
+
$pictureBox.Image = $image
|
|
70
|
+
|
|
71
|
+
if ($scale -lt 1.0) {
|
|
72
|
+
$pictureBox.SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::Zoom
|
|
73
|
+
$clientWidth = [int][Math]::Round($image.Width * $scale)
|
|
74
|
+
$clientHeight = [int][Math]::Round($image.Height * $scale)
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
$pictureBox.SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::CenterImage
|
|
78
|
+
$clientWidth = [Math]::Max($minimumClientWidth, $image.Width)
|
|
79
|
+
$clientHeight = [Math]::Max($minimumClientHeight, $image.Height)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
$form = New-Object System.Windows.Forms.Form
|
|
83
|
+
$form.Text = [System.IO.Path]::GetFileName($fullPath)
|
|
84
|
+
$form.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::None
|
|
85
|
+
$form.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen
|
|
86
|
+
$form.ClientSize = New-Object System.Drawing.Size($clientWidth, $clientHeight)
|
|
87
|
+
$form.KeyPreview = $true
|
|
88
|
+
$form.BackColor = [System.Drawing.Color]::FromArgb(24, 24, 24)
|
|
89
|
+
$form.Controls.Add($pictureBox)
|
|
90
|
+
|
|
91
|
+
$form.Add_KeyDown({
|
|
92
|
+
param($sender, $eventArguments)
|
|
93
|
+
if ($eventArguments.KeyCode -eq [System.Windows.Forms.Keys]::Escape) { $sender.Close() }
|
|
94
|
+
})
|
|
95
|
+
$form.Add_FormClosed({
|
|
96
|
+
$script:openWindowCount--
|
|
97
|
+
if ($script:openWindowCount -le 0) { [System.Windows.Forms.Application]::Exit() }
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
$openWindowCount++
|
|
101
|
+
$form.Show()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if ($openWindowCount -gt 0) {
|
|
105
|
+
[System.Windows.Forms.Application]::Run()
|
|
106
|
+
}
|
|
@@ -101,7 +101,7 @@ own. The workflow runs in the background and notifies this session on
|
|
|
101
101
|
completion. Watch live progress with `/workflows`.
|
|
102
102
|
|
|
103
103
|
The workflow returns
|
|
104
|
-
`{ converged, rounds, finalSha, blocker, standardsNote, copilotNote }`.
|
|
104
|
+
`{ converged, rounds, finalSha, blocker, standardsNote, copilotNote, reuseNote }`.
|
|
105
105
|
|
|
106
106
|
## Budget-aware round boundaries
|
|
107
107
|
|
|
@@ -207,8 +207,31 @@ round records nothing resumable and replays dirty.
|
|
|
207
207
|
Blocker: <blocker> # only when blocked
|
|
208
208
|
Standards: <standardsNote> # only when a round deferred code-standard findings
|
|
209
209
|
Copilot: <copilotNote> # only when Copilot was down or out of quota
|
|
210
|
+
Reuse: <reuseNote> # only when the reuse pass identified an improvement
|
|
210
211
|
```
|
|
211
212
|
|
|
213
|
+
## Reuse pass (before convergence)
|
|
214
|
+
|
|
215
|
+
Before the first round, one reuse lens (`code-quality-agent`) scans the full
|
|
216
|
+
`origin/main...HEAD` diff for places the PR re-implements behavior the codebase
|
|
217
|
+
already provides. It reports a reuse improvement only when all three criteria
|
|
218
|
+
hold, and drops any case where even one is in doubt:
|
|
219
|
+
|
|
220
|
+
- **Certain** — an existing symbol or module unquestionably covers the new
|
|
221
|
+
code's behavior, cited at `file:line`.
|
|
222
|
+
- **Behaviorally identical** — swapping the new code for the existing one
|
|
223
|
+
changes no observable behavior: same inputs, outputs, side effects, and error
|
|
224
|
+
handling.
|
|
225
|
+
- **Autonomously implementable** — the replacement is a mechanical edit (import
|
|
226
|
+
and call the existing symbol, delete the duplicate) needing no product
|
|
227
|
+
decision and no human judgment.
|
|
228
|
+
|
|
229
|
+
The reuse lens reports without editing. Qualifying improvements then run through
|
|
230
|
+
the same edit → verify → commit fix flow the rounds use, so they land in one
|
|
231
|
+
verified commit before convergence starts. The pass is best-effort: when no case
|
|
232
|
+
clears all three criteria, the run proceeds straight to convergence, and
|
|
233
|
+
`reuseNote` records what landed.
|
|
234
|
+
|
|
212
235
|
## What the workflow does each round
|
|
213
236
|
|
|
214
237
|
See [`reference/convergence.md`](reference/convergence.md) for the full round
|
|
@@ -227,8 +250,12 @@ suite (`python -m pytest`) and keep scratch work in ephemeral temp dirs.
|
|
|
227
250
|
- **Converge:** `parallel([Bugbot lens, code-review lens, bug-audit lens])` on
|
|
228
251
|
the current HEAD, full `origin/main...HEAD` diff. Dedup findings; one
|
|
229
252
|
`clean-coder` applies all fixes in a single commit, pushes, replies to and
|
|
230
|
-
resolves any bot threads; re-verify next round on the new HEAD.
|
|
231
|
-
|
|
253
|
+
resolves any bot threads; re-verify next round on the new HEAD. Every edit
|
|
254
|
+
step ends with a pre-commit gate check: before its turn ends, the fixer
|
|
255
|
+
dry-runs the CODE_RULES commit gate (`code_rules_gate.py --staged`) and keeps
|
|
256
|
+
fixing until that gate would accept the commit — it makes no commit itself.
|
|
257
|
+
When all three are clean on a stable HEAD, post the CLEAN bugteam audit
|
|
258
|
+
artifact.
|
|
232
259
|
A round whose findings are ALL code-standard violations (pure CODE_RULES/style,
|
|
233
260
|
no behavioral impact) passes for convergence purposes: the workflow files a
|
|
234
261
|
follow-up issue listing the findings, opens a draft environment-hardening PR
|
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Convergence — round shape and the ready definition
|
|
2
2
|
|
|
3
|
+
## Pre-flight: clear merge conflicts
|
|
4
|
+
|
|
5
|
+
Before the first round, the workflow checks once whether the PR branch conflicts
|
|
6
|
+
with `origin/main`. When GitHub reports a conflict (`mergeable` false or
|
|
7
|
+
`mergeable_state` dirty), one `clean-coder` rebases the branch onto `origin/main`
|
|
8
|
+
and resolves every conflict — gated the same way as every other code change: the
|
|
9
|
+
edit leaves the rebase in the working tree, a `code-verifier` binds a verdict to
|
|
10
|
+
it, and the commit step force-pushes with lease. The bug checks then run on a
|
|
11
|
+
conflict-free diff.
|
|
12
|
+
|
|
13
|
+
A PR that merges cleanly skips the rebase. A conflict that surfaces mid-run, when
|
|
14
|
+
`origin/main` advances during a later round, is caught by the convergence repair
|
|
15
|
+
at the end of the loop, which also rebases.
|
|
16
|
+
|
|
17
|
+
## Reuse pass (runs after the conflict pre-flight, before convergence)
|
|
18
|
+
|
|
19
|
+
One reuse lens (`code-quality-agent`) reviews the full `origin/main...HEAD` diff
|
|
20
|
+
for code that re-implements behavior the repository already provides. It reports a
|
|
21
|
+
reuse improvement only when all three criteria hold together, and omits any case
|
|
22
|
+
where even one is in doubt:
|
|
23
|
+
|
|
24
|
+
1. **Certain** — an existing symbol or module unquestionably covers the new
|
|
25
|
+
code's behavior, cited at `file:line`.
|
|
26
|
+
2. **Behaviorally the same** — swapping the new code for the existing one
|
|
27
|
+
changes no observable behavior: same inputs, outputs, side effects, and
|
|
28
|
+
error handling.
|
|
29
|
+
3. **Autonomously implementable** — the replacement is a mechanical edit (import
|
|
30
|
+
and call the existing symbol, drop the duplicate) needing no product
|
|
31
|
+
decision and no human judgment.
|
|
32
|
+
|
|
33
|
+
The lens reports without editing. Each qualifying improvement runs through the
|
|
34
|
+
same edit → verify → commit fix flow the rounds use, landing in one verified
|
|
35
|
+
commit before convergence begins. The pass is best-effort: when no case clears
|
|
36
|
+
all three criteria the run proceeds straight to convergence. Whatever the reuse
|
|
37
|
+
pass surfaces also joins the round findings, so the code-review lens re-checks
|
|
38
|
+
any improvement that did not land.
|
|
39
|
+
|
|
3
40
|
## The round loop
|
|
4
41
|
|
|
5
42
|
The workflow holds three states and moves between them until the PR is ready or
|
|
@@ -26,7 +63,10 @@ tracks CONVERGE passes only and is never the cap.
|
|
|
26
63
|
colliding threads.
|
|
27
64
|
4. **Any findings** → one `clean-coder` applies every fix in a single test-first
|
|
28
65
|
commit, pushes, then replies to and resolves each finding that carries a
|
|
29
|
-
GitHub review thread.
|
|
66
|
+
GitHub review thread. Before its turn ends, the edit step dry-runs the
|
|
67
|
+
CODE_RULES commit gate (`code_rules_gate.py --staged`) over its staged
|
|
68
|
+
changes and keeps fixing until that gate would accept the commit, so the
|
|
69
|
+
later commit step never hits a gate rejection. A round progresses when the fix lens lands a push that
|
|
30
70
|
moves HEAD, or when every finding was already addressed so no code change is
|
|
31
71
|
needed yet each finding thread is still resolved (the fix lens reports
|
|
32
72
|
`resolvedWithoutCommit` and the run re-converges on the unchanged HEAD). A
|
|
@@ -457,3 +457,93 @@ test('both standards-deferral call sites build standardsNote from the spawnStand
|
|
|
457
457
|
'expected no unconditional hardening-PR claim in standardsNote',
|
|
458
458
|
);
|
|
459
459
|
});
|
|
460
|
+
|
|
461
|
+
test('a reuse-audit lens builder exists', () => {
|
|
462
|
+
assert.match(convergeSource, /function runReuseAuditPass\(/);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test('the reuse pass runs once before the convergence loop', () => {
|
|
466
|
+
const reuseCallIndex = convergeSource.indexOf('await runReuseAuditPass(');
|
|
467
|
+
const loopIndex = convergeSource.indexOf('while (iterations < CONFIG.maxIterations)');
|
|
468
|
+
assert.notEqual(reuseCallIndex, -1, 'expected the reuse pass to be invoked');
|
|
469
|
+
assert.notEqual(loopIndex, -1, 'expected the convergence loop to exist');
|
|
470
|
+
assert.ok(
|
|
471
|
+
reuseCallIndex < loopIndex,
|
|
472
|
+
'expected the reuse pass to run before the convergence loop starts',
|
|
473
|
+
);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
test('the reuse lens prompt enumerates all three qualifying criteria and an omit rule', () => {
|
|
477
|
+
const reusePrompt = lensPromptBody('runReuseAuditPass');
|
|
478
|
+
assert.match(reusePrompt, /CERTAIN/);
|
|
479
|
+
assert.match(reusePrompt, /BEHAVIORALLY IDENTICAL/);
|
|
480
|
+
assert.match(reusePrompt, /AUTONOMOUSLY IMPLEMENTABLE/);
|
|
481
|
+
assert.match(
|
|
482
|
+
reusePrompt,
|
|
483
|
+
/when any one is in doubt, omit the finding/i,
|
|
484
|
+
'expected the reuse lens to drop any finding that fails a criterion',
|
|
485
|
+
);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test('the reuse lens reviews the full diff and does not edit', () => {
|
|
489
|
+
const reusePrompt = lensPromptBody('runReuseAuditPass');
|
|
490
|
+
assert.match(reusePrompt, /origin\/main\.\.\.HEAD/);
|
|
491
|
+
assert.match(
|
|
492
|
+
reusePrompt,
|
|
493
|
+
/Do NOT edit, commit, or push/,
|
|
494
|
+
'expected the reuse lens to report findings without editing',
|
|
495
|
+
);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test('the reuse pass applies its findings through applyFixes, not the standards-deferral path', () => {
|
|
499
|
+
const reuseCallIndex = convergeSource.indexOf('await runReuseAuditPass(');
|
|
500
|
+
const loopIndex = convergeSource.indexOf('while (iterations < CONFIG.maxIterations)');
|
|
501
|
+
const reuseBlock = convergeSource.slice(reuseCallIndex, loopIndex);
|
|
502
|
+
assert.match(
|
|
503
|
+
reuseBlock,
|
|
504
|
+
/applyFixes\(reuseHead, reuseFindings, 'reuse-pass'\)/,
|
|
505
|
+
'expected the reuse pass to apply its findings via applyFixes',
|
|
506
|
+
);
|
|
507
|
+
assert.doesNotMatch(
|
|
508
|
+
reuseBlock,
|
|
509
|
+
/spawnStandardsFollowUp/,
|
|
510
|
+
'expected the reuse pass to apply improvements, not defer them',
|
|
511
|
+
);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
test('the reuse lens runs under the Reuse phase', () => {
|
|
515
|
+
const reusePrompt = lensPromptBody('runReuseAuditPass');
|
|
516
|
+
assert.match(reusePrompt, /phase: 'Reuse'/);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
test('the pre-commit gate step is a shared constant that dry-runs the CODE_RULES commit gate', () => {
|
|
520
|
+
assert.match(convergeSource, /const PRE_COMMIT_GATE_STEP =/);
|
|
521
|
+
const stepStart = convergeSource.indexOf('const PRE_COMMIT_GATE_STEP =');
|
|
522
|
+
const stepEnd = convergeSource.indexOf('\n\n', stepStart);
|
|
523
|
+
const stepBody = convergeSource.slice(stepStart, stepEnd);
|
|
524
|
+
assert.match(stepBody, /code_rules_gate\.py/);
|
|
525
|
+
assert.match(stepBody, /--staged/);
|
|
526
|
+
assert.match(
|
|
527
|
+
stepBody,
|
|
528
|
+
/do NOT commit/i,
|
|
529
|
+
'expected the gate step to forbid committing — it is a dry committability check',
|
|
530
|
+
);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
const editStepBuilders = [
|
|
534
|
+
'applyFixesEdit',
|
|
535
|
+
'recoverCommitBlockEdit',
|
|
536
|
+
'recoverVerifyFailEdit',
|
|
537
|
+
'repairConvergenceEdit',
|
|
538
|
+
'standardsFollowUpEdit',
|
|
539
|
+
];
|
|
540
|
+
|
|
541
|
+
for (const builderName of editStepBuilders) {
|
|
542
|
+
test(`${builderName} appends the pre-commit gate step to its edit prompt`, () => {
|
|
543
|
+
assert.match(
|
|
544
|
+
lensPromptBody(builderName),
|
|
545
|
+
/\+\s*PRE_COMMIT_GATE_STEP/,
|
|
546
|
+
`expected ${builderName} to append PRE_COMMIT_GATE_STEP`,
|
|
547
|
+
);
|
|
548
|
+
});
|
|
549
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'node:assert';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
const workflowDirectory = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const convergeSource = readFileSync(join(workflowDirectory, 'converge.mjs'), 'utf8');
|
|
9
|
+
|
|
10
|
+
function functionBody(functionName) {
|
|
11
|
+
const functionStart = convergeSource.indexOf(`function ${functionName}(`);
|
|
12
|
+
assert.notEqual(functionStart, -1, `expected ${functionName} to exist`);
|
|
13
|
+
const nextFunctionMatch = /\n(?:async )?function /.exec(convergeSource.slice(functionStart + 1));
|
|
14
|
+
const functionEnd =
|
|
15
|
+
nextFunctionMatch === null ? convergeSource.length : functionStart + 1 + nextFunctionMatch.index;
|
|
16
|
+
return convergeSource.slice(functionStart, functionEnd);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const helperModule = new Function(
|
|
20
|
+
`${functionBody('isMergeConflicting')}\nreturn { isMergeConflicting };`,
|
|
21
|
+
)();
|
|
22
|
+
const { isMergeConflicting } = helperModule;
|
|
23
|
+
|
|
24
|
+
test('isMergeConflicting treats a dead check agent (null/undefined) as not conflicting', () => {
|
|
25
|
+
assert.equal(isMergeConflicting(null), false);
|
|
26
|
+
assert.equal(isMergeConflicting(undefined), false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('isMergeConflicting reports a conflict only when the check returned conflicting:true', () => {
|
|
30
|
+
assert.equal(isMergeConflicting({ conflicting: true }), true);
|
|
31
|
+
assert.equal(isMergeConflicting({ conflicting: false }), false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('checkMergeConflicts is a read-only mergeability probe that polls until GitHub computes it', () => {
|
|
35
|
+
const body = functionBody('checkMergeConflicts');
|
|
36
|
+
assert.match(body, /mergeable/, 'expected the probe to read the PR mergeable field');
|
|
37
|
+
assert.match(
|
|
38
|
+
body,
|
|
39
|
+
/do not edit, commit, push, or rebase|read only/i,
|
|
40
|
+
'expected the probe to be read-only',
|
|
41
|
+
);
|
|
42
|
+
assert.match(body, /agentType:\s*'Explore'/, 'expected the probe to use the read-only Explore agent');
|
|
43
|
+
assert.match(body, /schema:\s*MERGE_CONFLICT_SCHEMA/, 'expected the probe to return MERGE_CONFLICT_SCHEMA');
|
|
44
|
+
assert.match(body, /null/, 'expected the probe to handle GitHub returning mergeable:null while it computes');
|
|
45
|
+
assert.match(body, /sleep 5|Start-Sleep/, 'expected a shell-agnostic poll delay');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('resolveConflictsEdit rebases onto origin/main and makes no push', () => {
|
|
49
|
+
const body = functionBody('resolveConflictsEdit');
|
|
50
|
+
assert.match(body, /git rebase origin\/main/, 'expected the edit step to rebase onto origin/main');
|
|
51
|
+
assert.match(
|
|
52
|
+
body,
|
|
53
|
+
/do not push|no push|not push/i,
|
|
54
|
+
'expected the edit step to leave the push to the commit step',
|
|
55
|
+
);
|
|
56
|
+
assert.match(body, /agentType:\s*'clean-coder'/, 'expected the edit step to use clean-coder');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('resolveMergeConflicts runs check -> edit -> verify -> commit and gates the push on the verdict', () => {
|
|
60
|
+
const body = functionBody('resolveMergeConflicts');
|
|
61
|
+
const checkIndex = body.indexOf('checkMergeConflicts(');
|
|
62
|
+
const editIndex = body.indexOf('resolveConflictsEdit(');
|
|
63
|
+
const verifyIndex = body.indexOf('verifyRepairChanges(');
|
|
64
|
+
const commitIndex = body.indexOf('commitRepairFixes(');
|
|
65
|
+
assert.notEqual(checkIndex, -1, 'expected the conflict check to run');
|
|
66
|
+
assert.notEqual(editIndex, -1, 'expected the rebase edit step to run');
|
|
67
|
+
assert.notEqual(verifyIndex, -1, 'expected the verify step to run');
|
|
68
|
+
assert.notEqual(commitIndex, -1, 'expected the commit step to run');
|
|
69
|
+
assert.ok(
|
|
70
|
+
checkIndex < editIndex && editIndex < verifyIndex && verifyIndex < commitIndex,
|
|
71
|
+
'expected the order check -> edit -> verify -> commit',
|
|
72
|
+
);
|
|
73
|
+
assert.match(body, /verdictPassed\(/, 'expected the verifier verdict to gate the force-push');
|
|
74
|
+
assert.match(
|
|
75
|
+
body,
|
|
76
|
+
/commitRepairFixes\(head,\s*true\)/,
|
|
77
|
+
'expected the commit to force-with-lease (wasRebased=true) after a rebase',
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('resolveMergeConflicts rebases only when the check reports a conflict', () => {
|
|
82
|
+
const body = functionBody('resolveMergeConflicts');
|
|
83
|
+
assert.match(body, /isMergeConflicting\(/, 'expected the orchestrator to branch on the conflict decision');
|
|
84
|
+
assert.match(
|
|
85
|
+
body,
|
|
86
|
+
/if \(!isMergeConflicting\([^)]*\)\) return head/,
|
|
87
|
+
'expected a clean PR to return the unchanged HEAD without rebasing',
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('the merge-conflict pre-flight runs once before the round loop, ahead of the parallel bug-check lenses', () => {
|
|
92
|
+
const preflightCall = convergeSource.indexOf('await resolveMergeConflicts(');
|
|
93
|
+
const whileLoop = convergeSource.indexOf('while (iterations < CONFIG.maxIterations)');
|
|
94
|
+
const firstLens = convergeSource.indexOf('const lenses = await parallel(');
|
|
95
|
+
assert.notEqual(preflightCall, -1, 'expected the pre-flight resolveMergeConflicts call site');
|
|
96
|
+
assert.ok(preflightCall < whileLoop, 'expected the pre-flight to run before the round loop');
|
|
97
|
+
assert.ok(preflightCall < firstLens, 'expected the pre-flight to run before the first bug-check lenses');
|
|
98
|
+
});
|