mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-21 11:30:38 -07:00
180 lines
4.9 KiB
TypeScript
180 lines
4.9 KiB
TypeScript
|
|
/**
|
||
|
|
* @license
|
||
|
|
* Copyright 2025 Google LLC
|
||
|
|
* SPDX-License-Identifier: Apache-2.0
|
||
|
|
*/
|
||
|
|
|
||
|
|
import React, { act } from 'react';
|
||
|
|
import {
|
||
|
|
ShellToolMessage,
|
||
|
|
type ShellToolMessageProps,
|
||
|
|
} from './ShellToolMessage.js';
|
||
|
|
import { StreamingState, ToolCallStatus } from '../../types.js';
|
||
|
|
import { Text } from 'ink';
|
||
|
|
import type { Config } from '@google/gemini-cli-core';
|
||
|
|
import { renderWithProviders } from '../../../test-utils/render.js';
|
||
|
|
import { waitFor } from '../../../test-utils/async.js';
|
||
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||
|
|
import { SHELL_TOOL_NAME } from '@google/gemini-cli-core';
|
||
|
|
import { SHELL_COMMAND_NAME } from '../../constants.js';
|
||
|
|
import { StreamingContext } from '../../contexts/StreamingContext.js';
|
||
|
|
|
||
|
|
vi.mock('../TerminalOutput.js', () => ({
|
||
|
|
TerminalOutput: function MockTerminalOutput({
|
||
|
|
cursor,
|
||
|
|
}: {
|
||
|
|
cursor: { x: number; y: number } | null;
|
||
|
|
}) {
|
||
|
|
return (
|
||
|
|
<Text>
|
||
|
|
MockCursor:({cursor?.x},{cursor?.y})
|
||
|
|
</Text>
|
||
|
|
);
|
||
|
|
},
|
||
|
|
}));
|
||
|
|
|
||
|
|
// Mock child components or utilities if they are complex or have side effects
|
||
|
|
vi.mock('../GeminiRespondingSpinner.js', () => ({
|
||
|
|
GeminiRespondingSpinner: ({
|
||
|
|
nonRespondingDisplay,
|
||
|
|
}: {
|
||
|
|
nonRespondingDisplay?: string;
|
||
|
|
}) => {
|
||
|
|
const streamingState = React.useContext(StreamingContext)!;
|
||
|
|
if (streamingState === StreamingState.Responding) {
|
||
|
|
return <Text>MockRespondingSpinner</Text>;
|
||
|
|
}
|
||
|
|
return nonRespondingDisplay ? <Text>{nonRespondingDisplay}</Text> : null;
|
||
|
|
},
|
||
|
|
}));
|
||
|
|
|
||
|
|
vi.mock('../../utils/MarkdownDisplay.js', () => ({
|
||
|
|
MarkdownDisplay: function MockMarkdownDisplay({ text }: { text: string }) {
|
||
|
|
return <Text>MockMarkdown:{text}</Text>;
|
||
|
|
},
|
||
|
|
}));
|
||
|
|
|
||
|
|
describe('<ShellToolMessage />', () => {
|
||
|
|
const baseProps: ShellToolMessageProps = {
|
||
|
|
callId: 'tool-123',
|
||
|
|
name: SHELL_COMMAND_NAME,
|
||
|
|
description: 'A shell command',
|
||
|
|
resultDisplay: 'Test result',
|
||
|
|
status: ToolCallStatus.Executing,
|
||
|
|
terminalWidth: 80,
|
||
|
|
confirmationDetails: undefined,
|
||
|
|
emphasis: 'medium',
|
||
|
|
isFirst: true,
|
||
|
|
borderColor: 'green',
|
||
|
|
borderDimColor: false,
|
||
|
|
config: {
|
||
|
|
getEnableInteractiveShell: () => true,
|
||
|
|
} as unknown as Config,
|
||
|
|
};
|
||
|
|
|
||
|
|
const mockSetEmbeddedShellFocused = vi.fn();
|
||
|
|
const uiActions = {
|
||
|
|
setEmbeddedShellFocused: mockSetEmbeddedShellFocused,
|
||
|
|
};
|
||
|
|
|
||
|
|
// Helper to render with context
|
||
|
|
const renderWithContext = (
|
||
|
|
ui: React.ReactElement,
|
||
|
|
streamingState: StreamingState,
|
||
|
|
) =>
|
||
|
|
renderWithProviders(ui, {
|
||
|
|
uiActions,
|
||
|
|
uiState: { streamingState },
|
||
|
|
});
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
vi.clearAllMocks();
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('interactive shell focus', () => {
|
||
|
|
const shellProps: ShellToolMessageProps = {
|
||
|
|
...baseProps,
|
||
|
|
};
|
||
|
|
|
||
|
|
it('clicks inside the shell area sets focus to true', async () => {
|
||
|
|
const { stdin, lastFrame, simulateClick } = renderWithProviders(
|
||
|
|
<ShellToolMessage {...shellProps} />,
|
||
|
|
{
|
||
|
|
mouseEventsEnabled: true,
|
||
|
|
uiActions,
|
||
|
|
},
|
||
|
|
);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(lastFrame()).toContain('A shell command'); // Wait for render
|
||
|
|
});
|
||
|
|
|
||
|
|
await simulateClick(stdin, 2, 2); // Click at column 2, row 2 (1-based)
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(mockSetEmbeddedShellFocused).toHaveBeenCalledWith(true);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('handles focus for SHELL_TOOL_NAME (core shell tool)', async () => {
|
||
|
|
const coreShellProps: ShellToolMessageProps = {
|
||
|
|
...shellProps,
|
||
|
|
name: SHELL_TOOL_NAME,
|
||
|
|
};
|
||
|
|
|
||
|
|
const { stdin, lastFrame, simulateClick } = renderWithProviders(
|
||
|
|
<ShellToolMessage {...coreShellProps} />,
|
||
|
|
{
|
||
|
|
mouseEventsEnabled: true,
|
||
|
|
uiActions,
|
||
|
|
},
|
||
|
|
);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(lastFrame()).toContain('A shell command');
|
||
|
|
});
|
||
|
|
|
||
|
|
await simulateClick(stdin, 2, 2);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(mockSetEmbeddedShellFocused).toHaveBeenCalledWith(true);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('resets focus when shell finishes', async () => {
|
||
|
|
let updateStatus: (s: ToolCallStatus) => void = () => {};
|
||
|
|
|
||
|
|
const Wrapper = () => {
|
||
|
|
const [status, setStatus] = React.useState(ToolCallStatus.Executing);
|
||
|
|
updateStatus = setStatus;
|
||
|
|
return (
|
||
|
|
<ShellToolMessage
|
||
|
|
{...shellProps}
|
||
|
|
status={status}
|
||
|
|
embeddedShellFocused={true}
|
||
|
|
activeShellPtyId={1}
|
||
|
|
ptyId={1}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
const { lastFrame } = renderWithContext(<Wrapper />, StreamingState.Idle);
|
||
|
|
|
||
|
|
// Verify it is initially focused
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(lastFrame()).toContain('(Focused)');
|
||
|
|
});
|
||
|
|
|
||
|
|
// Now update status to Success
|
||
|
|
await act(async () => {
|
||
|
|
updateStatus(ToolCallStatus.Success);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Should call setEmbeddedShellFocused(false) because isThisShellFocused became false
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(mockSetEmbeddedShellFocused).toHaveBeenCalledWith(false);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|