import * as assert from 'assert'; import { OpenAICompatibleProvider } from '../src/ai/providers/openai-compatible'; import { GeminiProvider } from '../src/ai/providers/gemini'; import { ClaudeProvider } from '../src/ai/providers/claude'; import { EmptyContentError } from '../src/ai/providers/base'; const opts = { model: 'm', temperature: 0.2, maxTokens: 100, timeoutMs: 5000 }; function makeResponse(ok: boolean, status: number, jsonData?: unknown, text?: string): Response { return { ok, status, json: async () => jsonData, text: async () => text ?? '', } as Response; } suite('Provider Chat Tests', () => { const origFetch = (globalThis as any).fetch; let calls: Array<{ url: string; init: RequestInit }>; setup(() => { calls = []; }); teardown(() => { (globalThis as any).fetch = origFetch; }); function stubFetch(fn: (url: string, init: RequestInit) => Response): void { (globalThis as any).fetch = async (url: string, init: RequestInit) => { calls.push({ url, init }); return fn(url, init); }; } test('openai chat returns content with bearer auth', async () => { stubFetch(() => makeResponse(true, 200, { choices: [{ message: { content: 'FIX' }, finish_reason: 'stop' }] })); const p = new OpenAICompatibleProvider('key', 'https://api.x.test/v1', 'deepseek', 'DeepSeek'); assert.strictEqual(await p.chat('sys', 'user', opts), 'FIX'); assert.match(calls[0].url, /\/chat\/completions$/); assert.strictEqual((calls[0].init.headers as Record).Authorization, 'Bearer key'); }); test('openai 401 throws invalid key error', async () => { stubFetch(() => makeResponse(false, 401, undefined, 'bad key')); const p = new OpenAICompatibleProvider('key', 'https://api.x.test/v1', 'deepseek', 'DeepSeek'); await assert.rejects(() => p.chat('s', 'u', opts)); }); test('openai truncation raises EmptyContentError', async () => { stubFetch(() => makeResponse(true, 200, { choices: [{ message: { content: ' ' }, finish_reason: 'length' }] })); const p = new OpenAICompatibleProvider('key', 'https://api.x.test/v1', 'deepseek', 'DeepSeek'); await assert.rejects(() => p.chat('s', 'u', opts), EmptyContentError); }); test('openai empty content raises EmptyContentError', async () => { stubFetch(() => makeResponse(true, 200, { choices: [], error: { message: 'nope' } })); const p = new OpenAICompatibleProvider('key', 'https://api.x.test/v1', 'deepseek', 'DeepSeek'); await assert.rejects(() => p.chat('s', 'u', opts), EmptyContentError); }); test('openai other status throws', async () => { stubFetch(() => makeResponse(false, 500, undefined, 'boom')); const p = new OpenAICompatibleProvider('key', 'https://api.x.test/v1', 'deepseek', 'DeepSeek'); await assert.rejects(() => p.chat('s', 'u', opts)); }); test('gemini chat returns text', async () => { stubFetch(() => makeResponse(true, 200, { candidates: [{ content: { parts: [{ text: 'G' }] } }] })); const p = new GeminiProvider('key', 'https://gemini.test'); assert.strictEqual(await p.chat('s', 'u', opts), 'G'); assert.match(calls[0].url, /generateContent/); }); test('gemini empty content raises EmptyContentError', async () => { stubFetch(() => makeResponse(true, 200, { candidates: [], promptFeedback: { blockReason: 'safety' } })); const p = new GeminiProvider('key', 'https://gemini.test'); await assert.rejects(() => p.chat('s', 'u', opts), EmptyContentError); }); test('gemini non-ok throws', async () => { stubFetch(() => makeResponse(false, 400, undefined, 'bad')); const p = new GeminiProvider('key', 'https://gemini.test'); await assert.rejects(() => p.chat('s', 'u', opts)); }); test('claude chat returns text with api key header', async () => { stubFetch(() => makeResponse(true, 200, { content: [{ text: 'C' }], stop_reason: 'end_turn' })); const p = new ClaudeProvider('key', 'https://claude.test'); assert.strictEqual(await p.chat('s', 'u', opts), 'C'); assert.strictEqual((calls[0].init.headers as Record)['x-api-key'], 'key'); }); test('claude 401 throws', async () => { stubFetch(() => makeResponse(false, 401, undefined, 'bad')); const p = new ClaudeProvider('key', 'https://claude.test'); await assert.rejects(() => p.chat('s', 'u', opts)); }); test('claude empty content raises EmptyContentError', async () => { stubFetch(() => makeResponse(true, 200, { content: [], error: { message: 'x' } })); const p = new ClaudeProvider('key', 'https://claude.test'); await assert.rejects(() => p.chat('s', 'u', opts), EmptyContentError); }); });