diff --git a/apps/desktop/electron/active-ssh-route.test.ts b/apps/desktop/electron/active-ssh-route.test.ts deleted file mode 100644 index 0a342a2d67..0000000000 --- a/apps/desktop/electron/active-ssh-route.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import assert from 'node:assert/strict' - -import { test } from 'vitest' - -import { normalizeRegistry, REGISTRY_VERSION } from './connection-registry' -import { activeRegistrySshScope } from './active-ssh-route' - -function registry(lastUsed: string) { - return normalizeRegistry({ - version: REGISTRY_VERSION, - primary: 'local', - lastUsed, - connections: [ - { id: 'local', kind: 'local', label: 'This device' }, - { id: 'remote', kind: 'remote', label: 'Remote', url: 'https://gateway.test' }, - { id: 'ssh-box', kind: 'ssh', label: 'SSH box', host: 'box.test', user: 'hermes' } - ] - }) -} - -test('returns the v2 SSH pool scope for the last-used registry source', () => { - assert.equal(activeRegistrySshScope(registry('ssh-box'), 'default'), 'conn:ssh-box::default') -}) - -test('keeps the active profile in the v2 SSH pool scope', () => { - assert.equal(activeRegistrySshScope(registry('ssh-box'), 'worker'), 'conn:ssh-box::worker') -}) - -test('returns null for local and URL-backed registry sources', () => { - assert.equal(activeRegistrySshScope(registry('local'), 'default'), null) - assert.equal(activeRegistrySshScope(registry('remote'), 'default'), null) -}) diff --git a/apps/desktop/electron/active-ssh-route.ts b/apps/desktop/electron/active-ssh-route.ts deleted file mode 100644 index 2351a959f1..0000000000 --- a/apps/desktop/electron/active-ssh-route.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { backendScopeKey, type ConnectionRegistry } from './connection-registry' - -/** - * Return the live SSH pool scope for the registry connection currently used by - * the Sessions workspace. Non-SSH sources deliberately return null so callers - * can preserve the legacy v1 routing fallback. - */ -export function activeRegistrySshScope( - registry: ConnectionRegistry, - profile: null | string | undefined -): null | string { - const connection = registry.connections.find(candidate => candidate.id === registry.lastUsed) - - if (!connection || connection.kind !== 'ssh') { - return null - } - - return backendScopeKey(connection.id, profile) -} diff --git a/apps/desktop/electron/desktop-remote-route.test.ts b/apps/desktop/electron/desktop-remote-route.test.ts index a7a11e67a6..ce02163417 100644 --- a/apps/desktop/electron/desktop-remote-route.test.ts +++ b/apps/desktop/electron/desktop-remote-route.test.ts @@ -232,6 +232,77 @@ test('URL route fails closed for different token, headers, kind, or Cloud org', } }) + +test('profile remote wins over a registry-backed global SSH route', () => { + const route = resolveDesktopRemoteRoute({ + config: { + mode: 'ssh', + remote: { mode: 'ssh', host: 'global-box.test', user: 'hermes' }, + profiles: { + worker: { mode: 'remote', url: 'https://worker.test', authMode: 'token', token: tokenA } + } + }, + profile: 'worker', + registry: registry('global-ssh', [ + { id: 'global-ssh', kind: 'ssh', label: 'Global SSH', host: 'global-box.test', user: 'hermes' }, + { id: 'worker-remote', kind: 'remote', label: 'Worker', url: 'https://worker.test', token: tokenA } + ]) + }) + + assert.equal(route?.kind, 'remote') + assert.equal(route?.source, 'profile') + assert.equal(route?.connectionId, 'worker-remote') +}) + +test('profile SSH wins over a different registry primary SSH route', () => { + const route = resolveDesktopRemoteRoute({ + config: { + mode: 'ssh', + remote: { mode: 'ssh', host: 'global-box.test', user: 'hermes' }, + profiles: { + worker: { mode: 'ssh', host: 'worker-box.test', user: 'hermes' } + } + }, + profile: 'worker', + registry: registry('global-ssh', [ + { id: 'global-ssh', kind: 'ssh', label: 'Global SSH', host: 'global-box.test', user: 'hermes' }, + { id: 'worker-ssh', kind: 'ssh', label: 'Worker SSH', host: 'worker-box.test', user: 'hermes' } + ]) + }) + + assert.equal(route?.kind, 'ssh') + assert.equal(route?.source, 'profile') + assert.equal(route?.connectionId, 'worker-ssh') +}) + +test('environment remote wins over a registry-backed global SSH route', () => { + const route = resolveDesktopRemoteRoute({ + config: { + mode: 'ssh', + remote: { mode: 'ssh', host: 'global-box.test', user: 'hermes' } + }, + env: { url: 'https://env.test', token: 'env-token' }, + registry: registry('global-ssh', [ + { id: 'global-ssh', kind: 'ssh', label: 'Global SSH', host: 'global-box.test', user: 'hermes' } + ]) + }) + + assert.equal(route?.kind, 'remote') + assert.equal(route?.source, 'env') + assert.equal(route?.connectionId, undefined) +}) + +test('local route does not inherit an unrelated registry SSH connection', () => { + const route = resolveDesktopRemoteRoute({ + config: { mode: 'local' }, + registry: registry('local', [ + { id: 'unused-ssh', kind: 'ssh', label: 'Unused SSH', host: 'box.test', user: 'hermes' } + ]) + }) + + assert.equal(route, null) +}) + test('local config without overrides returns null', () => { assert.equal(resolveDesktopRemoteRoute({ config: { mode: 'local' }, registry: registry('local', []) }), null) }) diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index b4d34eb356..949096457e 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -144,7 +144,6 @@ import { updateEligibility, upsertConnection } from './connection-registry' -import { activeRegistrySshScope } from './active-ssh-route' import { describeCrashReason, installCrashForensics } from './crash-forensics' import { adoptServedDashboardToken } from './dashboard-token' import { loadOrCreateInstallationId, sshOwnershipId } from './desktop-installation' @@ -9521,35 +9520,26 @@ async function teardownSshConnection(profile) { function activeSshTerminalTarget() { const profile = primaryProfileKey() const config = readDesktopConnectionConfig() - const registryScope = activeRegistrySshScope(readDesktopConnectionsRegistry(), profile) + const route = resolveDesktopRemoteRoute({ + config, + env: { + token: process.env.HERMES_DESKTOP_REMOTE_TOKEN, + url: process.env.HERMES_DESKTOP_REMOTE_URL + }, + profile, + registry: readDesktopConnectionsRegistry() + }) - if (registryScope) { - const state = sshConnections.get(registryScope) - - return state && state.ssh ? { ssh: state.ssh, scope: registryScope } : 'pending' - } - - if (profileSshOverride(config, profile)) { - const scope = sshScopeKey(profile) - const state = sshConnections.get(scope) - - return state && state.ssh ? { ssh: state.ssh, scope } : 'pending' - } - - if (profileRemoteOverride(config, profile)) { + if (!route || route.kind !== 'ssh') { return null } - if (process.env.HERMES_DESKTOP_REMOTE_URL) { - return null - } + const scope = route.connectionId + ? backendScopeKey(route.connectionId, profile) + : sshScopeKey(route.source === 'profile' ? profile : null) + const state = sshConnections.get(scope) - if (config.mode === 'ssh') { - const state = sshConnections.get('') - return state && state.ssh ? { ssh: state.ssh, scope: '' } : 'pending' - } - - return null + return state && state.ssh ? { ssh: state.ssh, scope } : 'pending' } // Loopback reach for the browser pane. Scoped to the SSH connection that diff --git a/apps/desktop/electron/terminal-ipc.test.ts b/apps/desktop/electron/terminal-ipc.test.ts new file mode 100644 index 0000000000..75fb7e5dd8 --- /dev/null +++ b/apps/desktop/electron/terminal-ipc.test.ts @@ -0,0 +1,60 @@ +import assert from 'node:assert/strict' + +import { test } from 'vitest' + +import { resolveTerminalConnection } from './connection-apply' + +const ssh = { + host: 'registry-box.test', + user: 'hermes' +} + +test('terminal start preserves the selected SSH target and scope', async () => { + const target = { + ssh, + scope: 'connection:registry-ssh:profile:worker' + } + + const resolved = await resolveTerminalConnection( + () => target, + async () => { + throw new Error('backend fallback must not run for an active SSH target') + } + ) + + assert.equal(resolved, target) + assert.equal(resolved?.ssh, ssh) + assert.equal(resolved?.scope, 'connection:registry-ssh:profile:worker') +}) + +test('terminal start does not invent SSH when canonical routing selects local or remote HTTP', async () => { + let backendChecks = 0 + + const resolved = await resolveTerminalConnection( + () => null, + async () => { + backendChecks += 1 + } + ) + + assert.equal(resolved, null) + assert.equal(backendChecks, 0) +}) + +test('terminal start re-reads the SSH target after backend startup', async () => { + const target = { + ssh, + scope: 'connection:registry-ssh' + } + let ready = false + + const resolved = await resolveTerminalConnection( + () => (ready ? target : 'pending'), + async () => { + ready = true + } + ) + + assert.equal(resolved, target) + assert.equal(resolved?.scope, 'connection:registry-ssh') +})