Files
stack/packages/bus/tests/lifecycle-verbs.test.mjs

408 lines
27 KiB
JavaScript

// Row 194 (#1692, D1.1, V2-V5, D4.1): the broker half of the lifecycle verbs. Launch, regenerate
// and delete are requests the launcher's pass takes through the host ops. Terminate revokes and
// cancels at the verb and names the runs the pass signals. The launcher half is in cli lifecycle.test.mjs.
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdirSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { Store } from '../src/store.mjs';
import { Broker, LIFECYCLE_REQUEST_MS, TESTER_VERBS } from '../src/broker.mjs';
const shipped = (name) => JSON.parse(readFileSync(fileURLToPath(new URL(`../../../roles/${name}.json`, import.meta.url)), 'utf8'));
const none = { authority: { withinRole: ['message.send'], crossRole: [] } };
const R = 'a'.repeat(64);
const doc = (extra = {}) => ({
id: 'demo', human: 'jason', arbiters: { technical: 'pm', delivery: 'chief' },
vars: { 'bus.tester': true },
definitions: { 'chief-of-staff': {}, assistant: { messaging: shipped('assistant').messaging } },
roles: {
chief: { definition: 'chief-of-staff', authority: shipped('chief-of-staff').authority },
pm: { definition: 'pm', ...none },
coder: { definition: 'coder', ...none },
assistant: { definition: 'assistant', ...none },
},
retired: ['old'],
...extra,
});
const OTHER = { id: 'other', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, 'only-there': none } };
function setup(t, { organization = null, testerOn = undefined, processing = undefined } = {}) {
const root = mkdtempSync(join(tmpdir(), 'lifecycle-verbs-'));
const store = new Store(root);
t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); });
const tester = { on: true };
const b = new Broker({ store, businesses: { demo: doc(), other: OTHER }, organization,
testerOn: testerOn === undefined ? () => tester.on : testerOn, ...(processing ? { processing } : {}) });
const caps = {
human: b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }),
tester: b.bindTester({ business: 'demo' }),
reader: b.bindReader({ business: 'demo' }),
};
const call = (who, verb, args = {}) => b.request(caps[who], { verb, args });
const code = (fn) => { try { fn(); } catch (e) { return e.code; } return null; };
const ask = (who, verb, args = {}) => code(() => call(who, verb, args));
let runs = 0;
const bind = (role, { claim = true, thread, name = `${role}-${++runs}` } = {}) => {
caps[name] = b.bindLaunch({ business: 'demo', role, run: name, harness: 'pi', ...(thread ? { thread } : {}) });
if (claim) call(name, 'role.claim');
return name;
};
const lifecycle = () => store.get("SELECT count(*) n FROM events WHERE kind='action.allowed' AND json_extract(body,'$.action') LIKE 'lifecycle.%'").n;
const inputs = () => store.get("SELECT count(*) n FROM events WHERE kind='human.input'").n;
const take = (request) => b.lifecycleTake({ business: 'demo', request });
const done = (request, extra = {}) => b.lifecycleDone({ business: 'demo', request, ...extra });
const state = (request) => call('human', 'instance.request', { request });
return { b, store, caps, call, code, ask, bind, lifecycle, inputs, take, done, state, tester };
}
// Moves the clock past a request's 60 s, for the rest of the test.
const later = (t) => { const now = Date.now(); t.mock.method(Date, 'now', () => now + LIFECYCLE_REQUEST_MS + 1000); };
test('row 194 T12: agent and reader sessions refuse the lifecycle verbs before any effect; the tester gets launch, end and the request read, never regenerate', (t) => {
const f = setup(t);
f.bind('chief');
f.bind('pm');
const verbs = [['instance.launch', { instance: 'coder' }], ['instance.regenerate', { instance: 'coder' }],
['instance.end', { instance: 'coder' }], ['instance.request', { request: 'r1' }]];
for (const [verb, args] of verbs) {
assert.equal(f.ask('reader', verb, args), 'read-only', `reader ${verb}`);
for (const who of ['chief-1', 'pm-2']) assert.equal(f.ask(who, verb, args), 'human-required', `${who} ${verb}`);
}
assert.equal(f.ask('tester', 'instance.regenerate', { instance: 'coder' }), 'tester-refused');
assert.equal(f.lifecycle(), 0);
for (const verb of ['instance.launch', 'instance.end', 'instance.request']) assert.ok(TESTER_VERBS.includes(verb), verb);
assert.equal(TESTER_VERBS.includes('instance.regenerate'), false);
// Each human verb passes the D4 hook with its LIFECYCLE_HUMAN_VERBS entry.
const { request } = f.call('human', 'instance.launch', { instance: 'coder' });
f.call('human', 'instance.regenerate', { instance: 'pm' });
assert.deepEqual(f.call('human', 'instance.end', { instance: 'coder' }).cancelled, [request]);
});
test('row 194 V2 and V4 at the verb: the request is open, by the human, with a human.input; thread, retired, unknown and revoked refuse with no event', (t) => {
const f = setup(t);
const before = f.inputs();
const { request } = f.call('human', 'instance.launch', { instance: 'coder' });
const r = f.state(request);
assert.deepEqual([r.action, r.instance, r.by, r.state, r.done, r.cancelled], ['lifecycle.launch', 'coder', 'jason', 'open', null, null]);
assert.equal(f.inputs(), before + 1);
const raw = JSON.parse(f.store.get("SELECT body FROM events WHERE json_extract(body,'$.request')=? AND json_extract(body,'$.action')='lifecycle.launch'", request).body);
assert.deepEqual(raw, { action: 'lifecycle.launch', request, via: 'cli', by: 'jason' });
const count = f.lifecycle();
assert.equal(f.ask('human', 'instance.launch', { instance: 'assistant' }), 'not-thread-launch');
assert.equal(f.ask('human', 'instance.regenerate', { instance: 'assistant' }), 'regenerate-thread');
assert.equal(f.ask('human', 'instance.launch', { instance: 'old' }), 'instance-retired');
assert.equal(f.ask('human', 'instance.regenerate', { instance: 'old' }), 'instance-retired');
assert.equal(f.ask('human', 'instance.launch', { instance: 'ghost' }), 'unknown-role');
assert.equal(f.ask('human', 'instance.launch', { instance: 'coder', extra: 1 }), 'invalid-request');
assert.equal(f.ask('human', 'instance.launch', { instance: '../x' }), 'invalid-request');
f.call('human', 'launch.revoke');
assert.equal(f.ask('human', 'instance.launch', { instance: 'coder' }), 'launch-revoked');
assert.equal(f.ask('human', 'instance.regenerate', { instance: 'coder' }), 'launch-revoked');
assert.equal(f.lifecycle(), count);
assert.equal(f.ask('human', 'instance.request', { request: 'no-such' }), 'unknown-request');
});
test('row 194 D1.1 take: a request is taken once, closes once, and lifecycleRequests lists only what is not closed', (t) => {
const f = setup(t);
const { request } = f.call('human', 'instance.launch', { instance: 'coder' });
const second = f.call('human', 'instance.regenerate', { instance: 'pm' }).request;
assert.deepEqual(f.b.lifecycleRequests('demo').map((r) => [r.request, r.state]), [[request, 'open'], [second, 'open']]);
assert.equal(f.code(() => f.done(request, { run: 'run-x' })), 'request-not-taken');
const go = f.take(request);
assert.deepEqual([go.go, go.refusal, go.state, go.action, go.instance, go.by], [true, null, 'taken', 'lifecycle.launch', 'coder', 'jason']);
const again = f.take(request);
assert.deepEqual([again.go, again.refusal, again.state], [false, 'request-closed', 'taken']);
assert.equal(f.store.get("SELECT count(*) n FROM events WHERE json_extract(body,'$.action')='lifecycle.taken'").n, 1);
assert.equal(f.code(() => f.done(request, { refusal: 'Not A Code' })), 'invalid-request');
assert.equal(f.code(() => f.done(request, { run: '../x' })), 'invalid-request');
assert.equal(f.code(() => f.done(request, { extra: 1 })), 'invalid-request');
const closed = f.done(request, { run: 'run-x' });
assert.deepEqual([closed.state, closed.done], ['closed', { run: 'run-x', transaction: null, refusal: null }]);
assert.equal(f.code(() => f.done(request, { run: 'run-x' })), 'request-closed');
assert.deepEqual(f.take(request).refusal, 'request-closed');
assert.deepEqual(f.b.lifecycleRequests('demo').map((r) => r.request), [second]);
assert.equal(f.code(() => f.take('no-such')), 'unknown-request');
assert.equal(f.code(() => f.b.lifecycleRequests('ghost')), 'unknown-business');
// A request from one business isn't another's.
assert.equal(f.code(() => f.b.lifecycleTake({ business: 'other', request: second })), 'unknown-request');
});
test('row 194 D1.1 take: a launch.revoke after the request closes it launch-revoked, and a request past 60 s closes request-expired', (t) => {
const f = setup(t);
const revoked = f.call('human', 'instance.launch', { instance: 'coder' }).request;
const regen = f.call('human', 'instance.regenerate', { instance: 'coder' }).request;
f.call('human', 'launch.revoke');
for (const request of [revoked, regen]) {
const r = f.take(request);
assert.deepEqual([r.go, r.refusal, r.state, r.done.refusal], [false, 'launch-revoked', 'closed', 'launch-revoked']);
}
assert.equal(f.store.get("SELECT count(*) n FROM events WHERE json_extract(body,'$.action')='lifecycle.taken'").n, 0);
f.call('human', 'launch.restore');
const old = f.call('human', 'instance.launch', { instance: 'coder' }).request;
later(t);
const r = f.take(old);
assert.deepEqual([r.go, r.refusal, r.state], [false, 'request-expired', 'closed']);
assert.deepEqual(f.b.lifecycleRequests('demo'), []);
});
// T15 (D6): row 191's switch, read through the broker's processing reader as the bus reads its file.
test('row 194 T15 (bus): while halted, launch and regenerate refuse processing-halted at the verb and at the take, hire and fire refuse, terminate goes; an unreadable switch refuses with its code', (t) => {
const generation = '0b6e7f6e-4c1d-4a8e-9a51-6d2f3c4b5a69';
const sw = { state: { version: 1, halted: false, generation } };
const f = setup(t, { processing: { read: () => sw.state } });
const chief = f.bind('chief');
const coder = f.bind('coder');
const launch = f.call('human', 'instance.launch', { instance: 'coder' }).request;
const regen = f.call('human', 'instance.regenerate', { instance: 'pm' }).request;
const testerLaunch = f.call('tester', 'instance.launch', { instance: 'pm' }).request;
sw.state = { version: 1, halted: true, generation };
const count = f.lifecycle();
assert.equal(f.ask('human', 'instance.launch', { instance: 'coder' }), 'processing-halted');
assert.equal(f.ask('human', 'instance.regenerate', { instance: 'coder' }), 'processing-halted');
assert.equal(f.ask('tester', 'instance.launch', { instance: 'coder' }), 'processing-halted');
assert.equal(f.lifecycle(), count, 'a refused verb records no request');
for (const request of [launch, regen, testerLaunch]) {
const r = f.take(request);
assert.deepEqual([r.go, r.refusal, r.state, r.done.refusal], [false, 'processing-halted', 'closed', 'processing-halted'], request);
}
// Row 191's work table refuses the chief's hire and fire before anything runs.
assert.equal(f.ask(chief, 'instance.hire', { name: 'helper', definition: 'coder', reason: 'more hands' }), 'processing-halted');
assert.equal(f.ask(chief, 'instance.fire', { name: 'coder', reason: 'done' }), 'processing-halted');
// Terminate is allowed while halted: it revokes, and its end goes at the take.
const end = f.call('human', 'instance.end', { instance: 'coder' });
assert.deepEqual(end.runs, [coder]);
assert.equal(f.take(end.request).go, true);
sw.state = { halted: true, invalid: true, code: 'processing-control-invalid' };
assert.equal(f.ask('human', 'instance.launch', { instance: 'pm' }), 'processing-control-invalid');
sw.state = { version: 1, halted: false, generation };
const after = f.call('human', 'instance.launch', { instance: 'coder' }).request;
assert.equal(f.take(after).go, true);
});
test('row 194 T12a: the tester launches and ends on its own business with actor tester; bus.tester off in the file refuses the call and the take tester-off', (t) => {
const f = setup(t);
const inputs = f.inputs();
const { request } = f.call('tester', 'instance.launch', { instance: 'coder' });
const r = f.call('tester', 'instance.request', { request });
assert.deepEqual([r.by, r.state], ['tester', 'open']);
assert.equal(JSON.parse(f.store.get("SELECT body FROM events WHERE json_extract(body,'$.request')=? AND json_extract(body,'$.action')='lifecycle.launch'", request).body).via, 'tester');
assert.equal(f.inputs(), inputs, 'a tester request is not human input');
const human = f.call('human', 'instance.launch', { instance: 'pm' }).request;
// A tester reads only its own requests.
assert.equal(f.ask('tester', 'instance.request', { request: human }), 'unknown-request');
// The cap is bound to demo: an instance only `other` declares is unknown here.
assert.equal(f.ask('tester', 'instance.launch', { instance: 'only-there' }), 'unknown-role');
// bus.tester switched off in the file, with no restart.
f.tester.on = false;
assert.equal(f.ask('tester', 'instance.launch', { instance: 'coder' }), 'tester-off');
assert.equal(f.ask('tester', 'instance.end', { instance: 'coder' }), 'tester-off');
const off = f.take(request);
assert.deepEqual([off.go, off.refusal], [false, 'tester-off']);
assert.equal(f.take(human).go, true, 'the human request is not the tester switch');
// Anything but exactly true is off.
f.tester.on = 'true';
assert.equal(f.ask('tester', 'instance.launch', { instance: 'coder' }), 'tester-off');
f.tester.on = true;
const later2 = f.call('tester', 'instance.launch', { instance: 'coder' }).request;
const ended = f.call('tester', 'instance.end', { instance: 'coder' });
assert.deepEqual(ended.cancelled, [later2]);
assert.equal(f.state(ended.request).by, 'tester');
assert.equal(f.state(later2).cancelled.by, 'tester');
});
test('row 194 V3 terminate: claims revoked, open requests cancelled, bound runs named, taken requests listed; nothing at all answers not-held', (t) => {
const f = setup(t);
const held = f.bind('coder');
const unclaimed = f.bind('coder', { claim: false });
const open = f.call('human', 'instance.launch', { instance: 'coder' }).request;
const taken = f.call('human', 'instance.regenerate', { instance: 'coder' }).request;
f.take(taken);
const other = f.call('human', 'instance.launch', { instance: 'pm' }).request;
const out = f.call('human', 'instance.end', { instance: 'coder', reason: 'stuck on a loop' });
assert.deepEqual([out.runs, out.cancelled, out.taken], [[held, unclaimed], [open], [taken]]);
const claim = f.store.get("SELECT * FROM role_claims WHERE role='coder' ORDER BY seq DESC LIMIT 1");
assert.deepEqual([claim.op, claim.holder_run, claim.by, claim.reason], ['revoke', held, 'jason', 'stuck on a loop']);
assert.deepEqual([f.state(open).state, f.state(open).cancelled], ['closed', { by: 'jason' }]);
assert.equal(f.state(taken).state, 'taken', 'a taken request is past cancelling');
assert.equal(f.state(other).state, 'open', 'another instance keeps its request');
// The end is a request the pass takes, with the runs to signal. It never expires.
const end = f.state(out.request);
assert.deepEqual([end.action, end.runs, end.state], ['lifecycle.end', [held, unclaimed], 'open']);
later(t);
assert.equal(f.take(out.request).go, true);
assert.equal(f.take(open).refusal, 'request-closed');
// The runs stay named while bound and not ended.
f.done(taken, { run: 'coder-new' });
for (const run of [held, unclaimed]) f.b.endLaunch({ business: 'demo', run, reason: 'signalled' });
assert.equal(f.ask('human', 'instance.end', { instance: 'coder' }), 'not-held');
assert.equal(f.ask('human', 'instance.end', { instance: 'old' }), 'unknown-role');
assert.equal(f.ask('human', 'instance.end', { instance: 'coder', reason: '' }), 'invalid-request');
});
test('row 194 V3 terminate of a thread instance: the human ends every thread, the tester only its own', (t) => {
const f = setup(t);
const hp = f.call('human', 'project.create', { name: 'human work' }).id;
const tp = f.call('tester', 'project.create', { name: 'test work' }).id;
const H = f.call('human', 'thread.create', { project: hp, name: 'mine' }).id;
const T = f.call('tester', 'thread.create', { project: tp, name: 'probe' }).id;
const h = f.bind('assistant', { thread: H });
const tr = f.bind('assistant', { thread: T });
const tester = f.call('tester', 'instance.end', { instance: 'assistant' });
assert.deepEqual(tester.runs, [tr]);
assert.equal(f.store.get('SELECT op FROM thread_claims WHERE thread=? ORDER BY seq DESC LIMIT 1', H).op, 'claim');
assert.equal(f.store.get('SELECT op FROM thread_claims WHERE thread=? ORDER BY seq DESC LIMIT 1', T).op, 'revoke');
const human = f.call('human', 'instance.end', { instance: 'assistant' });
assert.deepEqual(human.runs, [h, tr]);
assert.equal(f.store.get('SELECT op FROM thread_claims WHERE thread=? ORDER BY seq DESC LIMIT 1', H).op, 'revoke');
});
// The writer stand-in: instances.check answers per instance, instances.set commits a transaction.
function writer(answers) {
const calls = [];
const organization = (id, verb, args, actor) => {
calls.push([verb, args, actor]);
if (verb === 'instances.check') {
const a = answers[args.instance] ?? { revision: R };
if (a instanceof Error) throw a;
return a;
}
if (verb === 'instances.set') return { transaction: `tx-${calls.length}`, restart: false };
return { ok: true };
};
return { calls, organization };
}
const refused = (code) => Object.assign(new Error(code), { code });
const lastListed = () => Object.assign(new Error('coder is the last name in launch.instances'), { name: 'BusinessError' });
test('row 194 T6 V5 order at the verb: the writer\'s named refusals, then instance-running, instance-starting, the last-listed refusal and the revision', (t) => {
const w = writer({ chief: refused('organization-instance-referenced'), pm: lastListed(), coder: lastListed() });
const f = setup(t, { organization: w.organization });
const del = (instance, revision = R) => f.ask('human', 'instances.set', { revision, instance, op: 'delete' });
// A named refusal comes before the broker's checks, even with a claim held.
f.bind('chief');
assert.equal(del('chief'), 'organization-instance-referenced');
// The last-listed refusal waits until running and starting have passed.
const run = f.bind('coder');
assert.equal(del('coder'), 'instance-running');
f.call('human', 'instance.end', { instance: 'coder' });
f.b.endLaunch({ business: 'demo', run, reason: 'signalled' });
const starting = f.call('human', 'instance.launch', { instance: 'coder' }).request;
assert.equal(del('coder'), 'instance-starting');
f.take(starting);
assert.equal(del('coder'), 'instance-starting', 'a taken request with no done is still starting');
f.done(starting, { refusal: 'launch-refused' });
assert.equal(del('coder'), 'invalid-request');
assert.equal(del('pm'), 'invalid-request');
assert.equal(del('assistant', 'b'.repeat(64)), 'revision-conflict');
assert.equal(del('assistant', 'not-a-revision'), 'invalid-request');
assert.equal(f.ask('human', 'instances.set', { revision: R, instance: 'assistant', op: 'delete', definition: 'x' }), 'invalid-request');
assert.ok(w.calls.every(([verb]) => verb === 'instances.check'), 'the checks write nothing');
assert.equal(f.store.get("SELECT count(*) n FROM events WHERE json_extract(body,'$.action')='lifecycle.delete'").n, 0);
const { request } = f.call('human', 'instances.set', { revision: R, instance: 'assistant', op: 'delete' });
const r = f.state(request);
assert.deepEqual([r.action, r.instance, r.by, r.revision, r.retry, r.state], ['lifecycle.delete', 'assistant', 'jason', R, false, 'open']);
assert.deepEqual(w.calls.at(-1), ['instances.check', { instance: 'assistant' }, 'jason']);
// An open delete is a starting request too: a second delete waits for the first.
assert.equal(del('assistant'), 'instance-starting');
});
test('row 194 V5: an open launch past its 60 s is not starting, as it can only close request-expired', (t) => {
const w = writer({});
const f = setup(t, { organization: w.organization });
f.call('human', 'instance.launch', { instance: 'coder' });
later(t);
assert.ok(f.call('human', 'instances.set', { revision: R, instance: 'coder', op: 'delete' }).request);
});
test('row 194 V5 after the take: lifecycleDelete re-checks running and starting, then commits through the writer with the human as actor', (t) => {
const w = writer({});
const f = setup(t, { organization: w.organization });
const { request } = f.call('human', 'instances.set', { revision: R, instance: 'coder', op: 'delete' });
const del = () => f.b.lifecycleDelete({ business: 'demo', request });
assert.equal(f.code(del), 'request-not-taken');
assert.equal(f.take(request).go, true);
// A launch recorded after the delete's verb and a claim taken since both stop it.
const launch = f.call('human', 'instance.launch', { instance: 'coder' }).request;
assert.equal(f.code(del), 'instance-starting');
f.call('human', 'instance.end', { instance: 'coder' });
const run = f.bind('coder');
assert.equal(f.code(del), 'instance-running');
f.call('human', 'instance.end', { instance: 'coder' });
f.b.endLaunch({ business: 'demo', run, reason: 'signalled' });
assert.equal(f.state(launch).cancelled.by, 'jason');
const out = del();
assert.deepEqual(w.calls.at(-1), ['instances.set', { revision: R, instance: 'coder', op: 'delete' }, 'jason']);
assert.deepEqual(out, { transaction: out.transaction, retry: false });
assert.match(out.transaction, /^tx-/);
assert.equal(f.done(request, { transaction: out.transaction }).done.transaction, out.transaction);
const launchReq = f.call('human', 'instance.launch', { instance: 'pm' }).request;
assert.equal(f.code(() => f.b.lifecycleDelete({ business: 'demo', request: launchReq })), 'request-not-taken');
});
test('row 194 V5 retry: a retired name deletes again only after workspace-rename-failed, and that request commits nothing', (t) => {
const w = writer({});
const f = setup(t, { organization: w.organization });
assert.equal(f.ask('human', 'instances.set', { revision: R, instance: 'old', op: 'delete' }), 'organization-instance-retired');
const { request } = f.call('human', 'instances.set', { revision: R, instance: 'pm', op: 'delete' });
f.take(request);
const { transaction } = f.b.lifecycleDelete({ business: 'demo', request });
f.done(request, { transaction, refusal: 'workspace-rename-failed' });
// The bus reloads after the commit, as the runtime does.
const next = doc({ retired: ['old', 'pm'], arbiters: { technical: 'chief', delivery: 'chief' } });
delete next.roles.pm;
f.b.reloadBusiness('demo', next);
const writes = w.calls.length;
const retry = f.call('human', 'instances.set', { revision: R, instance: 'pm', op: 'delete' }).request;
assert.deepEqual([f.state(retry).retry, f.state(retry).revision], [true, R]);
f.take(retry);
// S3: the retry answers the failed delete's transaction, which names the rename's target.
assert.ok(transaction);
assert.deepEqual(f.b.lifecycleDelete({ business: 'demo', request: retry }), { transaction, retry: true });
assert.equal(w.calls.length, writes, 'a retry neither checks nor writes the file');
f.done(retry);
assert.equal(f.ask('human', 'instances.set', { revision: R, instance: 'pm', op: 'delete' }), 'organization-instance-retired');
});
test('row 194 V5 runtime: a delete request, the take and lifecycleDelete commit the file with retired, log the human and reload so the name no longer binds', async (t) => {
const { startBroker } = await import('../src/runtime.mjs');
const { busOrganization } = await import('../src/organization-context.mjs');
const { loadBusiness } = await import('../../organization/src/organization-config.mjs');
const { resolveInstance } = await import('../../organization/src/resolve.mjs');
const { businessDoc, rolesCopy, systemFor, writeJson } = await import('../../organization/tests/helpers.mjs');
const root = mkdtempSync(join(tmpdir(), 'lifecycle-runtime-'));
const rolesDir = rolesCopy(root);
const file = writeJson(join(root, 'businesses/demo.json'), { ...businessDoc(root, 'demo'), plugins: { harness: ['pi'] } });
mkdirSync(join(root, 'data'), { mode: 0o700 });
const b = loadBusiness('demo', { dir: root, rolesDir });
const resolved = {};
for (const i of Object.keys(b.roles)) resolved[i] = resolveInstance({ system: systemFor(root), business: b, instance: i });
const bus = await startBroker({ dataRoot: join(root, 'data'), businesses: { demo: busOrganization(b, resolved) }, organization: { dir: root, rolesDir, system: systemFor(root) } });
t.after(async () => { await bus.close(); rmSync(root, { recursive: true, force: true }); });
const human = bus.broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
const ask = (verb, args) => bus.broker.request(human, { verb, args });
const code = (fn) => { try { fn(); } catch (e) { return e.code; } return null; };
const revision = () => ask('instances').revision;
ask('instances.set', { revision: revision(), instance: 'sec-1', op: 'add', definition: 'security-reviewer', launch: true });
const before = readFileSync(file, 'utf8');
// The real writer's refusals reach the verb, and none of them writes.
assert.equal(code(() => ask('instances.set', { revision: revision(), instance: 'coder', op: 'delete' })), 'organization-credentials-out-of-scope');
assert.equal(code(() => ask('instances.set', { revision: revision(), instance: 'pm', op: 'delete' })), 'organization-instance-referenced');
assert.equal(code(() => ask('instances.set', { revision: 'b'.repeat(64), instance: 'sec-1', op: 'delete' })), 'revision-conflict');
assert.equal(readFileSync(file, 'utf8'), before);
const { request } = ask('instances.set', { revision: revision(), instance: 'sec-1', op: 'delete' });
assert.equal(readFileSync(file, 'utf8'), before, 'the verb only records the request');
assert.deepEqual(bus.lifecycleRequests('demo').map((r) => [r.request, r.action, r.instance]), [[request, 'lifecycle.delete', 'sec-1']]);
assert.equal(bus.lifecycleTake({ business: 'demo', request }).go, true);
const { transaction } = bus.lifecycleDelete({ business: 'demo', request });
const after = JSON.parse(readFileSync(file, 'utf8'));
assert.equal(Object.hasOwn(after.roles, 'sec-1'), false);
assert.deepEqual(after.retired, ['sec-1']);
assert.equal(after.launch.instances.includes('sec-1'), false);
const log = readFileSync(join(root, 'businesses/.history/demo/changes.jsonl'), 'utf8').trim().split('\n').map(JSON.parse).at(-1);
assert.deepEqual([log.actor, log.state, log.transaction], ['jason', 'committed', transaction]);
assert.equal(bus.lifecycleDone({ business: 'demo', request, transaction }).state, 'closed');
assert.equal(code(() => bus.broker.bindLaunch({ business: 'demo', role: 'sec-1', run: 'run-1', harness: 'pi' })), 'unknown-role');
assert.equal(code(() => ask('instance.launch', { instance: 'sec-1' })), 'instance-retired');
});