import { trace } from '@opentelemetry/api'; import { describe, expect, it, vi } from 'vitest'; import { INTENTIONAL_CLOSE_CODE, ISocket, REQUEST_ABORTED_SOCKET_ERROR } from './socket.js'; import { ISocketWithClientAuth } from './socketWithClientAuth.js'; const PING_TIMEOUT_CLOSE_REASON = 'Ping timeout'; describe('ISocket', () => { it('clears abort cleanup when a no-response timeout rejects the request', async () => { vi.useFakeTimers(); try { const controller = new AbortController(); const addEventListener = vi.spyOn(controller.signal, 'addEventListener'); const removeEventListener = vi.spyOn(controller.signal, 'removeEventListener'); const ws = { addEventListener: vi.fn(), close: vi.fn(), send: vi.fn() }; const socket = new ISocket(ws as never, {}, [], { onClose: vi.fn(), getAbortSignal: () => controller.signal, timeouts: { noResponseTimeoutInSeconds: 1 } }); const pendingRequest = socket.request('editor.ping', {}); expect(addEventListener).toHaveBeenCalledWith('abort', expect.any(Function), { once: true }); const assertion = expect(pendingRequest).rejects.toMatchObject({ code: 7 }); await vi.advanceTimersByTimeAsync(1000); await assertion; expect(removeEventListener).toHaveBeenCalled(); } finally { vi.useRealTimers(); } }); it('rejects a pending request when getAbortSignal returns an aborted signal', async () => { const ws = { addEventListener: vi.fn(), close: vi.fn(), send: vi.fn() }; const controller = new AbortController(); const socket = new ISocket(ws as never, {}, [], { onClose: vi.fn(), getAbortSignal: () => controller.signal }); const pendingRequest = socket.request('editor.ping', {}); controller.abort(); await expect(pendingRequest).rejects.toMatchObject(REQUEST_ABORTED_SOCKET_ERROR); }); it('preserves the transport-close marker when rejecting pending requests with a reason', async () => { const ws = { addEventListener: vi.fn(), close: vi.fn(), send: vi.fn() }; const socket = new ISocket(ws as never, {}, [], { onClose: vi.fn() }); const pendingRequest = socket.request('editor.ping', {}); socket.closeAndRejectPendingRequests(PING_TIMEOUT_CLOSE_REASON); await expect(pendingRequest).rejects.toMatchObject({ code: 8, message: `[internal] Connection closed: ${PING_TIMEOUT_CLOSE_REASON}` }); expect(ws.close).toHaveBeenCalledWith(INTENTIONAL_CLOSE_CODE, PING_TIMEOUT_CLOSE_REASON); }); it('rejects a request whose signal is already aborted before it is sent', async () => { const ws = { addEventListener: vi.fn(), close: vi.fn(), send: vi.fn() }; const controller = new AbortController(); const socket = new ISocket(ws as never, {}, [], { onClose: vi.fn(), getAbortSignal: () => controller.signal }); controller.abort(); await expect(socket.request('editor.ping', {})).rejects.toMatchObject(REQUEST_ABORTED_SOCKET_ERROR); expect(ws.send).not.toHaveBeenCalled(); }); it('ignores a late response after an aborted request', async () => { let messageHandler: ((event: { data: string }) => void) | undefined; const ws = { addEventListener: vi.fn((event: string, handler: (event: { data: string }) => void) => { if (event === 'message') { messageHandler = handler; } }), close: vi.fn(), send: vi.fn() }; const controller = new AbortController(); const socket = new ISocket(ws as never, {}, [], { onClose: vi.fn(), getAbortSignal: () => controller.signal }); const pendingRequest = socket.request('editor.ping', {}); controller.abort(); await expect(pendingRequest).rejects.toMatchObject(REQUEST_ABORTED_SOCKET_ERROR); expect(() => { messageHandler?.({ data: JSON.stringify({ response: { id: 1, payload: 'too-late', error: null } }) }); }).not.toThrow(); }); }); describe('ISocketWithClientAuth', () => { it('still sends a refreshed token when an older in-flight request resolves after setAuthorization', async () => { let messageHandler: ((event: { data: string }) => void) | undefined; const sentMessages: Array<{ request: { id: number; setAuthorization?: string }; }> = []; const ws = { addEventListener: vi.fn((event: string, handler: (event: { data: string }) => void) => { if (event === 'message') { messageHandler = handler; } }), close: vi.fn(), send: vi.fn((message: string) => { sentMessages.push(JSON.parse(message)); }) }; const socket = new ISocketWithClientAuth(ws as never, 'Bearer old-token', {}, [], trace.getTracer('test'), { onClose: vi.fn() }); const firstRequest = socket.request('editor.ping', {}); expect(sentMessages[0].request.setAuthorization).toBe('Bearer old-token'); socket.setAuthorization('Bearer new-token'); messageHandler?.({ data: JSON.stringify({ response: { id: 1, payload: undefined, error: null } }) }); await firstRequest; const secondRequest = socket.request('editor.ping', {}); expect(sentMessages[1].request.setAuthorization).toBe('Bearer new-token'); messageHandler?.({ data: JSON.stringify({ response: { id: 2, payload: undefined, error: null } }) }); await secondRequest; }); });