From cbcbefed04a2d562a0ad84de97c08ee88513b5f3 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Tue, 11 Aug 2026 20:29:52 -0700 Subject: [PATCH] fix(daemon): make the occupancy residual total MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The module's contract is that 'unknown' is where every unanswerable question lands, but a throwing dependency escaped instead — routing a failed observation into the launch path rather than onto the safe residual. Latent today because both real implementations swallow their own failures, which is exactly the kind of thing that stops being true during a refactor. Co-authored-by: Orca --- src/main/daemon/daemon-occupancy.test.ts | 20 +++++++++------- src/main/daemon/daemon-occupancy.ts | 30 ++++++++++++++---------- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/main/daemon/daemon-occupancy.test.ts b/src/main/daemon/daemon-occupancy.test.ts index a534a3b95f1..4040885c16d 100644 --- a/src/main/daemon/daemon-occupancy.test.ts +++ b/src/main/daemon/daemon-occupancy.test.ts @@ -109,22 +109,24 @@ describe('resolveDaemonOccupancy when the daemon could not answer', () => { }) describe('resolveDaemonOccupancy when an injected dep throws', () => { - it('propagates an inspector rejection rather than degrading to unknown', async () => { - // Pins current behavior, and it is a gap: the shipped inspector swallows its own - // failures, so this only bites an injected or future-refactored one. A throw here - // escapes into the launch path instead of landing on the safe 'unknown' residual. + it('degrades an inspector rejection to unknown', async () => { + // Why: a question that could not be asked is exactly what the residual is for. Letting + // it escape would route a failed observation into the launch path. const inspectPtyOwnership = vi.fn(async () => { throw new Error('process table read exploded') }) - await expect(resolve({ listSessions: ipcAnswers(null), inspectPtyOwnership })).rejects.toThrow( - 'process table read exploded' + await expect(resolve({ listSessions: ipcAnswers(null), inspectPtyOwnership })).resolves.toEqual( + { + state: 'unknown', + liveSessions: null + } ) }) - it('propagates a failing listSessions dep, which the real one never does', async () => { + it('degrades a failing listSessions dep to unknown', async () => { // countLiveSessionsOverIpc catches internally and returns null; an injected dep is - // not held to that, and the module adds no guard of its own. + // not held to that, so the module guards it. await expect( resolve({ listSessions: async () => { @@ -132,6 +134,6 @@ describe('resolveDaemonOccupancy when an injected dep throws', () => { }, inspectPtyOwnership: ownershipIs('owns-live-ptys') }) - ).rejects.toThrow('socket vanished') + ).resolves.toEqual({ state: 'unknown', liveSessions: null }) }) }) diff --git a/src/main/daemon/daemon-occupancy.ts b/src/main/daemon/daemon-occupancy.ts index c0757cada68..db51c19c2cd 100644 --- a/src/main/daemon/daemon-occupancy.ts +++ b/src/main/daemon/daemon-occupancy.ts @@ -53,17 +53,23 @@ export async function resolveDaemonOccupancy(args: { deps?: DaemonOccupancyDeps }): Promise { const { socketPath, tokenPath, recordedPid, deps = {} } = args - const counted = await (deps.listSessions ?? countLiveSessionsOverIpc)(socketPath, tokenPath) - if (counted !== null) { - return counted > 0 - ? { state: 'occupied', liveSessions: counted } - : { state: 'empty', liveSessions: 0 } + const unknown: DaemonOccupancy = { state: 'unknown', liveSessions: null } + // Why catch rather than let it propagate: 'unknown' is this module's residual, and a + // question that could not be asked is the residual's whole purpose. An escaping throw + // would route a failed observation into the launch path instead. + try { + const counted = await (deps.listSessions ?? countLiveSessionsOverIpc)(socketPath, tokenPath) + if (counted !== null) { + return counted > 0 + ? { state: 'occupied', liveSessions: counted } + : { state: 'empty', liveSessions: 0 } + } + if (recordedPid === null) { + return unknown + } + const ownership = await (deps.inspectPtyOwnership ?? inspectDaemonPtyOwnership)(recordedPid) + return ownership === 'owns-live-ptys' ? { state: 'occupied', liveSessions: null } : unknown + } catch { + return unknown } - if (recordedPid === null) { - return { state: 'unknown', liveSessions: null } - } - const ownership = await (deps.inspectPtyOwnership ?? inspectDaemonPtyOwnership)(recordedPid) - return ownership === 'owns-live-ptys' - ? { state: 'occupied', liveSessions: null } - : { state: 'unknown', liveSessions: null } }