Files
stack/packages/bus/tests/store.test.mjs
T
jason.woltjeandClaude Opus 5.5 38828a2cb3 feat(bus): the bus and the broker core (row 37, S2, rocko)
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]>
2026-10-05 17:15:53 -05:00

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);
});