192 lines
15 KiB
JavaScript
192 lines
15 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
const secret = 'sk-test-NOT-A-SECRET-1234';
|
|
function scratch(t) { const root = mkdtempSync(join(tmpdir(), 'r4-')); t.after(() => rmSync(root, { recursive: true, force: true })); return root; }
|
|
function safe(label, Type, props = {}, omitted = secret) { return error => {
|
|
assert.ok(error instanceof Type, error.stack);
|
|
for (const [key, value] of Object.entries(props)) assert.equal(error[key], value);
|
|
assert.ok(error.message.includes(label), error.message);
|
|
assert.doesNotMatch(error.message + error.stack, /sk-test|NOT-A-SECRET|Unexpected token/);
|
|
assert.ok(!error.message.includes(omitted), error.message);
|
|
assert.equal(error.cause, undefined);
|
|
return true;
|
|
}; }
|
|
import { checkCallerOpId, checkName, checkRepoPath, parseManifest, validateRow, loadDoc, serialize } from '../src/queue.mjs';
|
|
import { QueueError } from '../src/errors.mjs';
|
|
for (const [field, fn] of [
|
|
['op', () => checkCallerOpId(secret + '/', 'op')],
|
|
['op', () => checkCallerOpId(secret.toLowerCase() + '.outcome', 'op')],
|
|
['owner', () => checkName(secret, 'owner')],
|
|
['path', () => checkRepoPath(secret + '/', 'path')],
|
|
['manifest', () => parseManifest(secret + '\n')],
|
|
['row', () => validateRow({ id: secret })],
|
|
['version', () => loadDoc(Buffer.from(serialize({ version: secret, canonicalRoot: '/x', revision: 0, rows: [], log: [] })))],
|
|
]) test(`queue ${field} semantic refusal omits the value`, () => assert.throws(fn, safe(field, QueueError, { code: 2 }, secret.toLowerCase())));
|
|
import { checkTransport, parseBriefSpec, genesisRows, parseMigrationMap, render, sha256, genesisReceipt } from '../src/queue.mjs';
|
|
import { MAP_ROWS, mapText } from './helpers.mjs';
|
|
const at = '2026-11-10T00:00:00.000Z';
|
|
function doc() {
|
|
const map = parseMigrationMap(mapText()), rows = genesisRows(map, new Map(map.rows.filter(r => r.brief).map(r => [r.id, 'a'.repeat(40)])), 'genesis-op-0001', at, 'sage');
|
|
const entry = { rev: 0, op: 'genesis-op-0001', verb: 'genesis', args: { root: '/x', branch: 'refactor', map: 'docs/map.md' }, by: 'sage', at, semantics: 1,
|
|
result: { mapBlob: 'b'.repeat(40), highWater: map.highWater, retired: map.retired, rows, legacyView: 'legacy\n', receipt: genesisReceipt('genesis-op-0001', rows.length) }, viewSha: sha256(render(rows, 0)) };
|
|
return { version: 1, canonicalRoot: '/x', revision: 0, rows, log: [entry] };
|
|
}
|
|
for (const [field, edit, value = secret] of [
|
|
['id', r => { r.id = secret; }], ['state', r => { r.state = secret; }],
|
|
['createdAt', r => { r.createdAt = secret; }],
|
|
['createdAt', r => { r.createdAt = '2026-02-30'; }, '2026-02-30'],
|
|
['claim seat', r => { r.claim = { seat: secret, op: 'claimed-op-0001' }; }],
|
|
]) test(`row ${field} omits semantic value ${value === secret ? 'sentinel' : 'calendar control'}`, () => {
|
|
const r = doc().rows[0]; edit(r); assert.throws(() => validateRow(r), safe(field, QueueError, { code: 2 }, value));
|
|
});
|
|
for (const [field, edit] of [
|
|
['rev', d => { d.log[0].rev = secret; }], ['verb', d => { d.log[0].verb = secret; }],
|
|
['semantics', d => { d.log[0].semantics = secret; }], ['revision', d => { d.revision = secret; }],
|
|
]) test(`queue ${field} omits semantic value from the log/document`, () => {
|
|
const d = doc(); edit(d); assert.throws(() => loadDoc(Buffer.from(serialize(d))), safe(field, QueueError, { code: 2 }));
|
|
});
|
|
test('transport comment omits its value', () => assert.throws(() => checkTransport({ outcome: 'posted', status: 201, comment: secret, detail: 'fixture' }), safe('comment', QueueError, { code: 2 })));
|
|
test('brief omits its unparseable spec', () => assert.throws(() => parseBriefSpec(secret), safe('brief', QueueError, { code: 2 })));
|
|
test('manifest duplicate path omits the value', () => assert.throws(() => parseManifest(`${'a'.repeat(64)} ${secret}\n`.repeat(2)), safe('manifest', QueueError, { code: 2 })));
|
|
|
|
import { canonArgs, applyEntry, replay } from '../src/queue.mjs';
|
|
const seatValue = secret.toLowerCase(), digestValue = 'a'.repeat(40);
|
|
function reviewRow(row, state = 'requesting') {
|
|
row.state = 'in-review';
|
|
const posted = state === 'posted';
|
|
row.review = { rounds: [{ n: 1, op: seatValue, by: 'darkwing', at, issue: 1511,
|
|
candidate: { kind: 'commit', digest: digestValue, text: null }, request: 'comment',
|
|
attempts: [{ op: seatValue, by: 'darkwing', at, state, comment: posted ? 1 : null,
|
|
transport: posted ? { outcome: 'posted', status: 201, comment: 1, detail: 'fixture' } : null, resolutions: [] }],
|
|
duplicateRisk: false, receipts: [] }] };
|
|
}
|
|
function operation(verb, args, edit = () => {}, options = {}) {
|
|
const state = replay(doc().log), row = state.rows.get(6);
|
|
edit(row);
|
|
if (!options.unchecked) validateRow(row);
|
|
const by = options.by ?? 'sage', op = options.op ?? 'operation-op-0001';
|
|
const entry = { verb, args: options.raw ? args : canonArgs(verb, { id: 6, ...args }), by, op, at, semantics: 2 };
|
|
return applyEntry(state, entry, options.resolved ?? {});
|
|
}
|
|
const ownerValue = row => { row.owner = seatValue; if (row.claim) row.claim.seat = seatValue; };
|
|
const restored = new Set(['claimed owner authority', 'start owner', 'review claimant', 'close gate owner', 'set owner reviewer', 'release claimant', 'assign existing owner', 'assign reviewer', 'note owner', 'add owner reviewer', 'unresolved request', 'missing approval', 'posted request', 'outcome actor', 'duplicate outcome', 'settled resolution', 'reviewer list', 'duplicate verdict']);
|
|
for (const [name, field, fn, omitted = seatValue] of [
|
|
['canonical duplicate reviewer', 'repeated value', () => canonArgs('set', { id: 6, field: 'reviewers', value: [seatValue, seatValue] })],
|
|
['canonical state', 'state', () => canonArgs('move', { id: 6, to: secret }), secret],
|
|
['canonical verb', 'verb', () => canonArgs(secret, {}), secret],
|
|
['claimed owner authority', 'owner', () => operation('move', { to: 'blocked', reason: 'pause' }, ownerValue, { by: 'outsider' })],
|
|
['start owner', 'owner', () => operation('move', { to: 'in-progress' }, r => { ownerValue(r); r.state = 'briefed'; r.claim = null; }, { by: 'outsider' })],
|
|
['review claimant', 'claimant', () => operation('move', { to: 'in-review', candidate: 'HEAD' }, ownerValue, { by: 'outsider' })],
|
|
['close gate owner', 'gate owner', () => operation('move', { to: 'done' }, r => { reviewRow(r, 'posted'); r.gateOwner = seatValue; }, { by: 'outsider' })],
|
|
['set owner reviewer', 'owner', () => operation('set', { field: 'reviewers', value: [seatValue] }, ownerValue)],
|
|
['set stored piece', 'piece', () => operation('set', { field: 'piece', value: secret }, r => { r.piece = secret; }), secret],
|
|
['set pinned brief', 'brief', () => { const r = doc().rows.find(r => r.id === 6), brief = { ...r.brief, blob: digestValue };
|
|
return operation('set', { field: 'brief', value: `${brief.path}#${brief.anchor}` }, r => { r.brief = brief; }, { resolved: { brief } }); }, digestValue],
|
|
['release claimant', 'claimant', () => operation('release', {}, ownerValue, { by: 'outsider' })],
|
|
['assign existing owner', 'owned', () => operation('assign', { seat: seatValue }, ownerValue)],
|
|
['assign reviewer', 'reviewers', () => operation('assign', { seat: seatValue }, r => { r.reviewers = [seatValue]; })],
|
|
['note owner', 'owner', () => operation('note', { text: 'note' }, ownerValue, { by: 'outsider' })],
|
|
['add owner reviewer', 'owner', () => operation('add', { piece: 'piece', gate: 'gate', brief: 'docs/brief.md#Brief', owner: seatValue, reviewers: [seatValue] }, () => {},
|
|
{ resolved: { brief: { path: 'docs/brief.md', anchor: 'Brief', blob: digestValue } } })],
|
|
['unresolved request', 'unresolved review request', () => operation('review-request', {}, r => reviewRow(r))],
|
|
['missing approval', 'approval', () => operation('move', { to: 'done' }, r => { reviewRow(r, 'posted'); r.gateOwner = 'sage'; r.reviewers = [seatValue]; })],
|
|
['missing attempt without round', 'review request', () => operation('review-resolve', { attempt: seatValue, comment: 1 })],
|
|
['missing attempt in round', 'review request', () => operation('review-resolve', { attempt: seatValue + '-missing', comment: 1 }, r => reviewRow(r))],
|
|
['posted request', 'posted request', () => operation('review-request', {}, r => reviewRow(r, 'posted'))],
|
|
['outcome op', 'outcome', () => operation('review-outcome', { attempt: seatValue, outcome: 'posted', status: 201, comment: 1, detail: 'fixture' }, r => reviewRow(r))],
|
|
['outcome actor', 'outcome', () => operation('review-outcome', { attempt: seatValue, outcome: 'posted', status: 201, comment: 1, detail: 'fixture' }, r => { reviewRow(r); r.review.rounds[0].attempts[0].by = seatValue; },
|
|
{ by: 'outsider', op: seatValue + '.outcome' })],
|
|
['duplicate outcome', 'outcome', () => operation('review-outcome', { attempt: seatValue, outcome: 'posted', status: 201, comment: 1, detail: 'fixture' }, r => reviewRow(r, 'posted'),
|
|
{ by: 'darkwing', op: seatValue + '.outcome' })],
|
|
['settled resolution', 'request', () => operation('review-resolve', { attempt: seatValue, comment: 1 }, r => reviewRow(r, 'posted'))],
|
|
['reviewer list', 'reviewer', () => operation('review-record', { verdict: 'approve', comment: 1, candidate: digestValue }, r => { reviewRow(r, 'posted'); r.reviewers = [seatValue]; }, { by: 'outsider' })],
|
|
['candidate mismatch', 'candidate', () => operation('review-record', { verdict: 'approve', comment: 1, candidate: 'b'.repeat(40) }, r => reviewRow(r, 'posted'), { by: 'filbert' }), digestValue],
|
|
['duplicate verdict', 'verdict', () => operation('review-record', { verdict: 'approve', comment: 1, candidate: digestValue }, r => { reviewRow(r, 'posted'); r.reviewers = [seatValue];
|
|
r.review.rounds[0].receipts = [{ reviewer: seatValue, op: 'receipt-op-0001', at, verdict: 'approve', comment: 1, candidate: digestValue }]; }, { by: seatValue })],
|
|
['legacy candidate evidence', 'candidate', () => operation('move', { to: 'done', evidence: `comment=1,round=1,candidate=${'b'.repeat(40)}` }, r => {
|
|
reviewRow(r); r.gateOwner = 'sage'; const round = r.review.rounds[0]; round.request = 'none'; delete round.attempts; delete round.duplicateRisk; delete round.receipts;
|
|
}), digestValue],
|
|
['direct unknown operation', 'verb', () => operation(secret, {}, () => {}, { raw: true }), secret],
|
|
// outcomeState also defends direct callers with an inconsistent transport record.
|
|
['inconsistent outcome record', 'outcome', () => operation('review-outcome', { attempt: seatValue, outcome: 'posted', status: 201, comment: 1, detail: 'fixture' }, r => reviewRow(r, 'failed'),
|
|
{ by: 'darkwing', op: seatValue + '.outcome', unchecked: true })],
|
|
]) test(`queue replay refusal ${name} ${restored.has(name) ? 'names validated identifiers' : 'omits stored values'}`, () => assert.throws(fn, restored.has(name) ? named(field, omitted, name === 'posted request' ? 'comment 1' : null) : safe(field, QueueError, { code: 2 }, omitted)));
|
|
|
|
test('migration map shape omits an unchecked row id', () => assert.throws(() => parseMigrationMap(mapText([{ id: secret }])), safe('migration map row', QueueError, { code: 2 })));
|
|
test('migration map brief shape omits an unchecked row id', () => {
|
|
const row = structuredClone(MAP_ROWS[1]); row.id = secret; row.brief = {};
|
|
assert.throws(() => parseMigrationMap(mapText([row])), safe('brief', QueueError, { code: 2 }));
|
|
});
|
|
test('migration map duplicate owner omits unvalidated values', () => {
|
|
const row = structuredClone(MAP_ROWS[1]); row.owner = secret; row.reviewers = [secret];
|
|
assert.throws(() => parseMigrationMap(mapText([row])), safe('owner', QueueError, { code: 2 }));
|
|
});
|
|
test('BASE control: genesis expected claim already omits the stored op', () => {
|
|
const d = doc(); d.log[0].op = seatValue; d.log[0].result.receipt = genesisReceipt(seatValue, d.rows.length);
|
|
assert.throws(() => loadDoc(Buffer.from(serialize(d))), safe('claim', QueueError, { code: 2 }, seatValue));
|
|
});
|
|
|
|
function named(label, value, additional = null) { return error => {
|
|
assert.ok(error instanceof QueueError); assert.equal(error.code, 2);
|
|
assert.ok(error.message.includes(label), error.message);
|
|
assert.ok(error.message.includes(value), error.message);
|
|
if (additional !== null) assert.ok(error.message.includes(additional), error.message);
|
|
return true;
|
|
}; }
|
|
|
|
// Each restored replay refusal is behind row validation or canonical arguments
|
|
// on the store's write path. Replace the relevant identifier at that boundary.
|
|
for (const [site, edit] of [
|
|
['claimed owner authority', r => { r.owner = secret; }],
|
|
['start owner', r => { r.owner = secret; }],
|
|
['review claimant', r => { r.claim.seat = secret; }],
|
|
['close gate owner', r => { r.gateOwner = secret; }],
|
|
['set owner reviewer', r => { r.owner = secret; }],
|
|
['release claimant', r => { r.claim.seat = secret; }],
|
|
['assign existing owner', r => { r.owner = secret; }],
|
|
['assign reviewer', r => { r.reviewers = [secret]; }],
|
|
['note owner', r => { r.owner = secret; }],
|
|
['unresolved request', r => { reviewRow(r); r.review.rounds[0].attempts[0].op = secret + '/'; }],
|
|
['missing approval', r => { reviewRow(r, 'posted'); r.reviewers = [secret]; }],
|
|
['posted request', r => { reviewRow(r, 'posted'); r.review.rounds[0].attempts[0].op = secret + '/'; }],
|
|
['posted request comment', r => { reviewRow(r, 'posted'); r.review.rounds[0].attempts[0].comment = secret; }],
|
|
['outcome actor', r => { reviewRow(r); r.review.rounds[0].attempts[0].by = secret; }],
|
|
['duplicate outcome', r => { reviewRow(r, 'posted'); r.review.rounds[0].attempts[0].op = secret + '/'; }],
|
|
['settled resolution', r => { reviewRow(r, 'posted'); r.review.rounds[0].attempts[0].op = secret + '/'; }],
|
|
['reviewer list', r => { r.reviewers = [secret]; }],
|
|
['duplicate verdict', r => { reviewRow(r, 'posted'); r.review.rounds[0].receipts = [{ reviewer: secret, op: 'receipt-op-0001', at, verdict: 'approve', comment: 1, candidate: digestValue }]; }],
|
|
]) test(`queue ${site} rejects malformed identifier before replay refusal`, () => {
|
|
const row = doc().rows.find(r => r.id === 6); edit(row);
|
|
assert.throws(() => validateRow(row), safe('', QueueError, { code: 2 }));
|
|
});
|
|
test('add owner reviewer rejects invalid owner before the conflict refusal', () => {
|
|
assert.throws(() => canonArgs('add', { piece: 'piece', gate: 'gate', brief: 'docs/brief.md#Brief', owner: secret, reviewers: [secret] }), safe('owner', QueueError, { code: 2 }));
|
|
});
|
|
test('claim mismatch names only the validated owner', () => {
|
|
const row = doc().rows.find(r => r.id === 6); row.claim.seat = secret;
|
|
assert.throws(() => validateRow(row), error => {
|
|
assert.ok(safe('claim seat', QueueError, { code: 2 })(error));
|
|
assert.ok(error.message.includes('darkwing'), error.message); return true;
|
|
});
|
|
});
|
|
test('duplicate logged op names the validated op; malformed op stops first', () => {
|
|
for (const op of ['genesis-op-0001', secret + '/']) {
|
|
const d = doc(); d.log[0].op = op; d.log.push({ ...structuredClone(d.log[0]), rev: 1 }); d.revision = 1;
|
|
assert.throws(() => loadDoc(Buffer.from(serialize(d))), op === 'genesis-op-0001' ? named('op id', op) : safe('op', QueueError, { code: 2 }));
|
|
}
|
|
});
|
|
test('unresolved request refusal names every open request', () => {
|
|
assert.throws(() => operation('review-request', {}, row => {
|
|
reviewRow(row);
|
|
row.review.rounds[0].attempts.push({ ...structuredClone(row.review.rounds[0].attempts[0]), op: 'second-request-01' });
|
|
}), named('unresolved review request', seatValue, 'second-request-01'));
|
|
});
|
|
test('approval refusal names every missing approver', () => {
|
|
assert.throws(() => operation('move', { to: 'done' }, row => {
|
|
reviewRow(row, 'posted'); row.gateOwner = 'sage'; row.reviewers = ['filbert', 'rocko'];
|
|
}), named('approval', 'filbert', 'rocko'));
|
|
});
|