fix(daemon): make the occupancy residual total

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 <help@stably.ai>
This commit is contained in:
Neil
2026-08-11 20:29:52 -07:00
co-authored by Orca
parent 533e317cee
commit cbcbefed04
2 changed files with 29 additions and 21 deletions
+11 -9
View File
@@ -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<typeof inspectDaemonPtyOwnership>(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 })
})
})
+18 -12
View File
@@ -53,17 +53,23 @@ export async function resolveDaemonOccupancy(args: {
deps?: DaemonOccupancyDeps
}): Promise<DaemonOccupancy> {
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 }
}