import { beforeEach, describe, expect, mock, test } from 'bun:test'; type ConfigResponse = { data: Record }; (mock as unknown as { restore?: () => void }).restore?.(); const configResolvers: Array<(response: ConfigResponse) => void> = []; let configCalls = 0; const promptAsyncCalls: unknown[][] = []; const promptAsyncResults: Array = []; const promptAsyncMock = mock(async (...args: unknown[]) => { promptAsyncCalls.push(args); const next = promptAsyncResults.shift(); if (next instanceof Error) throw next; return next ?? { response: new Response(null, { status: 200 }) }; }); mock.module('@opencode-ai/sdk/v2', () => ({ createOpencodeClient: mock(() => ({ config: { get: mock(() => { configCalls += 1; return new Promise((resolve) => { configResolvers.push(resolve); }); }), }, session: { promptAsync: promptAsyncMock, }, })), })); mock.module('@/contexts/runtimeAPIRegistry', () => ({ getRegisteredRuntimeAPIs: mock(() => null), })); mock.module('@/lib/runtime-url', () => ({ getRuntimeUrlResolver: mock(() => ({ api: (path: string) => path, })), })); mock.module('@/lib/runtime-switch', () => ({ getRuntimeApiBaseUrl: mock(() => ''), getRuntimeKey: mock(() => 'test-runtime'), })); mock.module('@/lib/runtime-fetch', () => ({ runtimeFetch: mock(async () => new Response(JSON.stringify([]), { headers: { 'Content-Type': 'application/json' }, })), })); mock.module('@/lib/startupTrace', () => ({ markStartupTrace: mock(() => undefined), })); const { opencodeClient } = await import(`./client?cache-test=${Date.now()}`); beforeEach(() => { promptAsyncCalls.length = 0; promptAsyncResults.length = 0; }); describe('opencodeClient getConfig cache', () => { test('cleared stale in-flight requests do not repopulate cache or delete newer in-flight requests', async () => { const first = opencodeClient.getConfig('/workspace/project'); expect(configCalls).toBe(1); opencodeClient.clearConfigCache(); const second = opencodeClient.getConfig('/workspace/project'); expect(configCalls).toBe(2); configResolvers[0]?.({ data: { model: 'old/model' } }); expect(await first).toEqual({ model: 'old/model' }); const third = opencodeClient.getConfig('/workspace/project'); expect(configCalls).toBe(2); configResolvers[1]?.({ data: { model: 'new/model' } }); expect(await second).toEqual({ model: 'new/model' }); expect(await third).toEqual({ model: 'new/model' }); const cached = await opencodeClient.getConfig('/workspace/project'); expect(cached).toEqual({ model: 'new/model' }); expect(configCalls).toBe(2); }); }); describe('opencodeClient prompt retry behavior', () => { const sendPrompt = (providerID = 'anthropic') => opencodeClient.sendMessage({ id: 'ses_1', providerID, modelID: 'claude-sonnet', text: 'hello', }); test('does not retry 504 prompt responses because the POST may already be accepted', async () => { promptAsyncResults.push({ response: new Response('gateway timeout', { status: 504 }) }); let error: unknown = null; try { await sendPrompt('anthropic-504'); } catch (caught) { error = caught; } expect(promptAsyncCalls.length).toBe(1); expect(error instanceof Error ? error.message : String(error)).toContain('Failed to send message (504)'); }); test('does not retry transport failures because the tunnel may have lost only the response', async () => { promptAsyncResults.push(new TypeError('Failed to fetch')); let error: unknown = null; try { await sendPrompt('anthropic-network'); } catch (caught) { error = caught; } expect(promptAsyncCalls.length).toBe(1); expect(error instanceof Error ? error.message : String(error)).toContain('Failed to fetch'); }); test('does not retry 503 prompt responses because proxy errors can be ambiguous too', async () => { promptAsyncResults.push({ response: new Response('starting', { status: 503 }) }); let error: unknown = null; try { await sendPrompt('anthropic-503'); } catch (caught) { error = caught; } expect(promptAsyncCalls.length).toBe(1); expect(error instanceof Error ? error.message : String(error)).toContain('Failed to send message (503)'); }); });