Files
stack/packages/bus/tests/revoke-window.test.mjs

141 lines
7.3 KiB
JavaScript

// Row 306 (#1813, row 295's N1): a run whose claim #revokeLost ended answers thread-revoked on every
// claim-needing call until the run ends, so the runner stops it instead of exiting session-invalid.
// A claim another cause ended still answers not-holder. access-revoke.test.mjs's fixture, copied, with
// a limit stub and a usage stub for the limit stop. BUILD.md (agents/morgana/work/queue-306) names
// the mutant each test kills.
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 grant = (over = {}) => ({
id: `g-${over.user ?? 'alice'}`, user: 'alice', business: 'demo', projects: ['*'], instances: ['assistant'], level: 'collaborator', ...over,
});
const BUSINESSES = {
demo: {
id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, vars: { 'bus.tester': true }, plugins: { harness: ['pi'] },
roles: {
chief: { definition: 'chief-of-staff', authority: { withinRole: ['message.send'], crossRole: [] } },
assistant: { definition: 'assistant', authority: { withinRole: ['message.send'], crossRole: [] } },
},
definitions: { 'chief-of-staff': {}, assistant: { messaging: shipped('assistant').messaging } },
},
};
const HIT = { path: ['tokens', 'instances', 'assistant'], limit: 100, count: 120, unknownTurns: 1 };
// Alice and Bob each hold a collaborator grant and own a thread; the human owns thread H. Each thread
// has a claimed run: a-run on A, b-run on B, h-run on H. `stops` are the limit checks' answers, in order.
function setup(t, { stops = [] } = {}) {
const root = mkdtempSync(join(tmpdir(), 'bus-revoke-window-'));
const store = new Store(root);
t.after(() => {
store.close();
rmSync(root, { recursive: true, force: true });
});
const access = { doc: { accessVersion: 1, revision: 1, users: [], grants: [grant(), grant({ user: 'bob' })] }, rev: 1 };
const b = new Broker({
store,
businesses: BUSINESSES,
access: () => access.doc,
accessStamp: () => String(access.rev),
limits: { mustStop: async () => stops.shift() ?? { stop: false } },
usage: { collect: async () => ({ recorded: true }), view: () => ({}) },
});
const caps = { human: b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }) };
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 edit = (fn) => {
access.doc = { ...access.doc, revision: access.doc.revision + 1, grants: fn(access.doc.grants) };
access.rev++;
};
const launch = (name, thread) => {
caps[name] = b.bindLaunch({ business: 'demo', role: 'assistant', run: `${name}-run`, harness: 'pi', thread });
return call(name, 'role.claim');
};
const claim = (run) => ({ ...store.get('SELECT op, reason, by FROM thread_claims WHERE holder_run=? ORDER BY seq DESC LIMIT 1', run) });
// The codes of the run's refusals, as request() logs them.
const refused = (run) =>
store.all("SELECT body FROM events WHERE kind='action.refused' AND actor_run=? ORDER BY seq", run).map((e) => JSON.parse(e.body).code);
const P = call('human', 'project.create', { name: 'shared' }).id;
caps.alice = b.bindUser({ business: 'demo', user: 'alice' });
caps.bob = b.bindUser({ business: 'demo', user: 'bob' });
const A = call('alice', 'thread.create', { project: P, name: 'alice work' }).id;
const B = call('bob', 'thread.create', { project: P, name: 'bob work' }).id;
const H = call('human', 'thread.create', { project: P, name: 'owner work' }).id;
launch('a', A);
launch('b', B);
launch('h', H);
return { b, store, caps, call, code, edit, claim, refused, A, B, H };
}
const withoutAlice = (gs) => gs.filter((g) => g.user !== 'alice');
// The run's claim-needing calls after the sweep: each answers thread-revoked, and each is logged.
function lateCalls(code) {
return [
code('a', 'message.receive'),
code('a', 'state'),
code('a', 'message.read', { id: 'm-none' }),
code('a', 'message.send', { to: 'human', body: 'late' }),
];
}
test('row 306 T1: a run the timer sweep ended answers thread-revoked on every claim-needing call, logged, and its claim still refuses run-revoked', (t) => {
const { b, code, edit, claim, refused } = setup(t);
edit(withoutAlice);
assert.deepEqual(b.accessSweep().map((h) => h.run), ['a-run']);
assert.deepEqual(claim('a-run'), { op: 'revoke', reason: 'access-revoked', by: 'access-timer' });
assert.deepEqual(lateCalls(code), Array(4).fill('thread-revoked'));
assert.deepEqual(refused('a-run'), Array(4).fill('thread-revoked'));
assert.equal(code('a', 'role.claim'), 'run-revoked', 'role.claim keeps its order');
assert.equal(code('b', 'message.receive'), null, 'another user\'s run stays');
assert.equal(code('h', 'message.receive'), null, 'the human\'s run stays');
});
test('row 306 T1: a user\'s request and access.reload end the run the same way, and it answers thread-revoked', (t) => {
for (const trigger of ['request', 'reload']) {
const { code, call, edit, claim } = setup(t);
edit(withoutAlice);
if (trigger === 'request') assert.equal(code('bob', 'thread.list'), null);
else assert.deepEqual(call('human', 'access.reload').ended.map((h) => h.run), ['a-run']);
assert.equal(claim('a-run').reason, 'access-revoked', trigger);
assert.equal(code('a', 'message.receive'), 'thread-revoked', trigger);
}
});
// The control (Dewey's C2 in dewey-0918): the first call after a pending limit stop answers from
// #limitStop and never reaches #agent, so only the run's next call checks how its claim ended.
test('row 306 T2: after the sweep, a run the human stopped and a run the limit stop ended still answer not-holder', async (t) => {
const { b, call, code, edit, claim, refused, B } = setup(t, { stops: [{ stop: true, check: 'token-limit', hit: HIT }] });
edit(withoutAlice);
assert.deepEqual(b.accessSweep().map((h) => h.run), ['a-run']);
// The human's stop takes the human's own thread only: H.
assert.deepEqual(call('human', 'assistant.stop', { instance: 'assistant' }).stopped.map((s) => s.run), ['h-run']);
assert.equal(code('h', 'message.receive'), 'not-holder', 'stopped by the human');
assert.equal(code('h', 'state'), 'not-holder', 'stopped by the human, again');
// Bob's turn ends at the limit, and the run's next receive ends its claim.
const { id } = call('bob', 'thread.send', { thread: B, body: 'look' });
call('b', 'message.receive');
await call('b', 'usage.begin', { turn: id });
await call('b', 'usage.end', { turn: id });
assert.equal(code('b', 'message.receive'), 'not-holder', 'the limit stop');
assert.equal(claim('b-run').by, 'limit-check');
assert.equal(code('b', 'message.receive'), 'not-holder', 'the limit-stopped run\'s next call');
assert.equal(code('b', 'state'), 'not-holder');
assert.deepEqual(refused('h-run'), ['not-holder', 'not-holder']);
assert.deepEqual(refused('b-run'), ['not-holder', 'not-holder', 'not-holder']);
});