Files
stack/packages/bus/tests/approval-binding.test.mjs
jason.woltjeandClaude Opus 5.5 d8487b3af8 bus: approvals bind to the decision and the role instance, not the run (row 136, #1630)
Round 2 candidate db463a7b18962ee4afa393565abac9c4dd694dba49b18db20db064cac21fab89
(Dewey, packet 8b4cad1f2), approved by Filbert in comment 28704. Sage's landing
gate on 05ad2e7d0: 24 steps rc 0, 23:58:57Z to 00:03:24Z.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-15 19:03:49 -05:00

196 lines
7.5 KiB
JavaScript

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 { Store } from '../src/store.mjs';
import { Broker } from '../src/broker.mjs';
// Row 136 (#1630): an approval binds to the decision, the business and the role instance, plus
// the thread when the raising run held one, not to the run that raised it. coder and coder2 are
// two instances of one definition; assistant is a thread instance.
const businesses = {
demo: {
id: 'demo',
human: 'jason',
arbiters: { technical: 'pm', delivery: 'pm' },
roles: {
coder: { definition: 'coder', authority: { withinRole: [], crossRole: [] } },
coder2: { definition: 'coder', authority: { withinRole: [], crossRole: [] } },
pm: { definition: 'pm', authority: { withinRole: [], crossRole: [] } },
assistant: { definition: 'assistant', authority: { withinRole: ['message.send'], crossRole: [] } },
},
definitions: { coder: {}, pm: {}, assistant: {} },
},
};
function setup(t) {
const root = mkdtempSync(join(tmpdir(), 'bus-binding-'));
let store, b;
const caps = {};
const ctx = {
get b() {
return b;
},
get store() {
return store;
},
caps,
call: (who, verb, args = {}) => b.request(caps[who], { verb, args }),
open(bs = businesses) {
store = new Store(root);
b = new Broker({ store, businesses: bs });
caps.human = b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
},
// A run of `role` that holds its claim; `thread` for the assistant.
run(name, role, thread) {
caps[name] = b.bindLaunch({ business: 'demo', role, run: name, harness: 'pi', ...(thread ? { thread } : {}) });
ctx.call(name, 'role.claim');
return caps[name];
},
end: (name) => b.endLaunch({ business: 'demo', run: name, reason: 'exited' }),
// A gated decision, so it routes to the human.
raise: (who, extra = {}) =>
ctx.call(who, 'decision.raise', {
action: 'deploy',
target: 'release1',
question: 'Ship release1?',
options: [
{ key: 'yes', text: 'Yes' },
{ key: 'no', text: 'No' },
],
recommendation: 'yes',
blocking: false,
...extra,
}),
approve: (d) => ctx.call('human', 'decision.resolve', { id: d.id, choice: 'yes' }),
use: (who, d) => b.authorize(caps[who], 'deploy', { decision: d.id, target: 'release1' }),
used: (d) =>
store.get(
"SELECT count(*) AS n FROM events WHERE kind='action.allowed' AND json_extract(body,'$.decision')=? AND json_extract(body,'$.operation') IS NOT 'decision.raise'",
d.id,
).n,
ops: (d) => store.all('SELECT op FROM decision_events WHERE decision=? ORDER BY seq', d.id).map((r) => r.op),
};
ctx.open();
t.after(() => {
store.close();
rmSync(root, { recursive: true, force: true });
});
return ctx;
}
// Two assistant threads in one project.
function threads(f) {
const project = f.call('human', 'project.create', { name: 'stack work' }).id;
const A = f.call('human', 'thread.create', { project, name: 'alpha' }).id;
const B = f.call('human', 'thread.create', { project, name: 'beta' }).id;
return { A, B };
}
test('REQ-DEC an approval outlives its run and a broker restart; a new run of the instance consumes it once', (t) => {
const f = setup(t);
f.run('run1', 'coder');
const d = f.raise('run1');
f.approve(d);
f.end('run1');
f.store.close();
f.open();
f.run('run2', 'coder');
assert.deepEqual(f.use('run2', d), { class: 'gated', decision: d.id });
assert.equal(f.used(d), 1);
// Once: neither the consuming run nor a later one uses it again.
assert.throws(() => f.use('run2', d), { code: 'decision-consumed' });
f.end('run2');
f.run('run3', 'coder');
assert.throws(() => f.use('run3', d), { code: 'decision-consumed' });
assert.equal(f.used(d), 1);
});
test('REQ-DEC another instance of the same definition cannot consume an approval, and its refusal consumes nothing', (t) => {
const f = setup(t);
f.run('run1', 'coder');
const d = f.raise('run1');
f.approve(d);
f.run('other', 'coder2');
assert.throws(() => f.use('other', d), { code: 'decision-mismatch' });
assert.equal(f.used(d), 0);
f.use('run1', d);
assert.equal(f.used(d), 1);
});
test('REQ-DEC REQ-THREAD-1 a thread approval is consumed only by a run holding the same thread', (t) => {
const f = setup(t);
const { A, B } = threads(f);
f.run('a1', 'assistant', A);
f.run('b1', 'assistant', B);
const d = f.raise('a1');
f.approve(d);
// The same instance in another thread.
assert.throws(() => f.use('b1', d), { code: 'decision-mismatch' });
assert.equal(f.used(d), 0);
f.end('a1');
f.run('a2', 'assistant', A);
f.use('a2', d);
assert.equal(f.used(d), 1);
assert.throws(() => f.use('a2', d), { code: 'decision-consumed' });
});
test('REQ-DEC a later run of the same instance supersedes an open decision; another instance cannot', (t) => {
const f = setup(t);
f.run('run1', 'coder');
const old = f.raise('run1');
f.end('run1');
f.run('run2', 'coder');
const replacement = f.raise('run2', { supersedes: old.id, question: 'Ship release1 tonight?' });
assert.equal(replacement.supersedes, old.id);
assert.deepEqual(f.ops(old), ['withdrawn']);
// The old decision is closed now, so superseding it again refuses.
assert.throws(() => f.raise('run2', { supersedes: old.id }), { code: 'supersede-refused' });
f.run('other', 'coder2');
assert.throws(() => f.raise('other', { supersedes: replacement.id }), { code: 'supersede-refused' });
assert.deepEqual(f.ops(replacement), []);
assert.equal(f.store.get('SELECT count(*) AS n FROM decisions').n, 2);
});
test('REQ-DEC REQ-THREAD-1 a thread decision is superseded only from the same thread', (t) => {
const f = setup(t);
const { A, B } = threads(f);
f.run('a1', 'assistant', A);
f.run('b1', 'assistant', B);
const old = f.raise('a1');
assert.throws(() => f.raise('b1', { supersedes: old.id }), { code: 'supersede-refused' });
assert.deepEqual(f.ops(old), []);
f.end('a1');
f.run('a2', 'assistant', A);
f.raise('a2', { supersedes: old.id });
assert.deepEqual(f.ops(old), ['withdrawn']);
});
// Before row 121 a thread instance's run held no thread, so a decision it raised has no thread
// claim. The broker below reads assistant as a plain role to raise one the way such a run did.
test('REQ-DEC REQ-THREAD-1 a thread instance decision raised before row 121 is not consumed or superseded by any thread run', (t) => {
const f = setup(t);
const { A } = threads(f);
f.store.close();
const plain = structuredClone(businesses);
plain.demo.roles.assistant.definition = 'assistant-before-121';
plain.demo.definitions['assistant-before-121'] = {};
f.open(plain);
f.run('old', 'assistant');
const d = f.raise('old');
f.approve(d);
// Left open: a resolved decision refuses supersede as closed, whatever its thread.
const open = f.raise('old', { question: 'Ship release2 tonight?' });
f.end('old');
assert.equal(f.store.get('SELECT count(*) AS n FROM thread_claims WHERE holder_run=?', 'old').n, 0);
f.store.close();
f.open();
f.run('a1', 'assistant', A);
assert.throws(() => f.use('a1', d), { code: 'decision-mismatch' });
assert.equal(f.used(d), 0);
assert.deepEqual(f.ops(d), ['resolved']);
assert.throws(() => f.raise('a1', { supersedes: open.id }), { code: 'supersede-refused' });
assert.deepEqual(f.ops(open), []);
});