Files
qinglong/test/back/completionReceiptOrphanAuditor.test.cjs
T

282 lines
9.0 KiB
JavaScript

require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const { afterEach, test } = require('node:test');
const {
CompletionReceiptOrphanFileDirectory,
} = require('../../back/runtime/adapters/fs/completionReceiptOrphanDirectory');
const {
CompletionReceiptOrphanAuditor,
} = require('../../back/runtime/application/completionReceiptOrphanAuditor');
const { parseArguments } = require('../../scripts/ql3-receipt-audit.cjs');
const REPOSITORY_ROOT = path.resolve(__dirname, '../..');
const CLI_PATH = path.join(REPOSITORY_ROOT, 'scripts', 'ql3-receipt-audit.cjs');
const OBSERVED_AT_MS = 1_800_000_000_000;
const OLD_MTIME_MS = OBSERVED_AT_MS - 10 * 60_000;
const YOUNG_MTIME_MS = OBSERVED_AT_MS - 1_000;
const OWNED_ID = '019f7400-0000-7000-8000-000000000001';
const ACTIVE_ID = '019f7400-0000-7000-8000-000000000002';
const TERMINAL_ID = '019f7400-0000-7000-8000-000000000003';
const UNKNOWN_ID = '019f7400-0000-7000-8000-000000000004';
const YOUNG_ID = '019f7400-0000-7000-8000-000000000005';
const JOURNAL_ONLY_ID = '019f7400-0000-7000-8000-000000000006';
const roots = [];
async function temporaryRoot() {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql3-orphan-audit-'));
roots.push(root);
return root;
}
async function writeAt(root, shard, name, modifiedAtMs = OLD_MTIME_MS) {
const directory = path.join(root, shard);
const target = path.join(directory, name);
await fs.mkdir(directory, { recursive: true });
await fs.writeFile(target, '{}');
const timestamp = new Date(modifiedAtMs);
await fs.utimes(target, timestamp, timestamp);
return target;
}
function ownershipSource(values) {
const ownership = new Map(values.map((value) => [value.attemptId, value]));
return {
async lookup(attemptIds) {
return new Map(
attemptIds
.filter((attemptId) => ownership.has(attemptId))
.map((attemptId) => [attemptId, ownership.get(attemptId)]),
);
},
};
}
afterEach(async () => {
await Promise.all(
roots
.splice(0)
.map((root) => fs.rm(root, { recursive: true, force: true })),
);
});
test('audits Journal ownership and quarantines only old regular orphans', async () => {
const root = await temporaryRoot();
const shard = '01';
const names = {
owned: `${OWNED_ID}.json`,
active: `${ACTIVE_ID}.json`,
terminal: `${TERMINAL_ID}.json`,
unknown: `${UNKNOWN_ID}.json`,
young: `${YOUNG_ID}.json`,
temporary: `.${UNKNOWN_ID}.${'a'.repeat(32)}.tmp`,
malformed: 'unexpected.bin',
unsafe: 'unsafe-link',
};
for (const [key, name] of Object.entries(names)) {
if (key !== 'unsafe') {
await writeAt(
root,
shard,
name,
key === 'young' ? YOUNG_MTIME_MS : OLD_MTIME_MS,
);
}
}
await fs.symlink(
path.join(root, shard, names.malformed),
path.join(root, shard, names.unsafe),
);
const auditor = new CompletionReceiptOrphanAuditor(
new CompletionReceiptOrphanFileDirectory(root),
ownershipSource([
{
attemptId: OWNED_ID,
attemptStatus: 'running',
journalState: 'pending',
},
{ attemptId: ACTIVE_ID, attemptStatus: 'running' },
{ attemptId: TERMINAL_ID, attemptStatus: 'succeeded' },
]),
);
const audit = await auditor.run({
observedAtMs: OBSERVED_AT_MS,
startShard: 1,
shardCount: 1,
maxEntriesPerShard: 16,
minimumAgeMs: 5 * 60_000,
});
assert.equal(audit.scannedEntries, 8);
assert.deepEqual(audit.overflowShards, []);
assert.equal(audit.counts.journaled, 1);
assert.equal(audit.counts.active_attempt, 1);
assert.equal(audit.counts.terminal_orphan, 1);
assert.equal(audit.counts.unknown_receipt, 1);
assert.equal(audit.counts.young_unknown_receipt, 1);
assert.equal(audit.counts.stale_temporary, 1);
assert.equal(audit.counts.unknown_entry, 1);
assert.equal(audit.counts.unsafe_entry, 1);
assert.equal(
audit.entries.filter((entry) => entry.action === 'eligible').length,
4,
);
const quarantine = await auditor.run({
mode: 'quarantine',
observedAtMs: OBSERVED_AT_MS,
startShard: 1,
shardCount: 1,
maxEntriesPerShard: 16,
minimumAgeMs: 5 * 60_000,
});
assert.equal(
quarantine.entries.filter((entry) => entry.action === 'quarantined').length,
4,
);
for (const name of [
names.terminal,
names.unknown,
names.temporary,
names.malformed,
]) {
await assert.rejects(fs.lstat(path.join(root, shard, name)), /ENOENT/);
}
for (const name of [names.owned, names.active, names.young, names.unsafe]) {
assert.ok(await fs.lstat(path.join(root, shard, name)));
}
const quarantineFiles = await fs.readdir(
path.join(root, '.orphan-quarantine', shard),
);
assert.equal(quarantineFiles.length, 4);
});
test('fails closed when a shard exceeds its fixed entry capacity', async () => {
const root = await temporaryRoot();
for (const name of ['a.bin', 'b.bin', 'c.bin']) {
await writeAt(root, '02', name);
}
const auditor = new CompletionReceiptOrphanAuditor(
new CompletionReceiptOrphanFileDirectory(root),
ownershipSource([]),
);
const report = await auditor.run({
mode: 'quarantine',
observedAtMs: OBSERVED_AT_MS,
startShard: 2,
shardCount: 1,
maxEntriesPerShard: 2,
minimumAgeMs: 0,
});
assert.deepEqual(report.overflowShards, ['02']);
assert.equal(report.scannedEntries, 2);
assert.ok(
report.entries.every((entry) => entry.action === 'blocked_overflow'),
);
assert.equal((await fs.readdir(path.join(root, '02'))).length, 3);
await assert.rejects(
auditor.run({ shardCount: 33 }),
/shardCount must be between 1 and 32/,
);
await assert.rejects(
auditor.run({ maxEntriesPerShard: 65 }),
/maxEntriesPerShard must be between 1 and 64/,
);
});
test('rejects shard and quarantine directory symlink escapes', async () => {
const root = await temporaryRoot();
const outside = await temporaryRoot();
await fs.symlink(outside, path.join(root, '03'));
const directory = new CompletionReceiptOrphanFileDirectory(root);
await assert.rejects(directory.inspectShard('03', 4), /not a safe directory/);
await writeAt(root, '04', 'orphan.bin');
const snapshot = await directory.inspectShard('04', 4);
await fs.symlink(outside, path.join(root, '.orphan-quarantine'));
await assert.rejects(
directory.quarantine(snapshot.entries[0]),
/not a safe directory/,
);
assert.ok(await fs.lstat(path.join(root, '04', 'orphan.bin')));
assert.throws(
() => new CompletionReceiptOrphanFileDirectory(path.parse(root).root),
/must not be a filesystem root/,
);
});
test('parses a bounded shard cursor and keeps audit as the default mode', () => {
const options = parseArguments([
'--start-shard=fe',
'--shards=2',
'--entries-per-shard=4',
'--minimum-age-ms=1000',
]);
assert.equal(options.mode, 'audit');
assert.equal(options.startShard, 254);
assert.equal(options.shardCount, 2);
assert.equal(options.maxEntriesPerShard, 4);
assert.throws(() => parseArguments(['--shards=33']), /between 1 and 32/);
assert.throws(() => parseArguments(['--start-shard=FF']), /lowercase hex/);
});
test(
'Node 24 CLI reads ownership without opening the database for writes',
{ skip: Number(process.versions.node.split('.')[0]) < 24 },
async () => {
const root = await temporaryRoot();
const databasePath = path.join(root, 'database.sqlite');
const receiptRoot = path.join(root, 'receipts');
const { DatabaseSync } = require('node:sqlite');
const database = new DatabaseSync(databasePath);
database.exec(`
CREATE TABLE RunAttempts (
id TEXT PRIMARY KEY,
status TEXT NOT NULL
);
CREATE TABLE CompletionReceiptJournals (
attempt_id TEXT PRIMARY KEY,
state TEXT NOT NULL
);
INSERT INTO RunAttempts (id, status)
VALUES ('${TERMINAL_ID}', 'succeeded');
INSERT INTO CompletionReceiptJournals (attempt_id, state)
VALUES ('${JOURNAL_ONLY_ID}', 'pending');
`);
database.close();
await writeAt(receiptRoot, '01', `${TERMINAL_ID}.json`);
await writeAt(receiptRoot, '01', `${JOURNAL_ONLY_ID}.json`);
const result = spawnSync(
process.execPath,
[
CLI_PATH,
'--json',
`--database=${databasePath}`,
`--root=${receiptRoot}`,
'--start-shard=01',
'--shards=1',
'--entries-per-shard=4',
'--minimum-age-ms=0',
],
{ cwd: REPOSITORY_ROOT, encoding: 'utf8' },
);
assert.equal(result.status, 0, result.stderr);
const report = JSON.parse(result.stdout);
assert.equal(report.counts.terminal_orphan, 1);
assert.equal(report.counts.journaled, 1);
assert.equal(
report.entries.find((entry) => entry.attemptId === TERMINAL_ID).action,
'eligible',
);
assert.ok(
await fs.lstat(path.join(receiptRoot, '01', `${TERMINAL_ID}.json`)),
);
},
);