Files

130 lines
4.4 KiB
JavaScript
Raw Permalink Normal View History

import { describe, expect, it } from 'bun:test';
import { createRelayHostLock } from './host-lock.js';
// In-memory fs standing in for the shared data dir.
const makeFakeFs = () => {
const files = new Map();
return {
readFileSync: (p) => {
if (!files.has(p)) {
const error = new Error('ENOENT');
error.code = 'ENOENT';
throw error;
}
return files.get(p);
},
writeFileSync: (p, data) => {
files.set(p, data);
},
unlinkSync: (p) => {
files.delete(p);
},
peek: (p) => files.get(p),
};
};
// Fake process: `alive` is the set of pids that respond to kill(pid, 0).
const makeFakeProcess = (pid, alive = new Set([pid])) => ({
pid,
kill: (target) => {
if (!alive.has(target)) {
const error = new Error('ESRCH');
error.code = 'ESRCH';
throw error;
}
return true;
},
});
const LOCK = '/data/relay-host.lock';
const silentLogger = { warn: () => {} };
describe('relay host lock', () => {
it('claims a free lock and reports holding it', () => {
const fs = makeFakeFs();
const lock = createRelayHostLock({ lockFilePath: LOCK, fs, process: makeFakeProcess(100), logger: silentLogger });
expect(lock.tryClaim()).toBe(true);
expect(lock.holdsClaim()).toBe(true);
expect(JSON.parse(fs.peek(LOCK)).pid).toBe(100);
});
it('refuses to claim while another LIVE process holds it', () => {
const fs = makeFakeFs();
const alive = new Set([100, 200]);
const first = createRelayHostLock({ lockFilePath: LOCK, fs, process: makeFakeProcess(100, alive), logger: silentLogger });
const second = createRelayHostLock({ lockFilePath: LOCK, fs, process: makeFakeProcess(200, alive), logger: silentLogger });
expect(first.tryClaim()).toBe(true);
expect(second.tryClaim()).toBe(false);
expect(second.holdsClaim()).toBe(false);
expect(second.liveClaimantPid()).toBe(100);
});
it('treats a dead claimant as free (stale claim takeover)', () => {
const fs = makeFakeFs();
const alive = new Set([200]); // pid 100 is gone
fs.writeFileSync(LOCK, JSON.stringify({ pid: 100 }));
const lock = createRelayHostLock({ lockFilePath: LOCK, fs, process: makeFakeProcess(200, alive), logger: silentLogger });
expect(lock.liveClaimantPid()).toBe(null);
expect(lock.tryClaim()).toBe(true);
expect(lock.holdsClaim()).toBe(true);
});
it('forceClaim overrides a live holder; the loser sees the takeover', () => {
const fs = makeFakeFs();
const alive = new Set([100, 200]);
const first = createRelayHostLock({ lockFilePath: LOCK, fs, process: makeFakeProcess(100, alive), logger: silentLogger });
const second = createRelayHostLock({ lockFilePath: LOCK, fs, process: makeFakeProcess(200, alive), logger: silentLogger });
expect(first.tryClaim()).toBe(true);
expect(second.forceClaim()).toBe(true);
expect(second.holdsClaim()).toBe(true);
expect(first.holdsClaim()).toBe(false);
expect(first.liveClaimantPid()).toBe(200);
});
it('release removes only its own claim', () => {
const fs = makeFakeFs();
const alive = new Set([100, 200]);
const first = createRelayHostLock({ lockFilePath: LOCK, fs, process: makeFakeProcess(100, alive), logger: silentLogger });
const second = createRelayHostLock({ lockFilePath: LOCK, fs, process: makeFakeProcess(200, alive), logger: silentLogger });
expect(first.tryClaim()).toBe(true);
second.release(); // not the holder — must be a no-op
expect(first.holdsClaim()).toBe(true);
first.release();
expect(fs.peek(LOCK)).toBeUndefined();
expect(first.liveClaimantPid()).toBe(null);
});
it('treats an unparsable claim file as free', () => {
const fs = makeFakeFs();
fs.writeFileSync(LOCK, 'not-json');
const lock = createRelayHostLock({ lockFilePath: LOCK, fs, process: makeFakeProcess(100), logger: silentLogger });
expect(lock.liveClaimantPid()).toBe(null);
expect(lock.tryClaim()).toBe(true);
});
it('an EPERM kill probe still counts as a live holder', () => {
const fs = makeFakeFs();
fs.writeFileSync(LOCK, JSON.stringify({ pid: 100 }));
const proc = {
pid: 200,
kill: () => {
const error = new Error('EPERM');
error.code = 'EPERM';
throw error;
},
};
const lock = createRelayHostLock({ lockFilePath: LOCK, fs, process: proc, logger: silentLogger });
expect(lock.liveClaimantPid()).toBe(100);
expect(lock.tryClaim()).toBe(false);
});
});