agent-dealer 1.3.4 → 1.3.5
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/bundle/server/dist/coordinator/operator-verification.integration.test.js +20 -2
- package/bundle/server/dist/coordinator/reviewer-visual-evidence.js +44 -4
- package/bundle/server/dist/coordinator/reviewer-visual-evidence.test.js +129 -1
- package/bundle/server/package.json +2 -2
- package/bundle/shared/package.json +1 -1
- package/package.json +1 -1
|
@@ -358,13 +358,15 @@ function recordVisualEvidence(issueId, opts) {
|
|
|
358
358
|
baseSha: "base1",
|
|
359
359
|
uiAffecting: true,
|
|
360
360
|
state: opts.state,
|
|
361
|
-
runId: opts.state === "
|
|
361
|
+
runId: opts.state === "available" || opts.state === "failed" ? 42 : null,
|
|
362
362
|
artifactNames: opts.state === "available" ? ["ui-screenshots", "ui-baseline", "ui-diff"] : [],
|
|
363
363
|
reason: opts.state === "available"
|
|
364
364
|
? null
|
|
365
365
|
: opts.state === "failed"
|
|
366
366
|
? "Visual run 42 for this head concluded failure — its captures are unusable"
|
|
367
|
-
:
|
|
367
|
+
: opts.state === "not_applicable"
|
|
368
|
+
? "repository has no Visual workflow (workflow visual.yml not found on the default branch) — nothing to capture"
|
|
369
|
+
: "no Visual workflow run found for head abc123",
|
|
368
370
|
},
|
|
369
371
|
author: "system",
|
|
370
372
|
});
|
|
@@ -420,6 +422,22 @@ test("NOT-384: available visual evidence on a UI head merges normally", async ()
|
|
|
420
422
|
assert.deepEqual(calls, [{ cwd: managedCwd, number: 42 }]);
|
|
421
423
|
assert.equal(getIssue(issueId).status, "done");
|
|
422
424
|
});
|
|
425
|
+
test("NOT-538: not_applicable visual evidence (no Visual workflow) on a UI head merges normally", async () => {
|
|
426
|
+
const calls = [];
|
|
427
|
+
setMergePrForTests(async (opts) => {
|
|
428
|
+
calls.push(opts);
|
|
429
|
+
return { ok: true };
|
|
430
|
+
});
|
|
431
|
+
const issueId = newIssue("It works");
|
|
432
|
+
startWorkflow(issueId);
|
|
433
|
+
recordVisualEvidence(issueId, { headSha: "abc123", state: "not_applicable" });
|
|
434
|
+
const result = await approve(issueId);
|
|
435
|
+
assert.equal(result.applied, true);
|
|
436
|
+
if (result.applied)
|
|
437
|
+
assert.equal(result.issueStatus, "done");
|
|
438
|
+
assert.deepEqual(calls, [{ cwd: managedCwd, number: 42 }]);
|
|
439
|
+
assert.equal(getIssue(issueId).status, "done");
|
|
440
|
+
});
|
|
423
441
|
test("NOT-384: [operator] criteria plus a visual hold raise one action naming both", async () => {
|
|
424
442
|
let merges = 0;
|
|
425
443
|
setMergePrForTests(async () => {
|
|
@@ -22,8 +22,10 @@
|
|
|
22
22
|
// Dealer's issue-artifact storage, stage a read-only copy inside the
|
|
23
23
|
// reviewer's worktree (outside tracked paths, ignored by `isWorktreeClean`),
|
|
24
24
|
// and record one `visual_evidence` artifact row. It never throws: every
|
|
25
|
-
// failure mode resolves to `missing` with a reason
|
|
26
|
-
//
|
|
25
|
+
// failure mode resolves to `missing` with a reason — except a lookup 404
|
|
26
|
+
// proving the repository has no `visual.yml`, which resolves to
|
|
27
|
+
// `not_applicable` (nothing to capture) — and a missing or failed capture
|
|
28
|
+
// never counts as a visual pass.
|
|
27
29
|
// 4. `readVisualEvidenceHold` — the merge-gate reader (auto-merge.ts): a
|
|
28
30
|
// UI-affecting head whose recorded evidence is `missing` or `failed` holds
|
|
29
31
|
// for `operator_verification` instead of merging or queuing a repair round.
|
|
@@ -334,6 +336,24 @@ function truncateReason(text, maxChars = 300) {
|
|
|
334
336
|
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
335
337
|
return oneLine.length > maxChars ? `${oneLine.slice(0, maxChars).trimEnd()}…` : oneLine;
|
|
336
338
|
}
|
|
339
|
+
/**
|
|
340
|
+
* NOT-538: true when a Visual run lookup failure means the repository's
|
|
341
|
+
* default branch has no `visual.yml` (`gh run list --workflow visual.yml`
|
|
342
|
+
* answers HTTP 404: "workflow visual.yml not found on the default branch"),
|
|
343
|
+
* rather than a transient or auth failure. Deliberately narrow — all three
|
|
344
|
+
* markers must be present — so auth, network, 5xx, and rate-limit failures
|
|
345
|
+
* keep failing closed to `missing`.
|
|
346
|
+
*/
|
|
347
|
+
export function isVisualWorkflowNotFoundError(message) {
|
|
348
|
+
const text = message.toLowerCase();
|
|
349
|
+
return text.includes("404") && text.includes("visual.yml") && text.includes("not found");
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* NOT-538: the recorded reason when the lookup proves the repository has no
|
|
353
|
+
* Visual workflow. The head still matches the UI paths (`uiAffecting: true`),
|
|
354
|
+
* but there is nothing to capture, so the merge gate must not hold.
|
|
355
|
+
*/
|
|
356
|
+
export const VISUAL_WORKFLOW_ABSENT_REASON = "repository has no Visual workflow (workflow visual.yml not found on the default branch) — nothing to capture";
|
|
337
357
|
function noEvidence(uiAffecting, base, reason) {
|
|
338
358
|
return {
|
|
339
359
|
...base,
|
|
@@ -357,7 +377,9 @@ function noEvidence(uiAffecting, base, reason) {
|
|
|
357
377
|
*
|
|
358
378
|
* Never throws and never returns usable evidence it does not have: unknown
|
|
359
379
|
* changed files fail closed to UI-affecting + `missing`, and every lookup,
|
|
360
|
-
* download, stage, or record failure degrades to `missing` with a reason
|
|
380
|
+
* download, stage, or record failure degrades to `missing` with a reason —
|
|
381
|
+
* except a lookup 404 proving the repository has no Visual workflow, which
|
|
382
|
+
* records `not_applicable` instead (NOT-538).
|
|
361
383
|
*/
|
|
362
384
|
export async function resolveReviewerVisualEvidence(opts) {
|
|
363
385
|
const base = { headSha: opts.headSha, baseSha: opts.baseSha };
|
|
@@ -411,7 +433,25 @@ export async function resolveReviewerVisualEvidence(opts) {
|
|
|
411
433
|
run = await fetcher.findRunForHead({ cwd: opts.worktreePath, headSha: opts.headSha });
|
|
412
434
|
}
|
|
413
435
|
catch (err) {
|
|
414
|
-
const
|
|
436
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
437
|
+
if (isVisualWorkflowNotFoundError(message)) {
|
|
438
|
+
const resolved = {
|
|
439
|
+
...base,
|
|
440
|
+
uiAffecting: true,
|
|
441
|
+
state: "not_applicable",
|
|
442
|
+
runId: null,
|
|
443
|
+
artifactNames: [],
|
|
444
|
+
stagedDir: null,
|
|
445
|
+
summaryChars: null,
|
|
446
|
+
reason: VISUAL_WORKFLOW_ABSENT_REASON,
|
|
447
|
+
staged: null,
|
|
448
|
+
summary: null,
|
|
449
|
+
files: null,
|
|
450
|
+
};
|
|
451
|
+
record(resolved, null);
|
|
452
|
+
return resolved;
|
|
453
|
+
}
|
|
454
|
+
const resolved = noEvidence(true, base, `Visual run lookup failed: ${truncateReason(message)}`);
|
|
415
455
|
record(resolved, null);
|
|
416
456
|
return resolved;
|
|
417
457
|
}
|
|
@@ -17,7 +17,7 @@ const { createIssue } = await import("../repository/issues.js");
|
|
|
17
17
|
const { createIssueArtifact } = await import("../repository/artifacts.js");
|
|
18
18
|
const { listArtifactsForIssueByKind } = await import("../repository/artifacts-for-issue.js");
|
|
19
19
|
const { isWorktreeClean, REVIEWER_VISUAL_STAGING_DIR_NAME } = await import("../adapters/git-worktree.js");
|
|
20
|
-
const { VISUAL_WORKFLOW_PATHS, VISUAL_ARTIFACT_NAMES, VISUAL_EVIDENCE_ARTIFACT_KIND, isUiAffectingChange, classifyVisualEvidence, githubVisualEvidenceFetcher, resolveReviewerVisualEvidence, stageVisualArtifacts, cleanupStagedVisualArtifacts, readVisualDiffSummary, reviewerVisualStagingDir, toReviewerVisualEvidenceInput, readVisualEvidenceHold, } = await import("./reviewer-visual-evidence.js");
|
|
20
|
+
const { VISUAL_WORKFLOW_PATHS, VISUAL_ARTIFACT_NAMES, VISUAL_EVIDENCE_ARTIFACT_KIND, VISUAL_WORKFLOW_ABSENT_REASON, isUiAffectingChange, classifyVisualEvidence, githubVisualEvidenceFetcher, resolveReviewerVisualEvidence, stageVisualArtifacts, cleanupStagedVisualArtifacts, readVisualDiffSummary, reviewerVisualStagingDir, toReviewerVisualEvidenceInput, readVisualEvidenceHold, } = await import("./reviewer-visual-evidence.js");
|
|
21
21
|
before(() => migrate());
|
|
22
22
|
beforeEach(() => {
|
|
23
23
|
getDb().exec(`
|
|
@@ -285,6 +285,8 @@ function fixtureFetcher(opts) {
|
|
|
285
285
|
return {
|
|
286
286
|
calls,
|
|
287
287
|
async findRunForHead() {
|
|
288
|
+
if (opts.failLookupError !== undefined)
|
|
289
|
+
throw new Error(opts.failLookupError);
|
|
288
290
|
if (opts.failLookup)
|
|
289
291
|
throw new Error("gh exploded");
|
|
290
292
|
return opts.run === undefined
|
|
@@ -494,3 +496,129 @@ test("NOT-384: gate reader holds only UI heads with missing/failed evidence at t
|
|
|
494
496
|
});
|
|
495
497
|
assert.equal(readVisualEvidenceHold(nonUiIssue, HEAD), null);
|
|
496
498
|
});
|
|
499
|
+
// NOT-538: a repository whose default branch has no visual.yml has nothing to
|
|
500
|
+
// capture — the lookup's "workflow not found" 404 resolves to not_applicable
|
|
501
|
+
// (the head still matches the UI paths) instead of holding the PR.
|
|
502
|
+
test("NOT-538: workflow-not-found 404 records not_applicable, never missing", async () => {
|
|
503
|
+
const issueId = makeIssue();
|
|
504
|
+
const worktree = initRepo();
|
|
505
|
+
try {
|
|
506
|
+
const resolved = await resolveReviewerVisualEvidence({
|
|
507
|
+
issueId,
|
|
508
|
+
worktreePath: worktree,
|
|
509
|
+
baseSha: "c".repeat(40),
|
|
510
|
+
headSha: HEAD,
|
|
511
|
+
changedFiles: ["packages/shared/src/types.ts"],
|
|
512
|
+
github: {},
|
|
513
|
+
fetcher: fixtureFetcher({
|
|
514
|
+
failLookupError: "gh run list failed: HTTP 404: workflow visual.yml not found on the default branch",
|
|
515
|
+
}),
|
|
516
|
+
});
|
|
517
|
+
assert.equal(resolved.state, "not_applicable");
|
|
518
|
+
assert.equal(resolved.uiAffecting, true);
|
|
519
|
+
assert.equal(resolved.runId, null);
|
|
520
|
+
assert.equal(resolved.staged, null);
|
|
521
|
+
assert.equal(resolved.reason, VISUAL_WORKFLOW_ABSENT_REASON);
|
|
522
|
+
assert.match(resolved.reason, /repository has no Visual workflow/);
|
|
523
|
+
assert.equal(toReviewerVisualEvidenceInput(resolved), undefined);
|
|
524
|
+
const rows = listArtifactsForIssueByKind(issueId, VISUAL_EVIDENCE_ARTIFACT_KIND);
|
|
525
|
+
assert.equal(rows.length, 1);
|
|
526
|
+
const content = JSON.parse(rows[0].contentJson);
|
|
527
|
+
assert.equal(content.state, "not_applicable");
|
|
528
|
+
assert.equal(content.uiAffecting, true);
|
|
529
|
+
// The recorded workflow-absent record releases the merge gate.
|
|
530
|
+
assert.equal(readVisualEvidenceHold(issueId, HEAD), null);
|
|
531
|
+
}
|
|
532
|
+
finally {
|
|
533
|
+
fs.rmSync(worktree, { recursive: true, force: true });
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
// NOT-538: every other lookup failure still fails closed to missing with the
|
|
537
|
+
// existing reason — only the workflow-not-found 404 skips the gate.
|
|
538
|
+
test("NOT-538: auth, network, 5xx, and rate-limit lookup failures still record missing", async () => {
|
|
539
|
+
const worktree = initRepo();
|
|
540
|
+
try {
|
|
541
|
+
const failures = {
|
|
542
|
+
auth: "gh run list failed: HTTP 401: Bad credentials",
|
|
543
|
+
network: "gh run list failed: Could not resolve host: api.github.com",
|
|
544
|
+
server: "gh run list failed: HTTP 502: Bad Gateway",
|
|
545
|
+
rateLimit: "gh run list failed: HTTP 403: API rate limit exceeded for user ID 123",
|
|
546
|
+
};
|
|
547
|
+
for (const [kind, message] of Object.entries(failures)) {
|
|
548
|
+
const resolved = await resolveReviewerVisualEvidence({
|
|
549
|
+
issueId: makeIssue(),
|
|
550
|
+
worktreePath: worktree,
|
|
551
|
+
baseSha: "c".repeat(40),
|
|
552
|
+
headSha: HEAD,
|
|
553
|
+
changedFiles: ["packages/shared/src/types.ts"],
|
|
554
|
+
github: {},
|
|
555
|
+
fetcher: fixtureFetcher({ failLookupError: message }),
|
|
556
|
+
});
|
|
557
|
+
assert.equal(resolved.state, "missing", `${kind} failure must stay missing`);
|
|
558
|
+
assert.equal(resolved.uiAffecting, true);
|
|
559
|
+
assert.match(resolved.reason, /^Visual run lookup failed: /, `${kind} failure keeps the lookup-failed reason`);
|
|
560
|
+
}
|
|
561
|
+
// Near-miss 404s that do not name the missing workflow also stay missing.
|
|
562
|
+
for (const message of [
|
|
563
|
+
"gh run list failed: HTTP 404: Not Found",
|
|
564
|
+
"gh run list failed: HTTP 404: repository not found",
|
|
565
|
+
]) {
|
|
566
|
+
const resolved = await resolveReviewerVisualEvidence({
|
|
567
|
+
issueId: makeIssue(),
|
|
568
|
+
worktreePath: worktree,
|
|
569
|
+
baseSha: "c".repeat(40),
|
|
570
|
+
headSha: HEAD,
|
|
571
|
+
changedFiles: ["packages/shared/src/types.ts"],
|
|
572
|
+
github: {},
|
|
573
|
+
fetcher: fixtureFetcher({ failLookupError: message }),
|
|
574
|
+
});
|
|
575
|
+
assert.equal(resolved.state, "missing", `${message} must stay missing`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
finally {
|
|
579
|
+
fs.rmSync(worktree, { recursive: true, force: true });
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
// NOT-538: the gate reader releases a UI-affecting head whose record is
|
|
583
|
+
// not_applicable because the repository has no Visual workflow, and still
|
|
584
|
+
// holds missing and failed.
|
|
585
|
+
test("NOT-538: gate reader releases workflow-absent not_applicable, holds missing and failed", () => {
|
|
586
|
+
const absentIssue = makeIssue();
|
|
587
|
+
createIssueArtifact({
|
|
588
|
+
issueId: absentIssue,
|
|
589
|
+
kind: VISUAL_EVIDENCE_ARTIFACT_KIND,
|
|
590
|
+
content: {
|
|
591
|
+
headSha: HEAD,
|
|
592
|
+
uiAffecting: true,
|
|
593
|
+
state: "not_applicable",
|
|
594
|
+
reason: VISUAL_WORKFLOW_ABSENT_REASON,
|
|
595
|
+
runId: null,
|
|
596
|
+
artifactNames: [],
|
|
597
|
+
},
|
|
598
|
+
author: "system",
|
|
599
|
+
});
|
|
600
|
+
assert.equal(readVisualEvidenceHold(absentIssue, HEAD), null);
|
|
601
|
+
const missingIssue = makeIssue();
|
|
602
|
+
createIssueArtifact({
|
|
603
|
+
issueId: missingIssue,
|
|
604
|
+
kind: VISUAL_EVIDENCE_ARTIFACT_KIND,
|
|
605
|
+
content: {
|
|
606
|
+
headSha: HEAD,
|
|
607
|
+
uiAffecting: true,
|
|
608
|
+
state: "missing",
|
|
609
|
+
reason: "Visual run lookup failed: HTTP 502: Bad Gateway",
|
|
610
|
+
runId: null,
|
|
611
|
+
artifactNames: [],
|
|
612
|
+
},
|
|
613
|
+
author: "system",
|
|
614
|
+
});
|
|
615
|
+
assert.equal(readVisualEvidenceHold(missingIssue, HEAD).state, "missing");
|
|
616
|
+
const failedIssue = makeIssue();
|
|
617
|
+
createIssueArtifact({
|
|
618
|
+
issueId: failedIssue,
|
|
619
|
+
kind: VISUAL_EVIDENCE_ARTIFACT_KIND,
|
|
620
|
+
content: { headSha: HEAD, uiAffecting: true, state: "failed", reason: "boom", runId: 9, artifactNames: [] },
|
|
621
|
+
author: "system",
|
|
622
|
+
});
|
|
623
|
+
assert.equal(readVisualEvidenceHold(failedIssue, HEAD).state, "failed");
|
|
624
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-dealer/server",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"test": "AGENT_DEALER_SKIP_GITHUB_HEALTH=1 AGENT_DEALER_SKIP_AGENT_HEALTH=1 npm run build -w @agent-dealer/shared && AGENT_DEALER_SKIP_GITHUB_HEALTH=1 AGENT_DEALER_SKIP_AGENT_HEALTH=1 node ../../scripts/run-workspace-tests.mjs"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@agent-dealer/shared": "1.3.
|
|
26
|
+
"@agent-dealer/shared": "1.3.5",
|
|
27
27
|
"@fastify/cors": "^11.0.1",
|
|
28
28
|
"@fastify/static": "^8.2.0",
|
|
29
29
|
"@modelcontextprotocol/sdk": "^1.29.0",
|