mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:00:24 +08:00
282 lines
9.0 KiB
JavaScript
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`)),
|
|
);
|
|
},
|
|
);
|