From 2529df8d5d9ed97f97ce0d1cf9d19ec831cd361a Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Fri, 16 Oct 2026 01:31:58 -0700 Subject: [PATCH] fix(ssh): a disconnect during the legacy relay bridge wait no longer republishes connected (#29544) Refs STA-10395 --- .../ssh-connect-decision-cancellation.test.ts | 46 ++++++++++++++++++- src/main/ipc/ssh-connect-flow.ts | 6 +++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/main/ipc/ssh-connect-decision-cancellation.test.ts b/src/main/ipc/ssh-connect-decision-cancellation.test.ts index 2cc7834882f..809b1195d78 100644 --- a/src/main/ipc/ssh-connect-decision-cancellation.test.ts +++ b/src/main/ipc/ssh-connect-decision-cancellation.test.ts @@ -13,11 +13,27 @@ vi.mock('../ssh/ssh-connection-manager', () => mocks.sshConnectionManager) vi.mock('../ssh/ssh-channel-multiplexer', () => mocks.sshChannelMultiplexer) vi.mock('../ssh/ssh-port-forward', () => mocks.sshPortForward) vi.mock('../ssh/ssh-port-scanner', () => mocks.sshPortScanner) +vi.mock( + import('../ssh/legacy-relay-bridge/legacy-relay-bridge-registry'), + async (importOriginal) => ({ + ...(await importOriginal()), + startLegacyRelayBridge: vi.fn(async () => {}), + stopLegacyRelayBridge: vi.fn() + }) +) import type { SshTarget } from '../../shared/ssh-types' import type { SshConnection } from '../ssh/ssh-connection' import { recordSshConnectionOpened } from '../ssh/ssh-connection-attribution' -import { decideHostServer, publishHostServerDecisionFailure } from './ssh-host-server-connect' +import { + decideHostServer, + publishHostServerDecisionFailure, + publishManagedServerConnect +} from './ssh-host-server-connect' +import { + startLegacyRelayBridge, + stopLegacyRelayBridge +} from '../ssh/legacy-relay-bridge/legacy-relay-bridge-registry' import { createSshIpcHarness } from './ssh-ipc-test-harness' const { mockSshStore, mockConnectionManager } = mocks @@ -103,6 +119,34 @@ describe('a connect cancelled during its server decision', () => { expect(mockConnectionManager.disconnectConnection).not.toHaveBeenCalledWith('ssh-1', opened) }) + it('does not publish connected when the user disconnected during the legacy bridge wait', async () => { + const target: SshTarget = { + id: 'ssh-1', + label: 'Server', + host: 'example.com', + port: 22, + username: 'deploy' + } + const opened = { id: 'managed-transport' } + vi.mocked(publishManagedServerConnect).mockClear() + mockSshStore.getTarget.mockReturnValue(target) + mockConnectionManager.getConnection.mockReturnValue(undefined) + mockConnectionManager.disconnect.mockResolvedValue(undefined) + vi.mocked(decideHostServer).mockImplementationOnce(async () => { + recordSshConnectionOpened(asTransport(opened)) + mockConnectionManager.getConnection.mockReturnValue(opened) + return { route: 'managed', environmentId: 'env-1' } + }) + vi.mocked(startLegacyRelayBridge).mockImplementationOnce(async () => { + await handlers.get('ssh:disconnect')!(null, { targetId: 'ssh-1' }) + }) + await expect( + Promise.resolve(handlers.get('ssh:connect')!(null, { targetId: 'ssh-1' })) + ).rejects.toThrow('SSH connection attempt was cancelled') + expect(publishManagedServerConnect).not.toHaveBeenCalled() + expect(stopLegacyRelayBridge).toHaveBeenCalledWith('ssh-1') + }) + it('closes the transport a still-current decision dialed when that decision fails', async () => { const target: SshTarget = { id: 'ssh-1', diff --git a/src/main/ipc/ssh-connect-flow.ts b/src/main/ipc/ssh-connect-flow.ts index aaf44f3a4b4..9e5bda1c1fc 100644 --- a/src/main/ipc/ssh-connect-flow.ts +++ b/src/main/ipc/ssh-connect-flow.ts @@ -127,6 +127,12 @@ async function doConnect( } // COMPAT(legacy-relay-bridge): panes an older relay still runs are known before the host is. await startLegacyRelayBridge(targetId) + // A disconnect or shutdown during the bridge wait already published; "connected" would be stale. + assertSshConnectsNotFenced() + if (!isCurrentConnectAttempt(targetId, authority)) { + await abandonDecisionTransport(targetId, owner, authority) + throw createCancelledConnectAttemptError() + } const published = publishManagedServerConnect( targetId, server.environmentId,