harness: a new PM run reads current launch state and open tasks before each turn, and old messages show their age, row 109 (#1603)
The broker gains a read-only state verb (launch state with the event that set it, open tasks capped). The runner fetches it before each turn and puts a "Current state" block at the top of the request; a failed read fails the turn. turnRequest() adds the message time. roles/pm.md gets one rule: the block is the truth for launch state and task status. Seat session, seat sandbox and cli launcher tests re-anchored to the new request shape. Built by Researcher, packet875c515e9. Filbert approved round 3, comment 28333, candidate manifest 0a62225e (11 paths). Sage gate on9774d70dbplus row109.patch: every package node suite and every scripts/test-*.sh green on the first run, skipped 0 in bus, harness and seat. Vikunja #259. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
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, TASK_PAGE } from '../src/broker.mjs';
|
||||
// Row 109 (#1603): `state` is what a run reads before it plans: the launch state with the time and
|
||||
// id of the event that set it, and the open tasks from the tracker snapshot, capped.
|
||||
const authority = { withinRole: ['message.send', 'role.launch'], crossRole: [] };
|
||||
const roles = { pm: { authority }, coder: { authority } };
|
||||
const businesses = {
|
||||
demo: { id: 'demo', human: 'jason', arbiters: { technical: 'pm', delivery: 'pm' }, launch: { by: 'pm', instances: ['coder'], max: { opus: 2, sonnet: 2 } }, roles },
|
||||
};
|
||||
const at = '2026-10-12T12:00:00.000Z';
|
||||
const digest = 'd'.repeat(64);
|
||||
const ref = (i) => `vikunja:3/${i}`;
|
||||
const fields = (i, done = false) => ({ project_id: 3, title: `Task ${i}`, description: `<p>${i}</p>`, done, bucket: 11 });
|
||||
|
||||
function setup(t) {
|
||||
const root = mkdtempSync(join(tmpdir(), 'bus-state-'));
|
||||
const store = new Store(root);
|
||||
const b = new Broker({ store, businesses });
|
||||
t.after(() => {
|
||||
store.close();
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
const human = b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
|
||||
const pm = b.bindLaunch({ business: 'demo', role: 'pm', run: 'demo-pm', harness: 'claude-code' });
|
||||
b.request(pm, { verb: 'role.claim' });
|
||||
const snap = (i, over = {}, read_at = at) => ({ task_ref: ref(i), updated: read_at, digest, fields: { ...fields(i), ...over }, via: 'board', read_at });
|
||||
const record = (...snapshots) => b.recordTask({ business: 'demo', snapshots });
|
||||
const state = (cap = pm, args = {}) => b.request(cap, { verb: 'state', args });
|
||||
return { b, store, human, pm, snap, record, state, reader: b.bindReader({ business: 'demo' }) };
|
||||
}
|
||||
|
||||
test('a business that never switched launches reads allowed with no time and no event', (t) => {
|
||||
const { state } = setup(t);
|
||||
assert.deepEqual(state().launch, { state: 'allowed', at: null, event: null });
|
||||
});
|
||||
|
||||
test('launches revoked and then restored read allowed, with the restore time and event id', (t) => {
|
||||
const { b, store, human, pm, state } = setup(t);
|
||||
const revoked = b.request(human, { verb: 'launch.revoke' }).id;
|
||||
assert.deepEqual(
|
||||
{ state: state().launch.state, event: state().launch.event },
|
||||
{ state: 'revoked', event: revoked },
|
||||
);
|
||||
const restored = b.request(human, { verb: 'launch.restore' }).id;
|
||||
const row = store.get('SELECT at FROM events WHERE id=?', restored);
|
||||
assert.deepEqual(state(pm).launch, { state: 'allowed', at: row.at, event: restored });
|
||||
assert.notEqual(restored, revoked);
|
||||
});
|
||||
|
||||
test('a task closed on the tracker leaves the open list, and total counts the open ones', (t) => {
|
||||
const { snap, record, state } = setup(t);
|
||||
record(snap(1), snap(2), snap(3));
|
||||
assert.deepEqual(state().tasks, {
|
||||
open: [1, 2, 3].map((i) => ({ task_ref: ref(i), title: `Task ${i}` })),
|
||||
total: 3,
|
||||
cap: TASK_PAGE,
|
||||
});
|
||||
record(snap(2, { done: true }, '2026-10-12T13:00:00.000Z'));
|
||||
const { open, total } = state().tasks;
|
||||
assert.deepEqual(open.map((x) => x.task_ref), [ref(1), ref(3)]);
|
||||
assert.equal(total, 2);
|
||||
});
|
||||
|
||||
test('a tombstone is not open', (t) => {
|
||||
const { snap, record, state } = setup(t);
|
||||
record(snap(1), { ...snap(2), fields: { gone: 'no-access' }, via: 'task' });
|
||||
assert.deepEqual(state().tasks.open.map((x) => x.task_ref), [ref(1)]);
|
||||
});
|
||||
|
||||
test('the open list stops at the cap and says so; total still counts every open task', (t) => {
|
||||
const { snap, record, state } = setup(t);
|
||||
record(...Array.from({ length: TASK_PAGE + 5 }, (_, k) => snap(k + 1)));
|
||||
const { open, total, cap } = state().tasks;
|
||||
assert.equal(open.length, TASK_PAGE);
|
||||
assert.equal(total, TASK_PAGE + 5);
|
||||
assert.equal(cap, TASK_PAGE);
|
||||
});
|
||||
|
||||
test('a role and a reader session read state; arguments, and an unbound cap, refuse', (t) => {
|
||||
const { reader, pm, state } = setup(t);
|
||||
assert.deepEqual(state(reader), state(pm));
|
||||
assert.throws(() => state(pm, { business: 'other' }), { code: 'invalid-request' });
|
||||
assert.throws(() => state('f'.repeat(64)), /unauthenticated/);
|
||||
});
|
||||
|
||||
test('a human session reads the same state, with the same cap', (t) => {
|
||||
const { snap, record, human, pm, state } = setup(t);
|
||||
record(...Array.from({ length: TASK_PAGE + 2 }, (_, k) => snap(k + 1)));
|
||||
const read = state(human);
|
||||
assert.deepEqual(read, state(pm));
|
||||
assert.equal(read.tasks.cap, TASK_PAGE);
|
||||
assert.equal(read.tasks.open.length, TASK_PAGE);
|
||||
assert.equal(read.tasks.total, TASK_PAGE + 2);
|
||||
});
|
||||
|
||||
test('reading state writes nothing: every table keeps its row count', (t) => {
|
||||
const { b, store, human, pm, reader, snap, record, state } = setup(t);
|
||||
b.request(human, { verb: 'launch.revoke' });
|
||||
record(snap(1), snap(2));
|
||||
const counts = () => Object.fromEntries(
|
||||
store.all("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
|
||||
.map(({ name }) => [name, store.get(`SELECT COUNT(*) AS n FROM "${name}"`).n]),
|
||||
);
|
||||
const before = counts();
|
||||
assert.ok(before.events > 0 && before.task_snapshots > 0);
|
||||
for (const cap of [pm, reader, human]) state(cap);
|
||||
assert.deepEqual(counts(), before);
|
||||
});
|
||||
Reference in New Issue
Block a user