Download packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 32.6 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx
-
curl -L -o ToolGroupMessage.test.tsx https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx
32.6 kB
| /** | |
| * @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_SUMMARY, | |
| 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('<ToolGroupMessage />', () => { | |
| afterEach(() => { | |
| vi.restoreAllMocks(); | |
| }); | |
| const createToolCall = ( | |
| overrides: Partial<IndividualToolCallDisplay> = {}, | |
| ): 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( | |
| <ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />, | |
| { | |
| 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( | |
| <ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />, | |
| { 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( | |
| <ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />, | |
| { 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( | |
| <ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />, | |
| { | |
| 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( | |
| <ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />, | |
| { | |
| 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( | |
| <ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />, | |
| { | |
| 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( | |
| <ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />, | |
| { | |
| 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', | |
| [TOPIC_PARAM_SUMMARY]: 'This is the summary', | |
| }, | |
| }), | |
| ]; | |
| const item = createItem(toolCalls); | |
| const { lastFrame, unmount } = await renderWithProviders( | |
| <ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />, | |
| { | |
| 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( | |
| <ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />, | |
| { | |
| 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 update_topic in the middle of other tools', async () => { | |
| const toolCalls = [ | |
| createToolCall({ | |
| callId: 'tool-1', | |
| name: 'read_file', | |
| status: CoreToolCallStatus.Success, | |
| }), | |
| createToolCall({ | |
| callId: 'topic-tool-middle', | |
| name: UPDATE_TOPIC_TOOL_NAME, | |
| args: { | |
| [TOPIC_PARAM_TITLE]: 'Middle Topic', | |
| }, | |
| }), | |
| createToolCall({ | |
| callId: 'tool-2', | |
| name: 'write_file', | |
| status: CoreToolCallStatus.Success, | |
| }), | |
| ]; | |
| const item = createItem(toolCalls); | |
| const { lastFrame, unmount } = await renderWithProviders( | |
| <ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />, | |
| { | |
| config: baseMockConfig, | |
| settings: fullVerbositySettings, | |
| }, | |
| ); | |
| expect(lastFrame()).toMatchSnapshot('update_topic_middle'); | |
| 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( | |
| <ToolGroupMessage | |
| {...baseProps} | |
| item={item} | |
| toolCalls={toolCalls} | |
| availableTerminalHeight={10} | |
| />, | |
| { | |
| 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( | |
| <ToolGroupMessage | |
| {...baseProps} | |
| item={item} | |
| toolCalls={toolCalls} | |
| terminalWidth={40} | |
| />, | |
| { | |
| 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( | |
| <ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />, | |
| { | |
| 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( | |
| <Scrollable height={12} hasFocus={true} scrollToBottom={true}> | |
| <ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} /> | |
| </Scrollable>, | |
| { | |
| 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( | |
| <ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />, | |
| { | |
| 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( | |
| <Scrollable height={6} hasFocus={true} scrollToBottom={true}> | |
| <ToolGroupMessage | |
| {...baseProps} | |
| item={item1} | |
| toolCalls={toolCalls1} | |
| /> | |
| <ToolGroupMessage | |
| {...baseProps} | |
| item={item2} | |
| toolCalls={toolCalls2} | |
| /> | |
| </Scrollable>, | |
| { | |
| 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( | |
| <ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />, | |
| { | |
| 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( | |
| <ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />, | |
| { | |
| 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( | |
| <ToolGroupMessage | |
| {...baseProps} | |
| item={item} | |
| toolCalls={toolCalls} | |
| availableTerminalHeight={20} | |
| />, | |
| { | |
| 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( | |
| <ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />, | |
| { 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( | |
| <ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />, | |
| { 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( | |
| <ToolGroupMessage | |
| {...baseProps} | |
| item={item} | |
| toolCalls={toolCalls} | |
| borderBottom={false} | |
| />, | |
| { 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( | |
| <ToolGroupMessage | |
| {...baseProps} | |
| item={item} | |
| toolCalls={toolCalls} | |
| borderTop={false} | |
| borderBottom={true} | |
| />, | |
| { | |
| 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( | |
| <ToolGroupMessage | |
| {...baseProps} | |
| item={item} | |
| toolCalls={toolCalls} | |
| borderTop={false} | |
| borderBottom={true} | |
| />, | |
| { | |
| 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( | |
| <ToolGroupMessage | |
| {...baseProps} | |
| item={item} | |
| toolCalls={toolCalls} | |
| borderTop={false} | |
| borderBottom={true} | |
| />, | |
| { | |
| 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( | |
| <ToolGroupMessage | |
| {...baseProps} | |
| item={item} | |
| toolCalls={toolCalls} | |
| borderTop={false} | |
| borderBottom={true} | |
| />, | |
| { | |
| 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( | |
| <ToolGroupMessage | |
| {...baseProps} | |
| item={initialItem} | |
| toolCalls={visibleTools} | |
| borderTop={false} | |
| borderBottom={true} | |
| />, | |
| { | |
| config: baseMockConfig, | |
| settings: lowVerbositySettings, | |
| }, | |
| ); | |
| await firstRender.waitUntilReady(); | |
| expect(firstRender.lastFrame()).toContain('visible-tool'); | |
| firstRender.unmount(); | |
| const secondRender = await renderWithProviders( | |
| <ToolGroupMessage | |
| {...baseProps} | |
| item={hiddenItem} | |
| toolCalls={hiddenTools} | |
| borderTop={false} | |
| borderBottom={true} | |
| />, | |
| { | |
| 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( | |
| <ToolGroupMessage | |
| {...baseProps} | |
| item={item} | |
| toolCalls={toolCalls} | |
| borderTop={false} | |
| borderBottom={true} | |
| />, | |
| { | |
| 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( | |
| <ToolGroupMessage | |
| item={item} | |
| toolCalls={toolCalls} | |
| terminalWidth={8} | |
| borderTop={false} | |
| borderBottom={true} | |
| />, | |
| { | |
| 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( | |
| <ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />, | |
| { config: baseMockConfig, settings: fullVerbositySettings }, | |
| ); | |
| if (visible) { | |
| expect(lastFrame()).toContain(name); | |
| } else { | |
| expect(lastFrame({ allowEmpty: true })).toBe(''); | |
| } | |
| unmount(); | |
| }, | |
| ); | |
| }); | |
| }); | |