139 lines
11 KiB
JavaScript
139 lines
11 KiB
JavaScript
// Row 165 (#1662, Q171, REQ-ROLE-3): instances and instances.set are human-only broker actions,
|
|
// logged with the verified actor. They stay outside ORGANIZATION_KEYS, so neither organization.set
|
|
// nor the chief of staff's change_organization reaches them (sage-0918).
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdtempSync, mkdirSync, 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_HUMAN_VERBS, ORGANIZATION_KEYS, TESTER_ORGANIZATION_VERBS } from '../src/broker.mjs';
|
|
|
|
const chiefAuthority = JSON.parse(readFileSync(fileURLToPath(new URL('../../../roles/chief-of-staff.json', import.meta.url)), 'utf8')).authority;
|
|
const none = { authority: { withinRole: ['message.send'], crossRole: [] } };
|
|
const biz = () => ({
|
|
demo: {
|
|
id: 'demo', human: 'jason', arbiters: { technical: 'pm', delivery: 'chief' }, definitions: {},
|
|
roles: { chief: { definition: 'chief-of-staff', authority: chiefAuthority }, pm: { definition: 'pm', ...none } },
|
|
projects: { stack: { root: '/nonexistent' } }, vars: { 'bus.tester': true },
|
|
},
|
|
});
|
|
const ADD = { revision: 'a'.repeat(64), instance: 'sec-1', op: 'add', definition: 'security-reviewer', launch: true };
|
|
|
|
test('row 165: instances and instances.set refuse every non-human session but the tester, the chief of staff included, and pass the verified actor', (t) => {
|
|
const root = mkdtempSync(join(tmpdir(), 'instances-verb-'));
|
|
const store = new Store(root);
|
|
t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); });
|
|
const calls = [];
|
|
const b = new Broker({ store, businesses: biz(), organization: (id, verb, args, actor) => (calls.push([id, verb, args, actor]), { ok: true }) });
|
|
const human = b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
|
|
const tester = b.bindTester({ business: 'demo' });
|
|
const caps = { reader: b.bindReader({ business: 'demo' }) };
|
|
for (const role of ['chief', 'pm']) {
|
|
caps[role] = b.bindLaunch({ business: 'demo', role, run: `${role}-run`, harness: 'pi' });
|
|
b.request(caps[role], { verb: 'role.claim' });
|
|
}
|
|
for (const [who, cap] of Object.entries(caps))
|
|
for (const verb of ['instances', 'instances.set'])
|
|
assert.throws(() => b.request(cap, { verb, args: verb === 'instances' ? {} : ADD }), /human-required/, `${who} ${verb}`);
|
|
// The chief of staff holds organization.change, but organization.set takes ORGANIZATION_KEYS only:
|
|
// the instance keys are refused before the writer, so change_organization can't add an instance.
|
|
for (const key of ['instance', 'op', 'definition', 'launch']) assert.equal(ORGANIZATION_KEYS.includes(key), false, key);
|
|
for (const args of [ADD, { revision: ADD.revision, instance: 'pm', op: 'change', definition: 'merge-gate' }])
|
|
assert.throws(() => b.request(caps.chief, { verb: 'organization.set', args }), /invalid-request/);
|
|
assert.equal(calls.length, 0);
|
|
assert.throws(() => b.request(human, { verb: 'instances.set', args: { ...ADD, actor: 'forged' } }), /invalid-request/);
|
|
assert.throws(() => b.request(human, { verb: 'instances.set', args: { ...ADD, holder: 'mos' } }), /invalid-request/);
|
|
assert.throws(() => b.request(human, { verb: 'instances.set', args: { ...ADD, credentials: {} } }), /invalid-request/);
|
|
assert.throws(() => b.request(human, { verb: 'instances.set', args: { revision: ADD.revision, op: 'add' } }), /invalid-request/);
|
|
assert.throws(() => b.request(human, { verb: 'instances', args: { x: 1 } }), /invalid-request/);
|
|
assert.equal(calls.length, 0);
|
|
b.request(human, { verb: 'instances' });
|
|
b.request(human, { verb: 'instances.set', args: ADD });
|
|
// Row 175 (#1673): the tester reads and writes instances through the same checks, as `tester`.
|
|
assert.throws(() => b.request(tester, { verb: 'instances.set', args: { ...ADD, actor: 'forged' } }), /invalid-request/);
|
|
b.request(tester, { verb: 'instances' });
|
|
b.request(tester, { verb: 'instances.set', args: ADD });
|
|
assert.deepEqual(calls, [['demo', 'instances', {}, 'jason'], ['demo', 'instances.set', ADD, 'jason'], ['demo', 'instances', {}, 'tester'], ['demo', 'instances.set', ADD, 'tester']]);
|
|
});
|
|
|
|
test('row 165 runtime: an add and a change reach the organization file with the human as actor; each refusal keeps the bytes and logs nothing', async (t) => {
|
|
const { startBroker } = await import('../src/runtime.mjs');
|
|
const { businessDoc, rolesCopy, systemFor, writeJson } = await import('../../organization/tests/helpers.mjs');
|
|
const root = mkdtempSync(join(tmpdir(), 'instances-runtime-'));
|
|
const rolesDir = rolesCopy(root);
|
|
writeJson(join(root, 'businesses/demo.json'), businessDoc(root, 'demo'));
|
|
mkdirSync(join(root, 'data'), { mode: 0o700 });
|
|
const bus = await startBroker({ dataRoot: join(root, 'data'), businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { authority: { withinRole: [], crossRole: [] } } } } }, 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 file = join(root, 'businesses/demo.json');
|
|
const log = () => readFileSync(join(root, 'businesses/.history/demo/changes.jsonl'), 'utf8').trim().split('\n').filter(Boolean).map(JSON.parse);
|
|
const view = bus.broker.request(human, { verb: 'instances' });
|
|
assert.equal(view.instances.chief.launcher, true);
|
|
assert.equal(view.definitions['merge-gate'].accepted, true);
|
|
const added = bus.broker.request(human, { verb: 'instances.set', args: { ...ADD, revision: view.revision } });
|
|
assert.equal(added.instances['sec-1'].launched, true);
|
|
// Row 194 (D3 item 3): this boot copy's coder has no credential references and the file's has, so
|
|
// the bus doesn't reload and asks for a restart. reload.test.mjs boots from the file.
|
|
assert.equal(added.restart, true);
|
|
assert.deepEqual(JSON.parse(readFileSync(file, 'utf8')).roles['sec-1'], { definition: 'security-reviewer' });
|
|
const first = log().at(-1);
|
|
assert.deepEqual([first.actor, first.state, first.transaction], ['jason', 'committed', added.transaction]);
|
|
const changed = bus.broker.request(human, { verb: 'instances.set', args: { revision: added.revision, instance: 'sec-1', op: 'change', definition: 'merge-gate' } });
|
|
assert.deepEqual(log().at(-1).changes.map(c => [c.path, c.old, c.new]), [['roles.sec-1.definition', 'security-reviewer', 'merge-gate']]);
|
|
const before = readFileSync(file, 'utf8');
|
|
const entries = log().length;
|
|
const code = (args) => { try { bus.broker.request(human, { verb: 'instances.set', args: { revision: changed.revision, ...args } }); } catch (e) { return e.code; } return null; };
|
|
// Row 194 (brief item 7, sage-1610 a): an add of a vikunja definition needs its bot.
|
|
assert.equal(code({ instance: 'reviewer-2', op: 'add', definition: 'reviewer' }), 'organization-tracker-required');
|
|
assert.equal(code({ instance: 'sec-1', op: 'change', definition: 'coder' }), 'organization-credentials-out-of-scope');
|
|
assert.equal(code({ instance: 'sec-1', op: 'add', definition: 'merge-gate' }), 'organization-instance-exists');
|
|
assert.equal(code({ instance: 'ghost', op: 'change', vars: {} }), 'organization-unknown-instance');
|
|
assert.equal(code({ instance: 'thread-2', op: 'add', definition: 'assistant' }), 'organization-definition-refused');
|
|
assert.equal(code({ instance: 'chief', op: 'change', launch: true }), 'invalid-request');
|
|
assert.equal(code({ instance: 'gate-2', op: 'add', definition: 'merge-gate', revision: view.revision }), 'revision-conflict');
|
|
assert.equal(readFileSync(file, 'utf8'), before);
|
|
assert.equal(log().length, entries, 'a refused write logs nothing');
|
|
});
|
|
|
|
// Row 194 (#1692, D4, D4.1): the human lifecycle verbs so far, veto.set and instances.set's three ops.
|
|
test('row 194 T12: agent and reader sessions refuse veto.set; the tester gets add and change but not delete or veto.set; the human passes the hook for each listed verb', (t) => {
|
|
const root = mkdtempSync(join(tmpdir(), 'instances-verb-'));
|
|
const store = new Store(root);
|
|
t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); });
|
|
const calls = [];
|
|
const b = new Broker({ store, businesses: biz(), organization: (id, verb, args, actor) => (calls.push([verb, args.op, actor]), verb === 'instances.check' ? { revision: ADD.revision } : { ok: true }) });
|
|
const human = b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
|
|
const tester = b.bindTester({ business: 'demo' });
|
|
const caps = { reader: b.bindReader({ business: 'demo' }) };
|
|
for (const role of ['chief', 'pm']) caps[role] = b.bindLaunch({ business: 'demo', role, run: `${role}-run`, harness: 'pi' });
|
|
const VETO = { revision: ADD.revision, op: 'keep', name: 'pm' };
|
|
for (const [who, cap] of Object.entries(caps)) {
|
|
assert.throws(() => b.request(cap, { verb: 'veto.set', args: VETO }), /human-required/, who);
|
|
assert.throws(() => b.request(cap, { verb: 'instances.set', args: { ...ADD, op: 'delete' } }), /human-required/, who);
|
|
}
|
|
assert.throws(() => b.request(tester, { verb: 'veto.set', args: VETO }), /tester-refused/);
|
|
assert.throws(() => b.request(tester, { verb: 'instances.set', args: { revision: ADD.revision, instance: 'pm', op: 'delete' } }), /tester-refused/);
|
|
assert.equal(TESTER_ORGANIZATION_VERBS.includes('veto.set'), false);
|
|
assert.equal(calls.length, 0);
|
|
// veto.set takes exactly its three keys, and instances.set only its three ops.
|
|
for (const args of [{ ...VETO, actor: 'forged' }, { revision: VETO.revision, op: 'keep' }, { ...VETO, holder: 'mos' }])
|
|
assert.throws(() => b.request(human, { verb: 'veto.set', args }), /invalid-request/);
|
|
assert.throws(() => b.request(human, { verb: 'instances.set', args: { ...ADD, op: 'remove' } }), /invalid-request/);
|
|
assert.equal(calls.length, 0);
|
|
// The hook refuses a verb missing from LIFECYCLE_HUMAN_VERBS, so each call below passing shows the
|
|
// hook got that exact entry.
|
|
assert.deepEqual([...LIFECYCLE_HUMAN_VERBS].sort(), ['instance.end', 'instance.launch', 'instance.regenerate', 'instances.set add', 'instances.set change', 'instances.set delete', 'veto.set']);
|
|
b.request(human, { verb: 'veto.set', args: VETO });
|
|
b.request(human, { verb: 'instances.set', args: ADD });
|
|
b.request(human, { verb: 'instances.set', args: { revision: ADD.revision, instance: 'pm', op: 'change', vars: {} } });
|
|
// S2c: the delete is a request after the writer's check, and writes nothing yet.
|
|
assert.ok(b.request(human, { verb: 'instances.set', args: { revision: ADD.revision, instance: 'pm', op: 'delete' } }).request);
|
|
b.request(tester, { verb: 'instances.set', args: ADD });
|
|
b.request(tester, { verb: 'instances.set', args: { revision: ADD.revision, instance: 'pm', op: 'change', vars: {} } });
|
|
assert.deepEqual(calls, [['veto.set', 'keep', 'jason'], ['instances.set', 'add', 'jason'], ['instances.set', 'change', 'jason'],
|
|
['instances.check', undefined, 'jason'], ['instances.set', 'add', 'tester'], ['instances.set', 'change', 'tester']]);
|
|
});
|