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)