From bd50bff02a7665e6300a134c6e1e35a916694f17 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sat, 26 Sep 2026 01:42:50 +0000 Subject: [PATCH] test(cli): stop the pidfile zombie test racing the shell's reaper `sh -c 'true & ...; exec sleep 30'` only leaves a zombie if `true` exits after the exec. When it exits first, sh reaps it and /proc//stat is gone, so the poll threw ENOENT (failed twice in a row on #255's CI). Background a short sleep instead so the child outlives the exec, tolerate a missing stat file while polling, and assert the zombie state was reached. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../daemon/__tests__/pidfile-zombie.test.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/apps/cli/src/daemon/__tests__/pidfile-zombie.test.ts b/apps/cli/src/daemon/__tests__/pidfile-zombie.test.ts index 62c0a11..63abd40 100644 --- a/apps/cli/src/daemon/__tests__/pidfile-zombie.test.ts +++ b/apps/cli/src/daemon/__tests__/pidfile-zombie.test.ts @@ -32,15 +32,24 @@ describe.runIf(existsSync('/proc/self/stat'))('isProcessAlive on a zombie', () = }); it('treats an exited but unreaped process as gone', async () => { - // The shell backgrounds `true` and `exec`s into a process that never - // waits, so once `true` exits it stays a zombie for the life of the test. - parent = spawn('sh', ['-c', 'true & echo $!; exec sleep 30'], { stdio: ['ignore', 'pipe', 'ignore'] }); + // The shell backgrounds a short sleep and `exec`s into a process that never + // waits, so once the child exits it stays a zombie for the life of the test. + // The child must outlive the exec: a `true` that exits first is reaped by sh. + parent = spawn('sh', ['-c', 'sleep 0.5 & echo $!; exec sleep 30'], { stdio: ['ignore', 'pipe', 'ignore'] }); const [chunk] = (await once(parent.stdout!, 'data')) as [Buffer]; const pid = Number.parseInt(chunk.toString(), 10); - // `true` exits on its own schedule; the zombie state is the signal to wait on. + // The child exits on its own schedule; the zombie state is the signal to wait on. const deadline = Date.now() + 5000; - while (procStatState(readFileSync(`/proc/${pid}/stat`, 'utf-8')) !== 'Z' && Date.now() < deadline); + const state = () => { + try { + return procStatState(readFileSync(`/proc/${pid}/stat`, 'utf-8')); + } catch { + return null; + } + }; + while (state() !== 'Z' && Date.now() < deadline) await new Promise((r) => setTimeout(r, 10)); + expect(state()).toBe('Z'); expect(() => process.kill(pid, 0)).not.toThrow(); expect(isProcessAlive(pid)).toBe(false);