feat(harness): meta-harness and PM launching off T3 (#1523, row 41)
Slice 1 S6. packages/harness runs a role session under the pi or claude
adapter behind a tool gate, and packages/seat holds the session and its
process. The bus host gains a launch socket: `mosaic bus start
<business> --pm <harness>:<model>` runs the PM without a window, the PM
launches role sessions through the broker within the business file's
`launch.max`, and every launch, refusal and end is a launch-log line and
a broker event. `mosaic talk`, `mosaic launches off|on|list` and
`mosaic stop <run>` are new CLI verbs. packages/bus gains the IPC ops
identity, authorize, refuse, endLaunch and credentialStatus, plus
Broker.endLaunch (decision 77); no socket verb, event kind or schema
changed. The session environment is an allowlist, and the runner exits
before claiming if a founder credential reaches it.
Filbert built it (agents/filbert/work/s6). Darkwing reviewed on #1523:
round 1 changes (26991/26995), round 2 changes (27004/27010), round 3
changes (27012/27032), round 4 approve (27033/27034) on manifest
08a78972, 42 paths.
Sage's gate on 64b0f9f1 (12:32Z), sequential and teed: node suites
business 60, bus 74, cli 83, control-board 124, conversation 182,
discord 178, harness 56, ledger 78, mosaic 69, queue 149, runs 41,
seat 27, tasks 51, webui 39, every one 0 failed; test-auth 15,
test-conductor 17, test-config 24, test-discord 66,
test-extension-package 18, test-foundation 44, test-queue 27,
test-release 14, test-task 98 and 26 without Docker, all 0 failed.
64b0f9f1..ee7f909b touches none of the 42 paths. It landed after the
Q14 hold ended at 2026-10-11T17:19:23.857Z. The recorded PM-to-coder
run on a scratch data root is still owed before row 41 moves to done.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -203,6 +203,88 @@ test("a second host for the same data root refuses with exit 3 while the first r
|
||||
assert.equal(await host.close(0), 0);
|
||||
});
|
||||
|
||||
test("a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1", { timeout: 30000 }, async (t) => {
|
||||
const root = tmp(t);
|
||||
const f = fixture(root);
|
||||
makeDeployment(root);
|
||||
const boot = bootConfig({ system: loadSystem({ env: f.env }), businessId: "acme", env: f.env });
|
||||
const logs = [];
|
||||
const host = await startHost({ boot, business: "acme", requestTimeoutMs: 1000, log: (l) => logs.push(l) });
|
||||
// Hooks run in order: the broker resumes before the close, which a
|
||||
// stopped broker would hold.
|
||||
t.after(() => {
|
||||
try {
|
||||
process.kill(host.pids.broker, "SIGCONT");
|
||||
} catch {}
|
||||
});
|
||||
t.after(() => host.close(0));
|
||||
const record = (role, run) => ({ business: "acme", role, run, harness: "pi", pid: process.pid, startTime: startTimeOf(process.pid) });
|
||||
const coder = await host.bindLaunch(record("coder", "coder-run"));
|
||||
assert.equal((await host.op({ op: "identity", cap: coder.cap })).role, "coder");
|
||||
|
||||
// Stall the broker with a request waiting and a bind queued behind it.
|
||||
process.kill(host.pids.broker, "SIGSTOP");
|
||||
const code = (p) => p.then((v) => ({ answered: v }), (e) => e.code);
|
||||
const first = code(host.op({ op: "identity", cap: "bogus" }));
|
||||
const second = code(host.bindLaunch(record("reviewer", "reviewer-run")));
|
||||
assert.equal(await first, "broker-channel-broken");
|
||||
assert.equal(await second, "broker-channel-broken", "a queued request never reaches the broker");
|
||||
process.kill(host.pids.broker, "SIGCONT");
|
||||
|
||||
// The broker answers the stalled request late; nothing takes that reply.
|
||||
assert.equal(await host.done, 1);
|
||||
assert.ok(logs.some((l) => /^broker channel broken \(no reply within 1 s\); stopping the host$/.test(l)), logs.join("\n"));
|
||||
assert.equal(await code(host.op({ op: "identity", cap: coder.cap })), "broker-channel-broken");
|
||||
});
|
||||
|
||||
test("a broker reply with another request's id, or none, breaks the channel and the host exits 1", { timeout: 30000 }, async (t) => {
|
||||
// The broker echoes whatever id it's sent, so changing the id on the way
|
||||
// out gives the host the reply a confused broker would send.
|
||||
let what, tamper;
|
||||
spySends(t, (m) => m?.cap === "tamper" && tamper(m));
|
||||
for ([what, tamper] of [
|
||||
["another id", (m) => (m.id += 1000)],
|
||||
["no id", (m) => delete m.id],
|
||||
]) {
|
||||
const root = tmp(t);
|
||||
const f = fixture(root);
|
||||
makeDeployment(root);
|
||||
const boot = bootConfig({ system: loadSystem({ env: f.env }), businessId: "acme", env: f.env });
|
||||
const logs = [];
|
||||
const host = await startHost({ boot, business: "acme", log: (l) => logs.push(l) });
|
||||
t.after(() => host.close(0));
|
||||
const record = (role, run) => ({ business: "acme", role, run, harness: "pi", pid: process.pid, startTime: startTimeOf(process.pid) });
|
||||
const coder = await host.bindLaunch(record("coder", "coder-run"));
|
||||
await assert.rejects(host.op({ op: "identity", cap: "tamper" }), (e) => e.code === "broker-channel-broken", what);
|
||||
await assert.rejects(host.op({ op: "identity", cap: coder.cap }), (e) => e.code === "broker-channel-broken", what);
|
||||
assert.equal(await host.done, 1, what);
|
||||
assert.ok(logs.includes("broker channel broken (reply for another request); stopping the host"), `${what}: ${logs.join("\n")}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("a broker reply with no request waiting breaks the channel and the host exits 1", { timeout: 30000 }, async (t) => {
|
||||
// A request sent to the real broker past the host's queue: its reply
|
||||
// arrives when nothing is waiting (Darkwing round 2, Mr).
|
||||
const children = [];
|
||||
const onChild = ({ process: child }) => children.push(child);
|
||||
subscribe("child_process", onChild);
|
||||
t.after(() => unsubscribe("child_process", onChild));
|
||||
const root = tmp(t);
|
||||
const f = fixture(root);
|
||||
makeDeployment(root);
|
||||
const boot = bootConfig({ system: loadSystem({ env: f.env }), businessId: "acme", env: f.env });
|
||||
const logs = [];
|
||||
const host = await startHost({ boot, business: "acme", log: (l) => logs.push(l) });
|
||||
t.after(() => host.close(0));
|
||||
const record = { business: "acme", role: "coder", run: "coder-run", harness: "pi", pid: process.pid, startTime: startTimeOf(process.pid) };
|
||||
const coder = await host.bindLaunch(record);
|
||||
children.find((c) => c.pid === host.pids.broker).send({ op: "identity", cap: coder.cap, id: 1_000_000 });
|
||||
const late = new Promise((r) => setTimeout(r, 5000, "still running").unref());
|
||||
assert.equal(await Promise.race([host.done, late]), 1);
|
||||
assert.ok(logs.includes("broker channel broken (reply with no request waiting); stopping the host"), logs.join("\n"));
|
||||
await assert.rejects(host.op({ op: "identity", cap: coder.cap }), (e) => e.code === "broker-channel-broken");
|
||||
});
|
||||
|
||||
test("a notifier that refuses stops the broker and the host refuses with exit 3", async (t) => {
|
||||
const root = tmp(t);
|
||||
const f = fixture(root);
|
||||
|
||||
Reference in New Issue
Block a user