86 lines
4.0 KiB
JavaScript
86 lines
4.0 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const SourceDeleteJournal = require('../lib/source-delete-journal');
|
|
|
|
test('restores a staged source after an interrupted deletion', async (t) => {
|
|
const directory = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'mhu-delete-journal-'));
|
|
t.after(() => fs.promises.rm(directory, { recursive: true, force: true }));
|
|
const file = path.join(directory, 'source.bin');
|
|
const stagedFile = path.join(directory, '.source.bin.mhu-delete-test');
|
|
const journal = new SourceDeleteJournal(path.join(directory, 'journal.jsonl'));
|
|
await fs.promises.writeFile(file, 'source');
|
|
await journal.plan({ token: 'cleanup-1', file, stagedFile });
|
|
await fs.promises.rename(file, stagedFile);
|
|
|
|
const outcomes = await journal.recover();
|
|
|
|
assert.equal(await fs.promises.readFile(file, 'utf-8'), 'source');
|
|
assert.equal(outcomes[0].outcome, 'restored');
|
|
assert.deepEqual(await journal.recover(), []);
|
|
});
|
|
|
|
test('clears a completed deletion record when neither source nor stage remains', async (t) => {
|
|
const directory = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'mhu-delete-journal-'));
|
|
t.after(() => fs.promises.rm(directory, { recursive: true, force: true }));
|
|
const journal = new SourceDeleteJournal(path.join(directory, 'journal.jsonl'));
|
|
await journal.plan({ token: 'cleanup-2', file: path.join(directory, 'source.bin'), stagedFile: path.join(directory, '.staged') });
|
|
|
|
const outcomes = await journal.recover();
|
|
|
|
assert.equal(outcomes[0].outcome, 'cleared');
|
|
assert.deepEqual(await journal.recover(), []);
|
|
});
|
|
|
|
test('continues recovery after one malformed active entry', async (t) => {
|
|
const directory = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'mhu-delete-journal-'));
|
|
t.after(() => fs.promises.rm(directory, { recursive: true, force: true }));
|
|
const journal = new SourceDeleteJournal(path.join(directory, 'journal.jsonl'));
|
|
const file = path.join(directory, 'source.bin');
|
|
const stagedFile = path.join(directory, '.source.bin.mhu-delete-test');
|
|
await journal.plan({ token: 'broken', file: null, stagedFile: null });
|
|
await fs.promises.writeFile(stagedFile, 'source');
|
|
await journal.plan({ token: 'valid', file, stagedFile });
|
|
|
|
const outcomes = await journal.recover();
|
|
|
|
assert.equal(outcomes.find((entry) => entry.token === 'broken').outcome, 'recovery-failed');
|
|
assert.equal(outcomes.find((entry) => entry.token === 'valid').outcome, 'restored');
|
|
assert.equal(await fs.promises.readFile(file, 'utf-8'), 'source');
|
|
});
|
|
|
|
test('retries transient Windows rename locks during recovery', async (t) => {
|
|
const directory = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'mhu-delete-journal-'));
|
|
t.after(() => fs.promises.rm(directory, { recursive: true, force: true }));
|
|
const file = path.join(directory, 'source.bin');
|
|
const stagedFile = path.join(directory, '.source.bin.mhu-delete-test');
|
|
const journalPath = path.join(directory, 'journal.jsonl');
|
|
let renameCalls = 0;
|
|
const waits = [];
|
|
const injectedFs = {
|
|
promises: {
|
|
mkdir: (...args) => fs.promises.mkdir(...args),
|
|
open: (...args) => fs.promises.open(...args),
|
|
readFile: (...args) => fs.promises.readFile(...args),
|
|
lstat: (...args) => fs.promises.lstat(...args),
|
|
rename: async (...args) => {
|
|
renameCalls += 1;
|
|
if (renameCalls < 3) throw Object.assign(new Error('locked'), { code: 'EBUSY' });
|
|
return fs.promises.rename(...args);
|
|
}
|
|
}
|
|
};
|
|
const writer = new SourceDeleteJournal(journalPath);
|
|
await fs.promises.writeFile(stagedFile, 'source');
|
|
await writer.plan({ token: 'cleanup-retry', file, stagedFile });
|
|
const journal = new SourceDeleteJournal(journalPath, null, { fs: injectedFs, wait: async (delay) => waits.push(delay) });
|
|
|
|
const outcomes = await journal.recover();
|
|
|
|
assert.equal(outcomes[0].outcome, 'restored');
|
|
assert.equal(renameCalls, 3);
|
|
assert.deepEqual(waits, [100, 250]);
|
|
});
|