Rocko's round 2 candidate, approved by Darkwing (#1519 comment 26757).
build.patch 40d7e838, manifest 61519059, 24 files under packages/bus,
schema v3b (179ffe35, lead decision 60). Integration gate in a git
worktree of 942dca9e (S1 in the tree) plus the patch: bus 43/43 and
business 60/60 on Node 24 and 26, every package test and every
scripts/test-*.sh green, test-task 98/98 with the live-provider cases.
Rulings from lead decisions 62 and 63: the human proof is cooperative in
slice 1, and a self-raised cross-role decision routes to the human.
Single-use gated approvals follow in row 43.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
160 lines
5.4 KiB
JavaScript
160 lines
5.4 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdtempSync, rmSync, statSync, symlinkSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { DatabaseSync } from 'node:sqlite';
|
|
const { Store } = await import('../src/store.mjs').catch((e) => {
|
|
if (e.code === 'ERR_MODULE_NOT_FOUND') return {};
|
|
throw e;
|
|
});
|
|
const scratch = (t) => {
|
|
const root = mkdtempSync(join(tmpdir(), 'bus-store-'));
|
|
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
return root;
|
|
};
|
|
test('creates private WAL store and excludes a second writer until explicit close', (t) => {
|
|
assert.equal(typeof Store, 'function', 'Store implementation exists');
|
|
const root = scratch(t);
|
|
const s = new Store(root);
|
|
t.after(() => s.close());
|
|
assert.equal(statSync(join(root, 'bus')).mode & 0o777, 0o700);
|
|
assert.equal(statSync(join(root, 'bus/bus.sqlite')).mode & 0o777, 0o600);
|
|
assert.equal(s.get('PRAGMA journal_mode').journal_mode, 'wal');
|
|
assert.throws(() => new Store(root), /writer-locked/);
|
|
s.close();
|
|
const next = new Store(root);
|
|
next.close();
|
|
});
|
|
test('rollback is atomic and schema metadata is checked against trusted DDL, not just itself', (t) => {
|
|
assert.equal(typeof Store, 'function');
|
|
const root = scratch(t);
|
|
const s = new Store(root);
|
|
assert.throws(
|
|
() =>
|
|
s.transaction(() => {
|
|
s.run("INSERT INTO events(id,at,business,kind,body) VALUES('e','x','b','action.allowed','{}')");
|
|
throw Error('rollback');
|
|
}),
|
|
/rollback/,
|
|
);
|
|
assert.equal(s.get('SELECT count(*) n FROM events').n, 0);
|
|
s.close();
|
|
const db = new DatabaseSync(join(root, 'bus/bus.sqlite'));
|
|
db.exec('DROP TRIGGER events_no_update');
|
|
db.close();
|
|
assert.throws(() => new Store(root), /schema-mismatch/);
|
|
});
|
|
test('existing empty database and symlink runtime directory refuse, never initialize over damage', (t) => {
|
|
assert.equal(typeof Store, 'function');
|
|
const root = scratch(t);
|
|
const s = new Store(root);
|
|
s.close();
|
|
const db = new DatabaseSync(join(root, 'bus/bus.sqlite'));
|
|
db.exec('DROP TRIGGER meta_no_delete; DELETE FROM meta');
|
|
db.close();
|
|
assert.throws(() => new Store(root), /schema-mismatch/);
|
|
const other = scratch(t);
|
|
symlinkSync(join(root, 'bus'), join(other, 'bus'));
|
|
assert.throws(() => new Store(other), /unsafe-path/);
|
|
});
|
|
test('crash during a transaction recovers no partial event after explicit fixture-only lock removal', async (t) => {
|
|
const { spawn } = await import('node:child_process');
|
|
const { once } = await import('node:events');
|
|
const { unlinkSync } = await import('node:fs');
|
|
const root = scratch(t);
|
|
const child = spawn(
|
|
process.execPath,
|
|
[new URL('./fixtures/crash-writer.mjs', import.meta.url).pathname, root],
|
|
{ stdio: ['ignore', 'pipe', 'pipe'] },
|
|
);
|
|
t.after(() => {
|
|
if (child.exitCode === null) child.kill('SIGKILL');
|
|
});
|
|
const ready = await Promise.race([
|
|
once(child.stdout, 'data'),
|
|
once(child, 'exit').then(() => {
|
|
throw Error('crash fixture exited early');
|
|
}),
|
|
]);
|
|
assert.match(String(ready[0]), /inserted/);
|
|
const exit = once(child, 'exit');
|
|
child.kill('SIGKILL');
|
|
await exit;
|
|
assert.throws(() => new Store(root), /writer-locked/);
|
|
unlinkSync(join(root, 'bus/writer.lock')); // Known dead test child, never production recovery.
|
|
const recovered = new Store(root);
|
|
try {
|
|
assert.equal(recovered.get("SELECT count(*) n FROM events WHERE id='uncommitted'").n, 0);
|
|
} finally {
|
|
recovered.close();
|
|
}
|
|
});
|
|
test('writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers', (t) => {
|
|
const root = scratch(t),
|
|
store = new Store(root);
|
|
t.after(() => store.close());
|
|
const insert = (at) =>
|
|
store.run(
|
|
'INSERT INTO events(id,at,business,kind,body) VALUES(?,?,?,?,?)',
|
|
'time-' + at,
|
|
at,
|
|
'demo',
|
|
'action.allowed',
|
|
'{}',
|
|
);
|
|
for (const at of [
|
|
'2026-10-04T00:00:00Z',
|
|
'2026-10-04T01:00:00.000+01:00',
|
|
'2026-10-04T00:00:00.000000Z',
|
|
'2026-02-30T00:00:00.000Z',
|
|
])
|
|
assert.throws(() => insert(at), /invalid-timestamp/);
|
|
assert.equal(store.get('SELECT count(*) n FROM events').n, 0);
|
|
insert('2026-10-04T00:00:00.000Z');
|
|
assert.throws(
|
|
() =>
|
|
store.run(
|
|
"INSERT INTO events(seq,id,at,business,kind,body) VALUES(-1,'low','2026-10-04T00:00:00Z','demo','action.allowed','{}')",
|
|
),
|
|
/invalid-timestamp/,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
store.transaction(() => {
|
|
insert('2026-10-04T00:00:01.000Z');
|
|
store.run(
|
|
'INSERT INTO task_snapshots(at,business,task_ref,updated,digest,fields,source,via,read_at) VALUES(?,?,?,?,?,?,?,?,?)',
|
|
'2026-10-04T00:00:02.000Z',
|
|
'demo',
|
|
'vikunja:1/1',
|
|
'external',
|
|
'a'.repeat(64),
|
|
'{"bucket":1}',
|
|
'poll',
|
|
'board',
|
|
'2026-10-04T00:00:01Z',
|
|
);
|
|
}),
|
|
/invalid-timestamp/,
|
|
);
|
|
assert.equal(store.get('SELECT count(*) n FROM events').n, 1, 'whole transaction rolls back');
|
|
assert.equal(store.get('SELECT count(*) n FROM task_snapshots').n, 0);
|
|
});
|
|
test('async transactions refuse before invoking their function', async (t) => {
|
|
const root = scratch(t),
|
|
store = new Store(root);
|
|
t.after(() => store.close());
|
|
let ran = false;
|
|
assert.throws(
|
|
() =>
|
|
store.transaction(async () => {
|
|
ran = true;
|
|
await Promise.resolve();
|
|
}),
|
|
/async-transaction-refused/,
|
|
);
|
|
await Promise.resolve();
|
|
assert.equal(ran, false);
|
|
});
|