Skip to main content
Glama
web-search.test.ts6.47 kB
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { webSearch } from '../../src/tools/web-search.js'; import { ResponseFormat } from '../../src/types.js'; import type { Ollama } from 'ollama'; import { HttpError } from '../../src/utils/http-error.js'; // Mock fetch globally global.fetch = vi.fn(); describe('webSearch', () => { const mockOllama = {} as Ollama; const testQuery = 'test search query'; const maxResults = 5; beforeEach(() => { vi.clearAllMocks(); process.env.OLLAMA_API_KEY = 'test-api-key'; }); afterEach(() => { delete process.env.OLLAMA_API_KEY; }); it('should throw error if OLLAMA_API_KEY is not set', async () => { // Arrange delete process.env.OLLAMA_API_KEY; // Act & Assert await expect(webSearch(mockOllama, testQuery, maxResults, ResponseFormat.JSON)) .rejects.toThrow('OLLAMA_API_KEY environment variable is required'); }); it('should successfully perform web search', async () => { // Arrange const mockResponse = { results: [ { title: 'Result 1', url: 'https://example.com/1', snippet: 'Test snippet 1' }, { title: 'Result 2', url: 'https://example.com/2', snippet: 'Test snippet 2' }, ], }; (global.fetch as any).mockResolvedValueOnce({ ok: true, json: async () => mockResponse, }); // Act const result = await webSearch(mockOllama, testQuery, maxResults, ResponseFormat.JSON); // Assert expect(global.fetch).toHaveBeenCalledWith( 'https://ollama.com/api/web_search', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer test-api-key', }, body: JSON.stringify({ query: testQuery, max_results: maxResults }), signal: expect.any(AbortSignal), } ); expect(result).toContain('Result 1'); expect(result).toContain('Result 2'); }); it('should successfully complete on first attempt (no retry needed)', async () => { // Arrange const mockResponse = { results: [ { title: 'Result 1', url: 'https://example.com/1', snippet: 'Test' }, ], }; (global.fetch as any).mockResolvedValueOnce({ ok: true, json: async () => mockResponse, }); // Act const result = await webSearch(mockOllama, testQuery, maxResults, ResponseFormat.JSON); // Assert expect(result).toContain('Result 1'); expect(global.fetch).toHaveBeenCalledTimes(1); }); it('should retry on 429 rate limit error and eventually succeed', async () => { // Arrange const mockResponse = { results: [ { title: 'Success after retry', url: 'https://example.com', snippet: 'Test' }, ], }; // Mock setTimeout to execute immediately (avoid real delays in tests) vi.spyOn(global, 'setTimeout').mockImplementation(((callback: any) => { Promise.resolve().then(() => callback()); return 0 as any; }) as any); // First call returns 429, second call succeeds (global.fetch as any) .mockResolvedValueOnce({ ok: false, status: 429, statusText: 'Too Many Requests', headers: { get: vi.fn().mockReturnValue(null), }, }) .mockResolvedValueOnce({ ok: true, json: async () => mockResponse, }); // Act const result = await webSearch(mockOllama, testQuery, maxResults, ResponseFormat.JSON); // Assert expect(result).toContain('Success after retry'); expect(global.fetch).toHaveBeenCalledTimes(2); }); it('should throw error on non-retryable HTTP errors', async () => { // Arrange - 501 Not Implemented is not retried (global.fetch as any).mockResolvedValueOnce({ ok: false, status: 501, statusText: 'Not Implemented', headers: { get: vi.fn().mockReturnValue(null), }, }); // Act & Assert await expect(webSearch(mockOllama, testQuery, maxResults, ResponseFormat.JSON)) .rejects.toThrow('Web search failed: 501 Not Implemented'); }); it('should throw error on network timeout (no status code)', async () => { // Arrange const networkError = new Error('Network timeout - no response from server'); (global.fetch as any).mockRejectedValueOnce(networkError); // Act & Assert await expect(webSearch(mockOllama, testQuery, maxResults, ResponseFormat.JSON)) .rejects.toThrow('Network timeout - no response from server'); // Should not retry network errors expect(global.fetch).toHaveBeenCalledTimes(1); }); it('should throw error when response.json() fails (malformed JSON)', async () => { // Arrange (global.fetch as any).mockResolvedValueOnce({ ok: true, json: async () => { throw new Error('Unexpected token < in JSON at position 0'); }, }); // Act & Assert await expect(webSearch(mockOllama, testQuery, maxResults, ResponseFormat.JSON)) .rejects.toThrow('Unexpected token < in JSON at position 0'); }); it('should handle fetch abort/cancel errors', async () => { // Arrange const abortError = new Error('The operation was aborted'); abortError.name = 'AbortError'; (global.fetch as any).mockRejectedValueOnce(abortError); // Act & Assert // Note: fetchWithTimeout transforms AbortError to timeout message await expect(webSearch(mockOllama, testQuery, maxResults, ResponseFormat.JSON)) .rejects.toThrow('Request to https://ollama.com/api/web_search timed out after 30000ms'); // Should not retry abort errors expect(global.fetch).toHaveBeenCalledTimes(1); }); it('should eventually fail after multiple 429 retries', async () => { // Arrange vi.spyOn(global, 'setTimeout').mockImplementation(((callback: any) => { Promise.resolve().then(() => callback()); return 0 as any; }) as any); // Always return 429 (will exhaust retries) (global.fetch as any).mockResolvedValue({ ok: false, status: 429, statusText: 'Too Many Requests', headers: { get: vi.fn().mockReturnValue(null), }, }); // Act & Assert await expect(webSearch(mockOllama, testQuery, maxResults, ResponseFormat.JSON)) .rejects.toThrow('Web search failed: 429 Too Many Requests'); // Should attempt initial + 3 retries = 4 total expect(global.fetch).toHaveBeenCalledTimes(4); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/rawveg/ollama-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server