/** * @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'); }); });