157 lines
4.7 KiB
TypeScript
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',
|
|
);
|
|
});
|
|
});
|