Files
stack/packages/bus/tests/end-launch.test.mjs
T
jason.woltjeandClaude Opus 5.5 fe5c68fa03 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]>
2026-10-11 12:22:50 -05:00

193 lines
9.6 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { fork } from 'node:child_process';
import { once } from 'node:events';
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { Store } from '../src/store.mjs';
import { Broker } from '../src/broker.mjs';
import { Client } from '../src/client.mjs';
const businesses = {
demo: {
id: 'demo',
human: 'jason',
arbiters: { technical: 'cto', delivery: 'pm' },
launch: { by: 'pm', instances: ['coder'], max: { opus: 2, sonnet: 2 } },
roles: {
pm: { authority: { withinRole: ['message.send', 'role.launch'], crossRole: [] } },
cto: { authority: { withinRole: ['message.send'], crossRole: [] } },
coder: { authority: { withinRole: ['message.send'], crossRole: [] } },
},
},
};
function setup(t) {
const root = mkdtempSync(join(tmpdir(), 'bus-end-'));
const store = new Store(root);
const b = new Broker({ store, businesses });
t.after(() => {
store.close();
rmSync(root, { recursive: true, force: true });
});
const agent = (role, run = role + '-run') => b.bindLaunch({ business: 'demo', role, run, harness: 'pi' });
return { b, store, agent };
}
const call = (b, cap, verb, args = {}) => b.request(cap, { verb, args });
test('endLaunch writes session.ended, releases the run claim and kills its capabilities', (t) => {
const { b, store, agent } = setup(t);
const c = agent('coder');
call(b, c, 'role.claim');
assert.deepEqual(b.endLaunch({ business: 'demo', run: 'coder-run', reason: 'stopped', exitCode: 0 }), {
released: true,
});
const ended = store.get("SELECT * FROM events WHERE kind='session.ended'");
assert.equal(ended.subject, 'coder-run');
assert.equal(ended.actor_role, 'coder');
assert.equal(ended.actor_run, 'coder-run');
assert.deepEqual(JSON.parse(ended.body), { reason: 'stopped', exitCode: 0, released: true });
const claim = store.get("SELECT * FROM role_claims WHERE role='coder' ORDER BY seq DESC LIMIT 1");
assert.equal(claim.op, 'release');
assert.equal(claim.by, 'coder-run');
assert.throws(() => call(b, c, 'message.send', { to: 'pm', body: 'still here?' }), /unauthenticated/);
assert.throws(() => b.identity(c), /unauthenticated/);
});
test('endLaunch refuses an unknown run, a second end and a rebind of the ended run', (t) => {
const { b, agent } = setup(t);
agent('coder');
assert.throws(() => b.endLaunch({ business: 'demo', run: 'nope', reason: 'exited' }), /unknown-run/);
assert.throws(() => b.endLaunch({ business: 'demo', run: 'coder-run', reason: 'bad reason' }), /invalid-request/);
assert.throws(
() => b.endLaunch({ business: 'demo', run: 'coder-run', reason: 'exited', exitCode: 1.5 }),
/invalid-request/,
);
assert.deepEqual(b.endLaunch({ business: 'demo', run: 'coder-run', reason: 'exited', exitCode: null }), {
released: false,
});
assert.throws(() => b.endLaunch({ business: 'demo', run: 'coder-run', reason: 'exited' }), /run-ended/);
assert.throws(() => agent('coder'), /duplicate-run/);
});
test('a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound', (t) => {
const { b, store, agent } = setup(t);
agent('coder');
agent('pm');
b.endLaunch({ business: 'demo', run: 'coder-run', reason: 'stopped', exitCode: 0 });
const again = new Broker({ store, businesses });
assert.throws(() => again.bindLaunch({ business: 'demo', role: 'coder', run: 'coder-run', harness: 'pi' }), /run-ended/);
assert.throws(() => again.endLaunch({ business: 'demo', run: 'coder-run', reason: 'host-lost' }), /unknown-run/);
assert.throws(() => again.bindLaunch({ business: 'demo', role: 'cto', run: 'pm-run', harness: 'pi' }), /launch-record-mismatch/);
again.bindLaunch({ business: 'demo', role: 'pm', run: 'pm-run', harness: 'pi' });
assert.deepEqual(again.endLaunch({ business: 'demo', run: 'pm-run', reason: 'host-lost' }), { released: false });
assert.equal(store.get("SELECT count(*) n FROM events WHERE kind='session.ended'").n, 2);
});
test('endLaunch leaves a claim another run took alone', (t) => {
const { b, store, agent } = setup(t);
const first = agent('coder', 'c1');
call(b, first, 'role.claim');
call(b, first, 'role.release');
call(b, agent('coder', 'c2'), 'role.claim');
assert.deepEqual(b.endLaunch({ business: 'demo', run: 'c1', reason: 'exited', exitCode: 0 }), { released: false });
assert.equal(store.get("SELECT holder_run FROM role_claims ORDER BY seq DESC LIMIT 1").holder_run, 'c2');
});
test('refuse records action.refused against the caller with the code only', (t) => {
const { b, store, agent } = setup(t);
const pm = agent('pm');
assert.equal(b.refuse(pm, 'capacity-full'), 'capacity-full');
const e = store.get("SELECT * FROM events WHERE kind='action.refused'");
assert.equal(e.actor_role, 'pm');
assert.deepEqual(JSON.parse(e.body), { code: 'capacity-full' });
assert.throws(() => b.refuse(pm, 'Not A Code'), /invalid-request/);
assert.throws(() => b.refuse('f'.repeat(64), 'capacity-full'), /unauthenticated/);
});
function launch(t) {
const child = fork(new URL('../src/process.mjs', import.meta.url), [], { stdio: ['ignore', 'pipe', 'pipe', 'ipc'] });
t.after(() => {
if (child.exitCode === null) child.kill('SIGKILL');
});
return child;
}
async function ask(child, message) {
const reply = once(child, 'message');
child.send(message);
return (await reply)[0];
}
test('launches off refuses role.launch with launch-revoked until launches on', (t) => {
const { b, agent } = setup(t);
const pm = agent('pm');
call(b, pm, 'role.claim');
const human = b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
assert.equal(b.authorize(pm, 'role.launch', { target: 'coder' }).class, 'within-role');
call(b, human, 'launch.revoke');
assert.throws(() => b.authorize(pm, 'role.launch', { target: 'coder' }), { code: 'launch-revoked' });
// Other actions are untouched; only launching stops.
assert.equal(b.authorize(pm, 'message.send').class, 'within-role');
call(b, human, 'launch.restore');
assert.equal(b.authorize(pm, 'role.launch', { target: 'coder' }).class, 'within-role');
});
test('broker process: launch ops authorize role.launch, record refusals and end runs', async (t) => {
const root = mkdtempSync(join(tmpdir(), 'bus-launch-ops-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
const token = join(root, 'token');
writeFileSync(token, 'fixture-token-6c1d', { mode: 0o600 });
const config = structuredClone(businesses);
config.demo.roles.coder.credentials = { vikunja: { file: token, expires: '2099-01-01' } };
const child = launch(t);
const data = join(root, 'data');
mkdirSync(data);
const ready = await ask(child, {
op: 'boot',
config: { dataRoot: data, businesses: config, launches: [], readers: [] },
});
assert.equal(ready.ok, true, ready.error);
const record = (role, run) => ({ business: 'demo', role, run, harness: 'pi', pid: process.pid, startTime: '1' });
const pm = (await ask(child, { op: 'bindLaunch', record: record('pm', 'pm-1') })).launch.cap;
const cto = (await ask(child, { op: 'bindLaunch', record: record('cto', 'cto-1') })).launch.cap;
assert.deepEqual((await ask(child, { op: 'identity', cap: pm })).result, {
business: 'demo',
role: 'pm',
run: 'pm-1',
human: null,
});
assert.equal((await ask(child, { op: 'identity', cap: 'f'.repeat(64) })).error, 'unauthenticated');
// A launch is authorized for the role's holder only, so both runs claim first, as the runner does.
const reader = new Client({ path: ready.path, cap: cto });
await new Client({ path: ready.path, cap: pm }).call('role.claim');
await reader.call('role.claim');
const ok = await ask(child, { op: 'authorizeLaunch', cap: pm, instance: 'coder' });
assert.equal(ok.ok, true);
assert.equal(ok.result.class, 'within-role');
// The cto is not the business's launcher, so role.launch is gated for it and the refusal is evidence.
const no = await ask(child, { op: 'authorizeLaunch', cap: cto, instance: 'coder' });
assert.equal(no.ok, false);
assert.equal(no.error, 'decision-required');
assert.equal((await ask(child, { op: 'refuse', cap: pm, code: 'capacity-full' })).result, 'capacity-full');
assert.deepEqual((await ask(child, { op: 'credentialStatus', business: 'demo', role: 'coder' })).result, [
{ service: 'vikunja', date: '2099-01-01', state: 'valid' },
]);
assert.deepEqual((await ask(child, { op: 'credentialStatus', business: 'demo', role: 'pm' })).result, []);
const end = await ask(child, { op: 'endLaunch', record: { business: 'demo', run: 'pm-1', reason: 'stopped', exitCode: 0 } });
assert.deepEqual(end, { ok: true, result: { released: true } });
// The host's request id comes back on every launch-op and bind reply, refusals too.
assert.equal((await ask(child, { op: 'identity', cap: cto, id: 7 })).id, 7);
assert.deepEqual(await ask(child, { op: 'identity', cap: 'f'.repeat(64), id: 8 }), { ok: false, error: 'unauthenticated', id: 8 });
assert.equal((await ask(child, { op: 'bindLaunch', record: record('coder', 'coder-1'), id: 9 })).id, 9);
assert.deepEqual(await ask(child, { op: 'bindLaunch', record: record('coder', 'coder-1'), id: 10 }), { ok: false, error: 'duplicate-run', id: 10 });
await assert.rejects(new Client({ path: ready.path, cap: pm }).call('agents'), /unauthenticated/);
const trail = await reader.call('trail', { subject: 'pm-1' });
assert.deepEqual(
trail.filter((r) => r.table === 'events').map((r) => r.kind),
['session.launched', 'session.ended'],
);
const exit = once(child, 'exit');
child.send({ op: 'close' });
assert.equal((await exit)[0], 0);
});