// Row 177 (#1675, Q173): the tester's own threads. Owner `tester`; neither the human nor the // tester reaches the other's threads. The assistant's lists are the shipped roles/assistant.json's, // and in a tester's thread the broker reads the tester as the human. import test from 'node:test'; import assert from 'node:assert/strict'; import { 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 } 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 roles = { chief: { definition: 'chief-of-staff', ...none }, assistant: { definition: 'assistant', ...none }, }; const demo = (extra = {}, messaging = shipped('assistant').messaging) => ({ demo: { id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, vars: { 'bus.tester': true }, roles, definitions: { 'chief-of-staff': {}, assistant: { messaging } }, ...extra, }, }); function setup(t, businesses = demo()) { const root = mkdtempSync(join(tmpdir(), 'bus-tester-threads-')); const store = new Store(root); t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); }); const b = new Broker({ store, businesses }); const caps = { human: b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }), tester: b.bindTester({ business: 'demo' }), }; const call = (who, verb, args = {}) => b.request(caps[who], { verb, args }); const code = (who, verb, args = {}) => { try { call(who, verb, args); } catch (e) { return e.code; } return null; }; const launch = (name, thread) => { caps[name] = b.bindLaunch({ business: 'demo', role: 'assistant', run: `${name}-run`, harness: 'pi', thread }); return call(name, 'role.claim'); }; const inputs = () => store.get("SELECT count(*) n FROM events WHERE kind='human.input'").n; return { b, store, caps, call, code, launch, inputs }; } // A project of each, with one thread of the human's (H) and one of the tester's (T). function both(t, businesses) { const ctx = setup(t, businesses); const hp = ctx.call('human', 'project.create', { name: 'human work' }).id; const tp = ctx.call('tester', 'project.create', { name: 'test work' }).id; const H = ctx.call('human', 'thread.create', { project: hp, name: 'mine' }).id; const T = ctx.call('tester', 'thread.create', { project: tp, name: 'probe' }).id; return { ...ctx, hp, tp, H, T }; } test('row 177 T1: a tester thread is owned by tester, and neither side reaches or lists the other', (t) => { const { store, call, code, hp, H, T } = both(t); assert.equal(store.get('SELECT owner FROM threads WHERE id=?', T).owner, 'tester'); assert.equal(store.get('SELECT owner FROM threads WHERE id=?', H).owner, 'jason'); assert.deepEqual(call('human', 'thread.list').map((x) => x.id), [H]); assert.deepEqual(call('tester', 'thread.list').map((x) => x.id), [T]); assert.deepEqual(call('tester', 'thread.list', { project: hp }), []); for (const [verb, args] of [ ['thread.open', {}], ['thread.send', { body: 'hello' }], ['thread.messages', {}], ]) { assert.equal(code('human', verb, { thread: T, ...args }), 'thread-not-owned', `human ${verb}`); assert.equal(code('tester', verb, { thread: H, ...args }), 'thread-not-owned', `tester ${verb}`); } // The accepted leak: names are unique per project, not per owner. assert.equal(code('tester', 'thread.create', { project: hp, name: 'mine' }), 'thread-exists'); // A tester may make a thread in the human's project; it is still the tester's. const X = call('tester', 'thread.create', { project: hp, name: 'side' }).id; assert.deepEqual(call('human', 'thread.list', { project: hp }).map((x) => x.id), [H]); assert.deepEqual(call('tester', 'thread.list').map((x) => x.id), [T, X]); }); test('row 177 T2: the tester thread.send is from tester-cli with no human.input; its open is by tester', (t) => { const { store, call, inputs, H, T } = both(t); const before = inputs(); const sent = call('tester', 'thread.send', { thread: T, body: 'hello' }); assert.equal(sent.request, null); assert.equal(inputs(), before); const m = store.get('SELECT * FROM messages WHERE id=?', sent.id); assert.equal(m.from_role, 'tester'); assert.equal(m.from_run, 'tester-cli'); assert.equal(m.to_role, 'assistant'); assert.equal(store.get('SELECT thread FROM message_threads WHERE message=?', sent.id).thread, T); assert.deepEqual(call('tester', 'thread.messages', { thread: T }).map((x) => x.id), [sent.id]); assert.deepEqual(call('tester', 'thread.open', { thread: T }), { opened: T }); assert.equal(store.get('SELECT by FROM thread_opens WHERE thread=?', T).by, 'tester'); // The human's send still writes its human.input. const own = call('human', 'thread.send', { thread: H, body: 'mine' }); assert.ok(own.request); assert.equal(inputs(), before + 1); }); test('row 177 T3: the lists read the tester as the human in its own thread, and nowhere else', (t) => { const { code, call, T } = both(t); assert.ok(call('tester', 'thread.send', { thread: T, body: 'in the thread' }).id); // Outside a thread the tester is still not on the assistant's lists. assert.equal(code('tester', 'message.send', { to: 'assistant', body: 'direct' }), 'message-sender-refused'); }); test('row 177 T3: an assistant that would not hear the human refuses the tester too', (t) => { const { code, T } = both(t, demo({}, { sendTo: ['human', 'chief-of-staff'], receiveFrom: ['chief-of-staff'] })); assert.equal(code('tester', 'thread.send', { thread: T, body: 'hello' }), 'message-sender-refused'); }); test('row 177 T4: the tester thread run answers the tester, never the human', (t) => { const { store, call, code, launch, H, T } = both(t); const asked = call('tester', 'thread.send', { thread: T, body: 'probe' }).id; launch('a', T); const reply = call('a', 'message.send', { to: 'tester', body: 'answer', class: 'RESULT', in_reply_to: asked }).id; const r = store.get('SELECT * FROM messages WHERE id=?', reply); assert.equal(r.to_role, 'tester'); assert.equal(store.get('SELECT thread FROM message_threads WHERE message=?', reply).thread, T); assert.deepEqual(call('tester', 'thread.messages', { thread: T }).map((x) => x.id), [asked, reply]); assert.equal(call('human', 'messages').messages.some((m) => m.id === reply), false); assert.deepEqual(call('human', 'message.receive'), []); assert.deepEqual(call('tester', 'message.receive').map((m) => m.id), [reply]); // The run's own message to the human refuses. assert.equal(code('a', 'message.send', { to: 'human', body: 'psst' }), 'tester-thread-human'); assert.equal(call('human', 'messages').total, 0); // A run of the human's thread still answers the human. call('human', 'thread.send', { thread: H, body: 'mine' }); launch('h', H); assert.ok(call('h', 'message.send', { to: 'human', body: 'yours' }).id); assert.equal(call('human', 'messages').total, 1); // Outside a tester's thread the lists still don't name the tester, so the human's thread run // can't answer it even in reply to a tester's message. assert.equal(code('h', 'message.send', { to: 'tester', body: 'x', in_reply_to: asked }), 'message-recipient-refused'); }); test('row 177 T5: each stop revokes only its own threads, recorded by its own actor', (t) => { const { store, call, code, launch, H, T } = both(t); launch('a', T); launch('h', H); const last = (thread) => store.get('SELECT * FROM thread_claims WHERE thread=? ORDER BY seq DESC LIMIT 1', thread); assert.deepEqual(call('tester', 'assistant.stop', { instance: 'assistant' }), { stopped: [{ thread: T, run: 'a-run' }] }); assert.equal(last(T).op, 'revoke'); assert.equal(last(T).by, 'tester'); assert.equal(last(T).reason, 'stopped by the tester'); assert.equal(last(H).op, 'claim'); assert.equal(code('tester', 'assistant.stop', { instance: 'assistant' }), 'not-held'); assert.deepEqual(call('human', 'assistant.stop', { instance: 'assistant' }), { stopped: [{ thread: H, run: 'h-run' }] }); assert.equal(last(H).by, 'jason'); assert.equal(last(H).reason, 'stopped by the human'); assert.equal(code('human', 'assistant.stop', { instance: 'assistant' }), 'not-held'); }); test('row 177 T5: the human stop never ends a tester run', (t) => { const { store, code, launch, T } = both(t); launch('a', T); assert.equal(code('human', 'assistant.stop', { instance: 'assistant' }), 'not-held'); assert.equal(store.get('SELECT op FROM thread_claims WHERE thread=? ORDER BY seq DESC LIMIT 1', T).op, 'claim'); }); test('row 177 T6: a business whose human is tester refuses invalid-business', (t) => { const root = mkdtempSync(join(tmpdir(), 'bus-tester-threads-boot-')); const store = new Store(root); t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); }); const businesses = demo(); businesses.demo.human = 'tester'; assert.throws(() => new Broker({ store, businesses }), { code: 'invalid-business' }); }); test('row 177 T7: the human trail shows a tester thread message as the tester', (t) => { const { call, T } = both(t); const sent = call('tester', 'thread.send', { thread: T, body: 'probe' }).id; const rows = call('human', 'trail', { subject: sent }).filter((r) => r.table === 'messages'); assert.equal(rows.length, 1); assert.equal(rows[0].from_role, 'tester'); assert.equal(rows[0].from_run, 'tester-cli'); }); // Row 177 r2 (#1675, F1): an action outside the assistant's grants is gated, and an uncited gated // raise routes to the human, so the tester-thread run's raise refuses before anything is stored. const raise = (action = 'task.create') => ({ action, question: `${action}?`, options: [ { key: 'yes', text: 'Yes' }, { key: 'no', text: 'No' }, ], recommendation: 'no', blocking: false, }); test('row 177 T8: a tester-thread run never raises a decision to the human; a human-thread run still does', (t) => { const { store, call, code, launch, H, T } = both(t); const decisions = () => store.get('SELECT count(*) n FROM decisions').n; launch('a', T); launch('h', H); assert.equal(code('a', 'decision.raise', raise()), 'tester-thread-human'); assert.deepEqual(call('human', 'inbox'), []); assert.equal(decisions(), 0); const raised = call('h', 'decision.raise', raise()); assert.equal(store.get('SELECT route_to FROM decisions WHERE id=?', raised.id).route_to, 'human'); assert.deepEqual(call('human', 'inbox').map((d) => d.id), [raised.id]); }); test('row 177 T8: a tester-thread raise routed to the arbiter stays allowed', (t) => { const crossRole = { definition: 'assistant', authority: { withinRole: ['message.send'], crossRole: ['task.create'] } }; const { store, call, launch, T } = both(t, demo({ roles: { ...roles, assistant: crossRole } })); launch('a', T); const raised = call('a', 'decision.raise', raise()); assert.equal(store.get('SELECT route_to FROM decisions WHERE id=?', raised.id).route_to, 'chief'); assert.deepEqual(call('human', 'inbox'), []); }); // Row 182 (#1680, Filbert N1 on row 177): a tester's message.send to a threaded instance takes its // thread from the in_reply_to chain, so the owner check runs there too. The assistant here has no // lists, so #checkMessaging lets the tester through and the chain is what decides. `stack` is a // project the business names, for the spec notice test. const open = () => demo({ definitions: { 'chief-of-staff': {}, assistant: {} }, projects: { stack: { root: '/nonexistent/r182' } } }); const rows = (store) => ['messages', 'message_threads'].map((x) => store.get(`SELECT count(*) n FROM ${x}`).n); test('row 182 T9: a tester reply into the human\'s thread refuses thread-not-owned and stores nothing', (t) => { const { store, call, code, launch, H } = both(t, open()); launch('h', H); const mine = call('human', 'thread.send', { thread: H, body: 'mine' }).id; const before = [rows(store), call('human', 'thread.messages', { thread: H })]; assert.equal(code('tester', 'message.send', { to: 'assistant', body: 'in', in_reply_to: mine }), 'thread-not-owned'); assert.deepEqual([rows(store), call('human', 'thread.messages', { thread: H })], before); assert.deepEqual(call('h', 'message.receive').map((m) => m.id), [mine]); }); test('row 182 T9: a tester reply into its own thread lands there', (t) => { const { store, call, launch, T } = both(t, open()); launch('a', T); const asked = call('tester', 'thread.send', { thread: T, body: 'probe' }).id; const again = call('tester', 'message.send', { to: 'assistant', body: 'more', in_reply_to: asked }).id; assert.equal(store.get('SELECT thread FROM message_threads WHERE message=?', again).thread, T); assert.deepEqual(call('tester', 'thread.messages', { thread: T }).map((x) => x.id), [asked, again]); assert.deepEqual(call('a', 'message.receive').map((m) => m.id), [asked, again]); }); // The chief holds its role as a plain run, outside any thread. const chief = ({ b, caps, call }) => { caps.chief = b.bindLaunch({ business: 'demo', role: 'chief', run: 'chief-run', harness: 'pi' }); call('chief', 'role.claim'); }; test('row 182 T9: the chief\'s reply up a chain into a tester\'s thread lands there', (t) => { const ctx = both(t, open()); const { store, call, launch, T } = ctx; launch('a', T); chief(ctx); const asked = call('tester', 'thread.send', { thread: T, body: 'ask the chief' }).id; const relay = call('a', 'message.send', { to: 'chief', body: 'for you', in_reply_to: asked }).id; const back = call('chief', 'message.send', { to: 'assistant', body: 'answer', in_reply_to: relay }).id; assert.equal(store.get('SELECT thread FROM message_threads WHERE message=?', back).thread, T); assert.deepEqual(call('a', 'message.receive').map((m) => m.id), [asked, back]); assert.deepEqual(call('tester', 'thread.messages', { thread: T }).map((x) => x.id), [asked, relay, back]); }); test('row 182 T9: a tester reply to a spec notice refuses thread-not-owned; the chief\'s relay still lands', (t) => { const ctx = both(t, open()); const { store, call, code, launch } = ctx; const project = call('human', 'project.create', { name: 'stack', repo: 'stack' }).id; const S = call('human', 'thread.create', { project, name: 'spec' }).id; launch('s', S); chief(ctx); // The row #specNotice (broker.mjs) writes for a human save of the spec in project stack. The save // itself needs spec-review.test.mjs's git fixture, which has no tester. const notice = 'n-182'; store.run( 'INSERT INTO messages(id,at,business,from_role,from_run,to_role,class,in_reply_to,decision,corrects,body) VALUES(?,?,?,?,?,?,?,?,?,?,?)', notice, new Date().toISOString(), 'demo', '_bus', 'bus', 'chief', 'RESULT', null, null, null, 'The human saved spec/north-star.md in project stack: commit c1, from the editor\'s base c0.', ); const before = rows(store); assert.equal(code('tester', 'message.send', { to: 'assistant', body: 'in', in_reply_to: notice }), 'thread-not-owned'); assert.deepEqual(rows(store), before); // The same chain from the chief reaches the human's thread, so the tester's send got as far as the notice. const relay = call('chief', 'message.send', { to: 'assistant', body: 'relay', in_reply_to: notice }).id; assert.equal(store.get('SELECT thread FROM message_threads WHERE message=?', relay).thread, S); });