test(cli): add unit tests for ToolGroupDisplay and implement tool hiding

This commit is contained in:
Michael Bleigh
2026-04-12 16:40:48 -07:00
parent 9e03476e03
commit 8548c6675f
2 changed files with 316 additions and 2 deletions

View File

@@ -0,0 +1,304 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, afterEach } from 'vitest';
import { renderWithProviders } from '../../../test-utils/render.js';
import { createMockSettings } from '../../../test-utils/settings.js';
import { ToolGroupDisplay } from './ToolGroupDisplay.js';
import {
CoreToolCallStatus,
UPDATE_TOPIC_DISPLAY_NAME,
} from '@google/gemini-cli-core';
import type {
HistoryItemToolDisplayGroup,
ToolDisplayItem,
} from '../../types.js';
describe('<ToolGroupDisplay />', () => {
afterEach(() => {
vi.restoreAllMocks();
});
const createToolItem = (
overrides: Partial<ToolDisplayItem> = {},
): ToolDisplayItem => ({
status: CoreToolCallStatus.Success,
name: 'test-tool',
description: 'Test description',
...overrides,
});
const createHistoryItem = (
tools: ToolDisplayItem[],
overrides: Partial<HistoryItemToolDisplayGroup> = {},
): HistoryItemToolDisplayGroup => ({
type: 'tool_display_group',
tools,
borderColor: 'gray',
borderDimColor: true,
borderTop: true,
borderBottom: true,
...overrides,
});
const fullVerbositySettings = createMockSettings({
ui: { errorVerbosity: 'full', compactToolOutput: false },
});
const compactSettings = createMockSettings({
ui: { compactToolOutput: true },
});
describe('Golden Snapshots', () => {
it('renders notices at the top (hoisting)', async () => {
const tools = [
createToolItem({ name: 'Tool A', format: 'box' }),
createToolItem({
name: UPDATE_TOPIC_DISPLAY_NAME,
description: 'New Topic',
format: 'notice',
}),
];
const item = createHistoryItem(tools);
const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
{ settings: fullVerbositySettings },
);
const output = lastFrame();
// Notice should be before Tool A
expect(output.indexOf(UPDATE_TOPIC_DISPLAY_NAME)).toBeLessThan(
output.indexOf('Tool A'),
);
expect(output).toMatchSnapshot();
});
it('renders in compact mode (no box borders)', async () => {
const tools = [
createToolItem({ name: 'Tool A' }),
createToolItem({ name: 'Tool B' }),
];
const item = createHistoryItem(tools);
const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
{ settings: compactSettings },
);
const output = lastFrame();
// Should not contain box drawing characters for the outer box
expect(output).not.toContain('╭');
expect(output).not.toContain('╰');
expect(output).toMatchSnapshot();
});
it('renders in boxed mode (full verbosity)', async () => {
const tools = [createToolItem({ name: 'Tool A' })];
const item = createHistoryItem(tools);
const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
{ settings: fullVerbositySettings },
);
const output = lastFrame();
expect(output).toContain('╭');
expect(output).toContain('╰');
expect(output).toMatchSnapshot();
});
it('renders standalone notices without a box', async () => {
const tools = [
createToolItem({
name: 'Notice Only',
format: 'notice',
}),
];
const item = createHistoryItem(tools);
const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
{ settings: fullVerbositySettings },
);
const output = lastFrame();
expect(output).not.toContain('╭');
expect(output).toMatchSnapshot();
});
it('renders error message when display info is missing', async () => {
// Create an item that effectively has no display properties
const tools = [
{
status: CoreToolCallStatus.Executing,
originalRequestName: 'missing-tool',
} as ToolDisplayItem,
];
const item = createHistoryItem(tools);
const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
);
const output = lastFrame();
expect(output).toContain('Error: Tool display missing');
expect(output).toMatchSnapshot();
});
it('hides tools awaiting approval (confirming)', async () => {
const tools = [
createToolItem({
name: 'Confirming Tool',
status: CoreToolCallStatus.AwaitingApproval,
}),
];
const item = createHistoryItem(tools);
const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
);
// Should render nothing (null)
expect(lastFrame({ allowEmpty: true })).toBe('');
});
});
describe('Result Formatting', () => {
it('renders text results with summary below', async () => {
const tools = [
createToolItem({
result: { type: 'text', text: 'Detailed output' },
resultSummary: 'Short summary',
format: 'box',
}),
];
const item = createHistoryItem(tools);
const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
{ settings: fullVerbositySettings },
);
const output = lastFrame();
expect(output).toContain('Detailed output');
expect(output).toContain('Short summary');
// Summary should be below detailed output
expect(output.indexOf('Detailed output')).toBeLessThan(
output.indexOf('Short summary'),
);
expect(output).toMatchSnapshot();
});
it('renders compact tools with summary on same line', async () => {
const tools = [
createToolItem({
resultSummary: 'Success summary',
format: 'compact',
}),
];
const item = createHistoryItem(tools);
const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
);
const output = lastFrame();
expect(output).toContain('→ Success summary');
expect(output).toMatchSnapshot();
});
it('renders placeholder for diff results', async () => {
const tools = [
createToolItem({
result: {
type: 'diff',
beforeText: 'old',
afterText: 'new',
path: 'file.ts',
},
}),
];
const item = createHistoryItem(tools);
const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
{ settings: fullVerbositySettings },
);
const output = lastFrame();
expect(output).toContain('[Diff Display: 3 -> 3 chars]');
expect(output).toMatchSnapshot();
});
it('renders placeholder for terminal results', async () => {
const tools = [
createToolItem({
result: { type: 'terminal' },
}),
];
const item = createHistoryItem(tools);
const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
{ settings: fullVerbositySettings },
);
expect(lastFrame()).toContain('[Terminal Output]');
});
it('renders placeholder for agent results', async () => {
const tools = [
createToolItem({
result: { type: 'agent', threadId: 'thread-123' },
}),
];
const item = createHistoryItem(tools);
const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
{ settings: fullVerbositySettings },
);
expect(lastFrame()).toContain('[Subagent: thread-123]');
});
});
describe('Border & Margin Logic', () => {
it('forces top border on box when it follows a notice', async () => {
const tools = [
createToolItem({ name: 'Notice', format: 'notice' }),
createToolItem({ name: 'Tool in Box', format: 'box' }),
];
// Even if item.borderTop is false (continuing a group),
// the box should have a top border because it follows a notice.
const item = createHistoryItem(tools, { borderTop: false });
const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
{ settings: fullVerbositySettings },
);
const output = lastFrame();
expect(output).toContain('Notice');
expect(output).toContain('╭'); // Top border for the box
expect(output).toMatchSnapshot();
});
it('applies bottom margin in compact mode when group is at boundary', async () => {
const tools = [createToolItem({ name: 'Compact Tool' })];
const item = createHistoryItem(tools, { borderBottom: true });
const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
{ settings: compactSettings },
);
// This is hard to assert via string check, but ensure match snapshot
// captures the vertical spacing.
expect(lastFrame()).toMatchSnapshot();
});
});
});

View File

@@ -6,6 +6,7 @@
import type React from 'react';
import { Box, Text } from 'ink';
import { CoreToolCallStatus } from '@google/gemini-cli-core';
import type {
HistoryItem,
HistoryItemWithoutId,
@@ -35,14 +36,23 @@ export const ToolGroupDisplay: React.FC<ToolGroupDisplayProps> = ({
const { tools, borderColor, borderDimColor, borderTop, borderBottom } =
item as HistoryItemToolDisplayGroup;
const noticeTools = tools.filter((t) => t.format === 'notice');
const otherTools = tools.filter(
const visibleTools = tools.filter(
(t) => t.status !== CoreToolCallStatus.AwaitingApproval,
);
const noticeTools = visibleTools.filter((t) => t.format === 'notice');
const otherTools = visibleTools.filter(
(t) => t.format !== 'notice' && t.format !== 'hidden',
);
const hasOtherTools = otherTools.length > 0;
const isClosingSlice = tools.length === 0 && borderBottom;
// If no tools are visible and it's not an explicit closing slice, hide the group
if (visibleTools.length === 0 && !isClosingSlice) {
return null;
}
// Standard view behavior: If compact mode is enabled, non-notice tools
// are typically rendered without an outer box.
const shouldShowBox =