2025-06-29 15:32:26 -04:00
|
|
|
/**
|
|
|
|
|
* @license
|
|
|
|
|
* Copyright 2025 Google LLC
|
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
*/
|
|
|
|
|
|
2025-07-12 21:09:12 -07:00
|
|
|
import { expect, describe, it, vi, beforeEach } from 'vitest';
|
2025-06-29 15:32:26 -04:00
|
|
|
import { ShellTool } from './shell.js';
|
|
|
|
|
import { Config } from '../config/config.js';
|
2025-07-12 21:09:12 -07:00
|
|
|
import * as summarizer from '../utils/summarizer.js';
|
|
|
|
|
import { GeminiClient } from '../core/client.js';
|
2025-07-25 12:25:32 -07:00
|
|
|
import { ToolExecuteConfirmationDetails } from './tools.js';
|
2025-07-25 12:05:21 -07:00
|
|
|
import os from 'os';
|
2025-06-29 15:32:26 -04:00
|
|
|
|
2025-07-12 21:09:12 -07:00
|
|
|
describe('ShellTool Bug Reproduction', () => {
|
|
|
|
|
let shellTool: ShellTool;
|
|
|
|
|
let config: Config;
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
config = {
|
|
|
|
|
getCoreTools: () => undefined,
|
|
|
|
|
getExcludeTools: () => undefined,
|
|
|
|
|
getDebugMode: () => false,
|
|
|
|
|
getGeminiClient: () => ({}) as GeminiClient,
|
|
|
|
|
getTargetDir: () => '.',
|
2025-07-15 10:22:31 -07:00
|
|
|
getSummarizeToolOutputConfig: () => ({
|
|
|
|
|
[shellTool.name]: {},
|
|
|
|
|
}),
|
2025-07-12 21:09:12 -07:00
|
|
|
} 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(
|
2025-07-25 12:05:21 -07:00
|
|
|
{ command: 'echo hello' },
|
2025-07-12 21:09:12 -07:00
|
|
|
abortSignal,
|
2025-07-25 12:25:32 -07:00
|
|
|
() => {},
|
2025-07-12 21:09:12 -07:00
|
|
|
);
|
|
|
|
|
|
2025-07-25 12:05:21 -07:00
|
|
|
expect(result.returnDisplay).toBe('hello' + os.EOL);
|
2025-07-12 21:09:12 -07:00
|
|
|
expect(result.llmContent).toBe('summarized output');
|
|
|
|
|
expect(summarizeSpy).toHaveBeenCalled();
|
|
|
|
|
});
|
2025-07-15 10:22:31 -07:00
|
|
|
|
|
|
|
|
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(
|
2025-07-25 12:05:21 -07:00
|
|
|
{ command: 'echo hello' },
|
2025-07-15 10:22:31 -07:00
|
|
|
abortSignal,
|
2025-07-25 12:25:32 -07:00
|
|
|
() => {},
|
2025-07-15 10:22:31 -07:00
|
|
|
);
|
|
|
|
|
|
2025-07-25 12:05:21 -07:00
|
|
|
expect(result.returnDisplay).toBe('hello' + os.EOL);
|
2025-07-15 10:22:31 -07:00
|
|
|
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;
|
2025-07-25 12:25:32 -07:00
|
|
|
await shellTool.execute({ command: 'echo "hello"' }, abortSignal, () => {});
|
2025-07-15 10:22:31 -07:00
|
|
|
|
|
|
|
|
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;
|
2025-07-25 12:25:32 -07:00
|
|
|
await shellTool.execute({ command: 'echo "hello"' }, abortSignal, () => {});
|
2025-07-15 10:22:31 -07:00
|
|
|
|
|
|
|
|
expect(summarizeSpy).toHaveBeenCalledWith(
|
|
|
|
|
expect.any(String),
|
|
|
|
|
expect.any(Object),
|
|
|
|
|
expect.any(Object),
|
|
|
|
|
undefined,
|
|
|
|
|
);
|
|
|
|
|
});
|
2025-07-24 10:13:00 -07:00
|
|
|
|
|
|
|
|
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;
|
2025-07-25 15:57:30 -07:00
|
|
|
const command =
|
|
|
|
|
os.platform() === 'win32' ? 'echo %GEMINI_CLI%' : 'echo "$GEMINI_CLI"';
|
|
|
|
|
const result = await shellTool.execute({ command }, abortSignal, () => {});
|
2025-07-24 10:13:00 -07:00
|
|
|
|
2025-07-25 12:05:21 -07:00
|
|
|
expect(result.returnDisplay).toBe('1' + os.EOL);
|
2025-07-24 10:13:00 -07:00
|
|
|
});
|
2025-07-12 21:09:12 -07:00
|
|
|
});
|
2025-07-25 12:25:32 -07:00
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
});
|
|
|
|
|
});
|