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
|
@@ -6,10 +6,11 @@ import fs from "node:fs";
|
|
|
6
6
|
import os from "node:os";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
process.env.AGENT_DEALER_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-muse-probe-"));
|
|
9
|
+
import { MUSE_CODE_CONTRIBUTOR_MODEL } from "@agent-dealer/shared";
|
|
9
10
|
import { MuseCapacityHost } from "./muse-host.js";
|
|
10
11
|
import { hasFreshMuseCapacityPair, isMusePaidFallbackEnabled, maybeProbeMuseCapacity, MUSE_CAPACITY_PROBE_SERVE_ARGV, MUSE_CAPACITY_REFRESH_ENV, resetMuseCapacityProbeStateForTests, runMuseCapacityProbe, } from "./muse-probe.js";
|
|
11
12
|
import { ingestMuseUsagePayload } from "./muse.js";
|
|
12
|
-
const { migrate } = await import("../db/index.js");
|
|
13
|
+
const { migrate, getDataDir } = await import("../db/index.js");
|
|
13
14
|
const { createAgent } = await import("../repository/agents.js");
|
|
14
15
|
const { clearAllCapacitySnapshots, listCapacitySnapshots } = await import("../repository/runtime-capacity.js");
|
|
15
16
|
const FAKE = new URL("./fixtures/fake-muse-serve.mjs", import.meta.url).pathname;
|
|
@@ -181,3 +182,189 @@ test("direct probe success requires the complete pair", async () => {
|
|
|
181
182
|
await host.shutdown();
|
|
182
183
|
}
|
|
183
184
|
});
|
|
185
|
+
// NOT-300: the probe log must explain the next failure. A stub host drives
|
|
186
|
+
// runMuseServeTurn to the exact turn outcome — no live model call.
|
|
187
|
+
function stubProbeHost(opts = {}) {
|
|
188
|
+
const sessionId = "sess-stub-1";
|
|
189
|
+
const session = { sessionId, modelId: MUSE_CODE_CONTRIBUTOR_MODEL };
|
|
190
|
+
let notifications = 0;
|
|
191
|
+
return {
|
|
192
|
+
ensureStarted: async () => {
|
|
193
|
+
if (opts.failStart)
|
|
194
|
+
throw opts.failStart;
|
|
195
|
+
return true;
|
|
196
|
+
},
|
|
197
|
+
execRequest: async (_id, method) => {
|
|
198
|
+
if (method === "session/start")
|
|
199
|
+
return { result: { session } };
|
|
200
|
+
if (method === "turn/start")
|
|
201
|
+
return { result: { turnId: "turn-stub-1" } };
|
|
202
|
+
if (method === "session/read") {
|
|
203
|
+
return { result: { session, history: { items: [] } } };
|
|
204
|
+
}
|
|
205
|
+
return { result: {} };
|
|
206
|
+
},
|
|
207
|
+
waitForHostNotification: async () => {
|
|
208
|
+
notifications += 1;
|
|
209
|
+
if (opts.hangTurnMs !== undefined && notifications === 1) {
|
|
210
|
+
await new Promise((r) => setTimeout(r, opts.hangTurnMs));
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
if (opts.turnParams !== undefined || opts.hangTurnMs === undefined) {
|
|
214
|
+
return {
|
|
215
|
+
method: "turn/completed",
|
|
216
|
+
params: opts.turnParams ??
|
|
217
|
+
{
|
|
218
|
+
sessionId,
|
|
219
|
+
terminal: "failed",
|
|
220
|
+
error: { kind: "other", message: "boom" },
|
|
221
|
+
durationMs: 982,
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
method: "turn/completed",
|
|
227
|
+
params: { sessionId, terminal: "cancelled", reason: "cancel requested" },
|
|
228
|
+
};
|
|
229
|
+
},
|
|
230
|
+
isConnected: () => true,
|
|
231
|
+
cancelExecTurn: async () => { },
|
|
232
|
+
readUsage: async () => ({ ok: false, reason: "missing" }),
|
|
233
|
+
shutdown: async () => { },
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function probeLogPath() {
|
|
237
|
+
return path.join(getDataDir(), "capacity", "muse-probe.log");
|
|
238
|
+
}
|
|
239
|
+
function probeLogSize() {
|
|
240
|
+
return fs.existsSync(probeLogPath()) ? fs.statSync(probeLogPath()).size : 0;
|
|
241
|
+
}
|
|
242
|
+
function probeLogLinesSince(bytes) {
|
|
243
|
+
const text = fs.existsSync(probeLogPath())
|
|
244
|
+
? fs.readFileSync(probeLogPath(), "utf8").slice(bytes)
|
|
245
|
+
: "";
|
|
246
|
+
return text
|
|
247
|
+
.split("\n")
|
|
248
|
+
.filter((l) => l.trim())
|
|
249
|
+
.map((l) => JSON.parse(l));
|
|
250
|
+
}
|
|
251
|
+
const PROBE_LOG_ALLOWED_KEYS = [
|
|
252
|
+
"admitted",
|
|
253
|
+
"durationMs",
|
|
254
|
+
"event",
|
|
255
|
+
"failureDetail",
|
|
256
|
+
"failureKind",
|
|
257
|
+
"model",
|
|
258
|
+
"ok",
|
|
259
|
+
"terminal",
|
|
260
|
+
"trigger",
|
|
261
|
+
"ts",
|
|
262
|
+
"windowsUpdated",
|
|
263
|
+
];
|
|
264
|
+
function assertProbeLogShape(line) {
|
|
265
|
+
assert.deepEqual(Object.keys(line).sort(), [...PROBE_LOG_ALLOWED_KEYS].sort());
|
|
266
|
+
const detail = line.failureDetail;
|
|
267
|
+
if (detail !== null) {
|
|
268
|
+
assert.deepEqual(Object.keys(detail).sort(), ["kind", "message"]);
|
|
269
|
+
}
|
|
270
|
+
const raw = JSON.stringify(line);
|
|
271
|
+
for (const secret of ["Reply with exactly", "transcript", "META_API_KEY", "password"]) {
|
|
272
|
+
assert.ok(!raw.includes(secret), `log line leaks ${secret}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
test("failed turn records failureDetail kind+message in the probe log", async () => {
|
|
276
|
+
const before = probeLogSize();
|
|
277
|
+
const stderr = [];
|
|
278
|
+
const origError = console.error;
|
|
279
|
+
console.error = (...args) => {
|
|
280
|
+
stderr.push(args.map(String).join(" "));
|
|
281
|
+
};
|
|
282
|
+
try {
|
|
283
|
+
const host = stubProbeHost();
|
|
284
|
+
const result = await runMuseCapacityProbe(Date.now(), { host, timeoutMs: 10_000 });
|
|
285
|
+
assert.equal(result.ok, false);
|
|
286
|
+
assert.equal(result.failureKind, "turn_failed");
|
|
287
|
+
assert.deepEqual(result.failureDetail, { kind: "other", message: "boom" });
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
console.error = origError;
|
|
291
|
+
}
|
|
292
|
+
assert.match(stderr.join("\n"), /fallback failed: turn_failed \(other: boom\)/);
|
|
293
|
+
const lines = probeLogLinesSince(before);
|
|
294
|
+
assert.equal(lines.length, 1);
|
|
295
|
+
assertProbeLogShape(lines[0]);
|
|
296
|
+
assert.equal(lines[0].failureKind, "turn_failed");
|
|
297
|
+
assert.deepEqual(lines[0].failureDetail, { kind: "other", message: "boom" });
|
|
298
|
+
});
|
|
299
|
+
test("thrown probe error records failureDetail with the error message", async () => {
|
|
300
|
+
const before = probeLogSize();
|
|
301
|
+
const host = stubProbeHost({ failStart: new Error("x") });
|
|
302
|
+
const result = await runMuseCapacityProbe(Date.now(), { host, timeoutMs: 10_000 });
|
|
303
|
+
assert.equal(result.ok, false);
|
|
304
|
+
assert.equal(result.failureKind, "exception");
|
|
305
|
+
assert.equal(result.failureDetail?.kind, "exception");
|
|
306
|
+
assert.ok(result.failureDetail?.message.includes("x"));
|
|
307
|
+
const lines = probeLogLinesSince(before);
|
|
308
|
+
assert.equal(lines.length, 1);
|
|
309
|
+
assertProbeLogShape(lines[0]);
|
|
310
|
+
assert.equal(lines[0].failureKind, "exception");
|
|
311
|
+
assert.ok(String(lines[0].failureDetail.message).includes("x"));
|
|
312
|
+
});
|
|
313
|
+
test("timed-out turn records the timeout failureDetail", async () => {
|
|
314
|
+
const before = probeLogSize();
|
|
315
|
+
const host = stubProbeHost({ hangTurnMs: 400 });
|
|
316
|
+
const result = await runMuseCapacityProbe(Date.now(), { host, timeoutMs: 200 });
|
|
317
|
+
assert.equal(result.ok, false);
|
|
318
|
+
assert.equal(result.failureKind, "timeout");
|
|
319
|
+
assert.deepEqual(result.failureDetail, {
|
|
320
|
+
kind: "other",
|
|
321
|
+
message: "turn timed out and was cancelled",
|
|
322
|
+
});
|
|
323
|
+
const lines = probeLogLinesSince(before);
|
|
324
|
+
assert.equal(lines.length, 1);
|
|
325
|
+
assertProbeLogShape(lines[0]);
|
|
326
|
+
assert.equal(lines[0].failureKind, "timeout");
|
|
327
|
+
assert.deepEqual(lines[0].failureDetail, result.failureDetail);
|
|
328
|
+
});
|
|
329
|
+
test("failureDetail message is truncated to 300 chars with controls stripped", async () => {
|
|
330
|
+
const before = probeLogSize();
|
|
331
|
+
const long = `AB\nCD\t${"x".repeat(1000)}\u0000\u001f END`;
|
|
332
|
+
const host = stubProbeHost({
|
|
333
|
+
turnParams: {
|
|
334
|
+
sessionId: "sess-stub-1",
|
|
335
|
+
terminal: "failed",
|
|
336
|
+
error: { kind: "other", message: long },
|
|
337
|
+
durationMs: 5,
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
const result = await runMuseCapacityProbe(Date.now(), { host, timeoutMs: 10_000 });
|
|
341
|
+
assert.equal(result.failureKind, "turn_failed");
|
|
342
|
+
const message = result.failureDetail?.message ?? "";
|
|
343
|
+
assert.equal(message.length, 300);
|
|
344
|
+
assert.ok(!/[\x00-\x1f\x7f]/.test(message));
|
|
345
|
+
assert.ok(message.startsWith("ABCD"));
|
|
346
|
+
const lines = probeLogLinesSince(before);
|
|
347
|
+
assert.equal(lines.length, 1);
|
|
348
|
+
assertProbeLogShape(lines[0]);
|
|
349
|
+
assert.equal(lines[0].failureDetail.message, message);
|
|
350
|
+
});
|
|
351
|
+
test("successful probe logs failureDetail null with existing fields unchanged", async () => {
|
|
352
|
+
const before = probeLogSize();
|
|
353
|
+
const host = new MuseCapacityHost(hostOpts("serve-turn-full"));
|
|
354
|
+
try {
|
|
355
|
+
const result = await runMuseCapacityProbe(Date.now(), { host, timeoutMs: 10_000 });
|
|
356
|
+
assert.equal(result.ok, true);
|
|
357
|
+
assert.equal(result.failureDetail, null);
|
|
358
|
+
}
|
|
359
|
+
finally {
|
|
360
|
+
await host.shutdown();
|
|
361
|
+
}
|
|
362
|
+
const lines = probeLogLinesSince(before);
|
|
363
|
+
assert.equal(lines.length, 1);
|
|
364
|
+
assertProbeLogShape(lines[0]);
|
|
365
|
+
assert.equal(lines[0].failureDetail, null);
|
|
366
|
+
assert.equal(lines[0].event, "muse_capacity_probe");
|
|
367
|
+
assert.equal(lines[0].trigger, "stale_60m");
|
|
368
|
+
assert.equal(lines[0].failureKind, null);
|
|
369
|
+
assert.equal(lines[0].ok, true);
|
|
370
|
+
});
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { admissionLimitOptions, getEffectiveMaxActiveIssues, getMaxActiveIssues, workerSpawnCeiling, } from "../repository/admission-settings.js";
|
|
11
11
|
import { checkAgentDeckHealth } from "../adapters/agent-deck.js";
|
|
12
12
|
import { healthForAgent } from "../adapters/agent-health.js";
|
|
13
|
+
import { ensureMuseCapabilityEscalation } from "../adapters/muse-capability.js";
|
|
13
14
|
import { getDb } from "../db/index.js";
|
|
14
15
|
import { getAgent } from "../repository/agents.js";
|
|
15
16
|
import { getIssue, listIssues } from "../repository/issues.js";
|
|
@@ -150,7 +151,19 @@ export async function checkRoleAgentHealthy(issue, role, ctx) {
|
|
|
150
151
|
if (!agent)
|
|
151
152
|
return { ok: false, reason: `${role} agent not found` };
|
|
152
153
|
const check = healthChecker ?? ((a, r) => defaultAgentHealth(a, r, ctx.deckOnline));
|
|
153
|
-
|
|
154
|
+
const result = await check(agent, role);
|
|
155
|
+
// NOT-308: a Muse verdict the gate cannot clear by itself (`missing`, or an exhausted
|
|
156
|
+
// `error`) must reach the operator as a human action on this issue — deduped per
|
|
157
|
+
// version and verdict kind, so repeated polls never pile up. Best-effort: escalation never fails health.
|
|
158
|
+
if (role === "developer" && agent.runtime === "muse_code") {
|
|
159
|
+
try {
|
|
160
|
+
ensureMuseCapabilityEscalation(issue.id);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// The gate already reported; a failed escalation must not flip it.
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
154
167
|
}
|
|
155
168
|
async function checkAgentsHealthy(issue, ctx) {
|
|
156
169
|
// Admit always starts a developer round — do not require reviewer health yet (NOT-156).
|
|
@@ -403,7 +403,10 @@ test("NOT-178: a Muse Code developer without credentials is refused at admission
|
|
|
403
403
|
});
|
|
404
404
|
// NOT-277: a Muse Code auto-update is re-validated automatically. A version whose developer
|
|
405
405
|
// shell/write check passes is admitted with no manual step; one that lost it is refused with the
|
|
406
|
-
// versions and capability named
|
|
406
|
+
// versions and capability named.
|
|
407
|
+
// NOT-308: an inconclusive check never blocks on a confirmed baseline (admitted, retried in
|
|
408
|
+
// the background); a confirmed loss escalates exactly one human action, and acknowledging
|
|
409
|
+
// it lifts the block.
|
|
407
410
|
test("NOT-277: a Muse Code update is admitted when capable, refused by name when not", async () => {
|
|
408
411
|
const { clearAgentHealthCaches, runtimeIssuesUncached } = await import("../adapters/agent-health.js");
|
|
409
412
|
const { setMuseCapabilityProbeForTests, settleMuseCapabilityCheckForTests } = await import("../adapters/muse-capability.js");
|
|
@@ -463,11 +466,22 @@ test("NOT-277: a Muse Code update is admitted when capable, refused by name when
|
|
|
463
466
|
const reason = getQueuedEntryForIssue(issue.id).waitReason;
|
|
464
467
|
assert.match(reason, /developer unhealthy/);
|
|
465
468
|
assert.match(reason, new RegExp(`Muse Code updated ${NEW} → ${BROKEN}: developer sessions no longer get shell/write access`));
|
|
469
|
+
// NOT-308: the confirmed loss escalates exactly one human action naming the update
|
|
470
|
+
// and the capability — and acknowledging it lifts the admission block.
|
|
471
|
+
const { listHumanActionsByRequestId } = await import("../repository/human-actions.js");
|
|
472
|
+
const { museCapabilityRequestId } = await import("../adapters/muse-capability.js");
|
|
473
|
+
const { resolveHumanActionAndAdvance } = await import("./commands.js");
|
|
474
|
+
const escalated = listHumanActionsByRequestId("muse_capability", museCapabilityRequestId(BROKEN, "missing"));
|
|
475
|
+
assert.equal(escalated.filter((a) => a.status === "open").length, 1);
|
|
476
|
+
assert.match(escalated[0].reason, new RegExp(`Muse Code updated ${NEW} → ${BROKEN}: developer sessions no longer get shell/write access`));
|
|
477
|
+
const acknowledged = resolveHumanActionAndAdvance(escalated[0].id, "test", "acknowledge");
|
|
478
|
+
assert.equal(acknowledged.ok, true);
|
|
479
|
+
assert.deepEqual(await developerHealth(), { ok: true }, "acknowledge lifts the block");
|
|
480
|
+
// NOT-308: an inconclusive check (the probe throws) does NOT block on the baseline.
|
|
466
481
|
await museReports("9.9.9-R1");
|
|
467
|
-
|
|
468
|
-
assert.equal(unverified.ok, false);
|
|
469
|
-
assert.match(unverified.reason, new RegExp(`Could not verify Muse Code developer shell/write access after version change \\(${BROKEN} → 9\\.9\\.9-R1\\)`));
|
|
482
|
+
assert.deepEqual(await developerHealth(), { ok: true });
|
|
470
483
|
assert.deepEqual(probed, [OLD, NEW, BROKEN, "9.9.9-R1"]);
|
|
484
|
+
assert.equal(listHumanActionsByRequestId("muse_capability", museCapabilityRequestId("9.9.9-R1", "unverified")).length, 0, "a first inconclusive result escalates nothing");
|
|
471
485
|
}
|
|
472
486
|
finally {
|
|
473
487
|
for (const [key, value] of Object.entries({
|
|
@@ -486,6 +500,185 @@ test("NOT-277: a Muse Code update is admitted when capable, refused by name when
|
|
|
486
500
|
setAdmissionHealthCheckerForTests(async () => ({ ok: true }));
|
|
487
501
|
}
|
|
488
502
|
});
|
|
503
|
+
// NOT-308: real-checker Muse harness — stub `muse --version`, stub gh login, stub the
|
|
504
|
+
// capability probe. Restores every override so later tests keep the unit-test skip.
|
|
505
|
+
async function withMuseHarness(probe, fn) {
|
|
506
|
+
const { clearAgentHealthCaches, invalidateMuseHealthCache, runtimeIssuesUncached } = await import("../adapters/agent-health.js");
|
|
507
|
+
const cap = await import("../adapters/muse-capability.js");
|
|
508
|
+
cap.resetMuseCapabilityStateForTests();
|
|
509
|
+
const stubDir = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-muse-stub-"));
|
|
510
|
+
const versionFile = path.join(stubDir, "version");
|
|
511
|
+
const stub = path.join(stubDir, "muse");
|
|
512
|
+
fs.writeFileSync(stub, `#!/bin/sh\ncat ${JSON.stringify(versionFile)}\n`);
|
|
513
|
+
fs.chmodSync(stub, 0o755);
|
|
514
|
+
const ghStub = path.join(stubDir, "gh");
|
|
515
|
+
fs.writeFileSync(ghStub, "#!/bin/sh\necho 'Logged in to github.com account dealer-test'\nexit 0\n");
|
|
516
|
+
fs.chmodSync(ghStub, 0o755);
|
|
517
|
+
cap.setMuseCapabilityProbeForTests(probe);
|
|
518
|
+
const saved = {
|
|
519
|
+
MUSE_CLI: process.env.MUSE_CLI,
|
|
520
|
+
META_API_KEY: process.env.META_API_KEY,
|
|
521
|
+
SKIP: process.env.AGENT_DEALER_SKIP_AGENT_HEALTH,
|
|
522
|
+
PATH: process.env.PATH,
|
|
523
|
+
};
|
|
524
|
+
process.env.MUSE_CLI = stub;
|
|
525
|
+
process.env.PATH = `${stubDir}${path.delimiter}${process.env.PATH ?? ""}`;
|
|
526
|
+
process.env.META_API_KEY = "k";
|
|
527
|
+
delete process.env.AGENT_DEALER_SKIP_AGENT_HEALTH;
|
|
528
|
+
setAdmissionHealthCheckerForTests(null);
|
|
529
|
+
clearAgentHealthCaches();
|
|
530
|
+
const writeVersion = (version) => {
|
|
531
|
+
fs.writeFileSync(versionFile, `Muse Code ${version.split("-")[0]} (${version})\n`);
|
|
532
|
+
};
|
|
533
|
+
try {
|
|
534
|
+
await fn({
|
|
535
|
+
versionFile,
|
|
536
|
+
show: writeVersion,
|
|
537
|
+
report: async (version) => {
|
|
538
|
+
writeVersion(version);
|
|
539
|
+
await runtimeIssuesUncached("muse_code");
|
|
540
|
+
await cap.settleMuseCapabilityCheckForTests();
|
|
541
|
+
},
|
|
542
|
+
reportUnsettled: async (version) => {
|
|
543
|
+
writeVersion(version);
|
|
544
|
+
// Targeted invalidation only: clearAgentHealthCaches would also reset the
|
|
545
|
+
// capability state under test.
|
|
546
|
+
invalidateMuseHealthCache();
|
|
547
|
+
return runtimeIssuesUncached("muse_code");
|
|
548
|
+
},
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
finally {
|
|
552
|
+
for (const [key, value] of Object.entries({
|
|
553
|
+
MUSE_CLI: saved.MUSE_CLI,
|
|
554
|
+
META_API_KEY: saved.META_API_KEY,
|
|
555
|
+
AGENT_DEALER_SKIP_AGENT_HEALTH: saved.SKIP,
|
|
556
|
+
PATH: saved.PATH,
|
|
557
|
+
})) {
|
|
558
|
+
if (value === undefined)
|
|
559
|
+
delete process.env[key];
|
|
560
|
+
else
|
|
561
|
+
process.env[key] = value;
|
|
562
|
+
}
|
|
563
|
+
cap.setMuseCapabilityProbeForTests(null);
|
|
564
|
+
clearAgentHealthCaches();
|
|
565
|
+
setAdmissionHealthCheckerForTests(async () => ({ ok: true }));
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
// NOT-308: while the new version is still being verified, developer admission is allowed
|
|
569
|
+
// on the confirmed baseline — no `runtime_capability` block while the check is in flight.
|
|
570
|
+
test("NOT-308: developer admission is allowed while the new version check is in flight", async () => {
|
|
571
|
+
const cap = await import("../adapters/muse-capability.js");
|
|
572
|
+
const OLD = "1.3.0-R3401.1";
|
|
573
|
+
const NEW = "1.4.0-R4161.1";
|
|
574
|
+
let release;
|
|
575
|
+
await withMuseHarness(async (version) => {
|
|
576
|
+
if (version === NEW) {
|
|
577
|
+
return new Promise((resolve) => {
|
|
578
|
+
release = () => resolve({ status: "capable" });
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
return { status: "capable" };
|
|
582
|
+
}, async ({ report, reportUnsettled }) => {
|
|
583
|
+
await report(OLD);
|
|
584
|
+
const issues = await reportUnsettled(NEW);
|
|
585
|
+
assert.deepEqual(issues.filter((i) => i.code === "runtime_capability"), [], "no runtime_capability block while verifying on a baseline");
|
|
586
|
+
assert.equal(cap.museCapabilityCheckInFlight(), true);
|
|
587
|
+
// The admission gate agrees: healthy, with the check still running.
|
|
588
|
+
const issue = readyIssue("muse-inflight", { runtimes: { dev: "muse_code", rev: "codex_local" } });
|
|
589
|
+
const { invalidateMuseHealthCache } = await import("../adapters/agent-health.js");
|
|
590
|
+
invalidateMuseHealthCache();
|
|
591
|
+
assert.deepEqual(await checkRoleAgentHealthy(issue, "developer", { deckOnline: true }), {
|
|
592
|
+
ok: true,
|
|
593
|
+
});
|
|
594
|
+
assert.equal(cap.museCapabilityCheckInFlight(), true);
|
|
595
|
+
release();
|
|
596
|
+
await cap.settleMuseCapabilityCheckForTests();
|
|
597
|
+
invalidateMuseHealthCache();
|
|
598
|
+
assert.deepEqual(await checkRoleAgentHealthy(issue, "developer", { deckOnline: true }), {
|
|
599
|
+
ok: true,
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
// NOT-308: three inconclusive polls escalate exactly one "could not verify" action and stop
|
|
604
|
+
// probing — admission never blocks.
|
|
605
|
+
test("NOT-308: three inconclusive polls escalate once, then stop probing, never blocking", async () => {
|
|
606
|
+
const cap = await import("../adapters/muse-capability.js");
|
|
607
|
+
const { invalidateMuseHealthCache } = await import("../adapters/agent-health.js");
|
|
608
|
+
const { listHumanActionsByRequestId } = await import("../repository/human-actions.js");
|
|
609
|
+
const OLD = "1.3.0-R3401.1";
|
|
610
|
+
const NEW = "1.4.0-R4161.1";
|
|
611
|
+
const probed = [];
|
|
612
|
+
await withMuseHarness(async (version) => {
|
|
613
|
+
probed.push(version);
|
|
614
|
+
if (version === NEW)
|
|
615
|
+
return { status: "error", detail: "probe session timed out" };
|
|
616
|
+
return { status: "capable" };
|
|
617
|
+
}, async ({ report, show }) => {
|
|
618
|
+
await report(OLD);
|
|
619
|
+
show(NEW);
|
|
620
|
+
const issue = readyIssue("muse-unverified", {
|
|
621
|
+
runtimes: { dev: "muse_code", rev: "codex_local" },
|
|
622
|
+
});
|
|
623
|
+
const poll = async () => {
|
|
624
|
+
// Targeted invalidation only: clearAgentHealthCaches would also reset the
|
|
625
|
+
// capability attempts under test.
|
|
626
|
+
invalidateMuseHealthCache();
|
|
627
|
+
const health = await checkRoleAgentHealthy(issue, "developer", { deckOnline: true });
|
|
628
|
+
await cap.settleMuseCapabilityCheckForTests();
|
|
629
|
+
return health;
|
|
630
|
+
};
|
|
631
|
+
const { ageMuseCapabilityCheckForTests } = await import("../adapters/muse-capability.js");
|
|
632
|
+
assert.deepEqual(await poll(), { ok: true }, "attempt 1: allowed");
|
|
633
|
+
ageMuseCapabilityCheckForTests(61_000);
|
|
634
|
+
assert.deepEqual(await poll(), { ok: true }, "attempt 2: allowed");
|
|
635
|
+
ageMuseCapabilityCheckForTests(122_000);
|
|
636
|
+
assert.deepEqual(await poll(), { ok: true }, "attempt 3: allowed");
|
|
637
|
+
assert.deepEqual(probed, [OLD, NEW, NEW, NEW]);
|
|
638
|
+
// The third poll settles exhausted, so the next polls escalate — exactly once.
|
|
639
|
+
assert.deepEqual(await poll(), { ok: true }, "exhausted: still allowed");
|
|
640
|
+
assert.deepEqual(await poll(), { ok: true }, "second post-exhaustion poll: still allowed");
|
|
641
|
+
const actions = listHumanActionsByRequestId("muse_capability", cap.museCapabilityRequestId(NEW, "unverified"));
|
|
642
|
+
assert.equal(actions.filter((a) => a.status === "open").length, 1);
|
|
643
|
+
assert.match(actions[0].reason, /Could not verify Muse Code developer shell\/write access/);
|
|
644
|
+
assert.match(actions[0].reason, new RegExp(`for ${NEW} after 3 inconclusive checks`));
|
|
645
|
+
// No fourth probe ever, however long the wait.
|
|
646
|
+
ageMuseCapabilityCheckForTests(10 * 60_000);
|
|
647
|
+
invalidateMuseHealthCache();
|
|
648
|
+
assert.deepEqual(await checkRoleAgentHealthy(issue, "developer", { deckOnline: true }), {
|
|
649
|
+
ok: true,
|
|
650
|
+
});
|
|
651
|
+
assert.deepEqual(probed, [OLD, NEW, NEW, NEW], "no further probes after exhaustion");
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
// NOT-308: with no confirmed baseline ever (fresh install), the unknown still fails
|
|
655
|
+
// closed — in flight and errored alike.
|
|
656
|
+
test("NOT-308: with no confirmed baseline, admission stays blocked while unconfirmed", async () => {
|
|
657
|
+
const cap = await import("../adapters/muse-capability.js");
|
|
658
|
+
const NEW = "1.4.0-R4161.1";
|
|
659
|
+
let release;
|
|
660
|
+
let calls = 0;
|
|
661
|
+
await withMuseHarness(async () => {
|
|
662
|
+
calls += 1;
|
|
663
|
+
return new Promise((resolve) => {
|
|
664
|
+
release = () => resolve({ status: "error", detail: "timed out" });
|
|
665
|
+
});
|
|
666
|
+
}, async ({ reportUnsettled }) => {
|
|
667
|
+
const issue = readyIssue("muse-fresh", { runtimes: { dev: "muse_code", rev: "codex_local" } });
|
|
668
|
+
const health = (i) => checkRoleAgentHealthy(i, "developer", { deckOnline: true });
|
|
669
|
+
await reportUnsettled(NEW);
|
|
670
|
+
assert.equal(cap.museCapabilityCheckInFlight(), true);
|
|
671
|
+
const blocked = await health(issue);
|
|
672
|
+
assert.equal(blocked.ok, false);
|
|
673
|
+
assert.match(blocked.reason, /verifying developer shell\/write access/);
|
|
674
|
+
release();
|
|
675
|
+
await cap.settleMuseCapabilityCheckForTests();
|
|
676
|
+
const errored = await health(issue);
|
|
677
|
+
assert.equal(errored.ok, false);
|
|
678
|
+
assert.match(errored.reason, /Could not verify Muse Code/);
|
|
679
|
+
assert.equal(calls, 1);
|
|
680
|
+
});
|
|
681
|
+
});
|
|
489
682
|
// NOT-178: Muse Code is developer-only, so it is never a healthy reviewer — even with the
|
|
490
683
|
// unit-test health skip on, and without probing the CLI.
|
|
491
684
|
test("NOT-178: a Muse Code reviewer is refused regardless of CLI health", async () => {
|