Files
stack/packages/bus/tests/tester-threads.test.mjs
darkwing 33ab5eeb91 row 182 (#1680): a tester message reaches only a thread the tester owns
#threadFor puts a message to a threaded instance in the thread found up its in_reply_to chain. It
checked the thread's instance but not its owner, so a tester reply to a message in a human's thread
landed in the human's thread. A new #testerOwns check refuses thread-not-owned when the sender is the
tester and the thread's owner isn't. It runs on both the thread branch and the row 143 spec-notice
branch. A role's reply into a tester's thread is unchanged, since that's how the answer comes back.
The bus README says so in its row 177 paragraph.

tester-threads.test.mjs adds four T9 tests: a tester reply into the human's thread refuses and
stores nothing, one into its own thread lands, the chief's reply into a tester thread lands, and a
tester reply to a spec notice refuses while the chief's relay still lands. messaging.test.mjs:203
used to send as the tester into a human thread. It now sends from a tester-owned thread.

Filbert approved round 1 in #1680 comment 29388. Their notes N1 (thread-instance-mismatch is
checked first, as before) and N2 (BUILD.md missed project-runtime.test.mjs:13 as a text reader of
broker.mjs, now corrected) need no change.
2026-10-20 00:47:49 -05:00

299 lines
15 KiB
JavaScript

// 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);
});