fix(ssh): re-check session identity before post-probe resume reconnect

The resume probe can take ~10s; if the user disconnected the target or the
session/connection was replaced during that window, reconnecting would
resurrect an intentionally torn-down connection (CodeRabbit).

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo-H
2026-07-08 15:12:11 -04:00
co-authored by Orca
parent 9ed75a0da7
commit bbcc506dab
2 changed files with 44 additions and 2 deletions
+35
View File
@@ -1468,6 +1468,41 @@ describe('SSH IPC handlers', () => {
expect(mockConnectionManager.reconnect).not.toHaveBeenCalled()
})
it('does not reconnect after resume when the target was disconnected during the probe', async () => {
const target: SshTarget = {
id: 'ssh-1',
label: 'Server',
host: 'example.com',
port: 22,
username: 'deploy'
}
const conn = {}
mockSshStore.getTarget.mockReturnValue(target)
mockConnectionManager.connect.mockResolvedValue(conn)
mockConnectionManager.getConnection.mockReturnValue(conn)
mockConnectionManager.getState.mockReturnValue({
targetId: 'ssh-1',
status: 'connected',
error: null,
reconnectAttempt: 0
})
mockMux.probeLiveness.mockResolvedValue(false)
await handlers.get('ssh:connect')!(null, { targetId: 'ssh-1' })
const resumeListener = powerMonitorOnMock.mock.calls.find(([event]) => event === 'resume')?.[1]
expect(resumeListener).toBeTypeOf('function')
resumeListener()
// Why: the probe window is seconds long; a user disconnect during it must
// win — reconnecting afterwards would resurrect the torn-down target.
mockConnectionManager.getConnection.mockReturnValue(undefined)
await vi.waitFor(() => expect(mockMux.probeLiveness).toHaveBeenCalledTimes(2))
await new Promise((resolve) => setTimeout(resolve, 0))
expect(mockConnectionManager.reconnect).not.toHaveBeenCalled()
})
it('extends active relay grace while the system is suspending', async () => {
const target: SshTarget = {
id: 'ssh-1',
+9 -2
View File
@@ -469,7 +469,8 @@ function registerPowerMonitorReconnect(): void {
}
const onResume = (): void => {
for (const [targetId, session] of activeSessions) {
const conn = connectionManager?.getConnection(targetId)
const manager = connectionManager
const conn = manager?.getConnection(targetId)
if (!conn) {
continue
}
@@ -480,8 +481,14 @@ function registerPowerMonitorReconnect(): void {
if (await isRelayLinkAliveAfterResume(session)) {
return
}
// Why: the probe can take ~10s. If the user disconnected or the
// session/connection was replaced meanwhile, reconnecting would
// resurrect a connection that was intentionally torn down.
if (activeSessions.get(targetId) !== session || manager?.getConnection(targetId) !== conn) {
return
}
try {
await connectionManager?.reconnect(targetId)
await manager?.reconnect(targetId)
} catch (err) {
console.warn(
`[ssh] Failed to reconnect ${targetId} after system resume: ${