test(pty): reproduce rejected SSH split orphan

This commit is contained in:
Jinwoo-H
2026-08-13 00:41:13 -04:00
parent de729d6067
commit 372bcbcaef
+138
View File
@@ -515,6 +515,7 @@ describe('registerPtyHandlers', () => {
'ssh-b',
'ssh-expired-runtime',
'ssh-fresh-fail',
'ssh-split-rejection',
'ssh-reattach-1',
'ssh-reattach-fail',
'ssh-reattach-ok',
@@ -8677,6 +8678,143 @@ describe('registerPtyHandlers', () => {
expect(proc.kill).toHaveBeenCalledOnce()
})
it('retains cleanup authority when rejected SSH split shutdown fails', async () => {
type RuntimeSpawnController = {
spawn(args: {
cols: number
rows: number
connectionId: string
worktreeId: string
tabId: string
leafId: string
persistHostSessionBinding: boolean
expectedSourceBinding: {
worktreeId: string
tabId: string
leafId: string
ptyId: string
}
}): Promise<{ id: string }>
}
const connectionId = 'ssh-split-rejection'
const appPtyId = `ssh:${connectionId}@@rejected-split-pty`
const liveRemotePtys = new Set<string>()
let shutdownFails = true
const shutdown = vi.fn(async (id: string) => {
if (shutdownFails) {
throw new Error('injected remote shutdown failure')
}
liveRemotePtys.delete(id)
})
registerSshPtyProvider(connectionId, {
spawn: vi.fn(async () => {
liveRemotePtys.add(appPtyId)
return { id: appPtyId }
}),
write: vi.fn(),
resize: vi.fn(),
shutdown,
sendSignal: vi.fn(),
getCwd: vi.fn(),
getInitialCwd: vi.fn(),
clearBuffer: vi.fn(),
acknowledgeDataEvent: vi.fn(),
onData: vi.fn(() => () => {}),
onReplay: vi.fn(() => () => {}),
onExit: vi.fn(() => () => {}),
listProcesses: vi.fn(async () =>
Array.from(liveRemotePtys, (id) => ({ id, cwd: '/remote/repo', title: 'shell' }))
),
hasChildProcesses: vi.fn(),
getForegroundProcess: vi.fn(),
serialize: vi.fn(),
revive: vi.fn(),
getDefaultShell: vi.fn(),
getProfiles: vi.fn()
} as never)
const store = {
persistPtyBinding: vi.fn(() => false),
upsertSshRemotePtyLease: vi.fn(),
markSshRemotePtyLease: vi.fn()
}
let controller: RuntimeSpawnController | null = null
const runtime = {
setPtyController: vi.fn((value) => {
controller = value
}),
preAllocateHandleForPty: vi.fn(() => 'term_rejected_split'),
registerPreAllocatedHandleForPty: vi.fn(),
registerPty: vi.fn(),
noteTerminalSpawnCommand: vi.fn(),
getDriver: vi.fn(() => ({ kind: 'host' })),
onPtySpawned: vi.fn(),
onPtyExit: vi.fn(),
onPtyData: vi.fn()
}
const error = vi.spyOn(console, 'error').mockImplementation(() => {})
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
const leafId = '33333333-3333-4333-8333-333333333333'
try {
registerPtyHandlers(
mainWindow as never,
runtime as never,
undefined,
undefined,
undefined,
store as never
)
await expect(
(controller as unknown as RuntimeSpawnController).spawn({
cols: 80,
rows: 24,
connectionId,
worktreeId: 'repo-1::/remote/repo',
tabId: 'tab-rejected-split',
leafId,
persistHostSessionBinding: true,
expectedSourceBinding: {
worktreeId: 'repo-1::/remote/repo',
tabId: 'tab-rejected-split',
leafId: '22222222-2222-4222-8222-222222222222',
ptyId: `ssh:${connectionId}@@source-pty`
}
})
).rejects.toThrow('terminal_split_source_not_found')
expect(runtime.registerPty).not.toHaveBeenCalled()
expect(liveRemotePtys.has(appPtyId)).toBe(true)
expect(getPtyIdsForConnection(connectionId)).toEqual([appPtyId])
expect(store.upsertSshRemotePtyLease).toHaveBeenCalledWith(
expect.objectContaining({
targetId: connectionId,
ptyId: 'rejected-split-pty',
state: 'attached'
})
)
expect(warn).toHaveBeenCalledWith(
'[pty] failed to clean up PTY after persistence failure:',
expect.objectContaining({ message: 'injected remote shutdown failure' })
)
shutdownFails = false
await handlers.get('pty:kill')!(null, { id: appPtyId })
expect(shutdown).toHaveBeenCalledTimes(2)
expect(liveRemotePtys.has(appPtyId)).toBe(false)
expect(getPtyIdsForConnection(connectionId)).toEqual([])
expect(store.markSshRemotePtyLease).toHaveBeenCalledWith(
connectionId,
'rejected-split-pty',
'terminated'
)
} finally {
error.mockRestore()
warn.mockRestore()
unregisterSshPtyProvider(connectionId)
}
})
it('reports lower-owner commit before rejecting an early-exited runtime incarnation', async () => {
const persistPtyBinding = vi.fn()
const onPtySpawnCommitted = vi.fn()