Files
gemini-cli/packages/core/src/tools/web-search.test.ts

258 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { Mock } from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { WebSearchToolParams } from './web-search.js';
import { WebSearchTool } from './web-search.js';
import type { Config } from '../config/config.js';
import { GeminiClient } from '../core/client.js';
import { ToolErrorType } from './tool-error.js';
// Mock GeminiClient and Config constructor
vi.mock('../core/client.js');
vi.mock('../config/config.js');
describe('WebSearchTool', () => {
const abortSignal = new AbortController().signal;
let mockGeminiClient: GeminiClient;
let tool: WebSearchTool;
beforeEach(() => {
const mockConfigInstance = {
getGeminiClient: () => mockGeminiClient,
getProxy: () => undefined,
generationConfigService: {
getResolvedConfig: vi.fn().mockImplementation(({ model }) => ({
model,
sdkConfig: {},
})),
},
} as unknown as Config;
mockGeminiClient = new GeminiClient(mockConfigInstance);
tool = new WebSearchTool(mockConfigInstance);
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('build', () => {
it('should return an invocation for a valid query', () => {
const params: WebSearchToolParams = { query: 'test query' };
const invocation = tool.build(params);
expect(invocation).toBeDefined();
expect(invocation.params).toEqual(params);
});
it('should throw an error for an empty query', () => {
const params: WebSearchToolParams = { query: '' };
expect(() => tool.build(params)).toThrow(
"The 'query' parameter cannot be empty.",
);
});
it('should throw an error for a query with only whitespace', () => {
const params: WebSearchToolParams = { query: ' ' };
expect(() => tool.build(params)).toThrow(
"The 'query' parameter cannot be empty.",
);
});
});
describe('getDescription', () => {
it('should return a description of the search', () => {
const params: WebSearchToolParams = { query: 'test query' };
const invocation = tool.build(params);
expect(invocation.getDescription()).toBe(
'Searching the web for: "test query"',
);
});
});
describe('execute', () => {
it('should return search results for a successful query', async () => {
const params: WebSearchToolParams = { query: 'successful query' };
(mockGeminiClient.generateContent as Mock).mockResolvedValue({
candidates: [
{
content: {
role: 'model',
parts: [{ text: 'Here are your results.' }],
},
},
],
});
const invocation = tool.build(params);
const result = await invocation.execute(abortSignal);
expect(result.llmContent).toBe(
'Web search results for "successful query":\n\nHere are your results.',
);
expect(result.returnDisplay).toBe(
'Search results for "successful query" returned.',
);
expect(result.sources).toBeUndefined();
});
it('should handle no search results found', async () => {
const params: WebSearchToolParams = { query: 'no results query' };
(mockGeminiClient.generateContent as Mock).mockResolvedValue({
candidates: [
{
content: {
role: 'model',
parts: [{ text: '' }],
},
},
],
});
const invocation = tool.build(params);
const result = await invocation.execute(abortSignal);
expect(result.llmContent).toBe(
'No search results or information found for query: "no results query"',
);
expect(result.returnDisplay).toBe('No information found.');
});
it('should return a WEB_SEARCH_FAILED error on failure', async () => {
const params: WebSearchToolParams = { query: 'error query' };
const testError = new Error('API Failure');
(mockGeminiClient.generateContent as Mock).mockRejectedValue(testError);
const invocation = tool.build(params);
const result = await invocation.execute(abortSignal);
expect(result.error?.type).toBe(ToolErrorType.WEB_SEARCH_FAILED);
expect(result.llmContent).toContain('Error:');
expect(result.llmContent).toContain('API Failure');
expect(result.returnDisplay).toBe('Error performing web search.');
});
it('should correctly format results with sources and citations', async () => {
const params: WebSearchToolParams = { query: 'grounding query' };
(mockGeminiClient.generateContent as Mock).mockResolvedValue({
candidates: [
{
content: {
role: 'model',
parts: [{ text: 'This is a test response.' }],
},
groundingMetadata: {
groundingChunks: [
{ web: { uri: 'https://example.com', title: 'Example Site' } },
{ web: { uri: 'https://google.com', title: 'Google' } },
],
groundingSupports: [
{
segment: { startIndex: 5, endIndex: 14 },
groundingChunkIndices: [0],
},
{
segment: { startIndex: 15, endIndex: 24 },
groundingChunkIndices: [0, 1],
},
],
},
},
],
});
const invocation = tool.build(params);
const result = await invocation.execute(abortSignal);
const expectedLlmContent = `Web search results for "grounding query":
This is a test[1] response.[1][2]
Sources:
[1] Example Site (https://example.com)
[2] Google (https://google.com)`;
expect(result.llmContent).toBe(expectedLlmContent);
expect(result.returnDisplay).toBe(
'Search results for "grounding query" returned.',
);
expect(result.sources).toHaveLength(2);
});
it('should insert markers at correct byte positions for multibyte text', async () => {
const params: WebSearchToolParams = { query: 'multibyte query' };
(mockGeminiClient.generateContent as Mock).mockResolvedValue({
candidates: [
{
content: {
role: 'model',
parts: [{ text: 'こんにちは! Gemini CLI✨' }],
},
groundingMetadata: {
groundingChunks: [
{
web: {
title: 'Japanese Greeting',
uri: 'https://example.test/japanese-greeting',
},
},
{
web: {
title: 'google-gemini/gemini-cli',
uri: 'https://github.com/google-gemini/gemini-cli',
},
},
{
web: {
title: 'Gemini CLI: your open-source AI agent',
uri: 'https://blog.google/technology/developers/introducing-gemini-cli-open-source-ai-agent/',
},
},
],
groundingSupports: [
{
segment: {
// Byte range of "こんにちは!" (utf-8 encoded)
startIndex: 0,
endIndex: 16,
},
groundingChunkIndices: [0],
},
{
segment: {
// Byte range of "Gemini CLI✨" (utf-8 encoded)
startIndex: 17,
endIndex: 33,
},
groundingChunkIndices: [1, 2],
},
],
},
},
],
});
const invocation = tool.build(params);
const result = await invocation.execute(abortSignal);
const expectedLlmContent = `Web search results for "multibyte query":
こんにちは![1] Gemini CLI✨[2][3]
Sources:
[1] Japanese Greeting (https://example.test/japanese-greeting)
[2] google-gemini/gemini-cli (https://github.com/google-gemini/gemini-cli)
[3] Gemini CLI: your open-source AI agent (https://blog.google/technology/developers/introducing-gemini-cli-open-source-ai-agent/)`;
expect(result.llmContent).toBe(expectedLlmContent);
expect(result.returnDisplay).toBe(
'Search results for "multibyte query" returned.',
);
expect(result.sources).toHaveLength(3);
});
});
});