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 } }