import { describe, it, expect, jest, beforeEach } from '@jest/globals';
import { WebSearchTool } from '../../src/tools/websearch.js';
import axios from 'axios';
// Mock axios
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;
describe('WebSearchTool', () => {
let tool: WebSearchTool;
beforeEach(() => {
tool = new WebSearchTool();
jest.clearAllMocks();
// Reset environment variables
delete process.env.BRAVE_API_KEY;
delete process.env.GOOGLE_API_KEY;
delete process.env.GOOGLE_SEARCH_ENGINE_ID;
delete process.env.BING_API_KEY;
});
describe('Tool Definition', () => {
it('should have correct name', () => {
expect(tool.name).toBe('websearch');
});
it('should have description', () => {
expect(tool.description).toBeTruthy();
expect(tool.description.length).toBeGreaterThan(0);
});
it('should have schema', () => {
expect(tool.schema).toBeDefined();
});
});
describe('DuckDuckGo Search', () => {
const mockDDGHtml = `
<html>
<body>
<div class="result">
<h2 class="result__title">
<a href="https://example.com/page1">Test Result 1</a>
</h2>
<div class="result__snippet">This is a test snippet 1</div>
</div>
<div class="result">
<h2 class="result__title">
<a href="https://example.com/page2">Test Result 2</a>
</h2>
<div class="result__snippet">This is a test snippet 2</div>
</div>
</body>
</html>
`;
beforeEach(() => {
mockedAxios.create.mockReturnValue({
post: jest.fn<any>().mockResolvedValue({ data: mockDDGHtml }),
} as any);
});
it('should search with DuckDuckGo by default', async () => {
const result = await tool.run({
query: 'test query',
});
expect(result.isError).toBeFalsy();
const results = JSON.parse(result.content[0].text);
expect(Array.isArray(results)).toBe(true);
expect(results.length).toBeGreaterThan(0);
expect(results[0].source).toBe('duckduckgo');
});
it('should extract titles and snippets', async () => {
const result = await tool.run({
query: 'test query',
engine: 'duckduckgo',
});
const results = JSON.parse(result.content[0].text);
expect(results[0].title).toBe('Test Result 1');
expect(results[0].snippet).toBe('This is a test snippet 1');
expect(results[0].url).toContain('example.com');
});
it('should respect result limit', async () => {
const result = await tool.run({
query: 'test query',
limit: 1,
});
const results = JSON.parse(result.content[0].text);
expect(results.length).toBeLessThanOrEqual(1);
});
it('should apply file type filter', async () => {
await tool.run({
query: 'test query',
filters: { fileType: 'pdf' },
});
const axiosInstance: any = mockedAxios.create.mock.results[0].value;
const postCall = axiosInstance.post.mock.calls[0];
const formData = postCall[1];
// Check that filetype: was added to query
expect(formData.toString()).toContain('filetype');
});
it('should apply domain filter', async () => {
await tool.run({
query: 'test query',
filters: { domain: 'github.com' },
});
const axiosInstance: any = mockedAxios.create.mock.results[0].value;
const postCall = axiosInstance.post.mock.calls[0];
const formData = postCall[1];
expect(formData.toString()).toContain('site');
});
it('should apply exact phrase filter', async () => {
await tool.run({
query: 'test query',
filters: { exactPhrase: true },
});
const axiosInstance: any = mockedAxios.create.mock.results[0].value;
const postCall = axiosInstance.post.mock.calls[0];
const formData = postCall[1];
// Query should be wrapped in quotes
expect(formData.toString()).toContain('"test query"');
});
});
describe('Brave Search', () => {
const mockBraveResponse = {
web: {
results: [
{
title: 'Brave Result 1',
url: 'https://example.com/brave1',
description: 'Brave snippet 1',
},
{
title: 'Brave Result 2',
url: 'https://example.com/brave2',
description: 'Brave snippet 2',
},
],
},
};
beforeEach(() => {
process.env.BRAVE_API_KEY = 'test-brave-key';
mockedAxios.create.mockReturnValue({
get: jest.fn<any>().mockResolvedValue({ data: mockBraveResponse }),
} as any);
});
it('should search with Brave when API key is provided', async () => {
const result = await tool.run({
query: 'test query',
engine: 'brave',
});
expect(result.isError).toBeFalsy();
const results = JSON.parse(result.content[0].text);
expect(results[0].source).toBe('brave');
expect(results[0].title).toBe('Brave Result 1');
});
it('should fail without API key', async () => {
delete process.env.BRAVE_API_KEY;
const result = await tool.run({
query: 'test query',
engine: 'brave',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('BRAVE_API_KEY');
});
it('should send API key in headers', async () => {
await tool.run({
query: 'test query',
engine: 'brave',
});
const axiosInstance: any = mockedAxios.create.mock.results[0].value;
const getCall = axiosInstance.get.mock.calls[0];
const config = getCall[1];
expect(config.headers['X-Subscription-Token']).toBe('test-brave-key');
});
});
describe('Google Search', () => {
const mockGoogleResponse = {
items: [
{
title: 'Google Result 1',
link: 'https://example.com/google1',
snippet: 'Google snippet 1',
},
],
};
beforeEach(() => {
process.env.GOOGLE_API_KEY = 'test-google-key';
process.env.GOOGLE_SEARCH_ENGINE_ID = 'test-cx';
mockedAxios.create.mockReturnValue({
get: jest.fn<any>().mockResolvedValue({ data: mockGoogleResponse }),
} as any);
});
it('should search with Google when API keys are provided', async () => {
const result = await tool.run({
query: 'test query',
engine: 'google',
});
expect(result.isError).toBeFalsy();
const results = JSON.parse(result.content[0].text);
expect(results[0].source).toBe('google');
});
it('should fail without API keys', async () => {
delete process.env.GOOGLE_API_KEY;
const result = await tool.run({
query: 'test query',
engine: 'google',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('GOOGLE_API_KEY');
});
it('should apply date range filter', async () => {
await tool.run({
query: 'test query',
engine: 'google',
filters: { dateRange: 'week' },
});
const axiosInstance: any = mockedAxios.create.mock.results[0].value;
const getCall = axiosInstance.get.mock.calls[0];
const config = getCall[1];
expect(config.params.dateRestrict).toBe('w1');
});
});
describe('Bing Search', () => {
const mockBingResponse = {
webPages: {
value: [
{
name: 'Bing Result 1',
url: 'https://example.com/bing1',
snippet: 'Bing snippet 1',
},
],
},
};
beforeEach(() => {
process.env.BING_API_KEY = 'test-bing-key';
mockedAxios.create.mockReturnValue({
get: jest.fn<any>().mockResolvedValue({ data: mockBingResponse }),
} as any);
});
it('should search with Bing when API key is provided', async () => {
const result = await tool.run({
query: 'test query',
engine: 'bing',
});
expect(result.isError).toBeFalsy();
const results = JSON.parse(result.content[0].text);
expect(results[0].source).toBe('bing');
});
it('should fail without API key', async () => {
delete process.env.BING_API_KEY;
const result = await tool.run({
query: 'test query',
engine: 'bing',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('BING_API_KEY');
});
});
describe('Error Handling', () => {
it('should handle network errors', async () => {
mockedAxios.create.mockReturnValue({
post: jest.fn<any>().mockRejectedValue(new Error('Network error')),
} as any);
const result = await tool.run({
query: 'test query',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Error');
});
it('should validate required query parameter', async () => {
const result = await tool.run({
query: '',
});
expect(result.isError).toBe(true);
});
it('should handle unsupported engine', async () => {
const result = await tool.run({
query: 'test',
engine: 'invalid' as any,
});
expect(result.isError).toBe(true);
});
});
describe('Result Normalization', () => {
it('should normalize results to common format', async () => {
const mockHtml = `
<div class="result">
<h2 class="result__title">
<a href="https://example.com">Title</a>
</h2>
<div class="result__snippet">Snippet</div>
</div>
`;
mockedAxios.create.mockReturnValue({
post: jest.fn<any>().mockResolvedValue({ data: mockHtml }),
} as any);
const result = await tool.run({
query: 'test',
});
const results = JSON.parse(result.content[0].text);
expect(results[0]).toHaveProperty('title');
expect(results[0]).toHaveProperty('url');
expect(results[0]).toHaveProperty('snippet');
expect(results[0]).toHaveProperty('source');
});
});
});