The broker stores the highest comment id seen per business and task in an append-only table, task_comment_cursors. The tasks adapter restores those ids before its startup reconcile, and the first reconcile reads each task's comment list even with no cached count, so a comment posted while the bus was down is found. Cursor rows, snapshots and comment events commit in one transaction. migrate-schema.mjs takes a stopped store from 3b to 3c offline; the store refuses a 3b database until it runs. scripts/land-work/read.mjs reads SCHEMA_VERSION from store.mjs. The live bus is 3b. From this commit on, land-work refuses it with bus-schema until Mos migrates in the schema window (agents/sage/work/window-plan/schema-chain-3c-3e.md). Candidate 87ef1321a1a08be64a20499889fc24b5549b47d6d33abfd86db95f62fb3cb6d0, Rocko packet agents/rocko/work/queue-49/r1 (ea5e70fa8), base7b166a0f1. Filbert approved in 28579 and Darkwing in 28587. Landing gate atebbbf62da: 24 suites green, 0 failed. 14 package suites passed 1598 tests (bus 178, tasks 97, business 92, cli 195, webui 112). The 10 script suites passed (land-work 84, queue 156). Follow-ups: P19 tasks 300 (duplicate cursor) and 304 (push a task's cursor only when consider runs for it). Co-Authored-By: Claude Opus 5.5 <[email protected]>
51 lines
2.8 KiB
JavaScript
51 lines
2.8 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';
|
|
|
|
function fixture(t) {
|
|
const root = mkdtempSync(join(tmpdir(), 'comment-cursors-'));
|
|
const store = new Store(root);
|
|
const roles = { pm: { authority: { withinRole: ['message.send'], crossRole: [] } } };
|
|
const b = new Broker({ store, businesses: {
|
|
demo: { id: 'demo', human: 'jason', arbiters: { technical: 'pm', delivery: 'pm' }, roles },
|
|
other: { id: 'other', human: 'jason', arbiters: { technical: 'pm', delivery: 'pm' }, roles },
|
|
} });
|
|
t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); });
|
|
return { store, b, record: (commentCursors, extra = {}) => b.recordTask({ business: 'demo', commentCursors, ...extra }) };
|
|
}
|
|
|
|
test('comment cursors isolate businesses, move only forward and remain append-only', t => {
|
|
const { store, b, record } = fixture(t);
|
|
record([{ task_id: 7, last_comment: 11 }]);
|
|
record([{ task_id: 7, last_comment: 15 }]);
|
|
b.recordTask({ business: 'other', commentCursors: [{ task_id: 7, last_comment: 2 }] });
|
|
assert.deepEqual(b.taskView('demo', 'comments').cursors.map(c => ({ ...c })), [{ task_id: 7, last_comment: 15 }]);
|
|
assert.deepEqual(b.taskView('other', 'comments').cursors.map(c => ({ ...c })), [{ task_id: 7, last_comment: 2 }]);
|
|
for (const last_comment of [11, 15]) assert.throws(() => record([{ task_id: 7, last_comment }]), /must advance/);
|
|
for (const sql of [
|
|
'UPDATE task_comment_cursors SET last_comment=20 WHERE seq=1',
|
|
'DELETE FROM task_comment_cursors WHERE seq=1',
|
|
"INSERT OR REPLACE INTO task_comment_cursors VALUES(1,'demo',7,20)",
|
|
]) assert.throws(() => store.run(sql), /append-only/);
|
|
assert.equal(store.get('SELECT count(*) n FROM task_comment_cursors').n, 3);
|
|
});
|
|
|
|
test('only trusted sync writes validated cursors; events and cursor share rollback', t => {
|
|
const { store, b, record } = fixture(t);
|
|
const cap = b.bindLaunch({ business: 'demo', role: 'pm', run: 'pm-run', harness: 'pi' });
|
|
for (const x of [null, {}, { task_id: 0, last_comment: 1 }, { task_id: 1, last_comment: 0 },
|
|
{ task_id: 1, last_comment: 1.5 }, { task_id: 1, last_comment: 2 ** 53 },
|
|
{ task_id: 1, last_comment: 1, extra: true }]) {
|
|
assert.throws(() => record([x]), e => e.code === 'invalid-request');
|
|
}
|
|
assert.throws(() => record([{ task_id: 1, last_comment: 1 }], { cap }), e => e.code === 'agent-required');
|
|
assert.throws(() => record([{ task_id: 1, last_comment: 1 }], {
|
|
events: [{ kind: 'human.input', body: {} }],
|
|
}), e => e.code === 'reserved-event');
|
|
assert.equal(store.get('SELECT count(*) n FROM task_comment_cursors').n, 0);
|
|
});
|