Files
gbrain/test/autopilot-lock.test.ts
T
vinsewandGitHub 34c0ff0c56 fix(autopilot): verify lock holder process before exiting (#477)
A lock file can outlive its autopilot process after a crash or forced termination. The previous mtime-only check treated that file as proof that another instance was running, so supervisor restarts could exit repeatedly until the file aged out.

Read the holder PID and probe it with signal 0. Keep the lock whenever that process is alive; take over only dead, malformed, empty, or self-owned locks. EPERM remains conservative and counts as alive, preventing two autopilot instances from running concurrently.

Add hermetic tests for missing, live, dead, malformed, and empty lock states.
2026-07-16 19:53:10 -07:00

59 lines
1.9 KiB
TypeScript

import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import { mkdtempSync, rmSync, writeFileSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { decideLockAcquisition, isPidAlive } from '../src/commands/autopilot.ts';
let tmp: string;
let lockPath: string;
beforeEach(() => {
tmp = mkdtempSync(join(tmpdir(), 'gbrain-autopilot-lock-'));
lockPath = join(tmp, 'autopilot.lock');
});
afterEach(() => {
rmSync(tmp, { recursive: true, force: true });
});
describe('isPidAlive', () => {
test('returns true for the current process', () => {
expect(isPidAlive(process.pid)).toBe(true);
});
test('returns false for invalid process ids', () => {
expect(isPidAlive(0)).toBe(false);
expect(isPidAlive(-1)).toBe(false);
expect(isPidAlive(Number.NaN)).toBe(false);
expect(isPidAlive(Number.POSITIVE_INFINITY)).toBe(false);
});
});
describe('decideLockAcquisition', () => {
test('acquires when no lock exists', () => {
expect(decideLockAcquisition(lockPath, process.pid)).toEqual({ action: 'acquire' });
});
test('takes over a lock whose holder is dead', () => {
writeFileSync(lockPath, '4194303');
expect(decideLockAcquisition(lockPath, process.pid)).toEqual({
action: 'takeover',
reason: 'dead pid 4194303',
});
});
test('keeps a lock whose holder is alive regardless of age', () => {
writeFileSync(lockPath, String(process.pid));
expect(decideLockAcquisition(lockPath, process.pid + 100_000)).toEqual({
action: 'exit',
holderPid: process.pid,
});
});
test('takes over malformed and empty locks', () => {
writeFileSync(lockPath, 'not-a-pid');
expect(decideLockAcquisition(lockPath, process.pid).action).toBe('takeover');
writeFileSync(lockPath, '');
expect(decideLockAcquisition(lockPath, process.pid).action).toBe('takeover');
});
});