Files
openmaic/OpenMAIC/tests/web-search/doubao.test.ts
2026-08-16 14:58:47 +08:00

157 lines
4.7 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
const proxyFetchMock = vi.hoisted(() => vi.fn());
vi.mock('@/lib/server/proxy-fetch', () => ({
proxyFetch: proxyFetchMock,
}));
import { searchWithDoubao } from '@/lib/web-search/doubao';
describe('searchWithDoubao', () => {
beforeEach(() => {
proxyFetchMock.mockReset();
});
it('calls the Doubao search API and maps WebResults, preferring Summary', async () => {
proxyFetchMock.mockResolvedValueOnce(
new Response(
JSON.stringify({
ResponseMetadata: { RequestId: 'req-1' },
Result: {
ResultCount: 3,
WebResults: [
{
Title: 'OpenMAIC',
Url: 'https://github.com/THU-MAIC/OpenMAIC',
Snippet: 'short blurb',
Summary: 'a longer, query-relevant summary',
Content: 'full article body',
RankScore: 0.95,
},
{
// No Summary → falls back to Content, then Snippet.
Title: '',
Url: 'https://example.com/fallback',
Snippet: 'snippet only',
},
{
// No Url → dropped.
Title: 'No url',
Summary: 'skipped because there is no url',
},
],
},
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
),
);
const result = await searchWithDoubao({
query: 'OpenMAIC token plan',
apiKey: 'ark-key',
maxResults: 10,
});
expect(proxyFetchMock).toHaveBeenCalledWith(
'https://open.feedcoopapi.com/search_api/web_search',
expect.objectContaining({
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ark-key',
},
body: JSON.stringify({
Query: 'OpenMAIC token plan',
SearchType: 'web',
Count: 10,
NeedSummary: true,
}),
}),
);
expect(result.query).toBe('OpenMAIC token plan');
expect(result.answer).toBe('');
expect(result.sources).toEqual([
{
title: 'OpenMAIC',
url: 'https://github.com/THU-MAIC/OpenMAIC',
content: 'a longer, query-relevant summary',
score: 0.95,
},
{
title: 'https://example.com/fallback',
url: 'https://example.com/fallback',
content: 'snippet only',
score: 0,
},
]);
});
it('appends the search path when given a bare host base URL', async () => {
proxyFetchMock.mockImplementation(() =>
Promise.resolve(
new Response(JSON.stringify({ Result: { WebResults: [] } }), {
status: 200,
headers: { 'content-type': 'application/json' },
}),
),
);
await searchWithDoubao({ query: 'q', apiKey: 'k', baseUrl: 'https://proxy.example.com' });
await searchWithDoubao({
query: 'q',
apiKey: 'k',
baseUrl: 'https://proxy.example.com/search_api/web_search',
});
expect(proxyFetchMock.mock.calls.map((call) => call[0])).toEqual([
'https://proxy.example.com/search_api/web_search',
'https://proxy.example.com/search_api/web_search',
]);
});
it('caps Count at 50 and truncates the query to 100 chars', async () => {
proxyFetchMock.mockResolvedValueOnce(
new Response(JSON.stringify({ Result: { WebResults: [] } }), {
status: 200,
headers: { 'content-type': 'application/json' },
}),
);
const longQuery = 'x'.repeat(150);
await searchWithDoubao({ query: longQuery, apiKey: 'k', maxResults: 999 });
const body = JSON.parse(proxyFetchMock.mock.calls[0][1].body as string);
expect(body.Query).toHaveLength(100);
expect(body.Count).toBe(50);
});
it('throws with the Doubao error code/message when the body carries an Error', async () => {
proxyFetchMock.mockResolvedValueOnce(
new Response(
JSON.stringify({
ResponseMetadata: {
Error: { CodeN: 10406, Code: '10406', Message: '免费搜索额度用尽' },
},
Result: null,
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
),
);
await expect(searchWithDoubao({ query: 'q', apiKey: 'k' })).rejects.toThrow(
'Doubao Web Search API error (10406): 免费搜索额度用尽',
);
});
it('throws on a non-OK HTTP response', async () => {
proxyFetchMock.mockResolvedValueOnce(
new Response('nope', { status: 500, statusText: 'Internal Server Error' }),
);
await expect(searchWithDoubao({ query: 'q', apiKey: 'k' })).rejects.toThrow(
'Doubao Web Search API error (500): nope',
);
});
});