| import { test, expect, describe } from 'bun:test' |
| import { WebSearchTool } from '../WebSearchTool' |
| import { jinaSearch } from '../jina_search' |
|
|
| describe('WebSearchTool', () => { |
| describe('Tool Properties', () => { |
| test('should have correct tool name', () => { |
| expect(WebSearchTool.name).toBe('WebSearch') |
| }) |
|
|
| test('should have correct description', () => { |
| expect(WebSearchTool.description).toBe('Search the web and return search results with titles, URLs, and snippets.') |
| }) |
|
|
| test('should be enabled', () => { |
| expect(WebSearchTool.isEnabled()).toBe(true) |
| }) |
|
|
| test('should be concurrency safe', () => { |
| expect(WebSearchTool.isConcurrencySafe()).toBe(true) |
| }) |
|
|
| test('should be read only', () => { |
| expect(WebSearchTool.isReadOnly()).toBe(true) |
| }) |
| }) |
|
|
| describe('Input Validation', () => { |
| test('should accept valid query', async () => { |
| const result = await WebSearchTool.validateInput( |
| { query: 'typescript' }, |
| {} |
| ) |
|
|
| expect(result.result).toBe(true) |
| }) |
|
|
| test('should accept query with allowed domains', async () => { |
| const result = await WebSearchTool.validateInput( |
| { |
| query: 'typescript', |
| allowed_domains: ['github.com', 'stackoverflow.com'] |
| }, |
| {} |
| ) |
|
|
| expect(result.result).toBe(true) |
| }) |
|
|
| test('should accept query with blocked domains', async () => { |
| const result = await WebSearchTool.validateInput( |
| { |
| query: 'typescript', |
| blocked_domains: ['example.com'] |
| }, |
| {} |
| ) |
|
|
| expect(result.result).toBe(true) |
| }) |
|
|
| test('should reject empty query', async () => { |
| const result = await WebSearchTool.validateInput( |
| { query: '' }, |
| {} |
| ) |
|
|
| expect(result.result).toBe(false) |
| expect(result.message).toContain('Missing query') |
| }) |
|
|
| test('should reject missing input', async () => { |
| const result = await WebSearchTool.validateInput( |
| {} as any, |
| {} |
| ) |
|
|
| expect(result.result).toBe(false) |
| expect(result.message).toContain('Error: Missing query') |
| }) |
|
|
| test.skip('should reject query with less than 2 characters', async () => { |
| |
| const result = await WebSearchTool.validateInput( |
| { query: 'a' }, |
| {} |
| ) |
|
|
| |
| expect(result.result).toBe(false) |
| }) |
| }) |
|
|
| describe('Permissions', () => { |
| test('should allow all web search requests', async () => { |
| const result = await WebSearchTool.checkPermissions( |
| { query: 'typescript', allowed_domains: ['github.com'] }, |
| {} |
| ) |
|
|
| expect(result.behavior).toBe('allow') |
| expect(result.updatedInput).toBeDefined() |
| |
| expect(result.updatedInput?.allowed_domains).toBeUndefined() |
| expect(result.decisionReason?.type).toBe('other') |
| }) |
|
|
| test('should remove blocked domains from input', async () => { |
| const result = await WebSearchTool.checkPermissions( |
| { query: 'typescript', blocked_domains: ['example.com'] }, |
| {} |
| ) |
|
|
| expect(result.behavior).toBe('allow') |
| expect(result.updatedInput?.blocked_domains).toBeUndefined() |
| }) |
| }) |
|
|
| describe('Tool Call - Successful Search', () => { |
| test('should perform web search with simple query', async () => { |
| const result = await WebSearchTool.call( |
| { query: 'typescript programming' }, |
| {}, |
| () => {}, |
| null, |
| (progress) => { |
| |
| expect(progress).toBeDefined() |
| } |
| ) |
|
|
| expect(result).toBeDefined() |
| expect(result.query).toBe('typescript programming') |
| expect(result.results).toBeDefined() |
| expect(Array.isArray(result.results)).toBe(true) |
| expect(result.durationSeconds).toBeGreaterThan(0) |
| }, 60000) |
|
|
| test('should return search results with proper structure', async () => { |
| const result = await WebSearchTool.call( |
| { query: 'javascript' }, |
| {}, |
| () => {}, |
| null |
| ) |
|
|
| expect(result.results).toBeDefined() |
| expect(result.results.length).toBeGreaterThan(0) |
|
|
| |
| const firstResult = result.results[0] |
| if (typeof firstResult !== 'string') { |
| expect(firstResult).toHaveProperty('tool_use_id') |
| expect(firstResult).toHaveProperty('content') |
| expect(Array.isArray(firstResult.content)).toBe(true) |
| } |
| }, 60000) |
|
|
| test('should handle empty results gracefully', async () => { |
| const result = await WebSearchTool.call( |
| { query: 'xyzabc123def456' }, |
| {}, |
| () => {}, |
| null |
| ) |
|
|
| expect(result.results).toBeDefined() |
| |
| }, 60000) |
| }) |
|
|
| describe('Tool Call - Domain Filtering', () => { |
| test('should filter results by allowed domains', async () => { |
| const result = await WebSearchTool.call( |
| { |
| query: 'github', |
| allowed_domains: ['github.com'] |
| }, |
| {}, |
| () => {}, |
| null |
| ) |
|
|
| expect(result).toBeDefined() |
| expect(result.results).toBeDefined() |
| }, 60000) |
|
|
| test('should filter results by blocked domains', async () => { |
| const result = await WebSearchTool.call( |
| { |
| query: 'programming', |
| blocked_domains: ['example.com'] |
| }, |
| {}, |
| () => {}, |
| null |
| ) |
|
|
| expect(result).toBeDefined() |
| expect(result.results).toBeDefined() |
| }, 60000) |
| }) |
|
|
| describe('Tool Call - Error Handling', () => { |
| test('should handle missing query gracefully', async () => { |
| const result = await WebSearchTool.call( |
| { query: '' } as any, |
| {}, |
| () => {}, |
| null |
| ) |
|
|
| expect(result.query).toBe('') |
| expect(result.results).toContain('Error: Missing query') |
| }) |
|
|
| test('should handle search errors gracefully', async () => { |
| |
| const result = await WebSearchTool.call( |
| { query: 'test' }, |
| {}, |
| () => {}, |
| null |
| ) |
|
|
| expect(result).toBeDefined() |
| expect(result.durationSeconds).toBeGreaterThan(0) |
| }, 60000) |
| }) |
|
|
| describe('Schema Validation', () => { |
| test('should have valid input schema', () => { |
| const schema = WebSearchTool.inputSchema |
| expect(schema).toBeDefined() |
| }) |
|
|
| test('should have valid output schema', () => { |
| const schema = WebSearchTool.outputSchema |
| expect(schema).toBeDefined() |
| }) |
|
|
| test('input schema should have required fields', () => { |
| const schema = WebSearchTool.inputSchema |
| const parsed = schema.safeParse({ query: 'test' }) |
| expect(parsed.success).toBe(true) |
| }) |
|
|
| test('input schema should allow optional domain filters', () => { |
| const schema = WebSearchTool.inputSchema |
| const parsed = schema.safeParse({ |
| query: 'test', |
| allowed_domains: ['example.com'], |
| blocked_domains: ['test.com'] |
| }) |
| expect(parsed.success).toBe(true) |
| }) |
| }) |
|
|
| describe('Tool Metadata', () => { |
| test('should provide activity description', () => { |
| const description = WebSearchTool.getActivityDescription({ |
| query: 'typescript' |
| }) |
|
|
| expect(description).toContain('Searching') |
| expect(description).toContain('typescript') |
| }) |
|
|
| test('should provide tool use summary', () => { |
| const summary = WebSearchTool.getToolUseSummary({ |
| query: 'javascript' |
| }) |
|
|
| expect(summary).toBeDefined() |
| }) |
|
|
| test('should return empty search text for extraction', () => { |
| const searchText = WebSearchTool.extractSearchText?.({ |
| results: ['test'] |
| } as any) |
|
|
| expect(searchText).toBe('') |
| }) |
| }) |
|
|
| describe('Auto Classifier Input', () => { |
| test('should format input for auto classifier', () => { |
| const input = WebSearchTool.toAutoClassifierInput({ |
| query: 'typescript' |
| }) |
|
|
| expect(input).toBe('typescript') |
| }) |
|
|
| test('should handle empty query', () => { |
| const input = WebSearchTool.toAutoClassifierInput({ |
| query: '' |
| }) |
|
|
| expect(input).toBe('') |
| }) |
| }) |
|
|
| describe('Tool Result Mapping', () => { |
| test('should map tool result to block param', () => { |
| const output = { |
| query: 'typescript', |
| results: [{ |
| tool_use_id: 'test-1', |
| content: [{ |
| title: 'TypeScript', |
| url: 'https://example.com', |
| snippet: 'Test snippet' |
| }] |
| }], |
| durationSeconds: 1.5 |
| } |
|
|
| const blockParam = WebSearchTool.mapToolResultToToolResultBlockParam( |
| output, |
| 'test-tool-use-id' |
| ) |
|
|
| expect(blockParam.tool_use_id).toBe('test-tool-use-id') |
| expect(blockParam.type).toBe('tool_result') |
| expect(blockParam.content).toBeDefined() |
| expect(typeof blockParam.content).toBe('string') |
| }) |
|
|
| test('should include source reminder in formatted output', () => { |
| const output = { |
| query: 'test', |
| results: [], |
| durationSeconds: 1 |
| } |
|
|
| const blockParam = WebSearchTool.mapToolResultToToolResultBlockParam( |
| output, |
| 'test-id' |
| ) |
|
|
| expect(blockParam.content).toContain('REMINDER: You MUST include the sources') |
| }) |
|
|
| test('should handle null/undefined results gracefully', () => { |
| const output = { |
| query: 'test', |
| results: [null, undefined, 'valid string'] as any, |
| durationSeconds: 1 |
| } |
|
|
| const blockParam = WebSearchTool.mapToolResultToToolResultBlockParam( |
| output, |
| 'test-id' |
| ) |
|
|
| expect(blockParam.content).toBeDefined() |
| expect(typeof blockParam.content).toBe('string') |
| }) |
|
|
| test('should handle missing output gracefully', () => { |
| const blockParam = WebSearchTool.mapToolResultToToolResultBlockParam( |
| undefined as any, |
| 'test-id' |
| ) |
|
|
| expect(blockParam.tool_use_id).toBe('test-id') |
| expect(blockParam.content).toBe('Error: Missing output') |
| }) |
| }) |
|
|
| describe('Jina Integration', () => { |
| test('jinaSearch should be available and functional', async () => { |
| const result = await jinaSearch('typescript') |
|
|
| expect(result).toBeDefined() |
| expect(typeof result).toBe('string') |
|
|
| if (!result.startsWith('Error:')) { |
| |
| expect(result.length).toBeGreaterThan(0) |
| } |
| }, 30000) |
|
|
| test('jinaSearch should handle missing API key', async () => { |
| |
| |
| expect(typeof jinaSearch).toBe('function') |
| }) |
|
|
| test('jinaSearch should handle empty query', async () => { |
| const result = await jinaSearch('') |
|
|
| expect(result).toBeDefined() |
| expect(typeof result).toBe('string') |
| }) |
| }) |
|
|
| describe('Result Formatting', () => { |
| test('should format search results with links', () => { |
| const output = { |
| query: 'test', |
| results: [{ |
| tool_use_id: 'test-1', |
| content: [{ |
| title: 'Test Title', |
| url: 'https://example.com', |
| snippet: 'Test snippet' |
| }] |
| }], |
| durationSeconds: 1 |
| } |
|
|
| const blockParam = WebSearchTool.mapToolResultToToolResultBlockParam( |
| output, |
| 'test-id' |
| ) |
|
|
| expect(blockParam.content).toContain('**Test Title**') |
| expect(blockParam.content).toContain('https://example.com') |
| expect(blockParam.content).toContain('Test snippet') |
| }) |
|
|
| test('should format multiple search results', () => { |
| const output = { |
| query: 'test', |
| results: [{ |
| tool_use_id: 'test-1', |
| content: [ |
| { |
| title: 'First Result', |
| url: 'https://example1.com', |
| snippet: 'First snippet' |
| }, |
| { |
| title: 'Second Result', |
| url: 'https://example2.com', |
| snippet: 'Second snippet' |
| } |
| ] |
| }], |
| durationSeconds: 1 |
| } |
|
|
| const blockParam = WebSearchTool.mapToolResultToToolResultBlockParam( |
| output, |
| 'test-id' |
| ) |
|
|
| expect(blockParam.content).toContain('1. **First Result**') |
| expect(blockParam.content).toContain('2. **Second Result**') |
| }) |
|
|
| test('should handle no results message', () => { |
| const output = { |
| query: 'test', |
| results: ['No results for: test'], |
| durationSeconds: 1 |
| } |
|
|
| const blockParam = WebSearchTool.mapToolResultToToolResultBlockParam( |
| output, |
| 'test-id' |
| ) |
|
|
| expect(blockParam.content).toContain('No results for: test') |
| }) |
| }) |
| }) |