fix(desktop): don't dial per-profile sockets for profiles served by the shared global-remote primary (#85665)

Under a global SSH/remote gateway, resolveProfileBackendRoute routes every
profile to the shared primary backend (case 3) and getConnection() returns
the primary descriptor tagged with the profile. ensureGatewayForProfile
still dialed a per-profile secondary socket at that descriptor; over SSH
the duplicate dial fails (per-backend tunnel/ticket) and the closed socket
became the ACTIVE gateway — every profile except the primary showed
'Hermes gateway is not connected' even though the primary socket was open.

Detect the shared-primary route and activate the primary socket instead;
$activeGatewayProfile still tracks the selected profile so per-request
?profile= scoping is unchanged. Hover pre-warm no-ops on this route.
Local pooled profiles and per-profile remote overrides are untouched
(pinned by test).
This commit is contained in:
Teknium
2026-08-13 15:06:00 -07:00
committed by GitHub
parent a3cda34137
commit d16e2366df
2 changed files with 117 additions and 0 deletions
@@ -0,0 +1,78 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
// The global-remote share (backend routing case 3): every profile is served
// by the PRIMARY backend over one host, and getConnection() tags the shared
// descriptor with `profile`. Dialing a second WebSocket at that descriptor
// used to fail over SSH (per-backend tunnel/ticket) and poison the active
// gateway with a closed socket — "Hermes gateway is not connected" for every
// profile except the primary. These tests pin the fix: a profile routed to
// the shared primary activates the primary socket instead of dialing.
vi.mock('@/hermes', () => ({
HermesGateway: class {
connectionState = 'closed'
connect = vi.fn(async () => {
throw new Error('dialed a socket for a shared-primary profile')
})
onEvent = vi.fn(() => () => {})
onState = vi.fn(() => () => {})
}
}))
vi.mock('@/store/session', () => ({ setGatewayState: vi.fn() }))
vi.mock('@/store/notify-baseline', () => ({ markNativeNotifyBaseline: vi.fn() }))
const { $gateway, configureGatewayRegistry, ensureGatewayForProfile, setPrimaryGateway } = await import('./gateway')
type DesktopStub = { getConnection: ReturnType<typeof vi.fn> }
function installDesktop(stub: DesktopStub): void {
;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = stub
}
function makePrimary(): { connectionState: string } {
// Only connectionState is consulted by setActive/isOpen for these paths.
return { connectionState: 'open' }
}
beforeEach(() => {
configureGatewayRegistry({
onEvent: vi.fn(),
primaryProfile: 'default'
} as never)
})
afterEach(() => {
vi.clearAllMocks()
delete (window as unknown as { hermesDesktop?: unknown }).hermesDesktop
})
describe('ensureGatewayForProfile under a shared global remote', () => {
it('activates the primary socket for a profile tagged onto the shared descriptor', async () => {
const primary = makePrimary()
setPrimaryGateway(primary as never, 'default')
installDesktop({
// Shared descriptor: primary connection tagged with the profile.
getConnection: vi.fn(async () => ({ port: 4242, profile: 'venture', token: 't' }))
})
await ensureGatewayForProfile('venture')
expect($gateway.get()).toBe(primary)
})
it('still pools a socket for profiles with their own descriptor (untagged)', async () => {
const primary = makePrimary()
setPrimaryGateway(primary as never, 'default')
installDesktop({
// Own descriptor: no profile tag → normal pooled path (dial attempted).
getConnection: vi.fn(async () => ({ port: 5151, token: 't2' }))
})
await ensureGatewayForProfile('worker')
// The pooled path dialed (our stub throws, so the socket stays closed and
// reconnect is scheduled) — the important part is it did NOT silently
// reuse the primary.
expect($gateway.get()).not.toBe(primary)
})
})
+39
View File
@@ -247,6 +247,29 @@ function createSecondary(profile: string): Secondary {
return entry
}
// True when `profile`'s backend route resolves to the SHARED primary backend
// (global-remote case 3 in resolveProfileBackendRoute): the descriptor comes
// back as the primary connection tagged with `profile`. Own-remote-override
// and local pooled descriptors are never tagged. Dialing a second socket at
// that descriptor is wrong — over SSH the second dial fails (tunnel/token are
// per-backend) and the closed socket poisons the active gateway with
// "not connected" even though the primary is open right next to it.
async function sharedPrimaryRoute(profile: string): Promise<boolean> {
const desktop = window.hermesDesktop
if (!desktop) {
return false
}
try {
const conn = await desktop.getConnection(profile)
return Boolean(conn && typeof conn === 'object' && (conn as { profile?: string }).profile)
} catch {
return false
}
}
// Open `profile`'s socket WITHOUT making it active — the hover-intent pre-warm
// (store/profile). Runs the same spawn + connect chain as a real switch, so by
// click time ensureGatewayForProfile finds an open socket and just activates
@@ -260,6 +283,11 @@ export async function openGatewayForProfile(profile: string): Promise<void> {
return
}
if (await sharedPrimaryRoute(key)) {
// Served by the primary backend — there is no per-profile socket to warm.
return
}
const entry = g.secondaries.get(key) ?? createSecondary(key)
entry.wantOpen = true
@@ -279,6 +307,17 @@ export async function ensureGatewayForProfile(profile: string): Promise<void> {
return
}
// Global-remote share (routing case 3): one remote host serves every
// profile through the PRIMARY socket, scoped per request. Activate the
// primary instead of dialing a doomed duplicate socket at the same
// descriptor — $activeGatewayProfile still moves to `key`, so request
// scoping and profile-aware surfaces behave identically.
if (await sharedPrimaryRoute(key)) {
setActive(g.primaryProfile)
return
}
let entry = g.secondaries.get(key)
if (!entry) {