From 71c090c69651db3cedd217c32bf49e09845e076b Mon Sep 17 00:00:00 2001 From: Arya Gummadi Date: Mon, 25 Aug 2025 14:42:18 -0700 Subject: [PATCH] feat: add golden snapshot test for ToolGroupMessage and improve success symbol (#7037) --- .../messages/ToolGroupMessage.test.tsx | 344 ++++++++++++++++++ .../components/messages/ToolMessage.test.tsx | 12 +- .../ui/components/messages/ToolMessage.tsx | 2 +- .../ToolGroupMessage.test.tsx.snap | 105 ++++++ 4 files changed, 456 insertions(+), 7 deletions(-) create mode 100644 packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx create mode 100644 packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx new file mode 100644 index 00000000..8e5961e9 --- /dev/null +++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx @@ -0,0 +1,344 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { render } from 'ink-testing-library'; +import { describe, it, expect, vi } from 'vitest'; +import { Text } from 'ink'; +import { ToolGroupMessage } from './ToolGroupMessage.js'; +import { IndividualToolCallDisplay, ToolCallStatus } from '../../types.js'; +import { Config, ToolCallConfirmationDetails } from '@google/gemini-cli-core'; + +// Mock child components to isolate ToolGroupMessage behavior +vi.mock('./ToolMessage.js', () => ({ + ToolMessage: function MockToolMessage({ + callId, + name, + description, + status, + emphasis, + }: { + callId: string; + name: string; + description: string; + status: ToolCallStatus; + emphasis: string; + }) { + const statusSymbol = { + [ToolCallStatus.Success]: '✓', + [ToolCallStatus.Pending]: 'o', + [ToolCallStatus.Executing]: '⊷', + [ToolCallStatus.Confirming]: '?', + [ToolCallStatus.Canceled]: '-', + [ToolCallStatus.Error]: 'x', + }[status]; + return ( + + MockTool[{callId}]: {statusSymbol} {name} - {description} ({emphasis}) + + ); + }, +})); + +vi.mock('./ToolConfirmationMessage.js', () => ({ + ToolConfirmationMessage: function MockToolConfirmationMessage({ + confirmationDetails, + }: { + confirmationDetails: ToolCallConfirmationDetails; + }) { + const displayText = + confirmationDetails?.type === 'info' + ? (confirmationDetails as { prompt: string }).prompt + : confirmationDetails?.title || 'confirm'; + return MockConfirmation: {displayText}; + }, +})); + +describe('', () => { + const mockConfig: Config = {} as Config; + + 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, + config: mockConfig, + isFocused: true, + }; + + describe('Golden Snapshots', () => { + it('renders single successful tool call', () => { + const toolCalls = [createToolCall()]; + const { lastFrame } = render( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); + + it('renders multiple tool calls with different statuses', () => { + 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 } = render( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); + + it('renders tool call awaiting confirmation', () => { + const toolCalls = [ + createToolCall({ + callId: 'tool-confirm', + name: 'confirmation-tool', + description: 'This tool needs confirmation', + status: ToolCallStatus.Confirming, + confirmationDetails: { + type: 'info', + title: 'Confirm Tool Execution', + prompt: 'Are you sure you want to proceed?', + onConfirm: vi.fn(), + }, + }), + ]; + const { lastFrame } = render( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); + + it('renders shell command with yellow border', () => { + const toolCalls = [ + createToolCall({ + callId: 'shell-1', + name: 'run_shell_command', + description: 'Execute shell command', + status: ToolCallStatus.Success, + }), + ]; + const { lastFrame } = render( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); + + 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 } = render( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); + + 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 } = render( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); + + it('renders when not focused', () => { + const toolCalls = [createToolCall()]; + const { lastFrame } = render( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); + + 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 } = render( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); + + it('renders empty tool calls array', () => { + const { lastFrame } = render( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); + }); + + describe('Border Color Logic', () => { + it('uses yellow border when tools are pending', () => { + const toolCalls = [createToolCall({ status: ToolCallStatus.Pending })]; + const { lastFrame } = render( + , + ); + // The snapshot will capture the visual appearance including border color + expect(lastFrame()).toMatchSnapshot(); + }); + + it('uses yellow border for shell commands even when successful', () => { + const toolCalls = [ + createToolCall({ + name: 'run_shell_command', + status: ToolCallStatus.Success, + }), + ]; + const { lastFrame } = render( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); + + 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 } = render( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); + }); + + 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 } = render( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); + }); + + describe('Confirmation Handling', () => { + it('shows confirmation dialog for first confirming tool only', () => { + const toolCalls = [ + createToolCall({ + callId: 'tool-1', + name: 'first-confirm', + status: ToolCallStatus.Confirming, + confirmationDetails: { + type: 'info', + title: 'Confirm First Tool', + prompt: 'Confirm first tool', + onConfirm: vi.fn(), + }, + }), + createToolCall({ + callId: 'tool-2', + name: 'second-confirm', + status: ToolCallStatus.Confirming, + confirmationDetails: { + type: 'info', + title: 'Confirm Second Tool', + prompt: 'Confirm second tool', + onConfirm: vi.fn(), + }, + }), + ]; + const { lastFrame } = render( + , + ); + // Should only show confirmation for the first tool + expect(lastFrame()).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/cli/src/ui/components/messages/ToolMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolMessage.test.tsx index 843ebf03..fab8aeba 100644 --- a/packages/cli/src/ui/components/messages/ToolMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolMessage.test.tsx @@ -71,19 +71,19 @@ describe('', () => { StreamingState.Idle, ); const output = lastFrame(); - expect(output).toContain('√'); // Success indicator + expect(output).toContain('✓'); // Success indicator expect(output).toContain('test-tool'); expect(output).toContain('A tool for testing'); expect(output).toContain('MockMarkdown:Test result'); }); describe('ToolStatusIndicator rendering', () => { - it('shows √ for Success status', () => { + it('shows ✓ for Success status', () => { const { lastFrame } = renderWithContext( , StreamingState.Idle, ); - expect(lastFrame()).toContain('√'); + expect(lastFrame()).toContain('✓'); }); it('shows o for Pending status', () => { @@ -125,7 +125,7 @@ describe('', () => { ); expect(lastFrame()).toContain('⊷'); expect(lastFrame()).not.toContain('MockRespondingSpinner'); - expect(lastFrame()).not.toContain('√'); + expect(lastFrame()).not.toContain('✓'); }); it('shows paused spinner for Executing status when streamingState is WaitingForConfirmation', () => { @@ -135,7 +135,7 @@ describe('', () => { ); expect(lastFrame()).toContain('⊷'); expect(lastFrame()).not.toContain('MockRespondingSpinner'); - expect(lastFrame()).not.toContain('√'); + expect(lastFrame()).not.toContain('✓'); }); it('shows MockRespondingSpinner for Executing status when streamingState is Responding', () => { @@ -144,7 +144,7 @@ describe('', () => { StreamingState.Responding, // Simulate app still responding ); expect(lastFrame()).toContain('MockRespondingSpinner'); - expect(lastFrame()).not.toContain('√'); + expect(lastFrame()).not.toContain('✓'); }); }); diff --git a/packages/cli/src/ui/components/messages/ToolMessage.tsx b/packages/cli/src/ui/components/messages/ToolMessage.tsx index a4d24529..d6ddb4f1 100644 --- a/packages/cli/src/ui/components/messages/ToolMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolMessage.tsx @@ -127,7 +127,7 @@ const ToolStatusIndicator: React.FC = ({ /> )} {status === ToolCallStatus.Success && ( - + )} {status === ToolCallStatus.Confirming && ( ? diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap new file mode 100644 index 00000000..ee798b67 --- /dev/null +++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap @@ -0,0 +1,105 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[` > Border Color Logic > uses gray border when all tools are successful and no shell commands 1`] = ` +" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + │MockTool[tool-123]: ✓ test-tool - A tool for testing (medium) │ + │ │ + │MockTool[tool-2]: ✓ another-tool - A tool for testing (medium) │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` > Border Color Logic > uses yellow border for shell commands even when successful 1`] = ` +" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + │MockTool[tool-123]: ✓ run_shell_command - A tool for testing (medium) │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` > Border Color Logic > uses yellow border when tools are pending 1`] = ` +" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + │MockTool[tool-123]: o test-tool - A tool for testing (medium) │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` > Confirmation Handling > shows confirmation dialog for first confirming tool only 1`] = ` +" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + │MockTool[tool-1]: ? first-confirm - A tool for testing (high) │ + │MockConfirmation: Confirm first tool │ + │ │ + │MockTool[tool-2]: ? second-confirm - A tool for testing (low) │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` > Golden Snapshots > renders empty tool calls array 1`] = ` +" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` > Golden Snapshots > renders mixed tool calls including shell command 1`] = ` +" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + │MockTool[tool-1]: ✓ read_file - Read a file (medium) │ + │ │ + │MockTool[tool-2]: ⊷ run_shell_command - Run command (medium) │ + │ │ + │MockTool[tool-3]: o write_file - Write to file (medium) │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` > Golden Snapshots > renders multiple tool calls with different statuses 1`] = ` +" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + │MockTool[tool-1]: ✓ successful-tool - This tool succeeded (medium) │ + │ │ + │MockTool[tool-2]: o pending-tool - This tool is pending (medium) │ + │ │ + │MockTool[tool-3]: x error-tool - This tool failed (medium) │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` > Golden Snapshots > renders shell command with yellow border 1`] = ` +" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + │MockTool[shell-1]: ✓ run_shell_command - Execute shell command (medium) │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` > Golden Snapshots > renders single successful tool call 1`] = ` +" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + │MockTool[tool-123]: ✓ test-tool - A tool for testing (medium) │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` > Golden Snapshots > renders tool call awaiting confirmation 1`] = ` +" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + │MockTool[tool-confirm]: ? confirmation-tool - This tool needs confirmation (high) │ + │MockConfirmation: Are you sure you want to proceed? │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` > Golden Snapshots > renders when not focused 1`] = ` +" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + │MockTool[tool-123]: ✓ test-tool - A tool for testing (medium) │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` > Golden Snapshots > renders with limited terminal height 1`] = ` +" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + │MockTool[tool-1]: ✓ tool-with-result - Tool with output (medium) │ + │ │ + │MockTool[tool-2]: ✓ another-tool - Another tool (medium) │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` > Golden Snapshots > renders with narrow terminal width 1`] = ` +" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + │MockTool[tool-123]: ✓ very-long-tool-name-that-might-wrap - This is a very long description that │ + │might cause wrapping issues (medium) │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` > Height Calculation > calculates available height correctly with multiple tools with results 1`] = ` +" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + │MockTool[tool-1]: ✓ test-tool - A tool for testing (medium) │ + │ │ + │MockTool[tool-2]: ✓ test-tool - A tool for testing (medium) │ + │ │ + │MockTool[tool-3]: ✓ test-tool - A tool for testing (medium) │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`;