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

170 lines
5.1 KiB
TypeScript
Raw Normal View History

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { expect, describe, it, vi, beforeEach } from 'vitest';
import { ShellTool } from './shell.js';
import { Config } from '../config/config.js';
import * as summarizer from '../utils/summarizer.js';
import { GeminiClient } from '../core/client.js';
import { ToolExecuteConfirmationDetails } from './tools.js';
import os from 'os';
describe('ShellTool Bug Reproduction', () => {
let shellTool: ShellTool;
let config: Config;
beforeEach(() => {
config = {
getCoreTools: () => undefined,
getExcludeTools: () => undefined,
getDebugMode: () => false,
getGeminiClient: () => ({}) as GeminiClient,
getTargetDir: () => '.',
getSummarizeToolOutputConfig: () => ({
[shellTool.name]: {},
}),
} as unknown as Config;
shellTool = new ShellTool(config);
});
it('should not let the summarizer override the return display', async () => {
const summarizeSpy = vi
.spyOn(summarizer, 'summarizeToolOutput')
.mockResolvedValue('summarized output');
const abortSignal = new AbortController().signal;
const result = await shellTool.execute(
{ command: 'echo hello' },
abortSignal,
() => {},
);
expect(result.returnDisplay).toBe('hello' + os.EOL);
expect(result.llmContent).toBe('summarized output');
expect(summarizeSpy).toHaveBeenCalled();
});
it('should not call summarizer if disabled in config', async () => {
config = {
getCoreTools: () => undefined,
getExcludeTools: () => undefined,
getDebugMode: () => false,
getGeminiClient: () => ({}) as GeminiClient,
getTargetDir: () => '.',
getSummarizeToolOutputConfig: () => ({}),
} as unknown as Config;
shellTool = new ShellTool(config);
const summarizeSpy = vi
.spyOn(summarizer, 'summarizeToolOutput')
.mockResolvedValue('summarized output');
const abortSignal = new AbortController().signal;
const result = await shellTool.execute(
{ command: 'echo hello' },
abortSignal,
() => {},
);
expect(result.returnDisplay).toBe('hello' + os.EOL);
expect(result.llmContent).not.toBe('summarized output');
expect(summarizeSpy).not.toHaveBeenCalled();
});
it('should pass token budget to summarizer', async () => {
config = {
getCoreTools: () => undefined,
getExcludeTools: () => undefined,
getDebugMode: () => false,
getGeminiClient: () => ({}) as GeminiClient,
getTargetDir: () => '.',
getSummarizeToolOutputConfig: () => ({
[shellTool.name]: { tokenBudget: 1000 },
}),
} as unknown as Config;
shellTool = new ShellTool(config);
const summarizeSpy = vi
.spyOn(summarizer, 'summarizeToolOutput')
.mockResolvedValue('summarized output');
const abortSignal = new AbortController().signal;
await shellTool.execute({ command: 'echo "hello"' }, abortSignal, () => {});
expect(summarizeSpy).toHaveBeenCalledWith(
expect.any(String),
expect.any(Object),
expect.any(Object),
1000,
);
});
it('should use default token budget if not specified', async () => {
config = {
getCoreTools: () => undefined,
getExcludeTools: () => undefined,
getDebugMode: () => false,
getGeminiClient: () => ({}) as GeminiClient,
getTargetDir: () => '.',
getSummarizeToolOutputConfig: () => ({
[shellTool.name]: {},
}),
} as unknown as Config;
shellTool = new ShellTool(config);
const summarizeSpy = vi
.spyOn(summarizer, 'summarizeToolOutput')
.mockResolvedValue('summarized output');
const abortSignal = new AbortController().signal;
await shellTool.execute({ command: 'echo "hello"' }, abortSignal, () => {});
expect(summarizeSpy).toHaveBeenCalledWith(
expect.any(String),
expect.any(Object),
expect.any(Object),
undefined,
);
});
it('should pass GEMINI_CLI environment variable to executed commands', async () => {
config = {
getCoreTools: () => undefined,
getExcludeTools: () => undefined,
getDebugMode: () => false,
getGeminiClient: () => ({}) as GeminiClient,
getTargetDir: () => '.',
getSummarizeToolOutputConfig: () => ({}),
} as unknown as Config;
shellTool = new ShellTool(config);
const abortSignal = new AbortController().signal;
const result = await shellTool.execute(
{ command: 'echo "$GEMINI_CLI"' },
abortSignal,
() => {},
);
expect(result.returnDisplay).toBe('1' + os.EOL);
});
});
describe('shouldConfirmExecute', () => {
it('should de-duplicate command roots before asking for confirmation', async () => {
const shellTool = new ShellTool({
getCoreTools: () => ['run_shell_command'],
getExcludeTools: () => [],
} as unknown as Config);
const result = (await shellTool.shouldConfirmExecute(
{
command: 'git status && git log',
},
new AbortController().signal,
)) as ToolExecuteConfirmationDetails;
expect(result.rootCommand).toEqual('git');
});
});