// Tests for findRunningSupervisors — discovering a live app supervisor by
// its PGH_SUP_NAME env when the pid file is missing or stale. This is the
// load-bearing piece of the `app stop/restart` lifecycle fix: without it
// a stale pid leaves the real daemon+supervisor running (holding the
// store lock / port) and the next start collides.
//
// /proc is faked via injected readdir/readFile so this runs anywhere.
import test from 'brittle'
import process from '#node:process'
import { findRunningSupervisors } from '../lib/app-state.js'
// Build a fake /proc from { pid: { env: {...}, cmd: [...] } }.
function fakeProc (procs) {
const readdir = () => Object.keys(procs)
const readFile = (p) => {
const m = p.match(/\/proc\/(\d+)\/(environ|cmdline)$/)
if (!m) throw new Error('ENOENT ' + p)
const proc = procs[m[1]]
if (!proc) throw new Error('ESRCH ' + p)
if (m[2] === 'environ') {
return Object.entries(proc.env || {}).map(([k, v]) => k + '=' + v).join('\0')
}
return (proc.cmd || []).join('\0')
}
return { procDir: '/proc', readdir, readFile }
}
const SUP_CMD = ['bare', '/home/x/.local/share/store/apps/pgh-experimental/lib/supervisor.js']
test('finds the supervisor for a name', (t) => {
const proc = fakeProc({
'100': { env: { PGH_SUP_NAME: 'my-daemon', HOME: '/home/x' }, cmd: SUP_CMD },
'101': { env: { PGH_APP_NAME: 'my-daemon' }, cmd: ['bare', 'server.js', 'serve'] } // the child — no PGH_SUP_NAME
})
t.alike(findRunningSupervisors('my-daemon', proc), [100])
})
test('exact-match avoids prefix collisions', (t) => {
const proc = fakeProc({
'200': { env: { PGH_SUP_NAME: 'pear-git-v3' }, cmd: SUP_CMD },
'201': { env: { PGH_SUP_NAME: 'pear-git-v3-experimental' }, cmd: SUP_CMD }
})
t.alike(findRunningSupervisors('pear-git-v3', proc), [200], 'does not match the -experimental sibling')
t.alike(findRunningSupervisors('pear-git-v3-experimental', proc), [201])
})
test('requires supervisor.js in cmdline (ignores look-alikes)', (t) => {
const proc = fakeProc({
// Carries the env but is not a supervisor process.
'300': { env: { PGH_SUP_NAME: 'my-daemon' }, cmd: ['bash', '-c', 'echo PGH_SUP_NAME=my-daemon'] }
})
t.alike(findRunningSupervisors('my-daemon', proc), [])
})
test('returns ALL matches (orphan herd) for reaping', (t) => {
const proc = fakeProc({
'400': { env: { PGH_SUP_NAME: 'sidecar' }, cmd: SUP_CMD },
'401': { env: { PGH_SUP_NAME: 'sidecar' }, cmd: SUP_CMD },
'402': { env: { PGH_SUP_NAME: 'other' }, cmd: SUP_CMD }
})
t.alike(findRunningSupervisors('sidecar', proc).sort((a, b) => a - b), [400, 401])
})
test('returns [] when /proc is unreadable (non-Linux)', (t) => {
const ok = findRunningSupervisors('x', { readdir: () => { throw new Error('ENOENT') } })
t.alike(ok, [])
})
test('skips non-numeric entries and unreadable processes', (t) => {
const proc = fakeProc({
'500': { env: { PGH_SUP_NAME: 'my-daemon' }, cmd: SUP_CMD }
})
// Inject extra junk entries the real /proc contains.
const readdir = () => ['self', 'cpuinfo', '500', 'net']
const readFile = (p) => {
if (/\/proc\/(self|cpuinfo|net)\//.test(p)) throw new Error('EACCES')
return proc.readFile(p)
}
t.alike(findRunningSupervisors('my-daemon', { readdir, readFile }), [500])
})