// Row 277 (#1782, design D1, duckworth-0118): `thread.claims` gives human and reader sessions the // thread claims held in the business. A thread run's role.claim writes thread_claims, which `agents` // doesn't read, so this is where the owner sees it. `agents` is unchanged for every session. import test from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { randomUUID } from 'node:crypto'; import { Store } from '../src/store.mjs'; import { Broker } from '../src/broker.mjs'; const AT = '2026-10-22T00:00:00.000Z'; const within = (grants) => ({ authority: { withinRole: grants, crossRole: [] } }); const ROW = ['thread', 'instance', 'holder_run', 'harness', 'at']; // Row 264a (#1769): a hand-made thread gets the default session the 3k-3l migration gives an old one. const DEFAULT_SESSION = [ 'INSERT INTO sessions(at,business,thread,id,instance,owner,name,op) SELECT at,business,id,id,instance,owner,\'default\',\'legacy-3l\' FROM threads WHERE id=?', 'INSERT INTO session_defaults(at,thread,session,by) SELECT at,id,id,\'migration-3l\' FROM threads WHERE id=?', ]; function setup(t) { const root = mkdtempSync(join(tmpdir(), 'bus-thread-claims-')); 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: { 'bus.tester': true }, roles: { chief: { definition: 'chief-of-staff', ...within(['message.send']) }, assistant: { definition: 'assistant', ...within(['message.send']) } }, }, }, }); const chief = b.bindLaunch({ business: 'demo', role: 'chief', run: 'chief-run', harness: 'pi', address: 'chief' }); b.request(chief, { verb: 'role.claim' }); const caps = { human: b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }), reader: b.bindReader({ business: 'demo' }), tester: b.bindTester({ business: 'demo' }), chief, }; // `who` is a session name from caps, or a run's capability. const call = (who, verb, args = {}) => b.request(Object.hasOwn(caps, who) ? caps[who] : who, { verb, args }); const project = call('human', 'project.create', { name: 'shared work' }).id; // The human's own thread, so the human's assistant.stop reaches its run. const thread = (name) => { const id = randomUUID(); store.run('INSERT INTO threads(id,at,business,project,instance,owner,name) VALUES(?,?,?,?,?,?,?)', id, AT, 'demo', project, 'assistant', 'jason', name); for (const sql of DEFAULT_SESSION) store.run(sql, id); return id; }; // A run on the thread's default session, or on `session` when one is named. const claim = (H, run, session) => { const cap = b.bindLaunch({ business: 'demo', role: 'assistant', run, harness: 'pi', thread: H, ...(session ? { session } : {}) }); call(cap, 'role.claim'); return cap; }; return { b, store, caps, call, thread, claim }; } test('T1: the human and the reader see a held thread claim; agents still lists the role only', (t) => { const { store, call, thread, claim } = setup(t); const H = thread('plan'); claim(H, 'assistant-run'); const at = store.get("SELECT at FROM thread_claims WHERE thread=? AND op='claim'", H).at; const rows = call('human', 'thread.claims'); // The store's rows have no prototype, so each is copied before the strict compare. assert.deepEqual(rows.map((r) => ({ ...r })), [{ thread: H, instance: 'assistant', holder_run: 'assistant-run', harness: 'pi', at }]); for (const r of rows) assert.deepEqual(Object.keys(r), ROW); assert.deepEqual(call('reader', 'thread.claims'), rows); assert.deepEqual(call('human', 'agents').map((r) => r.role), ['chief']); }); test('T2: a released or revoked thread claim is not listed, though its thread has an earlier claim row', (t) => { const { store, call, thread, claim } = setup(t); const H = thread('plan'); const first = claim(H, 'run-1'); call(first, 'role.release'); assert.deepEqual(call('human', 'thread.claims'), [], 'released'); claim(H, 'run-2'); assert.deepEqual(call('human', 'thread.claims').map((r) => r.holder_run), ['run-2'], 'claimed again'); call('human', 'assistant.stop', { instance: 'assistant' }); assert.deepEqual(call('human', 'thread.claims'), [], 'revoked'); // The thread's first row is still a claim, so only the latest row decides. assert.deepEqual(store.all('SELECT op FROM thread_claims WHERE thread=? ORDER BY seq', H).map((r) => r.op), ['claim', 'release', 'claim', 'revoke']); }); test('T3: a role session refuses human-required and the tester refuses tester-refused', (t) => { const { call, thread, claim } = setup(t); const run = claim(thread('plan'), 'assistant-run'); for (const who of ['chief', run]) assert.throws(() => call(who, 'thread.claims'), { code: 'human-required' }); assert.throws(() => call('tester', 'thread.claims'), { code: 'tester-refused' }); }); test('T4: agents answers each session the same before and after a thread run claims', (t) => { const { call, thread, claim } = setup(t); const sessions = ['human', 'reader', 'tester', 'chief']; const before = Object.fromEntries(sessions.map((who) => [who, call(who, 'agents')])); claim(thread('plan'), 'assistant-run'); assert.equal(call('human', 'thread.claims').length, 1); for (const who of sessions) assert.deepEqual(call(who, 'agents'), before[who], who); }); test('T5: the result carries neither the thread name a user wrote nor a message body', (t) => { const { store, call, thread, claim } = setup(t); const H = thread('alice secret thread name'); claim(H, 'assistant-run'); // The thread's message, written to the store as message-filter.test.mjs's put() writes one. const m = randomUUID(); store.run( 'INSERT INTO messages(id,at,business,from_role,from_run,to_role,class,in_reply_to,decision,corrects,body) VALUES(?,?,?,?,?,?,?,?,?,?,?)', m, AT, 'demo', 'assistant', 'assistant-run', 'human', 'INFO', null, null, null, 'alice secret message body', ); store.run('INSERT INTO message_threads(message,thread,session) VALUES(?,?,?)', m, H, H); const text = JSON.stringify(call('human', 'thread.claims')); assert.ok(text.includes(H), 'the thread row is there'); assert.doesNotMatch(text, /secret/); }); // Row 264a (#1769, sage-2591, duckworth-0124 C4): a claim is held per session, so one thread with two // held sessions lists both runs, and releasing one leaves the other. A per-thread latest row would show // only the last claim. test('T6: two held sessions of one thread list as two rows, each its session\'s latest claim', (t) => { const { call, thread, claim } = setup(t); const H = thread('plan'); const S = call('human', 'session.create', { thread: H, name: 'second', op: 'op-second' }).id; const first = claim(H, 'run-default'); claim(H, 'run-second', S); assert.deepEqual(call('human', 'thread.claims').map((r) => [r.thread, r.holder_run]), [[H, 'run-default'], [H, 'run-second']]); call(first, 'role.release'); assert.deepEqual(call('human', 'thread.claims').map((r) => r.holder_run), ['run-second'], 'the default released, the second held'); });