diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index a4ae90e9cd..887f430ee9 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -12744,6 +12744,11 @@ async function stopPoolBackend(profile: string) { const entry = backendPool.get(profile) await poolStopper.stop(profile) releaseLocalBackendSlot(entry) + // Remote / SSH-isolated pool entries keep `process: null`. Evicting the + // descriptor alone leaves sshConnections + the keep-alive WS armed, so + // idle-reaper / LRU retirement would pin the tunnel indefinitely (#106935). + await sshBootstrapCoordinator.cancelAndWait(profile) + await teardownSshConnection(profile) } async function teardownPoolBackendAndWait(profile) { diff --git a/apps/desktop/electron/ssh-isolated-keepalive.test.ts b/apps/desktop/electron/ssh-isolated-keepalive.test.ts index 963a6762c8..6f931912e4 100644 --- a/apps/desktop/electron/ssh-isolated-keepalive.test.ts +++ b/apps/desktop/electron/ssh-isolated-keepalive.test.ts @@ -70,6 +70,29 @@ describe('main.ts wiring for SSH-isolated keep-alive WS (#106935)', () => { expect(body).toMatch(/sshIsolatedKeepalives\.stop\(\s*scope/) }) + it('stopPoolBackend tears down the matching SSH lifecycle (idle-reaper / LRU)', () => { + const fnStart = mainSource.indexOf('async function stopPoolBackend(') + expect(fnStart).toBeGreaterThan(-1) + + const nextFn = mainSource.indexOf('\nasync function teardownPoolBackendAndWait(', fnStart + 1) + const body = mainSource.slice(fnStart, nextFn === -1 ? fnStart + 400 : nextFn) + + expect(body).toMatch(/sshBootstrapCoordinator\.cancelAndWait\(\s*profile/) + expect(body).toMatch(/teardownSshConnection\(\s*profile/) + }) + + it('idle reaper retires descriptors only through stopPoolBackend', () => { + const fnStart = mainSource.indexOf('function startPoolIdleReaper(') + expect(fnStart).toBeGreaterThan(-1) + + const nextFn = mainSource.indexOf('\nfunction releaseLocalBackendSlot(', fnStart + 1) + const body = mainSource.slice(fnStart, nextFn === -1 ? fnStart + 800 : nextFn) + + expect(body).toContain('stopPoolBackend(profile)') + expect(body).not.toMatch(/backendPool\.delete/) + expect(body).not.toMatch(/sshConnections\.delete/) + }) + it('imports the keep-alive registry from the electron helper (not inline in main)', () => { expect(mainSource).toMatch(/createSshIsolatedKeepaliveRegistry/) expect(mainSource).toMatch(/from '\.\/ssh-isolated-keepalive'/)