agent-dealer 1.2.7 → 1.2.9
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/adapters/agent-deck-bind.js +16 -6
- package/bundle/server/dist/adapters/agent-deck-bind.test.js +74 -0
- package/bundle/server/dist/adapters/agent-health.js +14 -3
- package/bundle/server/dist/adapters/github.js +84 -5
- package/bundle/server/dist/adapters/github.test.js +83 -1
- package/bundle/server/dist/adapters/muse-capability.js +443 -24
- package/bundle/server/dist/adapters/muse-capability.test.js +469 -25
- package/bundle/server/dist/adapters/muse-visual-qa.js +114 -0
- package/bundle/server/dist/adapters/muse-visual-qa.test.js +68 -0
- package/bundle/server/dist/adapters/worktree-deps.js +96 -0
- package/bundle/server/dist/adapters/worktree-deps.test.js +144 -0
- package/bundle/server/dist/capacity/muse-probe.js +56 -9
- package/bundle/server/dist/capacity/muse-probe.test.js +188 -1
- package/bundle/server/dist/coordinator/admission.js +14 -1
- package/bundle/server/dist/coordinator/admission.test.js +197 -4
- package/bundle/server/dist/coordinator/auto-merge.integration.test.js +276 -0
- package/bundle/server/dist/coordinator/auto-merge.js +244 -4
- package/bundle/server/dist/coordinator/checks-wait-config.js +19 -0
- package/bundle/server/dist/coordinator/commands.js +284 -23
- package/bundle/server/dist/coordinator/commands.test.js +47 -0
- package/bundle/server/dist/coordinator/deck-outage.integration.test.js +4 -2
- package/bundle/server/dist/coordinator/developer-effect.js +224 -4
- package/bundle/server/dist/coordinator/developer-effect.test.js +288 -3
- package/bundle/server/dist/coordinator/execution-report.js +6 -0
- package/bundle/server/dist/coordinator/execution-report.test.js +7 -0
- package/bundle/server/dist/coordinator/failure-cause.js +64 -1
- package/bundle/server/dist/coordinator/failure-cause.test.js +92 -1
- package/bundle/server/dist/coordinator/failure-reason.js +7 -0
- package/bundle/server/dist/coordinator/failure-reason.test.js +14 -0
- package/bundle/server/dist/coordinator/human-resolution.js +36 -1
- package/bundle/server/dist/coordinator/human-resolution.test.js +33 -0
- package/bundle/server/dist/coordinator/merge-conflict-sync.js +647 -0
- package/bundle/server/dist/coordinator/merge-conflict-sync.test.js +121 -0
- package/bundle/server/dist/coordinator/muse-developer.integration.test.js +9 -4
- package/bundle/server/dist/coordinator/muse-spawn.js +122 -29
- package/bundle/server/dist/coordinator/muse-spawn.test.js +210 -0
- package/bundle/server/dist/coordinator/operator-criteria.js +168 -0
- package/bundle/server/dist/coordinator/operator-criteria.test.js +78 -0
- package/bundle/server/dist/coordinator/operator-verification.integration.test.js +347 -0
- package/bundle/server/dist/coordinator/playbook-feedback.js +693 -0
- package/bundle/server/dist/coordinator/playbook-feedback.test.js +702 -0
- package/bundle/server/dist/coordinator/projection.js +10 -4
- package/bundle/server/dist/coordinator/projection.test.js +13 -0
- package/bundle/server/dist/coordinator/prompts-execution-contract.test.js +107 -0
- package/bundle/server/dist/coordinator/prompts.js +211 -2
- package/bundle/server/dist/coordinator/prompts.test.js +231 -0
- package/bundle/server/dist/coordinator/recovery.js +57 -3
- package/bundle/server/dist/coordinator/recovery.test.js +67 -8
- package/bundle/server/dist/coordinator/reflect-trigger.js +33 -169
- package/bundle/server/dist/coordinator/reflect-trigger.test.js +149 -200
- package/bundle/server/dist/coordinator/repair-cycle.integration.test.js +1 -0
- package/bundle/server/dist/coordinator/reviewer-effect.js +16 -1
- package/bundle/server/dist/coordinator/reviewer-result.js +6 -0
- package/bundle/server/dist/coordinator/routing.js +45 -6
- package/bundle/server/dist/coordinator/routing.test.js +76 -15
- package/bundle/server/dist/coordinator/session-timeouts.js +30 -0
- package/bundle/server/dist/coordinator/usage-cap-defer.js +61 -0
- package/bundle/server/dist/coordinator/usage-cap.integration.test.js +1 -1
- package/bundle/server/dist/coordinator/worker-loop.js +18 -4
- package/bundle/server/dist/db/ci-attempts-migration.test.js +70 -0
- package/bundle/server/dist/db/index.js +18 -0
- package/bundle/server/dist/db/schema.sql +5 -0
- package/bundle/server/dist/docs-execution-analysis.test.js +1 -0
- package/bundle/server/dist/repository/artifacts-for-issue.js +3 -3
- package/bundle/server/dist/repository/human-actions.js +14 -0
- package/bundle/server/dist/repository/issues.js +67 -6
- package/bundle/server/dist/repository/work-items.js +7 -0
- package/bundle/server/dist/repository/worker-sessions.js +51 -2
- package/bundle/server/dist/routes/human-actions.js +10 -8
- package/bundle/server/dist/routes/issues-execution-contract.test.js +321 -0
- package/bundle/server/dist/routes/issues.js +19 -2
- package/bundle/server/dist/runners/muse-code-args.js +25 -0
- package/bundle/server/dist/runners/muse-code-args.test.js +30 -0
- package/bundle/server/dist/runners/muse-code-jsonl.js +106 -11
- package/bundle/server/dist/runners/muse-config-core.js +18 -1
- package/bundle/server/dist/runners/muse-config.test.js +37 -0
- package/bundle/server/dist/runners/muse-serve-session.js +5 -0
- package/bundle/server/dist/runners/spawn-cli.js +116 -0
- package/bundle/server/dist/runners/spawn-cli.test.js +124 -0
- package/bundle/server/package.json +2 -2
- package/bundle/server/static-ui/assets/index-CU6fNntg.js +64 -0
- package/bundle/server/static-ui/assets/{index-DyAJNyfV.css → index-K_YcYkQU.css} +1 -1
- package/bundle/server/static-ui/index.html +2 -2
- package/bundle/shared/dist/execution-analysis.d.ts +22 -22
- package/bundle/shared/dist/execution-contract.d.ts +117 -0
- package/bundle/shared/dist/execution-contract.js +307 -0
- package/bundle/shared/dist/execution-contract.test.d.ts +2 -0
- package/bundle/shared/dist/execution-contract.test.js +499 -0
- package/bundle/shared/dist/execution-report.d.ts +9 -9
- package/bundle/shared/dist/execution-report.js +2 -0
- package/bundle/shared/dist/failure-cause.d.ts +4 -4
- package/bundle/shared/dist/failure-cause.js +2 -0
- package/bundle/shared/dist/human-actions.d.ts +4 -4
- package/bundle/shared/dist/human-actions.js +17 -0
- package/bundle/shared/dist/index.d.ts +85 -84
- package/bundle/shared/dist/index.js +3 -0
- package/bundle/shared/dist/issues.d.ts +176 -16
- package/bundle/shared/dist/issues.js +22 -0
- package/bundle/shared/dist/issues.test.js +3 -0
- package/bundle/shared/dist/outbound-draft.d.ts +12 -12
- package/bundle/shared/dist/worker-sessions.d.ts +10 -0
- package/bundle/shared/dist/worker-sessions.js +8 -0
- package/bundle/shared/dist/worker-sessions.test.js +1 -0
- package/bundle/shared/dist/workflow.d.ts +4 -4
- package/bundle/shared/dist/workflow.js +16 -0
- package/bundle/shared/dist/workflow.test.js +3 -0
- package/bundle/shared/package.json +1 -1
- package/dist/action.js +4 -1
- package/dist/action.test.js +42 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/bundle/server/static-ui/assets/index-yLyxRd-7.js +0 -64
|
@@ -153,20 +153,29 @@ function readAmbientCodexAuthPolicy(ambientCodexHome) {
|
|
|
153
153
|
return {};
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
/**
|
|
157
|
+
* NOT-305: header carrying the session's opaque Deck correlation UUID. Observability
|
|
158
|
+
* metadata only — Deck uses it to attribute its normalized playbook-fetch stream back
|
|
159
|
+
* to this durable Dealer session; it grants no authority.
|
|
160
|
+
*/
|
|
161
|
+
export const DECK_CORRELATION_HEADER = "x-agent-deck-correlation-id";
|
|
162
|
+
function deckLaunchHeaders(deckId, worktreePath, correlationId) {
|
|
163
|
+
const headers = {
|
|
158
164
|
"x-agent-deck-deck-id": deckId,
|
|
159
165
|
"x-agent-deck-workspace": worktreePath,
|
|
160
166
|
};
|
|
167
|
+
if (correlationId)
|
|
168
|
+
headers[DECK_CORRELATION_HEADER] = correlationId;
|
|
169
|
+
return headers;
|
|
161
170
|
}
|
|
162
171
|
/** claude's `--mcp-config` file schema — deck headers only, no Authorization. */
|
|
163
|
-
function urlHeaderMcpConfig(mcpUrl, deckId, worktreePath) {
|
|
172
|
+
function urlHeaderMcpConfig(mcpUrl, deckId, worktreePath, correlationId) {
|
|
164
173
|
return {
|
|
165
174
|
mcpServers: {
|
|
166
175
|
"agent-deck": {
|
|
167
176
|
type: "http",
|
|
168
177
|
url: mcpUrl,
|
|
169
|
-
headers: deckLaunchHeaders(deckId, worktreePath),
|
|
178
|
+
headers: deckLaunchHeaders(deckId, worktreePath, correlationId),
|
|
170
179
|
},
|
|
171
180
|
},
|
|
172
181
|
};
|
|
@@ -202,7 +211,7 @@ function ensureCursorMcpExcluded(worktreePath) {
|
|
|
202
211
|
async function materializeWorkerMcpConfig(opts) {
|
|
203
212
|
const mcpBase = getAgentDeckMcpUrl().replace(/\/mcp\/?$/, "");
|
|
204
213
|
const mcpUrl = `${mcpBase}/mcp`;
|
|
205
|
-
const headers = deckLaunchHeaders(opts.deckId, opts.worktreePath);
|
|
214
|
+
const headers = deckLaunchHeaders(opts.deckId, opts.worktreePath, opts.correlationId);
|
|
206
215
|
// NOT-278: Muse Code settings come only from `prepareMuseAttempt`
|
|
207
216
|
// (runners/muse-config.ts) — never emit a Claude/Codex/Cursor config for Muse. Refuse
|
|
208
217
|
// before anything is written rather than fall through to the Claude config below.
|
|
@@ -257,7 +266,7 @@ async function materializeWorkerMcpConfig(opts) {
|
|
|
257
266
|
const dir = getWorkerMcpConfigDir();
|
|
258
267
|
const filePath = path.join(dir, `claude-mcp-${opts.deckId.slice(0, 8)}-${randomUUID()}.json`);
|
|
259
268
|
try {
|
|
260
|
-
fs.writeFileSync(filePath, JSON.stringify(urlHeaderMcpConfig(mcpUrl, opts.deckId, opts.worktreePath)), { mode: 0o600 });
|
|
269
|
+
fs.writeFileSync(filePath, JSON.stringify(urlHeaderMcpConfig(mcpUrl, opts.deckId, opts.worktreePath, opts.correlationId)), { mode: 0o600 });
|
|
261
270
|
}
|
|
262
271
|
catch (err) {
|
|
263
272
|
fs.rmSync(filePath, { force: true });
|
|
@@ -416,6 +425,7 @@ export async function prepareWorkerDeckConnection(opts) {
|
|
|
416
425
|
deckId: opts.deckId,
|
|
417
426
|
worktreePath: opts.worktreePath,
|
|
418
427
|
policy: opts.policy,
|
|
428
|
+
correlationId: opts.correlationId,
|
|
419
429
|
});
|
|
420
430
|
}
|
|
421
431
|
catch (err) {
|
|
@@ -528,3 +528,77 @@ test("a deck that loses the connection mid-playbook is deck_unavailable", async
|
|
|
528
528
|
if (!result.ok)
|
|
529
529
|
assert.equal(result.kind, "deck_unavailable");
|
|
530
530
|
});
|
|
531
|
+
// NOT-305: every new worker session persists an opaque Deck correlation UUID before
|
|
532
|
+
// spawn, and every runtime launch config carries it as the exact
|
|
533
|
+
// `x-agent-deck-correlation-id` observability header (never authority).
|
|
534
|
+
const CORRELATION_ID = "aaaaaaaa-1111-4111-a111-aaaaaaaaaaaa";
|
|
535
|
+
test("prepareWorkerDeckConnection writes the exact correlation header for claude_code", async () => {
|
|
536
|
+
const result = await prepareWorkerDeckConnection({
|
|
537
|
+
policy: DENIED,
|
|
538
|
+
...BASE_OPTS,
|
|
539
|
+
correlationId: CORRELATION_ID,
|
|
540
|
+
verifyCallTool: async (name) => {
|
|
541
|
+
assert.equal(name, "get_bound_deck");
|
|
542
|
+
return textResult({ id: DECK, name: "personal-dev" });
|
|
543
|
+
},
|
|
544
|
+
});
|
|
545
|
+
assert.equal(result.ok, true);
|
|
546
|
+
if (result.ok) {
|
|
547
|
+
const written = JSON.parse(fs.readFileSync(result.mcpConfigPath, "utf8"));
|
|
548
|
+
const headers = written.mcpServers["agent-deck"].headers;
|
|
549
|
+
assert.equal(headers["x-agent-deck-correlation-id"], CORRELATION_ID);
|
|
550
|
+
await releaseWorkerDeckConnection({ mcpConfigPath: result.mcpConfigPath });
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
test("prepareWorkerDeckConnection omits the correlation header when the session has none", async () => {
|
|
554
|
+
const result = await prepareWorkerDeckConnection({
|
|
555
|
+
policy: DENIED,
|
|
556
|
+
...BASE_OPTS,
|
|
557
|
+
verifyCallTool: async () => textResult({ id: DECK, name: "personal-dev" }),
|
|
558
|
+
});
|
|
559
|
+
assert.equal(result.ok, true);
|
|
560
|
+
if (result.ok) {
|
|
561
|
+
const written = JSON.parse(fs.readFileSync(result.mcpConfigPath, "utf8"));
|
|
562
|
+
const headers = written.mcpServers["agent-deck"].headers;
|
|
563
|
+
assert.equal("x-agent-deck-correlation-id" in headers, false);
|
|
564
|
+
await releaseWorkerDeckConnection({ mcpConfigPath: result.mcpConfigPath });
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
test("prepareWorkerDeckConnection writes the exact correlation header for codex_local", async () => {
|
|
568
|
+
const result = await prepareWorkerDeckConnection({
|
|
569
|
+
policy: DENIED,
|
|
570
|
+
...BASE_OPTS,
|
|
571
|
+
runtime: "codex_local",
|
|
572
|
+
correlationId: CORRELATION_ID,
|
|
573
|
+
verifyCallTool: async () => textResult({ id: DECK }),
|
|
574
|
+
});
|
|
575
|
+
assert.equal(result.ok, true);
|
|
576
|
+
if (result.ok) {
|
|
577
|
+
const toml = fs.readFileSync(path.join(result.mcpConfigPath, "config.toml"), "utf8");
|
|
578
|
+
assert.match(toml, new RegExp(`x-agent-deck-correlation-id = "${CORRELATION_ID}"`));
|
|
579
|
+
await releaseWorkerDeckConnection({ mcpConfigPath: result.mcpConfigPath });
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
test("prepareWorkerDeckConnection writes the exact correlation header for cursor_local", async () => {
|
|
583
|
+
const repo = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-cursor-correlation-"));
|
|
584
|
+
initGitRepo(repo);
|
|
585
|
+
try {
|
|
586
|
+
const result = await prepareWorkerDeckConnection({
|
|
587
|
+
policy: DENIED,
|
|
588
|
+
deckId: DECK,
|
|
589
|
+
worktreePath: repo,
|
|
590
|
+
runtime: "cursor_local",
|
|
591
|
+
correlationId: CORRELATION_ID,
|
|
592
|
+
verifyCallTool: async () => textResult({ id: DECK }),
|
|
593
|
+
});
|
|
594
|
+
assert.equal(result.ok, true);
|
|
595
|
+
if (result.ok) {
|
|
596
|
+
const written = JSON.parse(fs.readFileSync(result.mcpConfigPath, "utf8"));
|
|
597
|
+
assert.equal(written.mcpServers["agent-deck"].headers["x-agent-deck-correlation-id"], CORRELATION_ID);
|
|
598
|
+
await releaseWorkerDeckConnection({ mcpConfigPath: result.mcpConfigPath });
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
finally {
|
|
602
|
+
fs.rmSync(repo, { recursive: true, force: true });
|
|
603
|
+
}
|
|
604
|
+
});
|
|
@@ -107,6 +107,15 @@ export function setRunCommandForTests(fn) {
|
|
|
107
107
|
function runCommand(cmd, args, timeoutMs = DEFAULT_PROBE_TIMEOUT_MS, env) {
|
|
108
108
|
return runCommandImpl(cmd, args, timeoutMs, env);
|
|
109
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* NOT-308: drop only the cached muse runtime issues — the `muse_capability` acknowledge
|
|
112
|
+
* path calls this so the lifted block is seen on the next health read instead of after
|
|
113
|
+
* the 60s TTL. Never touches capability state itself (unlike clearAgentHealthCaches,
|
|
114
|
+
* which resets it for tests).
|
|
115
|
+
*/
|
|
116
|
+
export function invalidateMuseHealthCache() {
|
|
117
|
+
runtimeIssueCache.delete("muse_code");
|
|
118
|
+
}
|
|
110
119
|
/** Exported for tests — clears the shared github + runtime health caches and soft-fail streak. */
|
|
111
120
|
export function clearAgentHealthCaches() {
|
|
112
121
|
runtimeIssueCache.clear();
|
|
@@ -199,9 +208,11 @@ async function cursorRuntimeIssues() {
|
|
|
199
208
|
* existence only, never read.
|
|
200
209
|
*
|
|
201
210
|
* NOT-277: with CLI and credentials present, the reported version is handed to the capability
|
|
202
|
-
* check — a version not yet checked runs one real developer-shell probe (muse-capability.ts)
|
|
203
|
-
* `runtime_capability` blocks
|
|
204
|
-
* not
|
|
211
|
+
* check — a version not yet checked runs one real developer-shell probe (muse-capability.ts).
|
|
212
|
+
* NOT-308: `runtime_capability` blocks only on a confirmed shell/write loss (`missing`,
|
|
213
|
+
* not operator-overridden); while the version is unchecked or inconclusive the check
|
|
214
|
+
* runs in the background and admission proceeds on the last confirmed baseline
|
|
215
|
+
* (fresh installs with no baseline still fail closed).
|
|
205
216
|
*/
|
|
206
217
|
async function museRuntimeIssues() {
|
|
207
218
|
const bin = resolveMuseBin();
|
|
@@ -386,6 +386,14 @@ export function createGithubAdapter(exec = defaultExec) {
|
|
|
386
386
|
return null;
|
|
387
387
|
}
|
|
388
388
|
},
|
|
389
|
+
async hasPullRequestWorkflow({ cwd, baseRef }) {
|
|
390
|
+
try {
|
|
391
|
+
return await baseRefHasPullRequestWorkflow(cwd, baseRef);
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
},
|
|
389
397
|
async publishReview({ cwd, number, event, bodyFilePath }) {
|
|
390
398
|
const result = await runReview(exec, cwd, number, event, bodyFilePath);
|
|
391
399
|
if (result.ok || event === "COMMENT" || !OWN_PR_REVIEW_PATTERN.test(result.reason)) {
|
|
@@ -491,6 +499,64 @@ export async function fetchChecksFailureEvidence(exec, opts) {
|
|
|
491
499
|
details,
|
|
492
500
|
};
|
|
493
501
|
}
|
|
502
|
+
/**
|
|
503
|
+
* NOT-311: does `origin/<baseRef>` contain a workflow triggered by `pull_request`?
|
|
504
|
+
* False when the tree has no workflow files at all; null when the tree itself cannot
|
|
505
|
+
* be read (unknown base ref, unparsable git output) so the caller waits fail-closed.
|
|
506
|
+
*/
|
|
507
|
+
export async function baseRefHasPullRequestWorkflow(cwd, baseRef) {
|
|
508
|
+
let listing;
|
|
509
|
+
try {
|
|
510
|
+
// `-r`: without it ls-tree prints the workflows *directory* entry itself rather
|
|
511
|
+
// than the files under it, so the scan would never see a workflow (NOT-311).
|
|
512
|
+
listing = (await run("git", ["ls-tree", "-r", `origin/${baseRef}`, "--name-only", ".github/workflows"], { cwd })).stdout;
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
const files = listing
|
|
518
|
+
.split("\n")
|
|
519
|
+
.map((s) => s.trim())
|
|
520
|
+
.filter((f) => f && /\.(ya?ml)$/i.test(f));
|
|
521
|
+
if (files.length === 0)
|
|
522
|
+
return false;
|
|
523
|
+
for (const file of files) {
|
|
524
|
+
let content;
|
|
525
|
+
try {
|
|
526
|
+
content = (await run("git", ["show", `origin/${baseRef}:${file}`], { cwd })).stdout;
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
if (workflowHasPullRequestTrigger(content))
|
|
532
|
+
return true;
|
|
533
|
+
}
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Heuristic `on:` trigger scan: matches inline (`on: pull_request`,
|
|
538
|
+
* `on: [push, pull_request]`) and block forms. Stops at the next top-level key so a
|
|
539
|
+
* later `pull_request` mention outside `on:` (a job name, a comment) cannot qualify.
|
|
540
|
+
* Leans toward waiting — a false positive only costs CI-wait backoff, while a false
|
|
541
|
+
* negative would send an unverified PR to review.
|
|
542
|
+
*/
|
|
543
|
+
export function workflowHasPullRequestTrigger(content) {
|
|
544
|
+
const lines = content.split("\n");
|
|
545
|
+
const onIdx = lines.findIndex((l) => /^\s*on\s*:/.test(l));
|
|
546
|
+
if (onIdx < 0)
|
|
547
|
+
return false;
|
|
548
|
+
const onLine = lines[onIdx];
|
|
549
|
+
if (/\bpull_request\b/.test(onLine.slice(onLine.indexOf("on") + 2)))
|
|
550
|
+
return true;
|
|
551
|
+
for (let i = onIdx + 1; i < lines.length; i++) {
|
|
552
|
+
const line = lines[i];
|
|
553
|
+
if (/^\S/.test(line))
|
|
554
|
+
break;
|
|
555
|
+
if (/\bpull_request\b/.test(line.replace(/#.*$/, "")))
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
494
560
|
export const realGithubAdapter = createGithubAdapter();
|
|
495
561
|
const REVIEW_FLAG = {
|
|
496
562
|
APPROVE: "--approve",
|
|
@@ -527,25 +593,38 @@ const NONE_STREAK_REQUIRED = 2;
|
|
|
527
593
|
* between resets the streak); if the deadline is hit before that streak completes, the
|
|
528
594
|
* poll fails closed as "timeout" rather than silently waving the handoff through.
|
|
529
595
|
*/
|
|
530
|
-
export async function pollPrChecks(
|
|
596
|
+
export async function pollPrChecks(
|
|
597
|
+
// NOT-310: narrowed to what the poll actually calls — the merge-conflict sync
|
|
598
|
+
// passes only a checks reader, and full adapters still satisfy this shape.
|
|
599
|
+
adapter, opts) {
|
|
600
|
+
return (await pollPrChecksDetailed(adapter, opts)).result;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* NOT-311: same bounded poll as {@link pollPrChecks}, but reporting what was last
|
|
604
|
+
* seen and whether an abort cut the wait short. `pollPrChecks` keeps the collapsed
|
|
605
|
+
* contract for callers that only branch on the result.
|
|
606
|
+
*/
|
|
607
|
+
export async function pollPrChecksDetailed(adapter, opts) {
|
|
531
608
|
const deadline = Date.now() + opts.timeoutMs;
|
|
532
609
|
let noneStreak = 0;
|
|
610
|
+
let lastSnapshot = null;
|
|
533
611
|
for (;;) {
|
|
534
612
|
if (opts.signal?.aborted)
|
|
535
|
-
return "timeout";
|
|
613
|
+
return { result: "timeout", lastSnapshot, aborted: true };
|
|
536
614
|
const snapshot = await adapter.checksSnapshot({ cwd: opts.cwd, number: opts.number, branch: opts.branch });
|
|
615
|
+
lastSnapshot = snapshot;
|
|
537
616
|
if (snapshot === "failure" || snapshot === "success")
|
|
538
|
-
return snapshot;
|
|
617
|
+
return { result: snapshot, lastSnapshot, aborted: false };
|
|
539
618
|
if (snapshot === "none") {
|
|
540
619
|
noneStreak++;
|
|
541
620
|
if (noneStreak >= NONE_STREAK_REQUIRED)
|
|
542
|
-
return "none";
|
|
621
|
+
return { result: "none", lastSnapshot, aborted: false };
|
|
543
622
|
}
|
|
544
623
|
else {
|
|
545
624
|
noneStreak = 0;
|
|
546
625
|
}
|
|
547
626
|
if (Date.now() >= deadline)
|
|
548
|
-
return "timeout";
|
|
627
|
+
return { result: "timeout", lastSnapshot, aborted: false };
|
|
549
628
|
await new Promise((resolve) => setTimeout(resolve, Math.min(opts.intervalMs, deadline - Date.now())));
|
|
550
629
|
}
|
|
551
630
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// packages/server/src/adapters/github.test.ts
|
|
2
2
|
import { test } from "node:test";
|
|
3
3
|
import assert from "node:assert/strict";
|
|
4
|
-
import { parsePrView, summarizeChecks, pollPrChecks, createGithubAdapter, fetchChecksFailureEvidence, sanitizeCiText, sanitizeUrl, extractActionsRunId, buildFailureExcerpt, formatChecksFailureDetails, CHECKS_EVIDENCE_MAX_EXCERPT_CHARS, CHECKS_EVIDENCE_MAX_EXCERPT_LINES, CHECKS_EVIDENCE_MAX_RAW_LOG_CHARS_PER_RUN, CHECKS_EVIDENCE_GH_LOG_MAX_BUFFER, CHECKS_FAILURE_GENERIC_REASON, PR_VIEW_FIELDS, } from "./github.js";
|
|
4
|
+
import { parsePrView, summarizeChecks, pollPrChecks, pollPrChecksDetailed, baseRefHasPullRequestWorkflow, workflowHasPullRequestTrigger, createGithubAdapter, fetchChecksFailureEvidence, sanitizeCiText, sanitizeUrl, extractActionsRunId, buildFailureExcerpt, formatChecksFailureDetails, CHECKS_EVIDENCE_MAX_EXCERPT_CHARS, CHECKS_EVIDENCE_MAX_EXCERPT_LINES, CHECKS_EVIDENCE_MAX_RAW_LOG_CHARS_PER_RUN, CHECKS_EVIDENCE_GH_LOG_MAX_BUFFER, CHECKS_FAILURE_GENERIC_REASON, PR_VIEW_FIELDS, } from "./github.js";
|
|
5
5
|
test("parsePrView extracts the ground-truth handoff fields, including draft status", () => {
|
|
6
6
|
const view = parsePrView(JSON.stringify({
|
|
7
7
|
number: 7,
|
|
@@ -205,6 +205,88 @@ test("pollPrChecks bails out as timeout immediately when the lease signal is alr
|
|
|
205
205
|
});
|
|
206
206
|
assert.equal(result, "timeout");
|
|
207
207
|
});
|
|
208
|
+
test("NOT-311: pollPrChecksDetailed reports the last snapshot so a timeout can tell pending from abort", async () => {
|
|
209
|
+
const pending = await pollPrChecksDetailed(fakeAdapter(["pending", "pending", "pending", "pending"]), {
|
|
210
|
+
cwd: "/x",
|
|
211
|
+
timeoutMs: 20,
|
|
212
|
+
intervalMs: 10,
|
|
213
|
+
});
|
|
214
|
+
assert.deepEqual(pending, { result: "timeout", lastSnapshot: "pending", aborted: false });
|
|
215
|
+
const controller = new AbortController();
|
|
216
|
+
controller.abort();
|
|
217
|
+
const aborted = await pollPrChecksDetailed(fakeAdapter(["pending"]), {
|
|
218
|
+
cwd: "/x",
|
|
219
|
+
timeoutMs: 1000,
|
|
220
|
+
intervalMs: 5,
|
|
221
|
+
signal: controller.signal,
|
|
222
|
+
});
|
|
223
|
+
assert.deepEqual(aborted, { result: "timeout", lastSnapshot: null, aborted: true });
|
|
224
|
+
const none = await pollPrChecksDetailed(fakeAdapter(["none", "none"]), { cwd: "/x", timeoutMs: 1000, intervalMs: 5 });
|
|
225
|
+
assert.deepEqual(none, { result: "none", lastSnapshot: "none", aborted: false });
|
|
226
|
+
const incompleteNoneStreak = await pollPrChecksDetailed(fakeAdapter(["none"]), {
|
|
227
|
+
cwd: "/x",
|
|
228
|
+
timeoutMs: 0,
|
|
229
|
+
intervalMs: 10,
|
|
230
|
+
});
|
|
231
|
+
assert.deepEqual(incompleteNoneStreak, { result: "timeout", lastSnapshot: "none", aborted: false });
|
|
232
|
+
});
|
|
233
|
+
test("NOT-311: workflowHasPullRequestTrigger matches inline, list, and block on: forms", () => {
|
|
234
|
+
assert.equal(workflowHasPullRequestTrigger("on: pull_request\njobs:\n x:\n runs-on: ubuntu-latest\n"), true);
|
|
235
|
+
assert.equal(workflowHasPullRequestTrigger("on: [push, pull_request]\n"), true);
|
|
236
|
+
assert.equal(workflowHasPullRequestTrigger("on:\n push:\n branches: [main]\n pull_request:\n branches: [main]\n"), true);
|
|
237
|
+
assert.equal(workflowHasPullRequestTrigger("on:\n push:\n branches: [main]\n"), false);
|
|
238
|
+
assert.equal(workflowHasPullRequestTrigger("on: push\n"), false);
|
|
239
|
+
assert.equal(workflowHasPullRequestTrigger("on:\n schedule:\n - cron: '0 0 * * *'\n"), false);
|
|
240
|
+
assert.equal(workflowHasPullRequestTrigger("jobs:\n x:\n runs-on: ubuntu-latest\n"), false);
|
|
241
|
+
// A pull_request mention outside `on:` (job name) must not qualify.
|
|
242
|
+
assert.equal(workflowHasPullRequestTrigger("on: push\njobs:\n pull_request_preview:\n runs-on: ubuntu-latest\n"), false);
|
|
243
|
+
});
|
|
244
|
+
test("NOT-311: baseRefHasPullRequestWorkflow reads the base ref tree, false without workflows, null on unknown ref", async () => {
|
|
245
|
+
const { execFileSync } = await import("node:child_process");
|
|
246
|
+
const fs = await import("node:fs");
|
|
247
|
+
const os = await import("node:os");
|
|
248
|
+
const path = await import("node:path");
|
|
249
|
+
const setup = (files) => {
|
|
250
|
+
const repo = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-gh-wf-repo-"));
|
|
251
|
+
const git = (...args) => execFileSync("git", args, { cwd: repo, encoding: "utf8" });
|
|
252
|
+
git("init", "-q", "-b", "main");
|
|
253
|
+
git("config", "user.email", "test@example.com");
|
|
254
|
+
git("config", "user.name", "Test");
|
|
255
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
256
|
+
fs.mkdirSync(path.join(repo, path.dirname(rel)), { recursive: true });
|
|
257
|
+
fs.writeFileSync(path.join(repo, rel), content);
|
|
258
|
+
}
|
|
259
|
+
git("add", ".");
|
|
260
|
+
git("commit", "-q", "-m", "init");
|
|
261
|
+
const remote = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-gh-wf-remote-"));
|
|
262
|
+
execFileSync("git", ["init", "-q", "--bare", "-b", "main", remote]);
|
|
263
|
+
git("remote", "add", "origin", remote);
|
|
264
|
+
git("push", "-q", "origin", "main");
|
|
265
|
+
git("fetch", "-q", "origin");
|
|
266
|
+
return { repo, remote };
|
|
267
|
+
};
|
|
268
|
+
const withCi = setup({
|
|
269
|
+
"README.md": "hi\n",
|
|
270
|
+
".github/workflows/ci.yml": "name: ci\non:\n pull_request:\n branches: [main]\njobs:\n x:\n runs-on: ubuntu-latest\n",
|
|
271
|
+
});
|
|
272
|
+
const pushOnly = setup({
|
|
273
|
+
"README.md": "hi\n",
|
|
274
|
+
".github/workflows/nightly.yml": "name: nightly\non:\n schedule:\n - cron: '0 0 * * *'\njobs:\n x:\n runs-on: ubuntu-latest\n",
|
|
275
|
+
});
|
|
276
|
+
const bare = setup({ "README.md": "hi\n" });
|
|
277
|
+
try {
|
|
278
|
+
assert.equal(await baseRefHasPullRequestWorkflow(withCi.repo, "main"), true);
|
|
279
|
+
assert.equal(await baseRefHasPullRequestWorkflow(pushOnly.repo, "main"), false);
|
|
280
|
+
assert.equal(await baseRefHasPullRequestWorkflow(bare.repo, "main"), false);
|
|
281
|
+
assert.equal(await baseRefHasPullRequestWorkflow(bare.repo, "no-such-branch"), null);
|
|
282
|
+
}
|
|
283
|
+
finally {
|
|
284
|
+
for (const { repo, remote } of [withCi, pushOnly, bare]) {
|
|
285
|
+
fs.rmSync(repo, { recursive: true, force: true });
|
|
286
|
+
fs.rmSync(remote, { recursive: true, force: true });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
});
|
|
208
290
|
// --- NOT-252: terminal checks_failed enrichment ---
|
|
209
291
|
const HEAD_SHA = "abc123def456abc123def456abc123def456abcd";
|
|
210
292
|
function prViewWithRollup(rollup, headSha = HEAD_SHA) {
|