Round 2 candidate db463a7b18962ee4afa393565abac9c4dd694dba49b18db20db064cac21fab89 (Dewey, packet8b4cad1f2), approved by Filbert in comment 28704. Sage's landing gate on05ad2e7d0: 24 steps rc 0, 23:58:57Z to 00:03:24Z. Co-Authored-By: Claude Opus 5.5 <[email protected]>
196 lines
7.5 KiB
JavaScript
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), []);
|
|
});
|