|
| 1 | +import { Router } from '../pmxt/router'; |
| 2 | + |
| 3 | +interface CapturedFetch { |
| 4 | + url: string; |
| 5 | + init?: RequestInit; |
| 6 | +} |
| 7 | + |
| 8 | +function installFetchSpy( |
| 9 | + handler: (req: CapturedFetch, callIndex: number) => Promise<Response> | Response, |
| 10 | +): jest.SpyInstance { |
| 11 | + const captured: CapturedFetch[] = []; |
| 12 | + const spy = jest.spyOn(global, 'fetch').mockImplementation(async (input, init) => { |
| 13 | + const url = typeof input === 'string' ? input : (input as URL | Request).toString(); |
| 14 | + const req: CapturedFetch = { url, init }; |
| 15 | + captured.push(req); |
| 16 | + return handler(req, captured.length); |
| 17 | + }); |
| 18 | + (spy as unknown as { captured: CapturedFetch[] }).captured = captured; |
| 19 | + return spy; |
| 20 | +} |
| 21 | + |
| 22 | +function captured(spy: jest.SpyInstance): CapturedFetch[] { |
| 23 | + return (spy as unknown as { captured: CapturedFetch[] }).captured; |
| 24 | +} |
| 25 | + |
| 26 | +function jsonResponse(payload: unknown, status = 200): Response { |
| 27 | + return new Response(JSON.stringify(payload), { |
| 28 | + status, |
| 29 | + headers: { 'Content-Type': 'application/json' }, |
| 30 | + }); |
| 31 | +} |
| 32 | + |
| 33 | +class DynamicResolveBaseRouter extends Router { |
| 34 | + public baseUrls: string[] = []; |
| 35 | + private _baseCallCount = 0; |
| 36 | + |
| 37 | + public override resolveBaseUrl(): string { |
| 38 | + return this.baseUrls[this._baseCallCount++] |
| 39 | + ?? this.baseUrls[this.baseUrls.length - 1] |
| 40 | + ?? super.resolveBaseUrl(); |
| 41 | + } |
| 42 | +} |
| 43 | + |
| 44 | +afterEach(() => { |
| 45 | + jest.restoreAllMocks(); |
| 46 | +}); |
| 47 | + |
| 48 | +describe('Router.compareMarketPrices transport contracts', () => { |
| 49 | + it('resolves base URL per invocation before dispatching sidecar reads', async () => { |
| 50 | + const spy = installFetchSpy(() => jsonResponse({ success: true, data: [] })); |
| 51 | + const router = new DynamicResolveBaseRouter({ autoStartServer: false }); |
| 52 | + router.baseUrls = ['http://localhost:4011', 'http://localhost:4012']; |
| 53 | + |
| 54 | + await router.compareMarketPrices({ marketId: 'mkt-1' }); |
| 55 | + await router.compareMarketPrices({ marketId: 'mkt-1' }); |
| 56 | + |
| 57 | + const reqs = captured(spy); |
| 58 | + expect(reqs).toHaveLength(2); |
| 59 | + expect(reqs[0].url).toBe('http://localhost:4011/api/router/compareMarketPrices?marketId=mkt-1'); |
| 60 | + expect(reqs[0].init?.method).toBe('GET'); |
| 61 | + expect(reqs[1].url).toBe('http://localhost:4012/api/router/compareMarketPrices?marketId=mkt-1'); |
| 62 | + expect(reqs[1].init?.method).toBe('GET'); |
| 63 | + }); |
| 64 | + |
| 65 | + it('retries once after a connection error before returning a result', async () => { |
| 66 | + const router = new DynamicResolveBaseRouter({ autoStartServer: false }); |
| 67 | + router.baseUrls = ['http://localhost:4013']; |
| 68 | + const ensureServer = jest |
| 69 | + .spyOn((router as any).serverManager, 'ensureServerRunning') |
| 70 | + .mockResolvedValue(undefined); |
| 71 | + |
| 72 | + let calls = 0; |
| 73 | + const spy = installFetchSpy(() => { |
| 74 | + calls += 1; |
| 75 | + if (calls === 1) { |
| 76 | + throw new Error('ECONNREFUSED'); |
| 77 | + } |
| 78 | + return jsonResponse({ success: true, data: [] }); |
| 79 | + }); |
| 80 | + |
| 81 | + await router.compareMarketPrices({ marketId: 'mkt-2' }); |
| 82 | + |
| 83 | + const reqs = captured(spy); |
| 84 | + expect(reqs).toHaveLength(2); |
| 85 | + expect(reqs[0].init?.method).toBe('GET'); |
| 86 | + expect(reqs[1].init?.method).toBe('GET'); |
| 87 | + expect(ensureServer).toHaveBeenCalledTimes(1); |
| 88 | + }); |
| 89 | + |
| 90 | + it('falls back to POST on 405 with exact args payload and preserves nested market mapping', async () => { |
| 91 | + const router = new DynamicResolveBaseRouter({ autoStartServer: false }); |
| 92 | + router.baseUrls = ['http://localhost:4014']; |
| 93 | + |
| 94 | + const spy = installFetchSpy((_req, callIndex) => { |
| 95 | + if (callIndex === 1) { |
| 96 | + return jsonResponse({ success: false, error: 'GET unsupported' }, 405); |
| 97 | + } |
| 98 | + return jsonResponse({ |
| 99 | + success: true, |
| 100 | + data: [ |
| 101 | + { |
| 102 | + market: { |
| 103 | + marketId: 'mkt-3', |
| 104 | + sourceExchange: 'polymarket', |
| 105 | + bestBid: 0.24, |
| 106 | + bestAsk: 0.76, |
| 107 | + }, |
| 108 | + confidence: 0.91, |
| 109 | + relation: 'identity', |
| 110 | + reasoning: 'identity match', |
| 111 | + }, |
| 112 | + ], |
| 113 | + }); |
| 114 | + }); |
| 115 | + |
| 116 | + const out = await router.compareMarketPrices({ marketId: 'mkt-3' }); |
| 117 | + const reqs = captured(spy); |
| 118 | + |
| 119 | + expect(reqs).toHaveLength(2); |
| 120 | + expect(reqs[0].url).toBe('http://localhost:4014/api/router/compareMarketPrices?marketId=mkt-3'); |
| 121 | + expect(reqs[0].init?.method).toBe('GET'); |
| 122 | + expect(reqs[1].url).toBe('http://localhost:4014/api/router/compareMarketPrices'); |
| 123 | + expect(reqs[1].init?.method).toBe('POST'); |
| 124 | + const body = JSON.parse((reqs[1].init?.body as string) ?? '{}'); |
| 125 | + expect(body).toEqual({ args: [{ marketId: 'mkt-3' }] }); |
| 126 | + |
| 127 | + expect(out).toHaveLength(1); |
| 128 | + const row = out[0] as any; |
| 129 | + expect(row.market.sourceExchange).toBe('polymarket'); |
| 130 | + expect(row.bestBid).toBe(0.24); |
| 131 | + expect(row.bestAsk).toBe(0.76); |
| 132 | + expect(row.venue).toBe('polymarket'); |
| 133 | + expect(row.relation).toBe('identity'); |
| 134 | + expect(row.confidence).toBe(0.91); |
| 135 | + expect(row.reasoning).toBe('identity match'); |
| 136 | + }); |
| 137 | +}); |
0 commit comments