mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
Add unit tests for web search core logic
This commit is contained in:
276
packages/core/src/tools/web-search/index.test.ts
Normal file
276
packages/core/src/tools/web-search/index.test.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { WebSearchTool } from './index.js';
|
||||
import type { Config } from '../../config/config.js';
|
||||
import type { WebSearchConfig } from './types.js';
|
||||
import { ApprovalMode } from '../../config/config.js';
|
||||
|
||||
describe('WebSearchTool', () => {
|
||||
let mockConfig: Config;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
mockConfig = {
|
||||
getApprovalMode: vi.fn(() => ApprovalMode.AUTO_EDIT),
|
||||
setApprovalMode: vi.fn(),
|
||||
getWebSearchConfig: vi.fn(),
|
||||
} as unknown as Config;
|
||||
});
|
||||
|
||||
describe('formatSearchResults', () => {
|
||||
it('should use answer when available and append sources', async () => {
|
||||
const webSearchConfig: WebSearchConfig = {
|
||||
provider: [
|
||||
{
|
||||
type: 'tavily',
|
||||
apiKey: 'test-key',
|
||||
},
|
||||
],
|
||||
default: 'tavily',
|
||||
};
|
||||
|
||||
(
|
||||
mockConfig.getWebSearchConfig as ReturnType<typeof vi.fn>
|
||||
).mockReturnValue(webSearchConfig);
|
||||
|
||||
// Mock fetch to return search results with answer
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
query: 'test query',
|
||||
answer: 'This is a concise answer from the search provider.',
|
||||
results: [
|
||||
{
|
||||
title: 'Result 1',
|
||||
url: 'https://example.com/1',
|
||||
content: 'Content 1',
|
||||
},
|
||||
{
|
||||
title: 'Result 2',
|
||||
url: 'https://example.com/2',
|
||||
content: 'Content 2',
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const tool = new WebSearchTool(mockConfig);
|
||||
const invocation = tool.build({ query: 'test query' });
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.llmContent).toContain(
|
||||
'This is a concise answer from the search provider.',
|
||||
);
|
||||
expect(result.llmContent).toContain('Sources:');
|
||||
expect(result.llmContent).toContain(
|
||||
'[1] Result 1 (https://example.com/1)',
|
||||
);
|
||||
expect(result.llmContent).toContain(
|
||||
'[2] Result 2 (https://example.com/2)',
|
||||
);
|
||||
});
|
||||
|
||||
it('should build informative summary when answer is not available', async () => {
|
||||
const webSearchConfig: WebSearchConfig = {
|
||||
provider: [
|
||||
{
|
||||
type: 'google',
|
||||
apiKey: 'test-key',
|
||||
searchEngineId: 'test-engine',
|
||||
},
|
||||
],
|
||||
default: 'google',
|
||||
};
|
||||
|
||||
(
|
||||
mockConfig.getWebSearchConfig as ReturnType<typeof vi.fn>
|
||||
).mockReturnValue(webSearchConfig);
|
||||
|
||||
// Mock fetch to return search results without answer
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
items: [
|
||||
{
|
||||
title: 'Google Result 1',
|
||||
link: 'https://example.com/1',
|
||||
snippet: 'This is a helpful snippet from the first result.',
|
||||
},
|
||||
{
|
||||
title: 'Google Result 2',
|
||||
link: 'https://example.com/2',
|
||||
snippet: 'This is a helpful snippet from the second result.',
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const tool = new WebSearchTool(mockConfig);
|
||||
const invocation = tool.build({ query: 'test query' });
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
// Should contain formatted results with title, snippet, and source
|
||||
expect(result.llmContent).toContain('1. **Google Result 1**');
|
||||
expect(result.llmContent).toContain(
|
||||
'This is a helpful snippet from the first result.',
|
||||
);
|
||||
expect(result.llmContent).toContain('Source: https://example.com/1');
|
||||
expect(result.llmContent).toContain('2. **Google Result 2**');
|
||||
expect(result.llmContent).toContain(
|
||||
'This is a helpful snippet from the second result.',
|
||||
);
|
||||
expect(result.llmContent).toContain('Source: https://example.com/2');
|
||||
|
||||
// Should include web_fetch hint
|
||||
expect(result.llmContent).toContain('web_fetch tool');
|
||||
});
|
||||
|
||||
it('should include optional fields when available', async () => {
|
||||
const webSearchConfig: WebSearchConfig = {
|
||||
provider: [
|
||||
{
|
||||
type: 'tavily',
|
||||
apiKey: 'test-key',
|
||||
},
|
||||
],
|
||||
default: 'tavily',
|
||||
};
|
||||
|
||||
(
|
||||
mockConfig.getWebSearchConfig as ReturnType<typeof vi.fn>
|
||||
).mockReturnValue(webSearchConfig);
|
||||
|
||||
// Mock fetch to return results with score and publishedDate
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
query: 'test query',
|
||||
results: [
|
||||
{
|
||||
title: 'Result with metadata',
|
||||
url: 'https://example.com',
|
||||
content: 'Content with metadata',
|
||||
score: 0.95,
|
||||
published_date: '2024-01-15',
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const tool = new WebSearchTool(mockConfig);
|
||||
const invocation = tool.build({ query: 'test query' });
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
// Should include relevance score
|
||||
expect(result.llmContent).toContain('Relevance: 95%');
|
||||
// Should include published date
|
||||
expect(result.llmContent).toContain('Published: 2024-01-15');
|
||||
});
|
||||
|
||||
it('should handle empty results gracefully', async () => {
|
||||
const webSearchConfig: WebSearchConfig = {
|
||||
provider: [
|
||||
{
|
||||
type: 'google',
|
||||
apiKey: 'test-key',
|
||||
searchEngineId: 'test-engine',
|
||||
},
|
||||
],
|
||||
default: 'google',
|
||||
};
|
||||
|
||||
(
|
||||
mockConfig.getWebSearchConfig as ReturnType<typeof vi.fn>
|
||||
).mockReturnValue(webSearchConfig);
|
||||
|
||||
// Mock fetch to return empty results
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
items: [],
|
||||
}),
|
||||
});
|
||||
|
||||
const tool = new WebSearchTool(mockConfig);
|
||||
const invocation = tool.build({ query: 'test query' });
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.llmContent).toContain('No search results found');
|
||||
});
|
||||
|
||||
it('should limit to top 5 results in fallback mode', async () => {
|
||||
const webSearchConfig: WebSearchConfig = {
|
||||
provider: [
|
||||
{
|
||||
type: 'google',
|
||||
apiKey: 'test-key',
|
||||
searchEngineId: 'test-engine',
|
||||
},
|
||||
],
|
||||
default: 'google',
|
||||
};
|
||||
|
||||
(
|
||||
mockConfig.getWebSearchConfig as ReturnType<typeof vi.fn>
|
||||
).mockReturnValue(webSearchConfig);
|
||||
|
||||
// Mock fetch to return 10 results
|
||||
const items = Array.from({ length: 10 }, (_, i) => ({
|
||||
title: `Result ${i + 1}`,
|
||||
link: `https://example.com/${i + 1}`,
|
||||
snippet: `Snippet ${i + 1}`,
|
||||
}));
|
||||
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ items }),
|
||||
});
|
||||
|
||||
const tool = new WebSearchTool(mockConfig);
|
||||
const invocation = tool.build({ query: 'test query' });
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
// Should only contain first 5 results
|
||||
expect(result.llmContent).toContain('1. **Result 1**');
|
||||
expect(result.llmContent).toContain('5. **Result 5**');
|
||||
expect(result.llmContent).not.toContain('6. **Result 6**');
|
||||
expect(result.llmContent).not.toContain('10. **Result 10**');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validation', () => {
|
||||
it('should throw validation error when query is empty', () => {
|
||||
const tool = new WebSearchTool(mockConfig);
|
||||
expect(() => tool.build({ query: '' })).toThrow(
|
||||
"The 'query' parameter cannot be empty",
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw validation error when provider is empty string', () => {
|
||||
const tool = new WebSearchTool(mockConfig);
|
||||
expect(() => tool.build({ query: 'test', provider: '' })).toThrow(
|
||||
"The 'provider' parameter cannot be empty",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('configuration', () => {
|
||||
it('should return error when web search is not configured', async () => {
|
||||
(
|
||||
mockConfig.getWebSearchConfig as ReturnType<typeof vi.fn>
|
||||
).mockReturnValue(null);
|
||||
|
||||
const tool = new WebSearchTool(mockConfig);
|
||||
const invocation = tool.build({ query: 'test query' });
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.error?.message).toContain('Web search is disabled');
|
||||
expect(result.llmContent).toContain('Web search is disabled');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user