/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { renderWithProviders } from '../../../test-utils/render.js'; import { describe, it, expect, vi, afterEach } from 'vitest'; import { ToolGroupMessage } from './ToolGroupMessage.js'; import type { IndividualToolCallDisplay } from '../../types.js'; import { ToolCallStatus } from '../../types.js'; import { Scrollable } from '../shared/Scrollable.js'; import { ASK_USER_DISPLAY_NAME, makeFakeConfig } from '@google/gemini-cli-core'; import os from 'node:os'; describe('', () => { afterEach(() => { vi.restoreAllMocks(); }); const createToolCall = ( overrides: Partial = {}, ): IndividualToolCallDisplay => ({ callId: 'tool-123', name: 'test-tool', description: 'A tool for testing', resultDisplay: 'Test result', status: ToolCallStatus.Success, confirmationDetails: undefined, renderOutputAsMarkdown: false, ...overrides, }); const baseProps = { groupId: 1, terminalWidth: 80, }; const baseMockConfig = makeFakeConfig({ model: 'gemini-pro', targetDir: os.tmpdir(), debugMode: false, folderTrust: false, ideMode: false, enableInteractiveShell: true, }); describe('Golden Snapshots', () => { it('renders single successful tool call', () => { const toolCalls = [createToolCall()]; const { lastFrame, unmount } = renderWithProviders( , { config: baseMockConfig, uiState: { pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }], }, }, ); expect(lastFrame()).toMatchSnapshot(); unmount(); }); it('hides confirming tools (standard behavior)', () => { const toolCalls = [ createToolCall({ callId: 'confirm-tool', status: ToolCallStatus.Confirming, confirmationDetails: { type: 'info', title: 'Confirm tool', prompt: 'Do you want to proceed?', onConfirm: vi.fn(), }, }), ]; const { lastFrame, unmount } = renderWithProviders( , { config: baseMockConfig }, ); // Should render nothing because all tools in the group are confirming expect(lastFrame()).toBe(''); unmount(); }); it('renders multiple tool calls with different statuses (only visible ones)', () => { const toolCalls = [ createToolCall({ callId: 'tool-1', name: 'successful-tool', description: 'This tool succeeded', status: ToolCallStatus.Success, }), createToolCall({ callId: 'tool-2', name: 'pending-tool', description: 'This tool is pending', status: ToolCallStatus.Pending, }), createToolCall({ callId: 'tool-3', name: 'error-tool', description: 'This tool failed', status: ToolCallStatus.Error, }), ]; const { lastFrame, unmount } = renderWithProviders( , { config: baseMockConfig, uiState: { pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }], }, }, ); // pending-tool should be hidden const output = lastFrame(); expect(output).toContain('successful-tool'); expect(output).not.toContain('pending-tool'); expect(output).toContain('error-tool'); expect(output).toMatchSnapshot(); unmount(); }); it('renders mixed tool calls including shell command', () => { const toolCalls = [ createToolCall({ callId: 'tool-1', name: 'read_file', description: 'Read a file', status: ToolCallStatus.Success, }), createToolCall({ callId: 'tool-2', name: 'run_shell_command', description: 'Run command', status: ToolCallStatus.Executing, }), createToolCall({ callId: 'tool-3', name: 'write_file', description: 'Write to file', status: ToolCallStatus.Pending, }), ]; const { lastFrame, unmount } = renderWithProviders( , { config: baseMockConfig, uiState: { pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }], }, }, ); // write_file (Pending) should be hidden const output = lastFrame(); expect(output).toContain('read_file'); expect(output).toContain('run_shell_command'); expect(output).not.toContain('write_file'); expect(output).toMatchSnapshot(); unmount(); }); it('renders with limited terminal height', () => { const toolCalls = [ createToolCall({ callId: 'tool-1', name: 'tool-with-result', description: 'Tool with output', resultDisplay: 'This is a long result that might need height constraints', }), createToolCall({ callId: 'tool-2', name: 'another-tool', description: 'Another tool', resultDisplay: 'More output here', }), ]; const { lastFrame, unmount } = renderWithProviders( , { config: baseMockConfig, uiState: { pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }], }, }, ); expect(lastFrame()).toMatchSnapshot(); unmount(); }); it('renders with narrow terminal width', () => { const toolCalls = [ createToolCall({ name: 'very-long-tool-name-that-might-wrap', description: 'This is a very long description that might cause wrapping issues', }), ]; const { lastFrame, unmount } = renderWithProviders( , { config: baseMockConfig, uiState: { pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }], }, }, ); expect(lastFrame()).toMatchSnapshot(); unmount(); }); it('renders empty tool calls array', () => { const { lastFrame, unmount } = renderWithProviders( , { config: baseMockConfig, uiState: { pendingHistoryItems: [{ type: 'tool_group', tools: [] }], }, }, ); expect(lastFrame()).toMatchSnapshot(); unmount(); }); it('renders header when scrolled', () => { const toolCalls = [ createToolCall({ callId: '1', name: 'tool-1', description: 'Description 1. This is a long description that will need to be truncated if the terminal width is small.', resultDisplay: 'line1\nline2\nline3\nline4\nline5', }), createToolCall({ callId: '2', name: 'tool-2', description: 'Description 2', resultDisplay: 'line1\nline2', }), ]; const { lastFrame, unmount } = renderWithProviders( , { config: baseMockConfig, uiState: { pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }], }, }, ); expect(lastFrame()).toMatchSnapshot(); unmount(); }); it('renders tool call with outputFile', () => { const toolCalls = [ createToolCall({ callId: 'tool-output-file', name: 'tool-with-file', description: 'Tool that saved output to file', status: ToolCallStatus.Success, outputFile: '/path/to/output.txt', }), ]; const { lastFrame, unmount } = renderWithProviders( , { config: baseMockConfig, uiState: { pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }], }, }, ); expect(lastFrame()).toMatchSnapshot(); unmount(); }); it('renders two tool groups where only the last line of the previous group is visible', () => { const toolCalls1 = [ createToolCall({ callId: '1', name: 'tool-1', description: 'Description 1', resultDisplay: 'line1\nline2\nline3\nline4\nline5', }), ]; const toolCalls2 = [ createToolCall({ callId: '2', name: 'tool-2', description: 'Description 2', resultDisplay: 'line1', }), ]; const { lastFrame, unmount } = renderWithProviders( , { config: baseMockConfig, uiState: { pendingHistoryItems: [ { type: 'tool_group', tools: toolCalls1 }, { type: 'tool_group', tools: toolCalls2 }, ], }, }, ); expect(lastFrame()).toMatchSnapshot(); unmount(); }); }); describe('Border Color Logic', () => { it('uses yellow border for shell commands even when successful', () => { const toolCalls = [ createToolCall({ name: 'run_shell_command', status: ToolCallStatus.Success, }), ]; const { lastFrame, unmount } = renderWithProviders( , { config: baseMockConfig, uiState: { pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }], }, }, ); expect(lastFrame()).toMatchSnapshot(); unmount(); }); it('uses gray border when all tools are successful and no shell commands', () => { const toolCalls = [ createToolCall({ status: ToolCallStatus.Success }), createToolCall({ callId: 'tool-2', name: 'another-tool', status: ToolCallStatus.Success, }), ]; const { lastFrame, unmount } = renderWithProviders( , { config: baseMockConfig, uiState: { pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }], }, }, ); expect(lastFrame()).toMatchSnapshot(); unmount(); }); }); describe('Height Calculation', () => { it('calculates available height correctly with multiple tools with results', () => { const toolCalls = [ createToolCall({ callId: 'tool-1', resultDisplay: 'Result 1', }), createToolCall({ callId: 'tool-2', resultDisplay: 'Result 2', }), createToolCall({ callId: 'tool-3', resultDisplay: '', // No result }), ]; const { lastFrame, unmount } = renderWithProviders( , { config: baseMockConfig, uiState: { pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }], }, }, ); expect(lastFrame()).toMatchSnapshot(); unmount(); }); }); describe('Ask User Filtering', () => { it.each([ ToolCallStatus.Pending, ToolCallStatus.Executing, ToolCallStatus.Confirming, ])('filters out ask_user when status is %s', (status) => { const toolCalls = [ createToolCall({ callId: `ask-user-${status}`, name: ASK_USER_DISPLAY_NAME, status, }), ]; const { lastFrame, unmount } = renderWithProviders( , { config: baseMockConfig }, ); expect(lastFrame()).toMatchSnapshot(); unmount(); }); it.each([ToolCallStatus.Success, ToolCallStatus.Error])( 'does NOT filter out ask_user when status is %s', (status) => { const toolCalls = [ createToolCall({ callId: `ask-user-${status}`, name: ASK_USER_DISPLAY_NAME, status, }), ]; const { lastFrame, unmount } = renderWithProviders( , { config: baseMockConfig }, ); expect(lastFrame()).toMatchSnapshot(); unmount(); }, ); it('shows other tools when ask_user is filtered out', () => { const toolCalls = [ createToolCall({ callId: 'other-tool', name: 'other-tool', status: ToolCallStatus.Success, }), createToolCall({ callId: 'ask-user-pending', name: ASK_USER_DISPLAY_NAME, status: ToolCallStatus.Pending, }), ]; const { lastFrame, unmount } = renderWithProviders( , { config: baseMockConfig }, ); expect(lastFrame()).toMatchSnapshot(); unmount(); }); it('renders nothing when only tool is in-progress AskUser with borderBottom=false', () => { // AskUser tools in progress are rendered by AskUserDialog, not ToolGroupMessage. // When AskUser is the only tool and borderBottom=false (no border to close), // the component should render nothing. const toolCalls = [ createToolCall({ callId: 'ask-user-tool', name: ASK_USER_DISPLAY_NAME, status: ToolCallStatus.Executing, }), ]; const { lastFrame, unmount } = renderWithProviders( , { config: baseMockConfig }, ); // AskUser tools in progress are rendered by AskUserDialog, so we expect nothing. expect(lastFrame()).toBe(''); unmount(); }); }); });