diff --git a/src/main/ssh/ssh-connection-manager.test.ts b/src/main/ssh/ssh-connection-manager.test.ts index 9f5f99509a1..5f33b6bab86 100644 --- a/src/main/ssh/ssh-connection-manager.test.ts +++ b/src/main/ssh/ssh-connection-manager.test.ts @@ -169,6 +169,30 @@ describe('SshConnectionManager', () => { expect(manager.getConnection(target.id)).toBe(replacement) }) + it('keeps a Disconnect that lands while a background connect closes the old transport', async () => { + const manager = new SshConnectionManager({ onStateChange: vi.fn() }) + await manager.connect(target) + const old = mockState.instances[0] + // The pooled transport dropped, so a background dial (tunnel, census) replaces it. + old.status = 'disconnected' + let finishOldClose!: () => void + old.disconnect.mockImplementationOnce( + () => + new Promise((resolve) => { + finishOldClose = resolve + }) + ) + + const background = manager.connect(target) + const rejected = expect(background).rejects.toThrow('cancelled') + await manager.disconnect(target.id) + finishOldClose() + await rejected + + expect(mockState.instances).toHaveLength(1) + expect(manager.getConnection(target.id)).toBeUndefined() + }) + it('leaves a connection whose own reconnect is asking the user alone', async () => { const manager = new SshConnectionManager({ onStateChange: vi.fn() }) const conn = await manager.connect(target) diff --git a/src/main/ssh/ssh-connection-manager.ts b/src/main/ssh/ssh-connection-manager.ts index 3c477360f49..f63ed3aa363 100644 --- a/src/main/ssh/ssh-connection-manager.ts +++ b/src/main/ssh/ssh-connection-manager.ts @@ -63,6 +63,11 @@ export class SshConnectionManager { if (existing) { this.retainSignIn(target.id, existing) await existing.disconnect() + // Why: a Disconnect or Remove while the old transport closed cancels this attempt; + // registering now would revive the host the user just let go. + if (this.connectingTargets.get(target.id) !== attempt) { + throw new Error(`Connection to ${target.label} was cancelled`) + } } const conn = new SshConnection(target, this.callbacks, this.takeRetainedSignIn(target))