agent-dealer 1.2.3 → 1.2.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/adapters/agent-health.js +25 -2
- package/bundle/server/dist/adapters/agent-health.test.js +55 -2
- package/bundle/server/dist/adapters/github.js +110 -12
- package/bundle/server/dist/adapters/github.test.js +274 -3
- package/bundle/server/dist/adapters/muse-capability.js +330 -0
- package/bundle/server/dist/adapters/muse-capability.test.js +378 -0
- package/bundle/server/dist/capacity/claude-local-cache.js +222 -63
- package/bundle/server/dist/capacity/claude-local-cache.test.js +179 -30
- package/bundle/server/dist/capacity/muse-host.js +111 -23
- package/bundle/server/dist/capacity/muse-host.test.js +125 -1
- package/bundle/server/dist/capacity/muse-lifecycle.test.js +2 -3
- package/bundle/server/dist/capacity/muse-probe.js +250 -0
- package/bundle/server/dist/capacity/muse-probe.test.js +183 -0
- package/bundle/server/dist/capacity/muse.js +16 -6
- package/bundle/server/dist/coordinator/admission.test.js +85 -0
- package/bundle/server/dist/coordinator/commands.js +35 -8
- package/bundle/server/dist/coordinator/developer-effect.js +40 -6
- package/bundle/server/dist/coordinator/developer-effect.test.js +121 -12
- package/bundle/server/dist/coordinator/human-resolution.js +31 -1
- package/bundle/server/dist/coordinator/muse-spawn.js +12 -2
- package/bundle/server/dist/coordinator/prompts.js +15 -0
- package/bundle/server/dist/coordinator/prompts.test.js +20 -0
- package/bundle/server/dist/coordinator/spawn.js +7 -0
- package/bundle/server/dist/coordinator/worktree-cwd-guard.js +48 -0
- package/bundle/server/dist/coordinator/worktree-cwd-guard.test.js +83 -0
- package/bundle/server/dist/routes/human-actions.js +10 -1
- package/bundle/server/dist/routes/human-actions.test.js +117 -1
- package/bundle/server/dist/routes/index.js +17 -14
- package/bundle/server/dist/routes/runtime-capacity.test.js +11 -3
- package/bundle/server/dist/runners/muse-serve-session.js +7 -6
- package/bundle/server/package.json +2 -2
- package/bundle/server/static-ui/assets/{index-CYqRh_-S.css → index-BII-LgB8.css} +1 -1
- package/bundle/server/static-ui/assets/index-Bq8wWpZm.js +60 -0
- package/bundle/server/static-ui/index.html +2 -2
- package/bundle/shared/dist/agents.d.ts +15 -15
- package/bundle/shared/dist/agents.js +6 -0
- package/bundle/shared/dist/index.d.ts +7 -7
- package/bundle/shared/package.json +1 -1
- package/dist/doctor.d.ts +22 -3
- package/dist/doctor.js +46 -12
- package/dist/doctor.test.js +41 -10
- package/package.json +1 -1
- package/bundle/server/static-ui/assets/index-UO4lHZw4.js +0 -60
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
// packages/server/src/adapters/muse-capability.test.ts
|
|
2
|
+
//
|
|
3
|
+
// NOT-277: Muse Code developer shell/write capability is re-validated once per newly reported
|
|
4
|
+
// version — cached per version, auto-confirmed on success, blocked by name on loss, fail-closed
|
|
5
|
+
// when the check cannot complete. The real probe runs against fixtures/fake-muse.mjs (never Meta).
|
|
6
|
+
import { describe, test, beforeEach } from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
process.env.AGENT_DEALER_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-muse-capability-"));
|
|
13
|
+
const { museCapabilityIssues, museCapabilityCheckInFlight, parseMuseVersion, settleMuseCapabilityCheckForTests, setMuseCapabilityProbeForTests, resetMuseCapabilityStateForTests, ageMuseCapabilityCheckForTests, defaultMuseCapabilityProbe, } = await import("./muse-capability.js");
|
|
14
|
+
const OLD = "1.3.0-R3401.1";
|
|
15
|
+
const NEW = "1.4.0-R4161.1";
|
|
16
|
+
const STATE = path.join(process.env.AGENT_DEALER_HOME, "muse-capability.json");
|
|
17
|
+
/** Probe stub answering from `results` per version and counting calls per version. */
|
|
18
|
+
function stubProbe(results) {
|
|
19
|
+
const calls = new Map();
|
|
20
|
+
setMuseCapabilityProbeForTests(async (version) => {
|
|
21
|
+
calls.set(version, (calls.get(version) ?? 0) + 1);
|
|
22
|
+
const r = results[version];
|
|
23
|
+
if (!r)
|
|
24
|
+
throw new Error(`no stub result for ${version}`);
|
|
25
|
+
if (r instanceof Error)
|
|
26
|
+
throw r;
|
|
27
|
+
return r;
|
|
28
|
+
});
|
|
29
|
+
return calls;
|
|
30
|
+
}
|
|
31
|
+
/** One health read for `version`, letting any check it starts settle. */
|
|
32
|
+
async function check(version) {
|
|
33
|
+
const first = museCapabilityIssues(version);
|
|
34
|
+
await settleMuseCapabilityCheckForTests();
|
|
35
|
+
return { first, settled: museCapabilityIssues(version) };
|
|
36
|
+
}
|
|
37
|
+
describe("muse-capability", { concurrency: false }, () => {
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
resetMuseCapabilityStateForTests();
|
|
40
|
+
setMuseCapabilityProbeForTests(null);
|
|
41
|
+
});
|
|
42
|
+
test("parseMuseVersion reads the build id from `muse --version`", () => {
|
|
43
|
+
assert.equal(parseMuseVersion("Muse Code 1.3.0 (1.3.0-R3401.1)\n"), OLD);
|
|
44
|
+
assert.equal(parseMuseVersion("1.4.0-R4161.1\n"), NEW);
|
|
45
|
+
assert.equal(parseMuseVersion(" \n"), null);
|
|
46
|
+
});
|
|
47
|
+
test("a version not yet checked blocks while its one-time check runs (never assumed capable)", async () => {
|
|
48
|
+
let release;
|
|
49
|
+
setMuseCapabilityProbeForTests(() => new Promise((resolve) => (release = () => resolve({ status: "capable" }))));
|
|
50
|
+
const issues = museCapabilityIssues(NEW);
|
|
51
|
+
assert.deepEqual(issues.map((i) => i.code), ["runtime_capability"]);
|
|
52
|
+
assert.match(issues[0].message, /verifying developer shell\/write access/);
|
|
53
|
+
assert.equal(museCapabilityCheckInFlight(), true);
|
|
54
|
+
// A second read while in flight does not start a second probe.
|
|
55
|
+
museCapabilityIssues(NEW);
|
|
56
|
+
release();
|
|
57
|
+
await settleMuseCapabilityCheckForTests();
|
|
58
|
+
assert.equal(museCapabilityCheckInFlight(), false);
|
|
59
|
+
assert.deepEqual(museCapabilityIssues(NEW), []);
|
|
60
|
+
});
|
|
61
|
+
// (a) same version as last check → no re-probe, cached result reused.
|
|
62
|
+
test("the same version is checked once; later reads reuse the cached result", async () => {
|
|
63
|
+
const calls = stubProbe({ [OLD]: { status: "capable" } });
|
|
64
|
+
assert.deepEqual((await check(OLD)).settled, []);
|
|
65
|
+
for (let i = 0; i < 5; i++)
|
|
66
|
+
assert.deepEqual(museCapabilityIssues(OLD), []);
|
|
67
|
+
assert.equal(museCapabilityCheckInFlight(), false);
|
|
68
|
+
assert.equal(calls.get(OLD), 1);
|
|
69
|
+
});
|
|
70
|
+
test("the cached result survives a restart (persisted), so a restart does not re-probe", async () => {
|
|
71
|
+
const calls = stubProbe({ [OLD]: { status: "capable" } });
|
|
72
|
+
await check(OLD);
|
|
73
|
+
// Drop in-memory state only, as a fresh process would start.
|
|
74
|
+
const persisted = fs.readFileSync(STATE, "utf8");
|
|
75
|
+
resetMuseCapabilityStateForTests();
|
|
76
|
+
fs.writeFileSync(STATE, persisted);
|
|
77
|
+
assert.deepEqual(museCapabilityIssues(OLD), []);
|
|
78
|
+
assert.equal(museCapabilityCheckInFlight(), false);
|
|
79
|
+
assert.equal(calls.get(OLD), 1);
|
|
80
|
+
});
|
|
81
|
+
// (b) new version, capability confirmed → recorded capable, admission proceeds, no manual step.
|
|
82
|
+
test("a new version that passes the check is recorded as the confirmed baseline automatically", async () => {
|
|
83
|
+
const calls = stubProbe({ [OLD]: { status: "capable" }, [NEW]: { status: "capable" } });
|
|
84
|
+
await check(OLD);
|
|
85
|
+
const { first, settled } = await check(NEW);
|
|
86
|
+
assert.match(first[0].message, new RegExp(`Muse Code updated ${OLD} → ${NEW}: verifying`));
|
|
87
|
+
assert.deepEqual(settled, []);
|
|
88
|
+
assert.equal(calls.get(NEW), 1);
|
|
89
|
+
const saved = JSON.parse(fs.readFileSync(STATE, "utf8"));
|
|
90
|
+
assert.equal(saved.confirmedVersion, NEW);
|
|
91
|
+
assert.equal(saved.lastChecked.version, NEW);
|
|
92
|
+
assert.equal(saved.lastChecked.status, "capable");
|
|
93
|
+
});
|
|
94
|
+
// (c) new version, capability missing → blocked with the named versions + capability.
|
|
95
|
+
test("a new version that lost shell/write is blocked with both versions and the capability named", async () => {
|
|
96
|
+
const calls = stubProbe({
|
|
97
|
+
[OLD]: { status: "capable" },
|
|
98
|
+
[NEW]: { status: "missing", detail: "probe session completed without running its shell command" },
|
|
99
|
+
});
|
|
100
|
+
await check(OLD);
|
|
101
|
+
const { settled } = await check(NEW);
|
|
102
|
+
assert.deepEqual(settled.map((i) => i.code), ["runtime_capability"]);
|
|
103
|
+
assert.match(settled[0].message, new RegExp(`^Muse Code updated ${OLD} → ${NEW}: developer sessions no longer get shell/write access`));
|
|
104
|
+
assert.match(settled[0].message, /developer admission blocked/);
|
|
105
|
+
// Stays blocked on this version without re-probing; the baseline is still the old version.
|
|
106
|
+
assert.deepEqual(museCapabilityIssues(NEW), settled);
|
|
107
|
+
assert.equal(calls.get(NEW), 1);
|
|
108
|
+
assert.equal(JSON.parse(fs.readFileSync(STATE, "utf8")).confirmedVersion, OLD);
|
|
109
|
+
});
|
|
110
|
+
test("a later update that restores the capability unblocks by itself", async () => {
|
|
111
|
+
const FIXED = "1.4.0-R4302.1";
|
|
112
|
+
stubProbe({
|
|
113
|
+
[OLD]: { status: "capable" },
|
|
114
|
+
[NEW]: { status: "missing", detail: "no shell" },
|
|
115
|
+
[FIXED]: { status: "capable" },
|
|
116
|
+
});
|
|
117
|
+
await check(OLD);
|
|
118
|
+
await check(NEW);
|
|
119
|
+
assert.deepEqual((await check(FIXED)).settled, []);
|
|
120
|
+
});
|
|
121
|
+
// (d) the check itself errors / times out → blocked with a distinct "could not verify" message.
|
|
122
|
+
for (const [label, result] of [
|
|
123
|
+
["throws", new Error("spawn EACCES")],
|
|
124
|
+
["times out", { status: "error", detail: `probe session on ${NEW} timed out` }],
|
|
125
|
+
]) {
|
|
126
|
+
test(`a check that ${label} fails closed with a distinct "could not verify" message`, async () => {
|
|
127
|
+
const calls = stubProbe({ [OLD]: { status: "capable" }, [NEW]: result });
|
|
128
|
+
await check(OLD);
|
|
129
|
+
const { settled } = await check(NEW);
|
|
130
|
+
assert.deepEqual(settled.map((i) => i.code), ["runtime_capability"]);
|
|
131
|
+
assert.match(settled[0].message, new RegExp(`^Could not verify Muse Code developer shell/write access after version change \\(${OLD} → ${NEW}\\)`));
|
|
132
|
+
assert.doesNotMatch(settled[0].message, /no longer get/);
|
|
133
|
+
// Not re-probed before the retry backoff, and the block stays up meanwhile.
|
|
134
|
+
assert.deepEqual(museCapabilityIssues(NEW), settled);
|
|
135
|
+
assert.equal(calls.get(NEW), 1);
|
|
136
|
+
assert.equal(JSON.parse(fs.readFileSync(STATE, "utf8")).confirmedVersion, OLD);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
test("a could-not-verify result is retried after the backoff, staying blocked until it passes", async () => {
|
|
140
|
+
let next = { status: "error", detail: "timed out" };
|
|
141
|
+
let calls = 0;
|
|
142
|
+
setMuseCapabilityProbeForTests(async () => {
|
|
143
|
+
calls += 1;
|
|
144
|
+
return next;
|
|
145
|
+
});
|
|
146
|
+
await check(NEW);
|
|
147
|
+
next = { status: "capable" };
|
|
148
|
+
ageMuseCapabilityCheckForTests(11 * 60_000);
|
|
149
|
+
const retrying = museCapabilityIssues(NEW);
|
|
150
|
+
assert.match(retrying[0].message, /Could not verify/);
|
|
151
|
+
await settleMuseCapabilityCheckForTests();
|
|
152
|
+
assert.equal(calls, 2);
|
|
153
|
+
assert.deepEqual(museCapabilityIssues(NEW), []);
|
|
154
|
+
});
|
|
155
|
+
test("consecutive updates name the exact previous version, not the last confirmed baseline", async () => {
|
|
156
|
+
const LATER = "1.4.0-R4302.1";
|
|
157
|
+
stubProbe({
|
|
158
|
+
[OLD]: { status: "capable" },
|
|
159
|
+
[NEW]: { status: "missing", detail: "no shell" },
|
|
160
|
+
[LATER]: { status: "missing", detail: "no shell" },
|
|
161
|
+
});
|
|
162
|
+
await check(OLD);
|
|
163
|
+
await check(NEW);
|
|
164
|
+
const { first, settled } = await check(LATER);
|
|
165
|
+
assert.match(first[0].message, new RegExp(`^Muse Code updated ${NEW} → ${LATER}: verifying`));
|
|
166
|
+
assert.match(settled[0].message, new RegExp(`^Muse Code updated ${NEW} → ${LATER}: developer sessions no longer get`));
|
|
167
|
+
assert.doesNotMatch(settled[0].message, new RegExp(OLD.replace(/\./g, "\\.")));
|
|
168
|
+
assert.equal(JSON.parse(fs.readFileSync(STATE, "utf8")).confirmedVersion, OLD);
|
|
169
|
+
});
|
|
170
|
+
test("consecutive could-not-verify updates also name the exact previous version", async () => {
|
|
171
|
+
const LATER = "1.4.0-R4302.1";
|
|
172
|
+
stubProbe({
|
|
173
|
+
[OLD]: { status: "capable" },
|
|
174
|
+
[NEW]: { status: "error", detail: "timed out" },
|
|
175
|
+
[LATER]: { status: "error", detail: "timed out" },
|
|
176
|
+
});
|
|
177
|
+
await check(OLD);
|
|
178
|
+
await check(NEW);
|
|
179
|
+
const { settled } = await check(LATER);
|
|
180
|
+
assert.match(settled[0].message, new RegExp(`after version change \\(${NEW} → ${LATER}\\)`));
|
|
181
|
+
});
|
|
182
|
+
test("a version reported mid-probe is checked once, after the running probe, and the stale verdict is discarded", async () => {
|
|
183
|
+
const LATER = "1.4.0-R4302.1";
|
|
184
|
+
const calls = [];
|
|
185
|
+
const release = new Map();
|
|
186
|
+
setMuseCapabilityProbeForTests((version) => new Promise((resolve) => {
|
|
187
|
+
calls.push(version);
|
|
188
|
+
release.set(version, () => resolve(version === NEW ? { status: "capable" } : { status: "missing", detail: "no shell" }));
|
|
189
|
+
}));
|
|
190
|
+
museCapabilityIssues(NEW);
|
|
191
|
+
assert.deepEqual(calls, [NEW]);
|
|
192
|
+
// Muse updates again while NEW is still being probed: no second concurrent probe.
|
|
193
|
+
const during = museCapabilityIssues(LATER);
|
|
194
|
+
assert.match(during[0].message, new RegExp(`^Muse Code updated ${NEW} → ${LATER}: verifying`));
|
|
195
|
+
museCapabilityIssues(LATER);
|
|
196
|
+
assert.deepEqual(calls, [NEW]);
|
|
197
|
+
// NEW's verdict lands after Muse moved on: discarded, and LATER is probed next (once).
|
|
198
|
+
release.get(NEW)();
|
|
199
|
+
await new Promise((r) => setImmediate(r));
|
|
200
|
+
assert.deepEqual(calls, [NEW, LATER]);
|
|
201
|
+
let saved = JSON.parse(fs.readFileSync(STATE, "utf8"));
|
|
202
|
+
assert.equal(saved.confirmedVersion, null);
|
|
203
|
+
assert.equal(saved.lastChecked, null);
|
|
204
|
+
assert.equal(saved.current.version, LATER);
|
|
205
|
+
release.get(LATER)();
|
|
206
|
+
await settleMuseCapabilityCheckForTests();
|
|
207
|
+
const settled = museCapabilityIssues(LATER);
|
|
208
|
+
assert.match(settled[0].message, new RegExp(`^Muse Code updated ${NEW} → ${LATER}: developer sessions no longer get`));
|
|
209
|
+
for (let i = 0; i < 3; i++)
|
|
210
|
+
museCapabilityIssues(LATER);
|
|
211
|
+
assert.equal(museCapabilityCheckInFlight(), false);
|
|
212
|
+
assert.deepEqual(calls, [NEW, LATER]);
|
|
213
|
+
saved = JSON.parse(fs.readFileSync(STATE, "utf8"));
|
|
214
|
+
assert.equal(saved.lastChecked.version, LATER);
|
|
215
|
+
});
|
|
216
|
+
test("onSettled fires once the check lands so the caller can drop its health cache", async () => {
|
|
217
|
+
stubProbe({ [NEW]: { status: "capable" } });
|
|
218
|
+
let settled = 0;
|
|
219
|
+
museCapabilityIssues(NEW, () => (settled += 1));
|
|
220
|
+
await settleMuseCapabilityCheckForTests();
|
|
221
|
+
await new Promise((r) => setImmediate(r));
|
|
222
|
+
assert.equal(settled, 1);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
// The real probe: one developer-posture session against the fake Muse, always a fresh `muse exec`.
|
|
226
|
+
describe("defaultMuseCapabilityProbe (fake muse)", { concurrency: false }, () => {
|
|
227
|
+
const FAKE_MUSE = path.join(path.dirname(fileURLToPath(import.meta.url)), "../coordinator/fixtures/fake-muse.mjs");
|
|
228
|
+
async function probeWith(scenario, extraEnv = {}, opts = {}) {
|
|
229
|
+
const env = {
|
|
230
|
+
MUSE_CLI: FAKE_MUSE,
|
|
231
|
+
FAKE_MUSE_SCENARIO: scenario,
|
|
232
|
+
FAKE_MUSE_VERSION: NEW,
|
|
233
|
+
...extraEnv,
|
|
234
|
+
};
|
|
235
|
+
const keys = [...Object.keys(env), "AGENT_DEALER_MUSE_RUNNER"];
|
|
236
|
+
const prev = Object.fromEntries(keys.map((k) => [k, process.env[k]]));
|
|
237
|
+
Object.assign(process.env, env);
|
|
238
|
+
// The serve lane is the production default; the probe must bypass it on its own.
|
|
239
|
+
delete process.env.AGENT_DEALER_MUSE_RUNNER;
|
|
240
|
+
try {
|
|
241
|
+
return await defaultMuseCapabilityProbe(NEW, opts);
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
for (const k of keys) {
|
|
245
|
+
if (prev[k] === undefined)
|
|
246
|
+
delete process.env[k];
|
|
247
|
+
else
|
|
248
|
+
process.env[k] = prev[k];
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
test("a session that runs its shell command is capable", async () => {
|
|
253
|
+
assert.deepEqual(await probeWith("capability-shell"), { status: "capable" });
|
|
254
|
+
});
|
|
255
|
+
test("a session that completes without a shell call is missing shell/write", async () => {
|
|
256
|
+
const result = await probeWith("capability-no-shell");
|
|
257
|
+
assert.equal(result.status, "missing");
|
|
258
|
+
});
|
|
259
|
+
test("a session that fails to run at all is an error (could not verify), not missing", async () => {
|
|
260
|
+
const result = await probeWith("auth");
|
|
261
|
+
assert.equal(result.status, "error");
|
|
262
|
+
});
|
|
263
|
+
test("a session that ran the shell but then timed out is could-not-verify, never capable", async () => {
|
|
264
|
+
const result = await probeWith("capability-shell-then-hang", {}, { timeoutMs: 1500 });
|
|
265
|
+
assert.equal(result.status, "error");
|
|
266
|
+
assert.match(result.detail, /timed out/);
|
|
267
|
+
});
|
|
268
|
+
test("a session that ran the shell but then failed is could-not-verify, never capable", async () => {
|
|
269
|
+
const result = await probeWith("capability-shell-then-fail");
|
|
270
|
+
assert.equal(result.status, "error");
|
|
271
|
+
assert.match(result.detail, /probe session (failed|exited)/);
|
|
272
|
+
});
|
|
273
|
+
test("the probe never runs on the shared serve host (it may still be the pre-update build)", async () => {
|
|
274
|
+
const { getMuseCapacityHost, resetMuseCapacityHostForTests } = await import("../capacity/muse-host.js");
|
|
275
|
+
await resetMuseCapacityHostForTests();
|
|
276
|
+
let serveSpawns = 0;
|
|
277
|
+
getMuseCapacityHost({
|
|
278
|
+
spawnImpl: (() => {
|
|
279
|
+
serveSpawns += 1;
|
|
280
|
+
throw new Error("serve host must not be used by the capability probe");
|
|
281
|
+
}),
|
|
282
|
+
});
|
|
283
|
+
try {
|
|
284
|
+
assert.deepEqual(await probeWith("capability-shell"), { status: "capable" });
|
|
285
|
+
assert.equal(serveSpawns, 0);
|
|
286
|
+
}
|
|
287
|
+
finally {
|
|
288
|
+
await resetMuseCapacityHostForTests();
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
test("a binary that changes version during the probe is could-not-verify, not capable", async () => {
|
|
292
|
+
const versionFile = path.join(fs.mkdtempSync(path.join(os.tmpdir(), "dealer-muse-ver-")), "version");
|
|
293
|
+
fs.writeFileSync(versionFile, NEW);
|
|
294
|
+
const result = await probeWith("capability-shell", {
|
|
295
|
+
FAKE_MUSE_VERSION_FILE: versionFile,
|
|
296
|
+
FAKE_MUSE_UPDATE_TO: "1.5.0-R5000.1",
|
|
297
|
+
});
|
|
298
|
+
assert.equal(result.status, "error");
|
|
299
|
+
assert.match(result.detail, /1\.5\.0-R5000\.1 after probing 1\.4\.0-R4161\.1/);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
// Admission checks the on-disk binary; developer work must not then run on a serve host that is
|
|
303
|
+
// still an older (possibly broken) build. A capable → B broken → C capable, host restarted onto B.
|
|
304
|
+
describe("developer sessions run only on the capability-checked build", { concurrency: false }, () => {
|
|
305
|
+
const FAKE_MUSE = path.join(path.dirname(fileURLToPath(import.meta.url)), "../coordinator/fixtures/fake-muse.mjs");
|
|
306
|
+
const B = NEW;
|
|
307
|
+
const C = "1.4.0-R4302.1";
|
|
308
|
+
/** One developer session with the shared host reporting `hostVersion`; returns the RPCs sent to it. */
|
|
309
|
+
async function sessionWithHostOn(hostVersion) {
|
|
310
|
+
const { getMuseCapacityHost, resetMuseCapacityHostForTests } = await import("../capacity/muse-host.js");
|
|
311
|
+
const { runMuseDeveloperSession } = await import("../coordinator/muse-spawn.js");
|
|
312
|
+
await resetMuseCapacityHostForTests();
|
|
313
|
+
const host = getMuseCapacityHost({
|
|
314
|
+
command: process.execPath,
|
|
315
|
+
args: [path.join(path.dirname(fileURLToPath(import.meta.url)), "../capacity/fixtures/fake-muse-serve.mjs")],
|
|
316
|
+
env: { META_API_KEY: "test-fake-key", FAKE_MSP_MODE: "persistent-full" },
|
|
317
|
+
readVersion: async () => hostVersion,
|
|
318
|
+
});
|
|
319
|
+
const sent = [];
|
|
320
|
+
const execRequest = host.execRequest.bind(host);
|
|
321
|
+
host.execRequest = (id, method, params, timeoutMs) => {
|
|
322
|
+
sent.push(method);
|
|
323
|
+
return execRequest(id, method, params, timeoutMs);
|
|
324
|
+
};
|
|
325
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-muse-bound-"));
|
|
326
|
+
const keys = ["MUSE_CLI", "FAKE_MUSE_SCENARIO", "FAKE_MUSE_VERSION", "AGENT_DEALER_MUSE_RUNNER"];
|
|
327
|
+
const prev = Object.fromEntries(keys.map((k) => [k, process.env[k]]));
|
|
328
|
+
Object.assign(process.env, { MUSE_CLI: FAKE_MUSE, FAKE_MUSE_SCENARIO: "success", FAKE_MUSE_VERSION: C });
|
|
329
|
+
delete process.env.AGENT_DEALER_MUSE_RUNNER;
|
|
330
|
+
try {
|
|
331
|
+
const { execFileSync } = await import("node:child_process");
|
|
332
|
+
execFileSync("git", ["init", "-q"], { cwd: dir });
|
|
333
|
+
const run = await runMuseDeveloperSession({
|
|
334
|
+
sessionId: "bound-session",
|
|
335
|
+
runtime: "muse_code",
|
|
336
|
+
policy: {},
|
|
337
|
+
model: null,
|
|
338
|
+
prompt: "implement",
|
|
339
|
+
cwd: dir,
|
|
340
|
+
timeoutMs: 30_000,
|
|
341
|
+
logPath: path.join(dir, "session.ndjson"),
|
|
342
|
+
});
|
|
343
|
+
assert.equal(run.exitCode, 0);
|
|
344
|
+
return sent;
|
|
345
|
+
}
|
|
346
|
+
finally {
|
|
347
|
+
for (const k of keys) {
|
|
348
|
+
if (prev[k] === undefined)
|
|
349
|
+
delete process.env[k];
|
|
350
|
+
else
|
|
351
|
+
process.env[k] = prev[k];
|
|
352
|
+
}
|
|
353
|
+
await resetMuseCapacityHostForTests();
|
|
354
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
beforeEach(() => {
|
|
358
|
+
resetMuseCapabilityStateForTests();
|
|
359
|
+
stubProbe({ [OLD]: { status: "capable" }, [B]: { status: "missing", detail: "no shell" }, [C]: { status: "capable" } });
|
|
360
|
+
});
|
|
361
|
+
test("C confirmed but the host still runs broken B: the session never reaches the host", async () => {
|
|
362
|
+
await check(OLD);
|
|
363
|
+
await check(B);
|
|
364
|
+
const { settled } = await check(C);
|
|
365
|
+
assert.deepEqual(settled, []);
|
|
366
|
+
assert.deepEqual(await sessionWithHostOn(B), [], "work ran on the checked exec lane, not the B host");
|
|
367
|
+
});
|
|
368
|
+
test("a host running the confirmed version serves the session", async () => {
|
|
369
|
+
await check(C);
|
|
370
|
+
const sent = await sessionWithHostOn(C);
|
|
371
|
+
assert.ok(sent.includes("session/start"), "serve lane used when the host runs the checked build");
|
|
372
|
+
});
|
|
373
|
+
test("while the reported version is not confirmed, no session reaches the host", async () => {
|
|
374
|
+
await check(OLD);
|
|
375
|
+
await check(B);
|
|
376
|
+
assert.deepEqual(await sessionWithHostOn(OLD), []);
|
|
377
|
+
});
|
|
378
|
+
});
|