Files
ECC/tests/lib/opencode-install-lock.test.js
T

425 lines
21 KiB
JavaScript

/** Cooperative OpenCode installation lock scopes, without child processes. */
'use strict';
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { acquireOpenCodeInstallLocks, withOpenCodeInstallLocks } = require('../../scripts/lib/install/opencode-install-lock');
const { acquireSettingsLock, getSettingsLockIdentity, sameFileIdentity } = require('../../scripts/lib/install/claude-settings-lock');
let passed = 0;
let failed = 0;
const lockPath = root => path.join(root, 'ecc-install-state.json.ecc.lock');
function test(name, callback) {
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-opencode-lock-')));
try { callback(root); passed++; console.log(` PASS ${name}`); }
catch (error) { failed++; console.error(` FAIL ${name}: ${error.stack}`); }
finally { fs.rmSync(root, { recursive: true, force: true }); }
}
test('same target refuses an independent writer and releases on success', root => {
assert.strictEqual(withOpenCodeInstallLocks([root], lease => {
assert.ok(Object.isFrozen(lease));
assert.ok(fs.existsSync(lockPath(root)));
assert.throws(() => acquireOpenCodeInstallLocks([root]), /Another ECC process.*OpenCode/);
return 'done';
}), 'done');
assert.strictEqual(fs.existsSync(lockPath(root)), false);
});
test('different roots can be held independently', root => {
const other = path.join(root, 'other');
withOpenCodeInstallLocks([root], () => withOpenCodeInstallLocks([other], () => {
assert.ok(fs.existsSync(lockPath(root)) && fs.existsSync(lockPath(other)));
}));
assert.strictEqual(fs.existsSync(lockPath(other)), false);
});
test('roots are deduplicated, sorted and released in reverse order', root => {
const a = path.join(root, 'a');
const b = path.join(root, 'b');
const linked = [];
const renamed = [];
const originalLink = fs.linkSync;
const originalRename = fs.renameSync;
fs.linkSync = (from, to) => { linked.push(to); return originalLink(from, to); };
fs.renameSync = (from, to) => { renamed.push(from); return originalRename(from, to); };
try { withOpenCodeInstallLocks([b, a, b], () => {}); }
finally { fs.linkSync = originalLink; fs.renameSync = originalRename; }
assert.deepStrictEqual(linked, [lockPath(a), lockPath(b)]);
assert.deepStrictEqual(renamed, [lockPath(b), lockPath(a)]);
});
test('nested use requires a real active lease with full coverage', root => {
const holder = acquireOpenCodeInstallLocks([root]);
const nested = acquireOpenCodeInstallLocks([root], holder.lease);
assert.strictEqual(nested.lease, holder.lease);
nested.release();
assert.ok(fs.existsSync(lockPath(root)));
assert.throws(() => acquireOpenCodeInstallLocks([path.join(root, 'missing')], holder.lease), /cover/);
assert.throws(() => acquireOpenCodeInstallLocks([root], true), /lease/);
assert.throws(() => acquireOpenCodeInstallLocks([root], {}), /lease/);
holder.release();
holder.release();
assert.throws(() => acquireOpenCodeInstallLocks([root], holder.lease), /lease/);
});
test('callback error retains identity and releases', root => {
const primary = new Error('callback failure');
assert.throws(() => withOpenCodeInstallLocks([root], () => { throw primary; }), error => error === primary);
assert.strictEqual(fs.existsSync(lockPath(root)), false);
});
test('second lock failure releases the first without disturbing the other owner', root => {
const a = path.join(root, 'a');
const b = path.join(root, 'b');
const other = acquireOpenCodeInstallLocks([b]);
const bytes = fs.readFileSync(lockPath(b));
try { assert.throws(() => acquireOpenCodeInstallLocks([b, a]), /Another ECC process/); }
finally { assert.deepStrictEqual(fs.readFileSync(lockPath(b)), bytes); other.release(); }
assert.strictEqual(fs.existsSync(lockPath(a)), false);
});
test('a replaced lock invalidates nested reuse and is preserved on release', root => {
const holder = acquireOpenCodeInstallLocks([root]);
fs.renameSync(lockPath(root), `${lockPath(root)}.owned`);
fs.writeFileSync(lockPath(root), 'replacement');
assert.throws(() => acquireOpenCodeInstallLocks([root], holder.lease), /changed/);
assert.throws(() => holder.release(), /changed/);
assert.strictEqual(fs.readFileSync(lockPath(root), 'utf8'), 'replacement');
});
test('release failure is attached without hiding the callback failure', root => {
const primary = new Error('primary');
assert.throws(() => withOpenCodeInstallLocks([root], () => {
fs.renameSync(lockPath(root), `${lockPath(root)}.owned`);
fs.writeFileSync(lockPath(root), 'replacement');
throw primary;
}), error => error === primary && /changed/.test(error.releaseError.message));
assert.strictEqual(fs.readFileSync(lockPath(root), 'utf8'), 'replacement');
});
test('invalid root and non-file lock paths refuse before callback', root => {
for (const roots of [null, ['relative'], [true], ['']]) {
assert.throws(() => acquireOpenCodeInstallLocks(roots), /root/);
}
fs.mkdirSync(lockPath(root));
assert.throws(() => withOpenCodeInstallLocks([root], () => assert.fail('must refuse')), /lock/);
});
test('lock engine still recovers a stale malformed lock', root => {
fs.writeFileSync(lockPath(root), 'stale');
const old = new Date(Date.now() - 600000);
fs.utimesSync(lockPath(root), old, old);
withOpenCodeInstallLocks([root], () => assert.match(fs.readFileSync(lockPath(root), 'utf8'), /"pid"/));
assert.strictEqual(fs.existsSync(lockPath(root)), false);
});
test('a root or lock reported as a symlink refuses without callback or outside access', root => {
for (const target of [root, lockPath(root)]) {
if (target !== root) fs.writeFileSync(target, 'unrelated target');
const original = fs.lstatSync;
fs.lstatSync = (...args) => {
const stats = original(...args);
if (args[0] === target) {
const symlink = Object.create(stats);
symlink.isSymbolicLink = () => true;
return symlink;
}
return stats;
};
try {
assert.throws(() => withOpenCodeInstallLocks([root], () => assert.fail('must refuse')), /symlink/);
} finally { fs.lstatSync = original; }
if (target !== root) assert.strictEqual(fs.readFileSync(target, 'utf8'), 'unrelated target');
}
});
test('permission errors remain permission errors', root => {
const original = fs.lstatSync;
const denied = Object.assign(new Error('permission denied'), { code: 'EACCES' });
fs.lstatSync = (...args) => { if (args[0] === lockPath(root)) throw denied; return original(...args); };
try { assert.throws(() => acquireOpenCodeInstallLocks([root]), error => error === denied); }
finally { fs.lstatSync = original; }
assert.strictEqual(fs.existsSync(lockPath(root)), false);
});
test('releasing all roots continues after one replaced lock and rejects later lease reuse', root => {
const a = path.join(root, 'a');
const b = path.join(root, 'b');
const holder = acquireOpenCodeInstallLocks([a, b]);
fs.renameSync(lockPath(b), `${lockPath(b)}.owned`);
fs.writeFileSync(lockPath(b), 'replacement');
assert.throws(() => holder.release(), /changed/);
assert.strictEqual(fs.existsSync(lockPath(a)), false);
assert.strictEqual(fs.readFileSync(lockPath(b), 'utf8'), 'replacement');
assert.throws(() => acquireOpenCodeInstallLocks([a], holder.lease), /lease/);
});
test('a root replaced during a held scope is preserved while other roots release and the lease expires', root => {
const a = path.join(root, 'a');
const b = path.join(root, 'b');
const movedRoot = path.join(root, 'b-original');
const replacementState = path.join(b, 'ecc-install-state.json');
let heldLease;
let originalLockBytes;
assert.throws(() => withOpenCodeInstallLocks([a, b], lease => {
heldLease = lease;
originalLockBytes = fs.readFileSync(lockPath(b));
fs.renameSync(b, movedRoot);
fs.mkdirSync(b);
fs.writeFileSync(lockPath(b), 'replacement root lock');
fs.writeFileSync(replacementState, 'replacement root state');
return 'release must refuse instead of returning this result';
}), /Refusing changed OpenCode install lock root/);
assert.strictEqual(fs.existsSync(lockPath(a)), false);
assert.strictEqual(fs.readFileSync(lockPath(b), 'utf8'), 'replacement root lock');
assert.strictEqual(fs.readFileSync(replacementState, 'utf8'), 'replacement root state');
assert.deepStrictEqual(fs.readdirSync(b).sort(), ['ecc-install-state.json', 'ecc-install-state.json.ecc.lock']);
assert.deepStrictEqual(fs.readFileSync(lockPath(movedRoot)), originalLockBytes);
assert.throws(() => acquireOpenCodeInstallLocks([a, b], heldLease), /Invalid or inactive OpenCode install lease/);
withOpenCodeInstallLocks([a], () => assert.ok(fs.existsSync(lockPath(a))));
assert.strictEqual(fs.existsSync(lockPath(a)), false);
});
test('an absent callback is rejected before a lock is created', root => {
assert.throws(() => withOpenCodeInstallLocks([root], null), /callback/);
assert.strictEqual(fs.existsSync(lockPath(root)), false);
});
test('a replacement immediately after lock publication cannot enter the protected callback', root => {
const target = lockPath(root);
const originalLink = fs.linkSync;
let callbacks = 0;
let replacements = 0;
fs.linkSync = (from, to) => {
originalLink(from, to);
if (to === target) {
replacements++;
fs.renameSync(target, `${target}.owned`);
fs.writeFileSync(target, 'replacement at acquisition boundary');
}
};
try {
assert.throws(() => withOpenCodeInstallLocks([root], () => { callbacks++; }), /changed .*lock/);
} finally { fs.linkSync = originalLink; }
assert.strictEqual(replacements, 1);
assert.strictEqual(callbacks, 0, 'A pathname replacement must never become the acquired identity');
assert.strictEqual(fs.readFileSync(target, 'utf8'), 'replacement at acquisition boundary');
assert.strictEqual(JSON.parse(fs.readFileSync(`${target}.owned`, 'utf8')).pid, process.pid);
});
test('a transient post-acquisition validation failure releases authentic locks and preserves the primary error', root => {
const a = path.join(root, 'a');
const b = path.join(root, 'b');
const primary = Object.assign(new Error('one-shot validation error'), { code: 'EIO' });
const originalLink = fs.linkSync;
const originalStat = fs.lstatSync;
let published = false;
let faults = 0;
let callbacks = 0;
let caught;
fs.linkSync = (from, to) => {
originalLink(from, to);
if (to === lockPath(b)) published = true;
};
fs.lstatSync = (...args) => {
if (published && args[0] === b && faults === 0) { faults++; throw primary; }
return originalStat(...args);
};
try {
try { withOpenCodeInstallLocks([a, b], () => { callbacks++; }); }
catch (error) { caught = error; }
} finally { fs.linkSync = originalLink; fs.lstatSync = originalStat; }
assert.strictEqual(faults, 1);
assert.strictEqual(callbacks, 0);
assert.strictEqual(caught, primary);
assert.strictEqual(caught.releaseError, undefined, 'Cleanup must not dereference a missing identity');
assert.strictEqual(fs.existsSync(lockPath(a)), false);
assert.strictEqual(fs.existsSync(lockPath(b)), false);
});
test('engine ownership metadata is immutable and derived from the acquired descriptor', root => {
const settingsPath = path.join(root, 'settings.json');
const release = acquireSettingsLock(settingsPath);
try {
const identity = getSettingsLockIdentity(release);
assert.ok(Object.isFrozen(identity));
assert.deepStrictEqual(Object.keys(identity).sort(), ['dev', 'ino']);
assert.ok(sameFileIdentity(identity, fs.lstatSync(`${settingsPath}.ecc.lock`, { bigint: true })));
assert.throws(() => { identity.ino = 0n; }, TypeError);
assert.throws(() => getSettingsLockIdentity(() => {}), /identity/);
} finally { release(); }
assert.strictEqual(fs.existsSync(`${settingsPath}.ecc.lock`), false);
});
function captureThrown(callback) {
try { return { didThrow: false, result: callback() }; }
catch (value) { return { didThrow: true, value }; }
}
const falsyThrownValues = [undefined, null, false, 0, '', NaN];
for (const [index, primary] of falsyThrownValues.entries()) {
for (const cleanupFails of [false, true]) {
test(`falsy callback value ${index} survives ${cleanupFails ? 'failed' : 'healthy'} cleanup`, root => {
let lease;
const caught = captureThrown(() => withOpenCodeInstallLocks([root], active => {
lease = active;
if (cleanupFails) {
fs.renameSync(lockPath(root), `${lockPath(root)}.owned`);
fs.writeFileSync(lockPath(root), 'replacement');
}
throw primary;
}));
assert.strictEqual(caught.didThrow, true, 'A thrown falsy value must not become success');
assert.ok(Object.is(caught.value, primary), 'Preserve the exact thrown value, including NaN');
assert.throws(() => acquireOpenCodeInstallLocks([root], lease), /inactive/);
if (cleanupFails) {
assert.strictEqual(fs.readFileSync(lockPath(root), 'utf8'), 'replacement');
assert.ok(fs.existsSync(`${lockPath(root)}.owned`));
} else assert.strictEqual(fs.existsSync(lockPath(root)), false);
});
}
}
for (const kind of ['frozen', 'nonextensible', 'nonwritable', 'accessor', 'proxy', 'primitive']) {
test(`${kind} callback primary survives cleanup failure without invoking accessors`, root => {
const a = path.join(root, 'a');
const b = path.join(root, 'b');
const marker = new Error('preexisting release diagnostic');
let accesses = 0;
let definitions = 0;
let primary = new Error('primary');
if (kind === 'frozen') Object.freeze(primary);
if (kind === 'nonextensible') Object.preventExtensions(primary);
if (kind === 'nonwritable') Object.defineProperty(primary, 'releaseError', { value: marker });
if (kind === 'accessor') Object.defineProperty(primary, 'releaseError', {
configurable: true,
get() { accesses++; throw new Error('getter must not run'); },
set() { accesses++; throw new Error('setter must not run'); }
});
if (kind === 'proxy') primary = new Proxy(primary, { defineProperty() {
definitions++;
assert.strictEqual(fs.existsSync(lockPath(a)), false, 'All safe cleanup precedes annotation');
throw new Error('annotation rejected');
} });
if (kind === 'primitive') primary = 'literal primary';
let lease;
const caught = captureThrown(() => withOpenCodeInstallLocks([a, b], active => {
lease = active;
fs.renameSync(lockPath(b), `${lockPath(b)}.owned`);
fs.writeFileSync(lockPath(b), 'replacement');
throw primary;
}));
assert.strictEqual(caught.didThrow, true);
assert.ok(Object.is(caught.value, primary));
assert.strictEqual(accesses, 0);
if (kind === 'proxy') assert.strictEqual(definitions, 1);
if (kind === 'nonwritable') assert.strictEqual(primary.releaseError, marker);
if (kind === 'accessor') {
const descriptor = Object.getOwnPropertyDescriptor(primary, 'releaseError');
assert.ok('value' in descriptor, 'Diagnostic should be an own data property');
assert.match(descriptor.value.message, /changed/);
}
assert.strictEqual(fs.existsSync(lockPath(a)), false);
assert.strictEqual(fs.readFileSync(lockPath(b), 'utf8'), 'replacement');
assert.ok(fs.existsSync(`${lockPath(b)}.owned`));
assert.throws(() => acquireOpenCodeInstallLocks([a], lease), /inactive/);
});
}
for (const kind of ['mutable', 'frozen', 'frozen array', 'non-array', 'readonly', 'accessor', 'proxy']) {
test(`three-root cleanup preserves ${kind} first failure and finishes reverse cleanup`, root => {
const roots = ['a', 'b', 'c'].map(name => path.join(root, name));
const [a, b, c] = roots;
const originalRename = fs.renameSync;
const secondary = new Error('second cleanup failure');
const priorDiagnostic = new Error('prior diagnostic');
const originalArray = Object.freeze([priorDiagnostic]);
let accesses = 0;
let primary = new Error('first cleanup failure');
if (kind === 'frozen') Object.freeze(primary);
if (kind === 'frozen array') primary.releaseErrors = originalArray;
if (kind === 'non-array') primary.releaseErrors = { push() { accesses++; throw new Error('caller push must not run'); } };
if (kind === 'readonly') Object.defineProperty(primary, 'releaseErrors', { value: originalArray });
if (kind === 'accessor') Object.defineProperty(primary, 'releaseErrors', {
get() { accesses++; throw new Error('caller getter must not run'); },
set() { accesses++; throw new Error('caller setter must not run'); }
});
if (kind === 'proxy') primary = new Proxy(primary, { defineProperty() {
assert.strictEqual(fs.existsSync(lockPath(a)), false, 'Finish cleanup before a diagnostic trap');
throw new Error('diagnostic trap');
} });
const holder = acquireOpenCodeInstallLocks(roots);
const attempts = [];
fs.renameSync = (from, to) => {
if (roots.some(candidate => lockPath(candidate) === from)) attempts.push(from);
if (from === lockPath(c)) throw primary;
if (from === lockPath(b)) throw secondary;
return originalRename(from, to);
};
let caught;
try { caught = captureThrown(() => holder.release()); }
finally { fs.renameSync = originalRename; }
assert.strictEqual(caught.didThrow, true);
assert.strictEqual(caught.value, primary);
assert.deepStrictEqual(attempts, [lockPath(c), lockPath(b), lockPath(a)]);
assert.strictEqual(fs.existsSync(lockPath(a)), false);
assert.ok(fs.existsSync(lockPath(b)) && fs.existsSync(lockPath(c)), 'Failed releases must not be claimed removed');
assert.strictEqual(accesses, 0);
assert.deepStrictEqual(originalArray, [priorDiagnostic], 'Never mutate a caller-owned diagnostics array');
if (['mutable', 'frozen array', 'non-array'].includes(kind)) {
assert.deepStrictEqual(primary.releaseErrors, [secondary]);
assert.notStrictEqual(primary.releaseErrors, originalArray);
}
if (kind === 'readonly') assert.strictEqual(primary.releaseErrors, originalArray);
assert.throws(() => acquireOpenCodeInstallLocks([a], holder.lease), /inactive/);
});
}
for (const [index, primary] of falsyThrownValues.entries()) {
test(`falsy first cleanup value ${index} is thrown after all remaining roots are attempted`, root => {
const roots = ['a', 'b', 'c'].map(name => path.join(root, name));
const [a, b, c] = roots;
const originalRename = fs.renameSync;
const secondary = new Error('second cleanup failure');
const attempts = [];
let lease;
let caught;
fs.renameSync = (from, to) => {
if (roots.some(candidate => lockPath(candidate) === from)) attempts.push(from);
if (from === lockPath(c)) throw primary;
if (from === lockPath(b)) throw secondary;
return originalRename(from, to);
};
try { caught = captureThrown(() => withOpenCodeInstallLocks(roots, active => { lease = active; return 'success'; })); }
finally { fs.renameSync = originalRename; }
assert.strictEqual(caught.didThrow, true);
assert.ok(Object.is(caught.value, primary));
assert.deepStrictEqual(attempts, [lockPath(c), lockPath(b), lockPath(a)]);
assert.strictEqual(fs.existsSync(lockPath(a)), false);
assert.ok(fs.existsSync(lockPath(b)) && fs.existsSync(lockPath(c)));
assert.throws(() => acquireOpenCodeInstallLocks([a], lease), /inactive/);
});
}
test('frozen acquisition failure survives rollback while replaced ownership is preserved', root => {
const roots = ['a', 'b', 'c'].map(name => path.join(root, name));
const [a, b, c] = roots;
const primary = Object.freeze(new Error('one-shot acquisition validation failure'));
const originalLink = fs.linkSync;
const originalStat = fs.lstatSync;
const originalRename = fs.renameSync;
let published = false;
let faults = 0;
let callbacks = 0;
const checked = [];
fs.linkSync = (from, to) => { originalLink(from, to); if (to === lockPath(c)) published = true; };
fs.lstatSync = (...args) => {
if (published && args[0] === c && faults === 0) {
faults++;
originalRename(lockPath(b), `${lockPath(b)}.owned`);
fs.writeFileSync(lockPath(b), 'replacement during rollback');
throw primary;
}
if (faults > 0 && roots.includes(args[0]) && args[1]?.bigint) checked.push(args[0]);
return originalStat(...args);
};
let caught;
try { caught = captureThrown(() => withOpenCodeInstallLocks(roots, () => { callbacks++; })); }
finally { fs.linkSync = originalLink; fs.lstatSync = originalStat; }
assert.strictEqual(caught.didThrow, true);
assert.strictEqual(caught.value, primary);
assert.strictEqual(faults, 1);
assert.strictEqual(callbacks, 0);
assert.deepStrictEqual(checked, [c, b, a]);
assert.strictEqual(fs.existsSync(lockPath(a)), false);
assert.strictEqual(fs.existsSync(lockPath(c)), false);
assert.strictEqual(fs.readFileSync(lockPath(b), 'utf8'), 'replacement during rollback');
assert.ok(fs.existsSync(`${lockPath(b)}.owned`));
});
console.log(`Results: Passed: ${passed}, Failed: ${failed}`);
process.exitCode = failed ? 1 : 0;