From dde7ed42b3b3fcceb94acda3f8b5703b0e6e7002 Mon Sep 17 00:00:00 2001 From: Adolanium <94890352+Adolanium@users.noreply.github.com> Date: Wed, 26 Aug 2026 13:37:17 +0300 Subject: [PATCH] fix(desktop): look up v1 SSH terminals with the bootstrap pool key resolveDesktopRemoteRoute tags v1 SSH with a registry connectionId when the host matches. The terminal then looked up sshConnections with backendScopeKey(connectionId, profile), but bootstrap stored the tunnel under sshScopeKey(profile or null). The pane stayed pending. Ignore the identity tag for v1 pool lookup. --- .../electron/desktop-remote-route.test.ts | 43 ++++++++++++++++++- apps/desktop/electron/desktop-remote-route.ts | 16 +++++++ apps/desktop/electron/main.ts | 6 +-- 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/apps/desktop/electron/desktop-remote-route.test.ts b/apps/desktop/electron/desktop-remote-route.test.ts index 2a731b77bb..b2f35ae885 100644 --- a/apps/desktop/electron/desktop-remote-route.test.ts +++ b/apps/desktop/electron/desktop-remote-route.test.ts @@ -3,7 +3,8 @@ import assert from 'node:assert/strict' import { test } from 'vitest' import { normalizeRegistry, REGISTRY_VERSION } from './connection-registry' -import { resolveDesktopRemoteRoute } from './desktop-remote-route' +import { backendScopeKey } from './connection-registry' +import { resolveDesktopRemoteRoute, v1SshTerminalPoolKey } from './desktop-remote-route' const tokenA = { encoding: 'plain', value: 'token-a' } const tokenB = { encoding: 'plain', value: 'token-b' } @@ -153,6 +154,46 @@ test('global SSH treats an omitted port as 22 and checks the primary route', () assert.equal(route?.connectionId, 'ssh-primary') }) +test('v1 settings SSH pool key ignores registry identity tags', () => { + const route = resolveDesktopRemoteRoute({ + config: { mode: 'ssh', remote: { mode: 'ssh', host: 'box.test', user: 'hermes' } }, + profile: 'worker', + registry: registry('ssh-primary', [ + { id: 'ssh-primary', kind: 'ssh', label: 'SSH primary', host: 'box.test', user: 'hermes', port: 22 } + ]) + }) + + assert.ok(route) + assert.equal(route.kind, 'ssh') + assert.equal(route.connectionId, 'ssh-primary') + assert.equal(v1SshTerminalPoolKey(route, 'worker'), '') + assert.notEqual(v1SshTerminalPoolKey(route, 'worker'), backendScopeKey(route.connectionId, 'worker')) +}) + +test('v1 profile SSH pool key is the profile, not conn:id::profile', () => { + const ssh = { + mode: 'ssh', + host: 'box.test', + user: 'hermes', + port: 2222, + keyPath: '/keys/a', + remoteHermesPath: '/srv/hermes', + remoteProfile: 'worker' + } + + const route = resolveDesktopRemoteRoute({ + config: { mode: 'local', profiles: { worker: ssh } }, + profile: 'worker', + registry: registry('local', [{ id: 'worker-ssh', kind: 'ssh', label: 'Worker SSH', ...ssh }]) + }) + + assert.ok(route) + assert.equal(route.kind, 'ssh') + assert.equal(route.connectionId, 'worker-ssh') + assert.equal(v1SshTerminalPoolKey(route, 'worker'), 'worker') + assert.notEqual(v1SshTerminalPoolKey(route, 'worker'), backendScopeKey(route.connectionId, 'worker')) +}) + test('profile route omits identity when two registry entries match exactly', () => { const block = { mode: 'remote', url: 'https://worker.test', authMode: 'token', token: tokenA } diff --git a/apps/desktop/electron/desktop-remote-route.ts b/apps/desktop/electron/desktop-remote-route.ts index e41df9b199..56ca6b74a8 100644 --- a/apps/desktop/electron/desktop-remote-route.ts +++ b/apps/desktop/electron/desktop-remote-route.ts @@ -51,6 +51,22 @@ function withConnectionId(route: T, connectionId?: string): T return connectionId ? { ...route, connectionId } : route } +/** + * Pool key used by v1 settings/profile SSH (`sshConnections`). + * + * Bootstrap stores the tunnel under sshScopeKey(profile or null). + * resolveDesktopRemoteRoute may also stamp a registry connectionId when + * the host matches a v2 row. That id is identity, not the pool key. + * Looking up backendScopeKey(connectionId, profile) misses the live tunnel. + */ +export function v1SshTerminalPoolKey(route: { source: string }, profile?: null | string): string { + if (route.source === 'profile') { + return connectionScopeKey(profile) || '' + } + + return '' +} + /** * Select one remote route with the existing precedence and freeze any exact * registry identity before I/O. A null result means the profile resolves diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index 9923fd473f..77a1bfe586 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -158,7 +158,7 @@ import { describeCrashReason, installCrashForensics } from './crash-forensics' import { adoptServedDashboardToken } from './dashboard-token' import { loadOrCreateInstallationId, sshOwnershipId } from './desktop-installation' import { formatDesktopLogLine } from './desktop-log-line' -import { resolveDesktopRemoteRoute } from './desktop-remote-route' +import { resolveDesktopRemoteRoute, v1SshTerminalPoolKey } from './desktop-remote-route' import { buildPosixCleanupScript, buildWindowsCleanupScript, @@ -10335,9 +10335,7 @@ function activeSshTerminalTarget(webContentsId?: number) { return null } - const scope = route.connectionId - ? backendScopeKey(route.connectionId, profile) - : sshScopeKey(route.source === 'profile' ? profile : null) + const scope = v1SshTerminalPoolKey(route, profile) const state = sshConnections.get(scope)