diff --git a/app/src/services/__tests__/socketService.test.ts b/app/src/services/__tests__/socketService.test.ts index 462356a5e..352ae0126 100644 --- a/app/src/services/__tests__/socketService.test.ts +++ b/app/src/services/__tests__/socketService.test.ts @@ -275,6 +275,25 @@ describe('socketService — resolveCoreSocketBaseUrl uses getCoreRpcUrl', () => ); expect(latestSocket.once).not.toHaveBeenCalledWith('queued-on-event', expect.any(Function)); }); + + it('reconnects when a stale disconnected socket exists for the same token', async () => { + const { io } = await import('socket.io-client'); + const ioMock = vi.mocked(io); + ioMock.mockClear(); + + hoisted.getCoreRpcUrlMock.mockResolvedValue('http://127.0.0.1:7788/rpc'); + + const { socketService } = await import('../socketService'); + socketService.disconnect(); + + socketService.connect('mock-jwt-stale-socket'); + await pollUntil(() => expect(ioMock).toHaveBeenCalledTimes(1)); + + // Same token, previous socket is disconnected=true in our mock. + // We should still create a fresh socket instead of returning early. + socketService.connect('mock-jwt-stale-socket'); + await pollUntil(() => expect(ioMock).toHaveBeenCalledTimes(2)); + }); }); describe('socketService — connectivity dispatch on socket events (lines 164, 212, 230, 237, 240)', () => { diff --git a/app/src/services/socketService.ts b/app/src/services/socketService.ts index 57cde7af1..88e72b009 100644 --- a/app/src/services/socketService.ts +++ b/app/src/services/socketService.ts @@ -161,6 +161,13 @@ class SocketService { } else if (!this.socket.disconnected) { // Socket is connecting, wait for it return; + } else { + // Stale disconnected socket instance for the same token. + // Drop it so this connect attempt can create a fresh socket; + // otherwise the async stale-invocation guard below (`|| this.socket`) + // returns early and leaves connectivity stuck at "connecting". + this.socket = null; + this.mcpTransport = null; } }