test(daemon): pin that a held session cannot be closed by a provider that never had it

Covers the held-daemon routing fence, including the coupling that is
invisible from the routing file: the thrown error must not match pty:kill's
already-gone predicate, or the close is swallowed into a synthesized exit and
the orphan is hidden again. A rename would otherwise reintroduce the bug
silently.

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil
2026-08-11 21:40:54 -07:00
co-authored by Orca
parent 88f30a702d
commit 246da423db
@@ -5,6 +5,7 @@ import type { DaemonPtyAdapter } from './daemon-pty-adapter'
import type { IPtyProvider, PtySpawnOptions, PtySpawnResult } from '../providers/types'
import type { PtyProcessInspection } from '../providers/pty-process-inspection'
import { SessionNotFoundError, TerminalSessionOwnerUnverifiedError } from './daemon-errors'
import { isSshPtyNotFoundError } from '../providers/ssh-pty-errors'
type ProviderMock = IPtyProvider & {
probePtyLiveness: (id: string) => Promise<boolean | null>
@@ -647,6 +648,27 @@ describe('DegradedDaemonPtyProvider', () => {
expect(provider.hasPty('legacy-session')).toBe(true)
})
it('routes every session operation for a mapped daemon session to that adapter', async () => {
const current = createDaemonAdapter('daemon', ['daemon-session'])
const fallback = createProvider('fallback')
const provider = new DegradedDaemonPtyProvider({ current, legacy: [], fallback })
await provider.discoverDaemonSessions()
provider.write('daemon-session', 'ls\n')
provider.resize('daemon-session', 120, 40)
await provider.sendSignal('daemon-session', 'SIGINT')
await provider.shutdown('daemon-session', {})
expect(current.write).toHaveBeenCalledWith('daemon-session', 'ls\n')
expect(current.resize).toHaveBeenCalledWith('daemon-session', 120, 40)
expect(current.sendSignal).toHaveBeenCalledWith('daemon-session', 'SIGINT')
expect(current.shutdown).toHaveBeenCalledWith('daemon-session', {})
expect(fallback.write).not.toHaveBeenCalled()
expect(fallback.resize).not.toHaveBeenCalled()
expect(fallback.sendSignal).not.toHaveBeenCalled()
expect(fallback.shutdown).not.toHaveBeenCalled()
})
it('keeps an exited legacy daemon poisoning listProcesses after construction', async () => {
const current = createDaemonAdapter('daemon', ['current-session'])
const legacy = createDaemonAdapter('legacy', ['legacy-session'])
@@ -662,3 +684,102 @@ describe('DegradedDaemonPtyProvider', () => {
expect(fallback.listProcesses).toHaveBeenCalledTimes(3)
})
})
describe('DegradedDaemonPtyProvider with a held daemon', () => {
const HELD_SESSION = 'wt-1@@held-daemon-session'
/** Held launch mode never connects to the wedged daemon, so discovery maps nothing and
* every daemon-owned id is unrouted — i.e. resolves to the in-process fallback. */
function createHeldDaemonProvider(): {
current: ReturnType<typeof createDaemonAdapter>
fallback: ReturnType<typeof createProvider>
provider: DegradedDaemonPtyProvider
} {
const current = createDaemonAdapter('daemon')
const fallback = createProvider('fallback')
return {
current,
fallback,
provider: new DegradedDaemonPtyProvider({ current, legacy: [], fallback })
}
}
it('rejects shutdown for a held daemon session instead of reporting a silent success', async () => {
const { current, fallback, provider } = createHeldDaemonProvider()
await provider.discoverDaemonSessions()
// Why: the fallback's shutdown resolves for ids it never had, so the pane would close
// while the daemon's agent keeps running as an orphan.
await expect(provider.shutdown(HELD_SESSION, {})).rejects.toBeInstanceOf(
TerminalSessionOwnerUnverifiedError
)
expect(fallback.shutdown).not.toHaveBeenCalled()
expect(current.shutdown).not.toHaveBeenCalled()
})
it('does not let the kill path mistake an unreachable owner for an already-gone pty', async () => {
const { provider } = createHeldDaemonProvider()
await provider.discoverDaemonSessions()
// Mirrors pty:kill's isPtyAlreadyGoneError (src/main/ipc/pty.ts), which is not exported:
// any error matching it is swallowed into a synthesized pty:exit and reported as success —
// exactly the orphan-hiding lie this routing exists to prevent. Renaming the thrown error
// back into that shape would silently reintroduce it.
const looksAlreadyGoneToPtyKill = (error: unknown): boolean =>
isSshPtyNotFoundError(error) ||
/Session not found/i.test(error instanceof Error ? error.message : String(error))
const error = await provider.shutdown(HELD_SESSION, {}).catch((err: unknown) => err)
expect(error).toBeInstanceOf(TerminalSessionOwnerUnverifiedError)
expect(looksAlreadyGoneToPtyKill(error)).toBe(false)
})
it('throws on write and resize for a held daemon session instead of swallowing input', async () => {
const { fallback, provider } = createHeldDaemonProvider()
await provider.discoverDaemonSessions()
// Why: the fallback's write/resize are `ptyProcesses.get(id)?.…` — typing would vanish.
expect(() => provider.write(HELD_SESSION, 'ls\n')).toThrow(TerminalSessionOwnerUnverifiedError)
expect(() => provider.resize(HELD_SESSION, 120, 40)).toThrow(
TerminalSessionOwnerUnverifiedError
)
expect(fallback.write).not.toHaveBeenCalled()
expect(fallback.resize).not.toHaveBeenCalled()
})
it('rejects sendSignal for a held daemon session', async () => {
const { fallback, provider } = createHeldDaemonProvider()
await provider.discoverDaemonSessions()
await expect(provider.sendSignal(HELD_SESSION, 'SIGINT')).rejects.toBeInstanceOf(
TerminalSessionOwnerUnverifiedError
)
expect(fallback.sendSignal).not.toHaveBeenCalled()
})
it('keeps refusing attach for a held daemon session', async () => {
const { fallback, provider } = createHeldDaemonProvider()
await provider.discoverDaemonSessions()
await expect(provider.attach(HELD_SESSION)).rejects.toBeInstanceOf(SessionNotFoundError)
expect(fallback.attach).not.toHaveBeenCalled()
})
it('still routes every operation for a locally spawned session the fallback owns', async () => {
const { current, fallback, provider } = createHeldDaemonProvider()
await provider.discoverDaemonSessions()
const fresh = await provider.spawn({ cols: 80, rows: 24 })
provider.write(fresh.id, 'echo hi\n')
provider.resize(fresh.id, 100, 30)
await expect(provider.sendSignal(fresh.id, 'SIGINT')).resolves.toBeUndefined()
await expect(provider.shutdown(fresh.id, {})).resolves.toBeUndefined()
expect(fallback.write).toHaveBeenCalledWith(fresh.id, 'echo hi\n')
expect(fallback.resize).toHaveBeenCalledWith(fresh.id, 100, 30)
expect(fallback.sendSignal).toHaveBeenCalledWith(fresh.id, 'SIGINT')
expect(fallback.shutdown).toHaveBeenCalledWith(fresh.id, {})
expect(current.write).not.toHaveBeenCalled()
expect(current.shutdown).not.toHaveBeenCalled()
})
})