Files
makelore/tests/unit/model-web-search.test.ts

192 lines
5.9 KiB
TypeScript

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<typeof fetch>().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<typeof fetch>().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<typeof fetch>().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<typeof fetch>().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<ModelWebSearchError>);
expect(fetchImpl).toHaveBeenCalledTimes(1);
});
it('fails closed before transport for an unsupported capability', async () => {
const fetchImpl = vi.fn<typeof fetch>();
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<ModelWebSearchError>);
expect(fetchImpl).not.toHaveBeenCalled();
});
});