1import { afterEach, describe, expect, it, vi } from 'vitest'; 2import { ATTEMPT_HEADER, askJev, BUDGET_HEADER, LEAD_HEADER, onBudget, resetBackoff } from './ask.mjs'; 3 4const answer = (body, status = 200) => new Response(JSON.stringify(body), { status }); 5 6describe('askJev', () => { 7 afterEach(() => { 8 vi.useRealTimers(); 9 vi.unstubAllGlobals(); 10 }); 11 12 it('asks again after a dropped connection or a 5xx, and gives up after two retries', async () => { 13 vi.useFakeTimers(); 14 const fetch = vi 15 .fn() 16 .mockRejectedValueOnce(new TypeError('Failed to fetch')) 17 .mockResolvedValueOnce(new Response('relay down', { status: 500 })) 18 .mockResolvedValueOnce(answer({ answers: { move: { choice: 'a' } } })); 19 vi.stubGlobal('fetch', fetch); 20 const asked = askJev({}, {}); 21 await vi.runAllTimersAsync(); 22 await expect(asked).resolves.toEqual({ move: { choice: 'a' } }); 23 expect(fetch).toHaveBeenCalledTimes(3); 24 25 const down = vi.fn().mockResolvedValue(new Response('relay down', { status: 502 })); 26 vi.stubGlobal('fetch', down); 27 const failed = askJev({}, {}).catch((e) => e.message); 28 await vi.runAllTimersAsync(); 29 expect(await failed).toMatch(/relay answered 502/); 30 expect(down).toHaveBeenCalledTimes(3); 31 }); 32 33 it("tells the relay each attempt's number and how far ahead of its section it went out", async () => { 34 vi.useFakeTimers(); 35 const fetch = vi 36 .fn() 37 .mockResolvedValueOnce(new Response('relay down', { status: 500 })) 38 .mockResolvedValueOnce(answer({ answers: {} })); 39 vi.stubGlobal('fetch', fetch); 40 const leads = [5.678, 4.5]; 41 const asked = askJev({}, {}, { secondsToSection: () => leads.shift() }); 42 await vi.runAllTimersAsync(); 43 await asked; 44 const headers = fetch.mock.calls.map(([, init]) => init.headers); 45 expect(headers[0]).toMatchObject({ [LEAD_HEADER]: '5.68', [ATTEMPT_HEADER]: '0' }); 46 expect(headers[1]).toMatchObject({ [LEAD_HEADER]: '4.50', [ATTEMPT_HEADER]: '1' });
not playing (or no timing at all): no lead header
56 it("reports a signed-in listener's budget, and says so when it is spent, until midnight or a sign-out", async () => { 57 const seen = []; 58 const stop = onBudget((v) => seen.push(v)); 59 const fetch = vi 60 .fn() 61 .mockResolvedValueOnce(new Response(JSON.stringify({ answers: {} }), { headers: { [BUDGET_HEADER]: '7/10' } })) 62 .mockResolvedValueOnce( 63 new Response('your daily Jev budget (10 calls) is spent', { 64 status: 429, 65 headers: { [BUDGET_HEADER]: 'spent', 'Retry-After': '3600' }, 66 }), 67 ) 68 .mockResolvedValue(answer({ answers: {} })); 69 vi.stubGlobal('fetch', fetch); 70 await askJev({}, {}); 71 await expect(askJev({}, {})).rejects.toThrow(/daily Jev budget is spent; it resets at 00:00 UTC/); 72 // backing off until then, without asking 73 await expect(askJev({}, {})).rejects.toThrow(/daily Jev budget is spent/); 74 expect(fetch).toHaveBeenCalledTimes(2); 75 expect(seen).toEqual(['7/10', 'spent']); 76 // signing out (or in as someone else) lifts it 77 resetBackoff(); 78 await expect(askJev({}, {})).resolves.toEqual({}); 79 stop(); 80 }); 81});