import { describe, expect, it, vi } from 'vitest'; import { ModelWebSearchError, createModelWebSearchAdapter, type FrozenSelectedModel, } from '../../electron/coding-runtime/pi/model-tools/web-search'; const selectedModel: FrozenSelectedModel = { accountId: 'niancode-user-models', runtimeProviderId: 'makelore-account-1', modelId: 'deepseek-v4-pro', generation: 7, baseUrl: 'http://127.0.0.1:13210/api/ai-proxy/v1', headers: { Authorization: 'Bearer frozen-token', 'X-Works-Square-AI-Token': 'frozen-token', }, capability: { schemaVersion: 1, adapter: 'bailian-chat-completions', supportsForcedSearch: true, sourceMode: 'inline-or-structured', billingAuthority: 'model-request', }, }; function jsonResponse(body: unknown, init?: ResponseInit): Response { return new Response(JSON.stringify(body), { ...init, headers: { 'content-type': 'application/json', ...init?.headers }, }); } describe('model Web Search adapter', () => { it('uses the frozen Bailian model transport with mandatory search directives', async () => { const fetchImpl = vi.fn().mockResolvedValue(jsonResponse({ choices: [{ message: { content: 'Flybird is a Flappy Bird-style game.' }, }], })); const adapter = createModelWebSearchAdapter({ fetchImpl }); await expect(adapter.search({ query: ' flybird 玩法 ', selectedModel, parentTurnId: 'turn-1', toolCallId: 'tool-1', }, new AbortController().signal)).resolves.toEqual({ schema: 'makelore-model-tool.v1', tool: 'web_search', status: 'succeeded', modelId: 'deepseek-v4-pro', answer: 'Flybird is a Flappy Bird-style game.', sources: [], sourceMode: 'inline-or-structured', }); expect(fetchImpl).toHaveBeenCalledTimes(1); expect(fetchImpl.mock.calls[0][0]).toBe( 'http://127.0.0.1:13210/api/ai-proxy/v1/chat/completions', ); expect(fetchImpl.mock.calls[0][1]).toMatchObject({ method: 'POST', headers: { Authorization: 'Bearer frozen-token', 'X-Works-Square-AI-Token': 'frozen-token', 'Content-Type': 'application/json', }, body: JSON.stringify({ model: 'deepseek-v4-pro', messages: [{ role: 'user', content: 'flybird 玩法' }], stream: false, enable_search: true, search_options: { forced_search: true }, }), }); }); it('normalizes and stably deduplicates structured sources when present', async () => { const fetchImpl = vi.fn().mockResolvedValue(jsonResponse({ choices: [{ message: { content: 'Grounded answer', sources: [ { title: ' First ', url: 'https://example.test/page#one' }, { title: 'Duplicate', url: 'https://example.test/page#two' }, { title: 'Second', url: 'https://second.test/' }, ], }, }], })); const adapter = createModelWebSearchAdapter({ fetchImpl }); const result = await adapter.search({ query: 'query', selectedModel, parentTurnId: 'turn-1', toolCallId: 'tool-1', }, new AbortController().signal); expect(result.sources).toEqual([ { title: 'First', url: 'https://example.test/page#one' }, { title: 'Second', url: 'https://second.test/' }, ]); }); it('uses the native Responses tool and normalizes URL citations', async () => { const fetchImpl = vi.fn().mockResolvedValue(jsonResponse({ output_text: 'Current answer', output: [{ type: 'message', content: [{ type: 'output_text', text: 'Current answer', annotations: [{ type: 'url_citation', title: 'Source', url: 'https://example.test/source', }], }], }], })); const adapter = createModelWebSearchAdapter({ fetchImpl }); const result = await adapter.search({ query: 'latest fact', selectedModel: { ...selectedModel, capability: { ...selectedModel.capability, adapter: 'openai-responses', sourceMode: 'structured', }, }, parentTurnId: 'turn-2', toolCallId: 'tool-2', }, new AbortController().signal); expect(JSON.parse(String((fetchImpl.mock.calls[0][1] as RequestInit).body))).toEqual({ model: 'deepseek-v4-pro', input: 'latest fact', tools: [{ type: 'web_search' }], tool_choice: 'required', }); expect(fetchImpl.mock.calls[0][0]).toBe( 'http://127.0.0.1:13210/api/ai-proxy/v1/responses', ); expect(result.sources).toEqual([ { title: 'Source', url: 'https://example.test/source' }, ]); }); it('maps rate limiting without switching model or transport', async () => { const fetchImpl = vi.fn().mockResolvedValue(jsonResponse( { error: { message: 'slow down' } }, { status: 429 }, )); const adapter = createModelWebSearchAdapter({ fetchImpl }); await expect(adapter.search({ query: 'query', selectedModel, parentTurnId: 'turn-1', toolCallId: 'tool-1', }, new AbortController().signal)).rejects.toMatchObject({ code: 'model_web_search_rate_limited', status: 429, retryable: false, } satisfies Partial); expect(fetchImpl).toHaveBeenCalledTimes(1); }); it('fails closed before transport for an unsupported capability', async () => { const fetchImpl = vi.fn(); const adapter = createModelWebSearchAdapter({ fetchImpl }); await expect(adapter.search({ query: 'query', selectedModel: { ...selectedModel, capability: undefined, }, parentTurnId: 'turn-1', toolCallId: 'tool-1', }, new AbortController().signal)).rejects.toMatchObject({ code: 'model_web_search_unsupported', status: 400, } satisfies Partial); expect(fetchImpl).not.toHaveBeenCalled(); }); });