/** * @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 { UPDATE_TOPIC_TOOL_NAME, TOPIC_PARAM_TITLE, TOPIC_PARAM_STRATEGIC_INTENT, makeFakeConfig, CoreToolCallStatus, ApprovalMode, ASK_USER_DISPLAY_NAME, WRITE_FILE_DISPLAY_NAME, EDIT_DISPLAY_NAME, READ_FILE_DISPLAY_NAME, GLOB_DISPLAY_NAME, } from '@google/gemini-cli-core'; import type { HistoryItem, HistoryItemWithoutId, IndividualToolCallDisplay, } from '../../types.js'; import { Scrollable } from '../shared/Scrollable.js'; import os from 'node:os'; import { createMockSettings } from '../../../test-utils/settings.js'; describe('', () => { afterEach(() => { vi.restoreAllMocks(); }); const createToolCall = ( overrides: Partial = {}, ): IndividualToolCallDisplay => ({ callId: 'tool-123', name: 'test-tool', args: {}, description: 'A tool for testing', resultDisplay: 'Test result', status: CoreToolCallStatus.Success, confirmationDetails: undefined, renderOutputAsMarkdown: false, ...overrides, }); const baseProps = { terminalWidth: 80, }; const createItem = ( tools: IndividualToolCallDisplay[], ): HistoryItem | HistoryItemWithoutId => ({ id: 1, type: 'tool_group', tools, }); const baseMockConfig = makeFakeConfig({ model: 'gemini-pro', targetDir: os.tmpdir(), debugMode: false, folderTrust: false, ideMode: false, enableInteractiveShell: true, }); const fullVerbositySettings = createMockSettings({ ui: { errorVerbosity: 'full' }, }); const lowVerbositySettings = createMockSettings({ ui: { errorVerbosity: 'low' }, }); describe('Golden Snapshots', () => { it('renders single successful tool call', async () => { const toolCalls = [createToolCall()]; const item = createItem(toolCalls); const { lastFrame, unmount } = await renderWithProviders( , { config: baseMockConfig, settings: fullVerbositySettings, uiState: { pendingHistoryItems: [ { type: 'tool_group', tools: toolCalls, }, ], }, }, ); expect(lastFrame({ allowEmpty: true })).toMatchSnapshot(); unmount(); }); it('hides confirming tools (standard behavior)', async () => { const toolCalls = [ createToolCall({ callId: 'confirm-tool', status: CoreToolCallStatus.AwaitingApproval, confirmationDetails: { type: 'info', title: 'Confirm tool', prompt: 'Do you want to proceed?', }, }), ]; const item = createItem(toolCalls); const { lastFrame, unmount } = await renderWithProviders( , { config: baseMockConfig, settings: fullVerbositySettings }, ); // Should now hide confirming tools (to avoid duplication with Global Queue) expect(lastFrame({ allowEmpty: true })).toBe(''); unmount(); }); it('renders canceled tool calls', async () => { const toolCalls = [ createToolCall({ callId: 'canceled-tool', name: 'canceled-tool', status: CoreToolCallStatus.Cancelled, }), ]; const item = createItem(toolCalls); const { lastFrame, unmount } = await renderWithProviders( , { config: baseMockConfig, settings: fullVerbositySettings }, ); const output = lastFrame(); expect(output).toMatchSnapshot('canceled_tool'); unmount(); }); it('renders multiple tool calls with different statuses (only visible ones)', async () => { const toolCalls = [ createToolCall({ callId: 'tool-1', name: 'successful-tool', description: 'This tool succeeded', status: CoreToolCallStatus.Success, }), createToolCall({ callId: 'tool-2', name: 'pending-tool', description: 'This tool is pending', status: CoreToolCallStatus.Scheduled, }), createToolCall({ callId: 'tool-3', name: 'error-tool', description: 'This tool failed', status: CoreToolCallStatus.Error, }), ]; const item = createItem(toolCalls); const { lastFrame, unmount } = await renderWithProviders( , { config: baseMockConfig, settings: fullVerbositySettings, uiState: { pendingHistoryItems: [ { type: 'tool_group', tools: toolCalls, }, ], }, }, ); // pending-tool should now be visible const output = lastFrame(); expect(output).toContain('successful-tool'); expect(output).toContain('pending-tool'); expect(output).toContain('error-tool'); expect(output).toMatchSnapshot(); unmount(); }); it('hides errored tool calls in low error verbosity mode', async () => { const toolCalls = [ createToolCall({ callId: 'tool-1', name: 'successful-tool', status: CoreToolCallStatus.Success, }), createToolCall({ callId: 'tool-2', name: 'error-tool', status: CoreToolCallStatus.Error, resultDisplay: 'Tool failed', }), ]; const item = createItem(toolCalls); const { lastFrame, unmount } = await renderWithProviders( , { config: baseMockConfig, uiState: { pendingHistoryItems: [ { type: 'tool_group', tools: toolCalls, }, ], }, }, ); const output = lastFrame(); expect(output).toContain('successful-tool'); expect(output).not.toContain('error-tool'); unmount(); }); it('keeps client-initiated errored tool calls visible in low error verbosity mode', async () => { const toolCalls = [ createToolCall({ callId: 'tool-1', name: 'client-error-tool', status: CoreToolCallStatus.Error, isClientInitiated: true, resultDisplay: 'Client tool failed', }), ]; const item = createItem(toolCalls); const { lastFrame, unmount } = await renderWithProviders( , { config: baseMockConfig, uiState: { pendingHistoryItems: [ { type: 'tool_group', tools: toolCalls, }, ], }, }, ); const output = lastFrame(); expect(output).toContain('client-error-tool'); unmount(); }); it('renders update_topic tool call using TopicMessage', async () => { const toolCalls = [ createToolCall({ callId: 'topic-tool', name: UPDATE_TOPIC_TOOL_NAME, args: { [TOPIC_PARAM_TITLE]: 'Testing Topic', [TOPIC_PARAM_STRATEGIC_INTENT]: 'This is the description', }, }), ]; const item = createItem(toolCalls); const { lastFrame, unmount } = await renderWithProviders( , { config: baseMockConfig, settings: fullVerbositySettings, }, ); const output = lastFrame(); expect(output).toContain('Testing Topic: '); expect(output).toContain('This is the description'); expect(output).toMatchSnapshot('update_topic_tool'); unmount(); }); it('renders update_topic tool call with summary instead of strategic_intent', async () => { const toolCalls = [ createToolCall({ callId: 'topic-tool-summary', name: UPDATE_TOPIC_TOOL_NAME, args: { [TOPIC_PARAM_TITLE]: 'Testing Topic', summary: 'This is the summary', }, }), ]; const item = createItem(toolCalls); const { lastFrame, unmount } = await renderWithProviders( , { config: baseMockConfig, settings: fullVerbositySettings, }, ); const output = lastFrame(); expect(output).toContain('Testing Topic: '); expect(output).toContain('This is the summary'); unmount(); }); it('renders mixed tool calls including update_topic', async () => { const toolCalls = [ createToolCall({ callId: 'topic-tool-mixed', name: UPDATE_TOPIC_TOOL_NAME, args: { [TOPIC_PARAM_TITLE]: 'Testing Topic', [TOPIC_PARAM_STRATEGIC_INTENT]: 'This is the description', }, }), createToolCall({ callId: 'tool-1', name: 'read_file', description: 'Read a file', status: CoreToolCallStatus.Success, }), createToolCall({ callId: 'tool-2', name: 'run_shell_command', description: 'Run command', status: CoreToolCallStatus.Executing, }), createToolCall({ callId: 'tool-3', name: 'write_file', description: 'Write to file', status: CoreToolCallStatus.Scheduled, }), ]; const item = createItem(toolCalls); const { lastFrame, unmount } = await renderWithProviders( , { config: baseMockConfig, settings: fullVerbositySettings, uiState: { pendingHistoryItems: [ { type: 'tool_group', tools: toolCalls, }, ], }, }, ); // write_file (Pending) should now be visible const output = lastFrame(); expect(output).toContain('read_file'); expect(output).toContain('run_shell_command'); expect(output).toContain('write_file'); expect(output).toMatchSnapshot(); unmount(); }); it('renders with limited terminal height', async () => { 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 item = createItem(toolCalls); const { lastFrame, unmount } = await renderWithProviders( , { config: baseMockConfig, settings: fullVerbositySettings, uiState: { pendingHistoryItems: [ { type: 'tool_group', tools: toolCalls, }, ], }, }, ); expect(lastFrame({ allowEmpty: true })).toMatchSnapshot(); unmount(); }); it('renders with narrow terminal width', async () => { const toolCalls = [ createToolCall({ name: 'very-long-tool-name-that-might-wrap', description: 'This is a very long description that might cause wrapping issues', }), ]; const item = createItem(toolCalls); const { lastFrame, unmount } = await renderWithProviders( , { config: baseMockConfig, settings: fullVerbositySettings, uiState: { pendingHistoryItems: [ { type: 'tool_group', tools: toolCalls, }, ], }, }, ); expect(lastFrame({ allowEmpty: true })).toMatchSnapshot(); unmount(); }); it('renders empty tool calls array', async () => { const toolCalls: IndividualToolCallDisplay[] = []; const item = createItem(toolCalls); const { lastFrame, unmount } = await renderWithProviders( , { config: baseMockConfig, settings: fullVerbositySettings, uiState: { pendingHistoryItems: [ { type: 'tool_group', tools: [], }, ], }, }, ); expect(lastFrame({ allowEmpty: true })).toMatchSnapshot(); unmount(); }); it('renders header when scrolled', async () => { 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 item = createItem(toolCalls); const { lastFrame, unmount } = await renderWithProviders( , { config: baseMockConfig, settings: fullVerbositySettings, uiState: { pendingHistoryItems: [ { type: 'tool_group', tools: toolCalls, }, ], }, }, ); expect(lastFrame({ allowEmpty: true })).toMatchSnapshot(); unmount(); }); it('renders tool call with outputFile', async () => { const toolCalls = [ createToolCall({ callId: 'tool-output-file', name: 'tool-with-file', description: 'Tool that saved output to file', status: CoreToolCallStatus.Success, outputFile: '/path/to/output.txt', }), ]; const item = createItem(toolCalls); const { lastFrame, unmount } = await renderWithProviders( , { config: baseMockConfig, settings: fullVerbositySettings, uiState: { pendingHistoryItems: [ { type: 'tool_group', tools: toolCalls, }, ], }, }, ); expect(lastFrame({ allowEmpty: true })).toMatchSnapshot(); unmount(); }); it('renders two tool groups where only the last line of the previous group is visible', async () => { const toolCalls1 = [ createToolCall({ callId: '1', name: 'tool-1', description: 'Description 1', resultDisplay: 'line1\nline2\nline3\nline4\nline5', }), ]; const item1 = createItem(toolCalls1); const toolCalls2 = [ createToolCall({ callId: '2', name: 'tool-2', description: 'Description 2', resultDisplay: 'line1', }), ]; const item2 = createItem(toolCalls2); const { lastFrame, unmount } = await renderWithProviders( , { config: baseMockConfig, settings: fullVerbositySettings, uiState: { pendingHistoryItems: [ { type: 'tool_group', tools: toolCalls1, }, { type: 'tool_group', tools: toolCalls2, }, ], }, }, ); expect(lastFrame({ allowEmpty: true })).toMatchSnapshot(); unmount(); }); }); describe('Border Color Logic', () => { it('uses yellow border for shell commands even when successful', async () => { const toolCalls = [ createToolCall({ name: 'run_shell_command', status: CoreToolCallStatus.Success, }), ]; const item = createItem(toolCalls); const { lastFrame, unmount } = await renderWithProviders( , { config: baseMockConfig, settings: fullVerbositySettings, uiState: { pendingHistoryItems: [ { type: 'tool_group', tools: toolCalls, }, ], }, }, ); expect(lastFrame({ allowEmpty: true })).toMatchSnapshot(); unmount(); }); it('uses gray border when all tools are successful and no shell commands', async () => { const toolCalls = [ createToolCall({ status: CoreToolCallStatus.Success }), createToolCall({ callId: 'tool-2', name: 'another-tool', status: CoreToolCallStatus.Success, }), ]; const item = createItem(toolCalls); const { lastFrame, unmount } = await renderWithProviders( , { config: baseMockConfig, settings: fullVerbositySettings, uiState: { pendingHistoryItems: [ { type: 'tool_group', tools: toolCalls, }, ], }, }, ); expect(lastFrame({ allowEmpty: true })).toMatchSnapshot(); unmount(); }); }); describe('Height Calculation', () => { it('calculates available height correctly with multiple tools with results', async () => { const toolCalls = [ createToolCall({ callId: 'tool-1', resultDisplay: 'Result 1', }), createToolCall({ callId: 'tool-2', resultDisplay: 'Result 2', }), createToolCall({ callId: 'tool-3', resultDisplay: '', // No result }), ]; const item = createItem(toolCalls); const { lastFrame, unmount } = await renderWithProviders( , { config: baseMockConfig, settings: fullVerbositySettings, uiState: { pendingHistoryItems: [ { type: 'tool_group', tools: toolCalls, }, ], }, }, ); expect(lastFrame({ allowEmpty: true })).toMatchSnapshot(); unmount(); }); }); describe('Ask User Filtering', () => { it.each([ { status: CoreToolCallStatus.Scheduled, resultDisplay: 'test result', shouldHide: true, }, { status: CoreToolCallStatus.Executing, resultDisplay: 'test result', shouldHide: true, }, { status: CoreToolCallStatus.AwaitingApproval, resultDisplay: 'test result', shouldHide: true, }, { status: CoreToolCallStatus.Success, resultDisplay: 'test result', shouldHide: false, }, { status: CoreToolCallStatus.Error, resultDisplay: '', shouldHide: true }, { status: CoreToolCallStatus.Error, resultDisplay: 'error message', shouldHide: false, }, ])( 'filtering logic for status=$status and hasResult=$resultDisplay', async ({ status, resultDisplay, shouldHide }) => { const toolCalls = [ createToolCall({ callId: `ask-user-${status}`, name: ASK_USER_DISPLAY_NAME, status, resultDisplay, }), ]; const item = createItem(toolCalls); const { lastFrame, unmount } = await renderWithProviders( , { config: baseMockConfig, settings: fullVerbositySettings }, ); if (shouldHide) { expect(lastFrame({ allowEmpty: true })).toBe(''); } else { expect(lastFrame()).toMatchSnapshot(); } unmount(); }, ); it('shows other tools when ask_user is filtered out', async () => { const toolCalls = [ createToolCall({ callId: 'other-tool', name: 'other-tool', status: CoreToolCallStatus.Success, }), createToolCall({ callId: 'ask-user-pending', name: ASK_USER_DISPLAY_NAME, status: CoreToolCallStatus.Scheduled, }), ]; const item = createItem(toolCalls); const { lastFrame, unmount } = await renderWithProviders( , { config: baseMockConfig, settings: fullVerbositySettings }, ); expect(lastFrame({ allowEmpty: true })).toMatchSnapshot(); unmount(); }); it('renders nothing when only tool is in-progress AskUser with borderBottom=false', async () => { // 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: CoreToolCallStatus.Executing, }), ]; const item = createItem(toolCalls); const { lastFrame, unmount } = await renderWithProviders( , { config: baseMockConfig, settings: fullVerbositySettings }, ); // AskUser tools in progress are rendered by AskUserDialog, so we expect nothing. expect(lastFrame({ allowEmpty: true })).toBe(''); unmount(); }); it('does not render a bottom-border fragment when all tools are filtered out', async () => { const toolCalls = [ createToolCall({ callId: 'hidden-error-tool', name: 'error-tool', status: CoreToolCallStatus.Error, resultDisplay: 'Hidden in low verbosity', isClientInitiated: false, }), ]; const item = createItem(toolCalls); const { lastFrame, unmount } = await renderWithProviders( , { config: baseMockConfig, settings: lowVerbositySettings, }, ); expect(lastFrame({ allowEmpty: true })).toBe(''); unmount(); }); it('still renders explicit closing slices for split static/pending groups', async () => { const toolCalls: IndividualToolCallDisplay[] = []; const item = createItem(toolCalls); const { lastFrame, unmount } = await renderWithProviders( , { config: baseMockConfig, settings: fullVerbositySettings, }, ); expect(lastFrame({ allowEmpty: true })).not.toBe(''); unmount(); }); it('does not render a border fragment when plan-mode tools are filtered out', async () => { const toolCalls = [ createToolCall({ callId: 'plan-write', name: WRITE_FILE_DISPLAY_NAME, approvalMode: ApprovalMode.PLAN, status: CoreToolCallStatus.Success, resultDisplay: 'Plan file written', }), ]; const item = createItem(toolCalls); const { lastFrame, unmount } = await renderWithProviders( , { config: baseMockConfig, settings: fullVerbositySettings, }, ); expect(lastFrame({ allowEmpty: true })).toBe(''); unmount(); }); it('does not render a border fragment when only confirming tools are present', async () => { const toolCalls = [ createToolCall({ callId: 'confirm-only', status: CoreToolCallStatus.AwaitingApproval, confirmationDetails: { type: 'info', title: 'Confirm', prompt: 'Proceed?', }, }), ]; const item = createItem(toolCalls); const { lastFrame, unmount } = await renderWithProviders( , { config: baseMockConfig, settings: fullVerbositySettings, }, ); expect(lastFrame({ allowEmpty: true })).toBe(''); unmount(); }); it('does not leave a border stub when transitioning from visible to fully filtered tools', async () => { const visibleTools = [ createToolCall({ callId: 'visible-success', name: 'visible-tool', status: CoreToolCallStatus.Success, resultDisplay: 'visible output', }), ]; const hiddenTools = [ createToolCall({ callId: 'hidden-error', name: 'hidden-error-tool', status: CoreToolCallStatus.Error, resultDisplay: 'hidden output', isClientInitiated: false, }), ]; const initialItem = createItem(visibleTools); const hiddenItem = createItem(hiddenTools); const firstRender = await renderWithProviders( , { config: baseMockConfig, settings: lowVerbositySettings, }, ); await firstRender.waitUntilReady(); expect(firstRender.lastFrame()).toContain('visible-tool'); firstRender.unmount(); const secondRender = await renderWithProviders( , { config: baseMockConfig, settings: lowVerbositySettings, }, ); await secondRender.waitUntilReady(); expect(secondRender.lastFrame({ allowEmpty: true })).toBe(''); secondRender.unmount(); }); it('keeps visible tools rendered with many filtered tools (stress case)', async () => { const visibleTool = createToolCall({ callId: 'visible-tool', name: 'visible-tool', status: CoreToolCallStatus.Success, resultDisplay: 'visible output', }); const hiddenTools = Array.from({ length: 50 }, (_, index) => createToolCall({ callId: `hidden-${index}`, name: `hidden-error-${index}`, status: CoreToolCallStatus.Error, resultDisplay: `hidden output ${index}`, isClientInitiated: false, }), ); const toolCalls = [visibleTool, ...hiddenTools]; const item = createItem(toolCalls); const { lastFrame, unmount } = await renderWithProviders( , { config: baseMockConfig, settings: lowVerbositySettings, }, ); const output = lastFrame(); expect(output).toContain('visible-tool'); expect(output).not.toContain('hidden-error-0'); expect(output).not.toContain('hidden-error-49'); unmount(); }); it('renders explicit closing slice even at very narrow terminal width', async () => { const toolCalls: IndividualToolCallDisplay[] = []; const item = createItem(toolCalls); const { lastFrame, unmount } = await renderWithProviders( , { config: baseMockConfig, settings: fullVerbositySettings, }, ); expect(lastFrame({ allowEmpty: true })).not.toBe(''); unmount(); }); }); describe('Plan Mode Filtering', () => { it.each([ { name: WRITE_FILE_DISPLAY_NAME, mode: ApprovalMode.PLAN, visible: false, }, { name: EDIT_DISPLAY_NAME, mode: ApprovalMode.PLAN, visible: false }, { name: WRITE_FILE_DISPLAY_NAME, mode: ApprovalMode.DEFAULT, visible: true, }, { name: READ_FILE_DISPLAY_NAME, mode: ApprovalMode.PLAN, visible: true }, { name: GLOB_DISPLAY_NAME, mode: ApprovalMode.PLAN, visible: true }, ])( 'filtering logic for $name in $mode mode', async ({ name, mode, visible }) => { const toolCalls = [ createToolCall({ callId: 'test-call', name, approvalMode: mode, }), ]; const item = createItem(toolCalls); const { lastFrame, unmount } = await renderWithProviders( , { config: baseMockConfig, settings: fullVerbositySettings }, ); if (visible) { expect(lastFrame()).toContain(name); } else { expect(lastFrame({ allowEmpty: true })).toBe(''); } unmount(); }, ); }); });