213 lines
14 KiB
JavaScript
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);
|
|
}
|
|
});
|