import test from 'node:test'; import assert from 'node:assert/strict'; import { startSolverRelayWs } from '../src/venues/near-intents/solver-relay-ws.mjs'; function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function installMockWebSocket() { const original = globalThis.WebSocket; const instances = []; class MockWebSocket { static CONNECTING = 0; static OPEN = 1; static CLOSING = 2; static CLOSED = 3; constructor(url, options) { this.url = url; this.options = options; this.readyState = MockWebSocket.CONNECTING; this.sent = []; this.listeners = new Map(); this.closeCalls = 0; this.emitErrorDuringClose = false; instances.push(this); } addEventListener(type, listener) { const existing = this.listeners.get(type) || []; existing.push(listener); this.listeners.set(type, existing); } send(payload) { this.sent.push(payload); } close() { this.closeCalls += 1; if (this.readyState === MockWebSocket.CLOSED) return; if (this.emitErrorDuringClose) { this.emit('error', new Error('socket failed while closing')); } this.readyState = MockWebSocket.CLOSED; this.emit('close', {}); } open() { this.readyState = MockWebSocket.OPEN; this.emit('open', {}); } error(error = new Error('socket failed')) { this.emit('error', error); } message(payload) { this.emit('message', { data: JSON.stringify(payload) }); } emit(type, event) { for (const listener of this.listeners.get(type) || []) listener(event); } } globalThis.WebSocket = MockWebSocket; return { instances, restore() { globalThis.WebSocket = original; }, }; } test('solver relay request timeout includes time spent waiting for websocket connection', async () => { const mock = installMockWebSocket(); const client = await startSolverRelayWs({ apiKey: 'api-key', wsUrl: 'wss://relay.example/ws', reconnectDelayMs: 1000, }); try { await assert.rejects( client.request('quote_response', [], { timeoutMs: 5 }), /quote_response timed out/, ); assert.equal(mock.instances[0].sent.length, 0); } finally { client.close(); mock.restore(); } }); test('solver relay reconnects after websocket error even when no close event is emitted by the runtime', async () => { const mock = installMockWebSocket(); const client = await startSolverRelayWs({ apiKey: 'api-key', wsUrl: 'wss://relay.example/ws', reconnectDelayMs: 1, }); try { assert.equal(mock.instances.length, 1); mock.instances[0].error(new Error('network failed')); await delay(10); assert.equal(client.getState().connected, false); assert.ok(client.getState().reconnect_count >= 2); assert.ok(mock.instances.length >= 2); } finally { client.close(); mock.restore(); } }); test('solver relay websocket close is reentrant-safe when close emits an error', async () => { const mock = installMockWebSocket(); const client = await startSolverRelayWs({ apiKey: 'api-key', wsUrl: 'wss://relay.example/ws', reconnectDelayMs: 1, }); try { assert.equal(mock.instances.length, 1); mock.instances[0].open(); mock.instances[0].emitErrorDuringClose = true; assert.doesNotThrow(() => { mock.instances[0].error(new Error('network failed')); }); await delay(10); assert.equal(mock.instances[0].closeCalls, 1); assert.equal(client.getState().connected, false); assert.ok(client.getState().reconnect_count >= 2); assert.ok(mock.instances.length >= 2); } finally { client.close(); mock.restore(); } }); test('solver relay sends queued request after connection and resolves rpc result', async () => { const mock = installMockWebSocket(); const client = await startSolverRelayWs({ apiKey: 'api-key', wsUrl: 'wss://relay.example/ws', reconnectDelayMs: 1000, subscriptions: ['quote_status'], }); try { const responsePromise = client.request('quote_response', [{ quote_id: 'quote-1' }], { timeoutMs: 50 }); mock.instances[0].open(); assert.equal(mock.instances[0].sent.length, 2); const sent = mock.instances[0].sent.map((entry) => JSON.parse(entry)); const request = sent.find((entry) => entry.method === 'quote_response'); assert.ok(request); mock.instances[0].message({ id: request.id, result: 'OK' }); await assert.doesNotReject(responsePromise); assert.equal(await responsePromise, 'OK'); } finally { client.close(); mock.restore(); } });