lease-owner.test.ts
1 import { describe, it } from 'node:test' 2 import assert from 'node:assert/strict' 3 import { isOwnerProcessDead, parseOwnerPid } from '@/lib/server/daemon/lease-owner' 4 5 function probeThrowing(code: string) { 6 return { 7 kill: () => { 8 const err = new Error('mock probe failure') as NodeJS.ErrnoException 9 err.code = code 10 throw err 11 }, 12 } 13 } 14 15 const probeAlive = { kill: () => true as const } 16 17 describe('parseOwnerPid', () => { 18 it('returns the pid for a well-formed owner string', () => { 19 assert.equal(parseOwnerPid('pid:12345:abc'), 12345) 20 assert.equal(parseOwnerPid('pid:1:xyz'), 1) 21 }) 22 23 it('returns null for unrecognised owner strings', () => { 24 assert.equal(parseOwnerPid(null), null) 25 assert.equal(parseOwnerPid(undefined), null) 26 assert.equal(parseOwnerPid(''), null) 27 assert.equal(parseOwnerPid('another process'), null) 28 assert.equal(parseOwnerPid('pid::abc'), null) 29 assert.equal(parseOwnerPid('pid:abc:xyz'), null) 30 assert.equal(parseOwnerPid('host:hostname:pid:1:abc'), null) 31 }) 32 33 it('rejects zero and negative pids', () => { 34 assert.equal(parseOwnerPid('pid:0:abc'), null) 35 assert.equal(parseOwnerPid('pid:-1:abc'), null) 36 }) 37 }) 38 39 describe('isOwnerProcessDead — bug #41 stale-lease recovery', () => { 40 it('returns true when the probe reports ESRCH (no such process)', () => { 41 assert.equal(isOwnerProcessDead('pid:99999:abc', probeThrowing('ESRCH')), true) 42 }) 43 44 it('returns false when the probe reports EPERM (process owned by someone else)', () => { 45 // EPERM means the process exists but signal delivery is blocked. Assume alive 46 // and do not steal the lease — bias towards waiting for TTL. 47 assert.equal(isOwnerProcessDead('pid:99999:abc', probeThrowing('EPERM')), false) 48 }) 49 50 it('returns false when the probe succeeds (process is alive)', () => { 51 assert.equal(isOwnerProcessDead('pid:99999:abc', probeAlive), false) 52 }) 53 54 it('returns false for any unknown probe error code (do not guess)', () => { 55 assert.equal(isOwnerProcessDead('pid:99999:abc', probeThrowing('EAGAIN')), false) 56 assert.equal(isOwnerProcessDead('pid:99999:abc', probeThrowing('UNKNOWN')), false) 57 }) 58 59 it('returns false for owner strings we cannot parse (different host, malformed, missing)', () => { 60 assert.equal(isOwnerProcessDead(null, probeThrowing('ESRCH')), false) 61 assert.equal(isOwnerProcessDead('another process', probeThrowing('ESRCH')), false) 62 assert.equal(isOwnerProcessDead('host:remote:pid:1:abc', probeThrowing('ESRCH')), false) 63 }) 64 65 it('refuses to declare its own pid dead even if probe lies', () => { 66 // Defence in depth: the current process is obviously alive; if a 67 // pathological probe returned ESRCH for its own pid, we must not 68 // act on that. 69 const owner = `pid:${process.pid}:self` 70 assert.equal(isOwnerProcessDead(owner, probeThrowing('ESRCH')), false) 71 }) 72 })