import * as assert from 'assert'; import { parseJsonResponse, chatWithRetry } from '../src/ai/engine'; import { EmptyContentError } from '../src/ai/providers/base'; import type { AIProvider } from '../src/ai/providers/base'; import type { ChatOptions } from '../src/ai/providers/base'; import { OpenAICompatibleProvider } from '../src/ai/providers/openai-compatible'; const OPTIONS: ChatOptions = { model: 'test', temperature: 0, maxTokens: 1024, timeoutMs: 5000, }; suite('AI Empty Response Handling', () => { test('parseJsonResponse throws empty-response error on blank input', () => { assert.throws(() => parseJsonResponse(''), /空响应|empty response/); assert.throws(() => parseJsonResponse(' \n\t '), /空响应|empty response/); }); test('parseJsonResponse parses valid JSON normally', () => { const parsed = parseJsonResponse('{"findings":[]}') as { findings: unknown[] }; assert.deepStrictEqual(parsed.findings, []); }); test('chatWithRetry retries once on EmptyContentError', async () => { const calls: string[] = []; const provider = { chat: async (system: string): Promise => { calls.push(system); if (calls.length === 1) { throw new EmptyContentError('finish_reason=length'); } return '{"findings":[]}'; }, } as unknown as AIProvider; const result = await chatWithRetry(provider, 'sys', 'user', OPTIONS); assert.strictEqual(result, '{"findings":[]}'); assert.strictEqual(calls.length, 2); }); test('chatWithRetry propagates error when retry also returns empty', async () => { const provider = { chat: async (): Promise => { throw new EmptyContentError('finish_reason=length'); }, } as unknown as AIProvider; await assert.rejects( () => chatWithRetry(provider, 'sys', 'user', OPTIONS), EmptyContentError ); }); test('chatWithRetry does not retry on non-empty-content errors', async () => { let calls = 0; const provider = { chat: async (): Promise => { calls++; throw new Error('boom'); }, } as unknown as AIProvider; await assert.rejects(() => chatWithRetry(provider, 'sys', 'user', OPTIONS), /boom/); assert.strictEqual(calls, 1); }); test('openai-compatible reports max_tokens truncation clearly', async () => { const originalFetch = globalThis.fetch; globalThis.fetch = (async () => ({ ok: true, json: async () => ({ choices: [{ message: { content: null }, finish_reason: 'length' }], }), text: async () => '', })) as unknown as typeof fetch; try { const provider = new OpenAICompatibleProvider('key', 'http://localhost', 'test', 'Test'); await assert.rejects( () => provider.chat('sys', 'user', { model: 'm', temperature: 0, maxTokens: 8192, timeoutMs: 5000, }), (err: unknown) => { assert.ok(err instanceof EmptyContentError); assert.match((err as Error).message, /max_tokens/); assert.match((err as Error).message, /8192/); return true; } ); } finally { globalThis.fetch = originalFetch; } }); });