Files
stack/packages/bus/tests/tester-writes.test.mjs

213 lines
14 KiB
JavaScript

// Row 175 (#1673, Q173, mos-0870 N2, mos-0872): the test system's tester. It reads what the human
// reads, makes seven writes recorded with actor `tester`, and every other human action still
// refuses it tester-refused. Row 177 (#1675) adds the thread verbs, tested in tester-threads.test.mjs.
import test from 'node:test';
import assert from 'node:assert/strict';
import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { Store } from '../src/store.mjs';
import { Broker, TESTER_VERBS, TESTER_ORGANIZATION_VERBS } from '../src/broker.mjs';
import { startBroker } from '../src/runtime.mjs';
import { businessDoc, rolesCopy, systemFor, writeJson } from '../../organization/tests/helpers.mjs';
const none = { authority: { withinRole: ['message.send'], crossRole: [] } };
// Row 177 (#1675): thread.list left this list; the tester's lists its own threads (tester-threads.test.mjs).
const READS = [['usage', {}], ['names', {}], ['limits', {}], ['plugins', {}], ['instances', {}], ['project.list', {}]];
test('row 175: TESTER_VERBS and the organization branch tester set are pinned', () => {
assert.deepEqual([...TESTER_VERBS], [
'message.send', 'message.receive', 'message.read', 'inbox', 'agents', 'tasks', 'trail', 'decisions', 'messages', 'replies',
'usage', 'names', 'limits', 'plugins', 'instances', 'project.list', 'thread.list', 'name.set', 'project.create', 'credentials',
// Row 177 (#1675): the tester's own threads.
'thread.create', 'thread.open', 'thread.send', 'thread.messages', 'assistant.stop',
// Row 194 (#1692, D4.1): launch and terminate, and the read of its own requests.
'instance.launch', 'instance.end', 'instance.request',
// Row 264a (#1769): the sessions of the tester's own threads.
'session.list', 'session.create',
// Row 271 (264d, #1776): clear context in the tester's own threads (mos-1325).
'session.clear', 'session.cancel', 'session.operation',
]);
assert.deepEqual([...TESTER_ORGANIZATION_VERBS], ['limits', 'plugins', 'instances', 'plugins.set', 'instances.set', 'routes.set', 'routes']);
assert.ok(Object.isFrozen(TESTER_VERBS) && Object.isFrozen(TESTER_ORGANIZATION_VERBS));
});
// A real organization file and writer behind the bus, as routes-verb.test.mjs's runtime test.
async function runtime(t) {
const root = mkdtempSync(join(tmpdir(), 'tester-writes-'));
const rolesDir = rolesCopy(root);
const doc = businessDoc(root, 'demo');
doc.plugins = { ...doc.plugins, harness: ['pi'] };
for (const r of Object.values(doc.roles)) r.vars = { harness: 'pi', model: 'claude-sonnet-5-5', thinking: 'high', ...r.vars };
const file = join(root, 'businesses/demo.json');
writeJson(file, doc);
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' }, vars: { 'bus.tester': true },
roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { definition: 'coder', ...none }, assistant: { definition: 'assistant', ...none } },
},
},
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 tester = bus.broker.bindTester({ business: 'demo' });
const call = (cap, verb, args = {}) => bus.broker.request(cap, { verb, args });
const code = (cap, verb, args = {}) => { try { call(cap, verb, args); } catch (e) { return e.code; } return null; };
const history = join(root, 'businesses/.history/demo/changes.jsonl');
const log = () => (existsSync(history) ? readFileSync(history, 'utf8').trim().split('\n').filter(Boolean).map(JSON.parse) : []);
const bytes = () => readFileSync(file, 'utf8');
return { root, file, bus, human, tester, call, code, log, bytes };
}
test('row 175: each of the six reads as the tester deep-equals the human read', async (t) => {
const r = await runtime(t);
r.call(r.human, 'name.set', { instance: 'coder', name: 'rex' });
const project = r.call(r.human, 'project.create', { name: 'alpha' }).id;
r.call(r.human, 'thread.create', { project, name: 'one' });
for (const [verb, args] of READS) assert.deepEqual(r.call(r.tester, verb, args), r.call(r.human, verb, args), verb);
// Not empty, so names has a row. Row 177: the human's thread is not the tester's to list.
assert.equal(r.call(r.human, 'thread.list').length, 1);
assert.deepEqual(r.call(r.tester, 'thread.list', { project }), []);
assert.equal(r.call(r.tester, 'names').length, 1);
assert.equal(r.call(r.tester, 'project.list').length, 1);
});
test('row 175: W1-W3 as the tester land with receipt actor tester; the human still logs the human', async (t) => {
const r = await runtime(t);
const p = r.call(r.tester, 'plugins.set', { revision: r.call(r.tester, 'plugins').revision, plugins: { 'search-source': 'searxng-url' } });
assert.deepEqual(JSON.parse(r.bytes()).plugins['search-source'], 'searxng-url');
assert.deepEqual([r.log().at(-1).actor, r.log().at(-1).state, r.log().at(-1).transaction], ['tester', 'committed', p.transaction]);
const i = r.call(r.tester, 'instances.set', { revision: p.revision, instance: 'sec-1', op: 'add', definition: 'security-reviewer' });
assert.deepEqual(JSON.parse(r.bytes()).roles['sec-1'].definition, 'security-reviewer');
assert.deepEqual([r.log().at(-1).actor, r.log().at(-1).transaction], ['tester', i.transaction]);
const route = { harness: 'pi', model: 'claude-opus-5-5', thinking: 'minimal', fallback: null };
const s = r.call(r.tester, 'routes.set', { revision: i.revision, routes: { coder: route } });
assert.deepEqual(s.instances.coder, route);
assert.deepEqual([r.log().at(-1).actor, r.log().at(-1).transaction], ['tester', s.transaction]);
const h = r.call(r.human, 'routes.set', { revision: s.revision, routes: { coder: { ...route, thinking: 'low' } } });
assert.deepEqual([r.log().at(-1).actor, r.log().at(-1).transaction], ['jason', h.transaction]);
});
test('row 175: W2 as the tester refuses credentials, protected definitions, human, tester and bus.tester, and changes nothing', async (t) => {
const r = await runtime(t);
const revision = r.call(r.tester, 'instances').revision;
const before = r.bytes();
const entries = r.log().length;
const refused = [
[{ instance: 'reviewer-2', op: 'add', definition: 'reviewer' }, 'organization-credentials-out-of-scope'],
// Row 194 (brief item 7): only the human's add binds credentials. A valid bot doesn't lift it.
[{ instance: 'reviewer-2', op: 'add', definition: 'reviewer', tracker: { bot: 'bot-reviewer-2', botId: 40 } }, 'organization-credentials-out-of-scope'],
[{ instance: 'thread-2', op: 'add', definition: 'assistant' }, 'organization-definition-refused'],
[{ instance: 'human', op: 'add', definition: 'merge-gate' }, 'invalid-request'],
[{ instance: 'tester', op: 'add', definition: 'merge-gate' }, 'invalid-request'],
// bus.tester is a business-only var (organization/src/vars.mjs), so no instance carries it.
[{ instance: 'gate-2', op: 'add', definition: 'merge-gate', vars: { 'bus.tester': true } }, 'invalid-request'],
[{ instance: 'coder', op: 'change', vars: { 'bus.tester': true } }, 'invalid-request'],
];
for (const [args, want] of refused) {
assert.equal(r.code(r.tester, 'instances.set', { revision, ...args }), want, JSON.stringify(args));
assert.equal(r.bytes(), before, JSON.stringify(args));
assert.equal(r.call(r.tester, 'instances').revision, revision);
assert.equal(r.log().length, entries, `a refused write logs nothing: ${JSON.stringify(args)}`);
}
// The control: the same add with an instance var lands, so the refusal above is the var.
r.call(r.tester, 'instances.set', { revision, instance: 'gate-2', op: 'add', definition: 'merge-gate', vars: { model: 'claude-sonnet-5-5' } });
assert.ok(r.log().length > entries);
assert.deepEqual([r.log().at(-1).actor, r.log().at(-1).state], ['tester', 'committed']);
});
test('row 175: the human-only actions refuse a tester tester-refused, and the file and store stay as they were', async (t) => {
const r = await runtime(t);
const revision = r.call(r.human, 'limits').revision;
const project = r.call(r.human, 'project.create', { name: 'alpha' }).id;
const thread = r.call(r.human, 'thread.create', { project, name: 'one' }).id;
const before = r.bytes();
// The runtime keeps its store to itself, so the human's own reads stand in for it.
const views = () => ['thread.list', 'project.list', 'names', 'messages', 'instances', 'limits', 'plugins', 'agents'].map((verb) => r.call(r.human, verb));
const seen = views();
const opened = r.call(r.human, 'thread.messages', { thread });
for (const [verb, args] of [
['limits.set', { revision, limits: { total: 0 } }],
['organization.set', { revision, limits: { total: 0 } }],
['organization.revision', {}],
// The gate runs before the argument checks: a bad key still refuses tester-refused.
['organization.set', { revision, nope: 1 }],
['organization.revision', { nope: 1 }],
['decision.resolve', { id: 'd-1', choice: 'yes' }],
['launch.revoke', {}],
['launch.restore', {}],
]) assert.equal(r.code(r.tester, verb, args), 'tester-refused', verb);
// Row 177 (#1675): the thread verbs are the tester's now, but the human's thread is not.
for (const [verb, args] of [
['thread.open', { thread }],
['thread.send', { thread, body: 'hello' }],
['thread.messages', { thread }],
]) assert.equal(r.code(r.tester, verb, args), 'thread-not-owned', verb);
assert.equal(r.code(r.tester, 'assistant.stop', { instance: 'assistant' }), 'not-held');
// The project file verbs answer a promise.
for (const [verb, args] of [['project.file.save', { project: 'stack', path: 'README.md', content: 'x', head: 'a'.repeat(40) }]])
await assert.rejects(Promise.resolve().then(() => r.call(r.tester, verb, args)), { code: 'project-spec-human-only' }, verb);
assert.equal(r.bytes(), before);
assert.equal(r.call(r.human, 'limits').revision, revision);
assert.deepEqual(views(), seen);
assert.deepEqual(r.call(r.human, 'thread.messages', { thread }), opened);
});
// The plain broker, as names.test.mjs: the in-store writes W4-W6.
function broker(t, vars = { 'bus.tester': true }) {
const root = mkdtempSync(join(tmpdir(), 'tester-writes-store-'));
const store = new Store(root);
t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); });
const b = new Broker({
store,
businesses: {
demo: {
id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, ...(vars ? { vars } : {}),
roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, dev: { definition: 'coder', ...none }, lead: { definition: 'pm', ...none } },
},
},
});
const call = (cap, verb, args = {}) => b.request(cap, { verb, args });
return { b, store, call };
}
test('row 175: W4 as the tester writes by tester and keeps every name check', (t) => {
const { b, store, call } = broker(t);
const human = b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
const tester = b.bindTester({ business: 'demo' });
const events = store.get('SELECT count(*) n FROM events').n;
assert.deepEqual(call(tester, 'name.set', { instance: 'dev', name: 'rex' }), { instance: 'dev', name: 'rex' });
assert.deepEqual({ ...store.get('SELECT instance, name, by FROM instance_names') }, { instance: 'dev', name: 'rex', by: 'tester' });
assert.equal(store.get('SELECT count(*) n FROM events').n, events, 'no authority consumed, no event');
call(human, 'name.set', { instance: 'lead', name: 'patty' });
assert.equal(store.get("SELECT by FROM instance_names WHERE instance='lead'").by, 'jason');
assert.throws(() => call(tester, 'name.set', { instance: 'dev', name: 'patty' }), { code: 'name-taken' });
for (const name of ['lead', 'human', 'tester']) assert.throws(() => call(tester, 'name.set', { instance: 'dev', name }), { code: 'name-is-instance' }, name);
for (const name of ['coder', 'pm']) assert.throws(() => call(tester, 'name.set', { instance: 'dev', name }), { code: 'name-is-role' }, name);
assert.equal(store.get('SELECT count(*) n FROM instance_names').n, 2);
assert.deepEqual(call(tester, 'names').map(({ instance, name }) => [instance, name]), [['dev', 'rex'], ['lead', 'patty']]);
});
test('row 175: W6 as the tester records created_by tester; the human records the human', (t) => {
const { b, store, call } = broker(t);
const human = b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
const tester = b.bindTester({ business: 'demo' });
call(tester, 'project.create', { name: 'alpha' });
call(human, 'project.create', { name: 'beta' });
assert.deepEqual(store.all('SELECT name, created_by FROM projects ORDER BY seq').map((p) => [p.name, p.created_by]), [['alpha', 'tester'], ['beta', 'jason']]);
assert.throws(() => call(tester, 'project.create', { name: 'alpha' }), { code: 'project-exists' });
assert.deepEqual(call(tester, 'project.list'), call(human, 'project.list'));
});
test('row 175: with bus.tester unset or not exactly true no tester binds, so no read or write runs', (t) => {
for (const vars of [null, {}, { 'bus.tester': false }, { 'bus.tester': 'true' }, { 'bus.tester': 1 }]) {
const { b, store } = broker(t, vars);
assert.throws(() => b.bindTester({ business: 'demo' }), { code: 'tester-off' }, JSON.stringify(vars));
for (const table of ['instance_names', 'projects', 'messages']) assert.equal(store.get(`SELECT count(*) n FROM ${table}`).n, 0, table);
}
});