import { describe, it, expect, beforeEach, vi } from 'vitest'; // Mock tavily const mockSearch = vi.fn(); vi.mock('@tavily/core', () => ({ tavily: vi.fn(() => ({ search: mockSearch, })), })); // Mock config vi.mock('../../../../src/utils/config.js', () => ({ getConfig: vi.fn(() => ({ tavilyApiKey: 'test-api-key', })), })); // Mock permission manager vi.mock('../../../../src/permission/index.js', () => ({ getPermissionManager: vi.fn(() => ({ checkWebPermission: vi.fn().mockResolvedValue({ allowed: true, action: 'allow', }), })), })); // Mock loadDescription vi.mock('../../../../src/tools/load_description.js', () => ({ loadDescription: vi.fn(() => '网络搜索'), })); import { webSearchTool } from '../../../../src/tools/web/web_search.js'; import { getPermissionManager } from '../../../../src/permission/index.js'; import { getConfig } from '../../../../src/utils/config.js'; describe('webSearchTool - 网络搜索工具', () => { beforeEach(() => { vi.clearAllMocks(); mockSearch.mockResolvedValue({ answer: '搜索摘要', results: [ { title: '结果1', url: 'https://example.com/1', content: '内容1' }, { title: '结果2', url: 'https://example.com/2', content: '内容2' }, ], }); }); describe('工具定义', () => { it('有正确的名称', () => { expect(webSearchTool.name).toBe('web_search'); }); it('有正确的元数据', () => { expect(webSearchTool.metadata.category).toBe('web'); expect(webSearchTool.metadata.keywords).toContain('search'); expect(webSearchTool.metadata.keywords).toContain('web'); }); it('定义了必需的 query 参数', () => { expect(webSearchTool.parameters.query.required).toBe(true); }); it('定义了可选参数', () => { expect(webSearchTool.parameters.max_results.required).toBe(false); expect(webSearchTool.parameters.search_depth.required).toBe(false); expect(webSearchTool.parameters.topic.required).toBe(false); expect(webSearchTool.parameters.include_answer.required).toBe(false); }); }); describe('execute - 执行', () => { it('成功搜索并返回结果', async () => { const result = await webSearchTool.execute({ query: 'test query' }); expect(result.success).toBe(true); expect(result.output).toContain('搜索结果'); expect(result.output).toContain('test query'); expect(result.output).toContain('搜索摘要'); expect(result.output).toContain('结果1'); expect(result.output).toContain('结果2'); }); it('无结果时显示提示', async () => { mockSearch.mockResolvedValue({ answer: null, results: [], }); const result = await webSearchTool.execute({ query: 'no results' }); expect(result.success).toBe(true); expect(result.output).toContain('未找到相关结果'); }); it('限制最大结果数量', async () => { const result = await webSearchTool.execute({ query: 'test', max_results: 100, // 超过限制 }); expect(result.success).toBe(true); expect(mockSearch).toHaveBeenCalledWith( 'test', expect.objectContaining({ maxResults: 20 }) // 最大 20 ); }); it('使用指定的搜索深度', async () => { await webSearchTool.execute({ query: 'test', search_depth: 'advanced', }); expect(mockSearch).toHaveBeenCalledWith( 'test', expect.objectContaining({ searchDepth: 'advanced' }) ); }); it('使用指定的主题', async () => { await webSearchTool.execute({ query: 'test', topic: 'news', }); expect(mockSearch).toHaveBeenCalledWith( 'test', expect.objectContaining({ topic: 'news' }) ); }); it('无 API Key 返回错误', async () => { vi.mocked(getConfig).mockReturnValue({} as any); const originalEnv = process.env.TAVILY_API_KEY; delete process.env.TAVILY_API_KEY; const result = await webSearchTool.execute({ query: 'test' }); expect(result.success).toBe(false); expect(result.error).toContain('未配置 Tavily API Key'); process.env.TAVILY_API_KEY = originalEnv; }); it('权限被拒绝时返回错误', async () => { vi.mocked(getPermissionManager).mockReturnValue({ checkWebPermission: vi.fn().mockResolvedValue({ allowed: false, action: 'deny', reason: '搜索不被允许', }), } as any); const result = await webSearchTool.execute({ query: 'test' }); expect(result.success).toBe(false); expect(result.error).toContain('权限被拒绝'); }); it('需要确认时返回提示', async () => { vi.mocked(getPermissionManager).mockReturnValue({ checkWebPermission: vi.fn().mockResolvedValue({ allowed: false, action: 'ask', needsConfirmation: true, }), } as any); const result = await webSearchTool.execute({ query: 'test' }); expect(result.success).toBe(false); expect(result.error).toContain('需要用户确认'); }); it('搜索失败返回错误', async () => { vi.mocked(getPermissionManager).mockReturnValue({ checkWebPermission: vi.fn().mockResolvedValue({ allowed: true }), } as any); mockSearch.mockRejectedValue(new Error('API 调用失败')); const result = await webSearchTool.execute({ query: 'test' }); expect(result.success).toBe(false); expect(result.error).toContain('搜索失败'); expect(result.error).toContain('API 调用失败'); }); it('截断过长的内容', async () => { mockSearch.mockResolvedValue({ answer: null, results: [ { title: '长内容结果', url: 'https://example.com', content: 'a'.repeat(500), // 超过 300 字符 }, ], }); const result = await webSearchTool.execute({ query: 'test' }); expect(result.success).toBe(true); expect(result.output).toContain('...'); }); }); });