mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-15 20:55:28 +02:00
Preserve the original contributor histories and apply the exact independently reviewed repair.
Source-Parent: 5785183108
Review-Manifest-SHA256: e223d98c24e357715527b786257901d2ef0bebadb75ac61e5c000fe60da10f9a
516 lines
24 KiB
JavaScript
516 lines
24 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('assert');
|
|
// Private facade: fault injection must not mutate shared fs or require.cache.
|
|
const fs = { ...require('fs') };
|
|
const vm = require('vm');
|
|
const { createRequire } = require('module');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const STORE = require.resolve('../../scripts/lib/state-store');
|
|
const LOCK = require.resolve('../../scripts/lib/state-store/file-lock');
|
|
function loadPrivate(filename, overrides = {}, source = fs.readFileSync(filename, 'utf8'), processFacade = process) {
|
|
const module = { exports: {} };
|
|
const nativeRequire = createRequire(filename);
|
|
const localRequire = id => Object.hasOwn(overrides, id) ? overrides[id] : nativeRequire(id);
|
|
vm.compileFunction(source, ['exports', 'require', 'module', '__filename', '__dirname', 'process'],
|
|
{ filename })(module.exports, localRequire, module, filename, path.dirname(filename), processFacade);
|
|
return module.exports;
|
|
}
|
|
const lockModule = loadPrivate(LOCK, { fs });
|
|
const { withStateStoreLock } = lockModule;
|
|
// sql.js is only initialized by the original integration cases, never the pure mode.
|
|
const { createStateStore } = loadPrivate(STORE, { fs, './file-lock': lockModule });
|
|
|
|
function thrownBy(fn) {
|
|
try { return { failed: false, value: fn() }; }
|
|
catch (error) { return { failed: true, error }; }
|
|
}
|
|
|
|
function lockFixture(overrides = {}, platform = 'linux') {
|
|
const calls = [];
|
|
const identity = { dev: 1n, ino: 2n, isFile: () => true, isSymbolicLink: () => false };
|
|
let present = true;
|
|
let closed = false;
|
|
const facade = {
|
|
openSync() { calls.push('open'); return 3; },
|
|
fstatSync() { calls.push('fstat'); return identity; },
|
|
writeFileSync() { calls.push('write'); },
|
|
lstatSync() { calls.push('lstat'); if (!present) throw Object.assign(new Error('missing'), { code: 'ENOENT' }); return identity; },
|
|
unlinkSync() { calls.push('unlink'); present = false; },
|
|
closeSync() { calls.push('close'); closed = true; },
|
|
};
|
|
const state = { calls, identity, get present() { return present; }, set present(value) { present = value; },
|
|
get closed() { return closed; } };
|
|
for (const [name, fn] of Object.entries(overrides)) {
|
|
const original = facade[name];
|
|
facade[name] = (...args) => fn(state, original, ...args);
|
|
}
|
|
const lock = loadPrivate(LOCK, { fs: facade, os: { hostname: () => 'fixture' } }, undefined,
|
|
{ platform, pid: 7 });
|
|
return { ...state, calls, run: callback => lock.withStateStoreLock('synthetic.db', callback, { timeoutMs: 0 }) };
|
|
}
|
|
|
|
async function runSynthetic(test) {
|
|
await test('owned lock unlinks before its only close', () => {
|
|
const fixture = lockFixture();
|
|
assert.strictEqual(fixture.run(() => 'done'), 'done');
|
|
assert.deepStrictEqual(fixture.calls, ['open', 'fstat', 'write', 'lstat', 'unlink', 'close']);
|
|
});
|
|
await test('a replacement created during close is never unlinked', () => {
|
|
let replacement = false;
|
|
const fixture = lockFixture({
|
|
closeSync(state, close) { close(); replacement = true; state.present = true; },
|
|
unlinkSync(state, unlink) { assert.ok(!replacement, 'must not unlink after releasing the descriptor'); unlink(); },
|
|
});
|
|
fixture.run(() => {});
|
|
assert.ok(replacement);
|
|
assert.strictEqual(fixture.calls.filter(call => call === 'close').length, 1);
|
|
});
|
|
for (const platform of ['linux', 'win32']) {
|
|
for (const replacement of [{ ino: 3n }, { dev: 9n }, { dev: 0n }, { isFile: () => false }, { isSymbolicLink: () => true }]) {
|
|
await test(`observed lock replacement is preserved (${platform}, ${Object.keys(replacement)[0]})`, () => {
|
|
const fixture = lockFixture({ lstatSync(state) { state.calls.push('lstat'); return { ...state.identity, ...replacement }; } }, platform);
|
|
const result = thrownBy(() => fixture.run(() => {}));
|
|
assert.ok(result.failed);
|
|
assert.match(result.error.message, /lock.*(changed|lost)/i);
|
|
assert.ok(!fixture.calls.includes('unlink'));
|
|
assert.strictEqual(fixture.calls.filter(call => call === 'close').length, 1);
|
|
});
|
|
}
|
|
}
|
|
for (const afterClose of ['missing', 'permission', 'replacement']) {
|
|
await test(`initial EPERM performs only a post-close read probe (${afterClose})`, () => {
|
|
const denied = Object.assign(new Error('denied'), { code: 'EPERM' });
|
|
const fixture = lockFixture({ lstatSync(state) {
|
|
state.calls.push('lstat');
|
|
if (!state.closed || afterClose === 'permission') throw denied;
|
|
if (afterClose === 'missing') throw Object.assign(new Error('gone'), { code: 'ENOENT' });
|
|
return { ...state.identity, ino: 10n };
|
|
} }, 'win32');
|
|
const result = thrownBy(() => fixture.run(() => {}));
|
|
assert.ok(result.failed);
|
|
if (afterClose === 'missing') assert.strictEqual(result.error.code, 'STATE_STORE_LOCK_LOST');
|
|
else assert.strictEqual(result.error, denied);
|
|
assert.deepStrictEqual(fixture.calls.slice(3), ['lstat', 'close', 'lstat']);
|
|
});
|
|
}
|
|
await test('close failure stops the pending-delete probe and preserves initial EPERM', () => {
|
|
const denied = Object.assign(new Error('denied'), { code: 'EPERM' });
|
|
const cleanup = new Error('close failed');
|
|
const fixture = lockFixture({
|
|
lstatSync(state) { state.calls.push('lstat'); throw denied; },
|
|
closeSync(state) { state.calls.push('close'); throw cleanup; },
|
|
});
|
|
const result = thrownBy(() => fixture.run(() => {}));
|
|
assert.ok(result.failed);
|
|
assert.strictEqual(result.error, denied);
|
|
assert.strictEqual(result.error.closeError, cleanup);
|
|
assert.deepStrictEqual(fixture.calls.slice(3), ['lstat', 'close']);
|
|
});
|
|
await test('initial missing lock is loss after descriptor closure', () => {
|
|
const fixture = lockFixture({ lstatSync(state) { state.calls.push('lstat'); throw Object.assign(new Error('gone'), { code: 'ENOENT' }); } });
|
|
const result = thrownBy(() => fixture.run(() => {}));
|
|
assert.ok(result.failed);
|
|
assert.strictEqual(result.error.code, 'STATE_STORE_LOCK_LOST');
|
|
assert.deepStrictEqual(fixture.calls.slice(3), ['lstat', 'close']);
|
|
});
|
|
await test('unlink EPERM is not mistaken for an initial pending-delete lookup', () => {
|
|
const denied = Object.assign(new Error('unlink denied'), { code: 'EPERM' });
|
|
const fixture = lockFixture({ unlinkSync(state) { state.calls.push('unlink'); throw denied; } });
|
|
const result = thrownBy(() => fixture.run(() => {}));
|
|
assert.ok(result.failed);
|
|
assert.strictEqual(result.error, denied);
|
|
assert.deepStrictEqual(fixture.calls.slice(3), ['lstat', 'unlink', 'close']);
|
|
});
|
|
const primaries = [null, undefined, false, 0, '', 'primitive', Object.freeze(new Error('frozen')),
|
|
Object.defineProperty(new Error('setter'), 'releaseError', { set() { throw new Error('setter must not mask'); } })];
|
|
for (const [index, primary] of primaries.entries()) {
|
|
for (const stage of ['callback', 'metadata', 'identity']) {
|
|
await test(`arbitrary primary survives lock cleanup (${index}, ${stage})`, () => {
|
|
const cleanup = new Error('cleanup failed');
|
|
const overrides = stage === 'callback'
|
|
? { unlinkSync() { throw cleanup; } }
|
|
: stage === 'metadata'
|
|
? { writeFileSync() { throw primary; }, unlinkSync() { throw cleanup; } }
|
|
: { fstatSync() { throw primary; }, closeSync() { throw cleanup; } };
|
|
const fixture = lockFixture(overrides);
|
|
const result = thrownBy(() => fixture.run(() => { if (stage === 'callback') throw primary; assert.fail('callback must not run'); }));
|
|
assert.ok(result.failed);
|
|
assert.strictEqual(result.error, primary);
|
|
});
|
|
}
|
|
for (const stage of ['migration', 'rollback']) {
|
|
await test(`arbitrary primary survives state-store ${stage} cleanup (${index})`, async () => {
|
|
const secondary = new Error('cleanup failed');
|
|
let closes = 0;
|
|
const fakeSQL = { Database: class {
|
|
run(sql) { if (sql === 'ROLLBACK') throw secondary; }
|
|
close() { closes++; throw secondary; }
|
|
} };
|
|
const api = loadPrivate(STORE, {
|
|
'sql.js': async () => fakeSQL,
|
|
'./migrations': { applyMigrations() { if (stage === 'migration') throw primary; return []; } },
|
|
'./queries': { createQueryApi: () => ({}) },
|
|
});
|
|
let failed = false;
|
|
let caught;
|
|
try {
|
|
const store = await api.createStateStore({ dbPath: ':memory:' });
|
|
store._database.transaction(() => { throw primary; })();
|
|
} catch (error) { failed = true; caught = error; }
|
|
assert.ok(failed);
|
|
assert.strictEqual(caught, primary);
|
|
assert.strictEqual(closes, 1);
|
|
});
|
|
}
|
|
}
|
|
for (const stage of ['initialize', 'previous-close']) {
|
|
for (const [index, primary] of [null, undefined, Object.freeze(new Error('reload primary'))].entries()) {
|
|
await test(`reload closes its unadopted handle and preserves ${stage} failure (${index})`, () => {
|
|
const handles = [];
|
|
const SQL = { Database: class {
|
|
constructor() { this.id = handles.length; this.closes = 0; handles.push(this); }
|
|
run() { if (stage === 'initialize') throw primary; }
|
|
close() {
|
|
this.closes++;
|
|
if (stage === 'previous-close' && this.id === 0) throw primary;
|
|
throw new Error('unadopted cleanup');
|
|
}
|
|
} };
|
|
// Export only the existing private wrapper in this isolated exact-source module.
|
|
const source = `${fs.readFileSync(STORE, 'utf8')}\nmodule.exports.testWrap = wrapSqlJsDatabase;`;
|
|
const api = loadPrivate(STORE, {
|
|
fs: { lstatSync() { throw Object.assign(new Error('absent'), { code: 'ENOENT' }); } },
|
|
'./file-lock': { ...lockModule, withStateStoreLock: (_path, callback) => callback() },
|
|
}, source);
|
|
const db = api.testWrap(SQL, 'synthetic.db');
|
|
if (stage === 'previous-close') db.withSnapshot(() => {});
|
|
const result = thrownBy(() => db.withSnapshot(() => assert.fail('must not adopt failed handle')));
|
|
assert.ok(result.failed);
|
|
assert.strictEqual(result.error, primary);
|
|
assert.strictEqual(handles.at(-1).closes, 1);
|
|
if (stage === 'previous-close') {
|
|
assert.strictEqual(handles[0].closes, 1);
|
|
assert.throws(() => db.withSnapshot(() => {}), /closed/);
|
|
}
|
|
db.close();
|
|
assert.ok(handles.every(handle => handle.closes === 1));
|
|
});
|
|
}
|
|
}
|
|
await test('reload adopts a ready replacement and closes each owned handle once', () => {
|
|
const calls = [];
|
|
let next = 0;
|
|
const SQL = { Database: class {
|
|
constructor() { this.id = next++; }
|
|
run() { calls.push(`ready:${this.id}`); }
|
|
close() { calls.push(`close:${this.id}`); }
|
|
} };
|
|
const source = `${fs.readFileSync(STORE, 'utf8')}\nmodule.exports.testWrap = wrapSqlJsDatabase;`;
|
|
const api = loadPrivate(STORE, {
|
|
fs: { lstatSync() { throw Object.assign(new Error('absent'), { code: 'ENOENT' }); } },
|
|
'./file-lock': { ...lockModule, withStateStoreLock: (_path, callback) => callback() },
|
|
}, source);
|
|
const db = api.testWrap(SQL, 'synthetic.db');
|
|
db.withSnapshot(() => {});
|
|
db.withSnapshot(() => {});
|
|
db.close(); db.close();
|
|
assert.deepStrictEqual(calls, ['ready:0', 'ready:1', 'close:0', 'close:1']);
|
|
});
|
|
for (const [index, primary] of [null, undefined, false, 0, '', 'primitive',
|
|
Object.freeze(new Error('frozen worker')), { get message() { throw new Error('getter'); } }].entries()) {
|
|
await test(`worker reports bounded arbitrary mutation failure after close (${index})`, async () => {
|
|
const filename = require.resolve('../../scripts/lib/control-pane/work-item-worker');
|
|
const source = fs.readFileSync(filename, 'utf8');
|
|
assert.strictEqual(source.split('mutate().then(').length, 2);
|
|
const captured = source.replace('mutate().then(', 'module.exports = mutate().then(');
|
|
const messages = [];
|
|
let closes = 0;
|
|
await loadPrivate(filename, {
|
|
worker_threads: { workerData: { dbPath: ':memory:', action: 'claim', args: {} },
|
|
parentPort: { postMessage(message) { messages.push(message); } } },
|
|
'../state-store': { createStateStore: async () => ({
|
|
_database: { transaction: fn => fn }, close() { closes++; throw new Error('close failed'); },
|
|
}) },
|
|
'./work-item-mutations': { claimWorkItem() { throw primary; } },
|
|
}, captured);
|
|
assert.strictEqual(closes, 1);
|
|
assert.strictEqual(messages.length, 1);
|
|
assert.strictEqual(messages[0].ok, false);
|
|
assert.strictEqual(typeof messages[0].error.message, 'string');
|
|
assert.ok(messages[0].error.message.length <= 1024);
|
|
if (index < 6) assert.strictEqual(messages[0].error.message, String(primary));
|
|
if (index === 6) assert.strictEqual(messages[0].error.message, primary.message);
|
|
});
|
|
}
|
|
await test('worker rejects close-only failure after successful mutation', async () => {
|
|
const filename = require.resolve('../../scripts/lib/control-pane/work-item-worker');
|
|
const source = fs.readFileSync(filename, 'utf8').replace('mutate().then(', 'module.exports = mutate().then(');
|
|
const messages = [];
|
|
await loadPrivate(filename, {
|
|
worker_threads: { workerData: { dbPath: ':memory:', action: 'claim', args: {} },
|
|
parentPort: { postMessage(message) { messages.push(message); } } },
|
|
'../state-store': { createStateStore: async () => ({
|
|
_database: { transaction: fn => fn }, close() { throw new Error('close-only'); },
|
|
}) },
|
|
'./work-item-mutations': { claimWorkItem() { return 'committed'; } },
|
|
}, source);
|
|
assert.deepStrictEqual(messages, [{ ok: false, error: { message: 'close-only', code: undefined } }]);
|
|
});
|
|
await test('cleanup-only lock failure still rejects a successful operation', () => {
|
|
const cleanup = new Error('close failed');
|
|
const fixture = lockFixture({ closeSync() { throw cleanup; } });
|
|
const result = thrownBy(() => fixture.run(() => 'committed'));
|
|
assert.ok(result.failed);
|
|
assert.strictEqual(result.error, cleanup);
|
|
});
|
|
}
|
|
|
|
|
|
const item = id => ({ id, source: 'manual', title: id, status: 'open' });
|
|
|
|
async function run() {
|
|
let passed = 0;
|
|
let failed = 0;
|
|
async function test(name, callback) {
|
|
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-state-persistence-'));
|
|
try {
|
|
await callback(path.join(directory, 'state.db'));
|
|
console.log(` PASS ${name}`);
|
|
passed += 1;
|
|
} catch (error) {
|
|
console.log(` FAIL ${name}\n ${error && error.stack ? error.stack : String(error)}`);
|
|
failed += 1;
|
|
} finally {
|
|
fs.rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
await runSynthetic(test);
|
|
if (process.argv.includes('--synthetic-only')) {
|
|
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
|
process.exitCode = failed ? 1 : 0;
|
|
return;
|
|
}
|
|
|
|
await test('queries, migration inspection and closing never rewrite an existing database', async dbPath => {
|
|
const initial = await createStateStore({ dbPath });
|
|
initial.upsertWorkItem(item('saved'));
|
|
initial.close();
|
|
const before = fs.readFileSync(dbPath);
|
|
const rename = fs.renameSync;
|
|
let writes = 0;
|
|
fs.renameSync = (from, to) => {
|
|
if (to === dbPath) writes += 1;
|
|
return rename(from, to);
|
|
};
|
|
try {
|
|
const reader = await createStateStore({ dbPath });
|
|
try {
|
|
assert.ok(reader.getWorkItemById('saved'));
|
|
reader.listWorkItems();
|
|
reader.getStatus();
|
|
assert.strictEqual(reader.getAppliedMigrations().length, 2);
|
|
} finally { reader.close(); }
|
|
} finally { fs.renameSync = rename; }
|
|
assert.strictEqual(writes, 0);
|
|
assert.deepStrictEqual(fs.readFileSync(dbPath), before);
|
|
});
|
|
|
|
await test('persistent PRAGMA changes survive snapshot reload and reopening', async dbPath => {
|
|
const store = await createStateStore({ dbPath });
|
|
try {
|
|
assert.strictEqual(store._database.pragma('user_version = 3252'), undefined);
|
|
assert.strictEqual(store._database.prepare('PRAGMA user_version').get().user_version, 3252);
|
|
store._database.pragma('application_id(2468)');
|
|
} finally { store.close(); }
|
|
const reopened = await createStateStore({ dbPath });
|
|
try {
|
|
assert.strictEqual(reopened._database.prepare('PRAGMA user_version').get().user_version, 3252);
|
|
assert.strictEqual(reopened._database.prepare('PRAGMA application_id').get().application_id, 2468);
|
|
} finally { reopened.close(); }
|
|
});
|
|
|
|
await test('read-only and connection-local PRAGMAs never rewrite database bytes', async dbPath => {
|
|
const store = await createStateStore({ dbPath });
|
|
const before = fs.readFileSync(dbPath);
|
|
const rename = fs.renameSync;
|
|
let writes = 0;
|
|
fs.renameSync = (from, to) => {
|
|
if (to === dbPath) writes += 1;
|
|
return rename(from, to);
|
|
};
|
|
try {
|
|
store._database.pragma('user_version');
|
|
store._database.pragma('table_info(work_items)');
|
|
store._database.pragma('integrity_check');
|
|
store._database.transaction(() => {
|
|
store._database.pragma('user_version');
|
|
store._database.pragma('table_info(work_items)');
|
|
})();
|
|
store._database.withSnapshot(() => {
|
|
store._database.pragma('cache_size = 256');
|
|
assert.strictEqual(store._database.prepare('PRAGMA cache_size').get().cache_size, 256);
|
|
});
|
|
assert.strictEqual(writes, 0);
|
|
assert.deepStrictEqual(fs.readFileSync(dbPath), before);
|
|
} finally {
|
|
fs.renameSync = rename;
|
|
store.close();
|
|
}
|
|
});
|
|
|
|
await test('PRAGMA persistence waits for commit and never commits a rolled-back transaction', async dbPath => {
|
|
const store = await createStateStore({ dbPath });
|
|
try {
|
|
store._database.transaction(() => {
|
|
store.upsertWorkItem(item('committed-pragma'));
|
|
store._database.pragma('table_info(work_items)');
|
|
store._database.pragma('user_version = 3252');
|
|
})();
|
|
store._database.transaction(() => store._database.pragma('application_id(2468)'))();
|
|
assert.strictEqual(store._database.prepare('PRAGMA application_id').get().application_id, 2468);
|
|
const before = fs.readFileSync(dbPath);
|
|
assert.throws(() => store._database.transaction(() => {
|
|
store.upsertWorkItem(item('rolled-back-pragma'));
|
|
store._database.pragma('user_version = 9999');
|
|
throw new Error('abort PRAGMA transaction');
|
|
})(), /abort PRAGMA transaction/);
|
|
assert.deepStrictEqual(fs.readFileSync(dbPath), before);
|
|
assert.strictEqual(store._database.prepare('PRAGMA user_version').get().user_version, 3252);
|
|
assert.deepStrictEqual(store.listWorkItems().items.map(row => row.id), ['committed-pragma']);
|
|
} finally { store.close(); }
|
|
});
|
|
|
|
await test('failed persistence is discarded, releases the lock and cannot overwrite a later writer', async dbPath => {
|
|
const first = await createStateStore({ dbPath });
|
|
const second = await createStateStore({ dbPath });
|
|
const before = fs.readFileSync(dbPath);
|
|
const rename = fs.renameSync;
|
|
const failure = new Error('simulated disk failure');
|
|
try {
|
|
fs.renameSync = (from, to) => {
|
|
if (to === dbPath) throw failure;
|
|
return rename(from, to);
|
|
};
|
|
try {
|
|
assert.throws(() => first.upsertWorkItem(item('failed')), error => error === failure);
|
|
} finally { fs.renameSync = rename; }
|
|
assert.deepStrictEqual(fs.readFileSync(dbPath), before);
|
|
assert.strictEqual(fs.existsSync(`${dbPath}.ecc-state.lock`), false);
|
|
second.upsertWorkItem(item('other-process'));
|
|
first.upsertWorkItem(item('retry'));
|
|
assert.deepStrictEqual(first.listWorkItems().items.map(row => row.id).sort(), ['other-process', 'retry']);
|
|
} finally {
|
|
first.close();
|
|
second.close();
|
|
}
|
|
});
|
|
|
|
await test('transaction exceptions roll back, preserve the error and release the writer lock', async dbPath => {
|
|
const store = await createStateStore({ dbPath });
|
|
const before = fs.readFileSync(dbPath);
|
|
const failure = new Error('cancel this transaction');
|
|
try {
|
|
assert.throws(() => store._database.transaction(() => {
|
|
store.upsertWorkItem(item('rolled-back'));
|
|
throw failure;
|
|
})(), error => error === failure);
|
|
assert.deepStrictEqual(fs.readFileSync(dbPath), before);
|
|
assert.strictEqual(fs.existsSync(`${dbPath}.ecc-state.lock`), false);
|
|
store.upsertWorkItem(item('next'));
|
|
assert.deepStrictEqual(store.listWorkItems().items.map(row => row.id), ['next']);
|
|
} finally { store.close(); }
|
|
});
|
|
|
|
await test('reloading a snapshot preserves foreign-key enforcement', async dbPath => {
|
|
const store = await createStateStore({ dbPath });
|
|
try {
|
|
store._database.exec('CREATE TABLE parent (id INTEGER PRIMARY KEY); CREATE TABLE child (parent_id INTEGER REFERENCES parent(id));');
|
|
assert.throws(() => store._database.exec('INSERT INTO child VALUES (42)'), /FOREIGN KEY constraint failed/);
|
|
store._database.exec('INSERT INTO parent VALUES (42)');
|
|
store._database.exec('INSERT INTO child VALUES (42)');
|
|
assert.strictEqual(store._database.prepare('SELECT COUNT(*) AS count FROM child').get().count, 1);
|
|
} finally { store.close(); }
|
|
});
|
|
|
|
await test('in-memory transactions roll back and closed handles cannot reopen silently', async () => {
|
|
const store = await createStateStore({ dbPath: ':memory:' });
|
|
store.upsertWorkItem(item('keep'));
|
|
assert.throws(() => store._database.transaction(() => {
|
|
store.upsertWorkItem(item('discard'));
|
|
throw new Error('abort');
|
|
})(), /abort/);
|
|
assert.deepStrictEqual(store.listWorkItems().items.map(row => row.id), ['keep']);
|
|
store.close();
|
|
store.close();
|
|
assert.throws(() => store.listWorkItems(), /closed/);
|
|
});
|
|
|
|
await test('failed rollback keeps the primary error and invalidates an uncertain in-memory handle', async () => {
|
|
const store = await createStateStore({ dbPath: ':memory:' });
|
|
const failure = new Error('application error after explicit rollback');
|
|
assert.throws(() => store._database.transaction(() => {
|
|
store._database.exec('ROLLBACK');
|
|
throw failure;
|
|
})(), error => error === failure && error.rollbackError instanceof Error);
|
|
assert.throws(() => store.listWorkItems(), /closed/);
|
|
store.close();
|
|
});
|
|
|
|
await test('live and abandoned locks time out without stealing ownership or touching the database', async dbPath => {
|
|
const lock = `${dbPath}.ecc-state.lock`;
|
|
fs.writeFileSync(dbPath, 'untouched');
|
|
for (const metadata of [{ pid: process.pid }, { pid: 999999999 }, null]) {
|
|
const bytes = JSON.stringify(metadata);
|
|
fs.writeFileSync(lock, bytes);
|
|
fs.utimesSync(lock, new Date(0), new Date(0));
|
|
assert.throws(() => withStateStoreLock(dbPath, () => assert.fail('lock was stolen'), { timeoutMs: 20 }),
|
|
error => error.code === 'STATE_STORE_BUSY' && error.message.includes(lock));
|
|
assert.strictEqual(fs.readFileSync(lock, 'utf8'), bytes);
|
|
assert.strictEqual(fs.readFileSync(dbPath, 'utf8'), 'untouched');
|
|
fs.unlinkSync(lock);
|
|
}
|
|
});
|
|
|
|
await test('failed lock metadata writes release the owned file', async dbPath => {
|
|
const write = fs.writeFileSync;
|
|
const failure = new Error('metadata write failure');
|
|
fs.writeFileSync = () => { throw failure; };
|
|
try {
|
|
assert.throws(() => withStateStoreLock(dbPath, () => assert.fail('callback should not run')), error => error === failure);
|
|
} finally { fs.writeFileSync = write; }
|
|
assert.strictEqual(fs.existsSync(`${dbPath}.ecc-state.lock`), false);
|
|
assert.strictEqual(withStateStoreLock(dbPath, () => 'retry'), 'retry');
|
|
});
|
|
|
|
await test('callback failure remains primary even when lock cleanup also fails', async dbPath => {
|
|
const unlink = fs.unlinkSync;
|
|
const primary = new Error('operation failed');
|
|
const cleanup = new Error('cleanup failed');
|
|
const lock = `${dbPath}.ecc-state.lock`;
|
|
fs.unlinkSync = file => {
|
|
if (file === lock) throw cleanup;
|
|
return unlink(file);
|
|
};
|
|
try {
|
|
assert.throws(() => withStateStoreLock(dbPath, () => { throw primary; }),
|
|
error => error === primary && error.releaseError === cleanup);
|
|
} finally {
|
|
fs.unlinkSync = unlink;
|
|
unlink(lock);
|
|
}
|
|
let caught = false;
|
|
try { withStateStoreLock(dbPath, () => { throw null; }); }
|
|
catch (error) { caught = true; assert.strictEqual(error, null); }
|
|
assert.ok(caught, 'even falsy thrown values must propagate');
|
|
});
|
|
|
|
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
|
process.exitCode = failed ? 1 : 0;
|
|
}
|
|
|
|
process.exitCode = 1;
|
|
run().catch(error => { console.error(error); process.exitCode = 1; });
|