2025-05-20 00:21:01 -07:00
|
|
|
/**
|
|
|
|
|
* @license
|
|
|
|
|
* Copyright 2025 Google LLC
|
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { act, renderHook } from '@testing-library/react';
|
2025-06-15 22:09:30 -04:00
|
|
|
import { vi } from 'vitest';
|
|
|
|
|
import { useShellCommandProcessor } from './shellCommandProcessor';
|
2025-06-25 05:41:11 -07:00
|
|
|
import { Config, GeminiClient } from '@google/gemini-cli-core';
|
2025-06-15 22:09:30 -04:00
|
|
|
import * as fs from 'fs';
|
|
|
|
|
import EventEmitter from 'events';
|
2025-07-18 17:30:28 -07:00
|
|
|
import { ToolCallStatus } from '../types';
|
2025-06-15 22:09:30 -04:00
|
|
|
|
|
|
|
|
// Mock dependencies
|
|
|
|
|
vi.mock('child_process');
|
|
|
|
|
vi.mock('fs');
|
2025-05-20 00:21:01 -07:00
|
|
|
vi.mock('os', () => ({
|
|
|
|
|
default: {
|
2025-06-15 22:09:30 -04:00
|
|
|
platform: () => 'linux',
|
|
|
|
|
tmpdir: () => '/tmp',
|
2025-07-22 06:26:40 +08:00
|
|
|
homedir: () => '/home/user',
|
2025-05-20 00:21:01 -07:00
|
|
|
},
|
2025-06-15 22:09:30 -04:00
|
|
|
platform: () => 'linux',
|
|
|
|
|
tmpdir: () => '/tmp',
|
2025-07-22 06:26:40 +08:00
|
|
|
homedir: () => '/home/user',
|
2025-06-15 22:09:30 -04:00
|
|
|
}));
|
2025-06-25 05:41:11 -07:00
|
|
|
vi.mock('@google/gemini-cli-core');
|
2025-06-15 22:09:30 -04:00
|
|
|
vi.mock('../utils/textUtils.js', () => ({
|
|
|
|
|
isBinary: vi.fn(),
|
2025-05-20 00:21:01 -07:00
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
describe('useShellCommandProcessor', () => {
|
2025-06-15 22:09:30 -04:00
|
|
|
let spawnEmitter: EventEmitter;
|
|
|
|
|
let addItemToHistoryMock: vi.Mock;
|
|
|
|
|
let setPendingHistoryItemMock: vi.Mock;
|
|
|
|
|
let onExecMock: vi.Mock;
|
|
|
|
|
let onDebugMessageMock: vi.Mock;
|
|
|
|
|
let configMock: Config;
|
|
|
|
|
let geminiClientMock: GeminiClient;
|
|
|
|
|
|
|
|
|
|
beforeEach(async () => {
|
|
|
|
|
const { spawn } = await import('child_process');
|
|
|
|
|
spawnEmitter = new EventEmitter();
|
|
|
|
|
spawnEmitter.stdout = new EventEmitter();
|
|
|
|
|
spawnEmitter.stderr = new EventEmitter();
|
|
|
|
|
(spawn as vi.Mock).mockReturnValue(spawnEmitter);
|
|
|
|
|
|
|
|
|
|
vi.spyOn(fs, 'existsSync').mockReturnValue(false);
|
|
|
|
|
vi.spyOn(fs, 'readFileSync').mockReturnValue('');
|
|
|
|
|
vi.spyOn(fs, 'unlinkSync').mockReturnValue(undefined);
|
|
|
|
|
|
|
|
|
|
addItemToHistoryMock = vi.fn();
|
|
|
|
|
setPendingHistoryItemMock = vi.fn();
|
|
|
|
|
onExecMock = vi.fn();
|
|
|
|
|
onDebugMessageMock = vi.fn();
|
|
|
|
|
|
|
|
|
|
configMock = {
|
|
|
|
|
getTargetDir: () => '/test/dir',
|
|
|
|
|
} as unknown as Config;
|
|
|
|
|
|
|
|
|
|
geminiClientMock = {
|
|
|
|
|
addHistory: vi.fn(),
|
|
|
|
|
} as unknown as GeminiClient;
|
|
|
|
|
});
|
2025-05-20 00:21:01 -07:00
|
|
|
|
2025-06-15 22:09:30 -04:00
|
|
|
afterEach(() => {
|
|
|
|
|
vi.restoreAllMocks();
|
2025-05-20 00:21:01 -07:00
|
|
|
});
|
|
|
|
|
|
2025-06-15 22:09:30 -04:00
|
|
|
const renderProcessorHook = () =>
|
2025-05-20 00:21:01 -07:00
|
|
|
renderHook(() =>
|
|
|
|
|
useShellCommandProcessor(
|
2025-06-15 22:09:30 -04:00
|
|
|
addItemToHistoryMock,
|
|
|
|
|
setPendingHistoryItemMock,
|
|
|
|
|
onExecMock,
|
|
|
|
|
onDebugMessageMock,
|
|
|
|
|
configMock,
|
|
|
|
|
geminiClientMock,
|
2025-05-20 00:21:01 -07:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
2025-06-15 22:09:30 -04:00
|
|
|
it('should execute a command and update history on success', async () => {
|
|
|
|
|
const { result } = renderProcessorHook();
|
|
|
|
|
const abortController = new AbortController();
|
2025-05-20 00:21:01 -07:00
|
|
|
|
|
|
|
|
act(() => {
|
2025-06-15 22:09:30 -04:00
|
|
|
result.current.handleShellCommand('ls -l', abortController.signal);
|
2025-05-20 00:21:01 -07:00
|
|
|
});
|
|
|
|
|
|
2025-06-15 22:09:30 -04:00
|
|
|
expect(onExecMock).toHaveBeenCalledTimes(1);
|
|
|
|
|
const execPromise = onExecMock.mock.calls[0][0];
|
2025-05-20 00:21:01 -07:00
|
|
|
|
2025-06-15 22:09:30 -04:00
|
|
|
// Simulate stdout
|
|
|
|
|
act(() => {
|
|
|
|
|
spawnEmitter.stdout.emit('data', Buffer.from('file1.txt\nfile2.txt'));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Simulate process exit
|
|
|
|
|
act(() => {
|
|
|
|
|
spawnEmitter.emit('exit', 0, null);
|
2025-05-20 00:21:01 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
2025-06-15 22:09:30 -04:00
|
|
|
await execPromise;
|
2025-05-20 00:21:01 -07:00
|
|
|
});
|
|
|
|
|
|
2025-06-15 22:09:30 -04:00
|
|
|
expect(addItemToHistoryMock).toHaveBeenCalledTimes(2);
|
|
|
|
|
expect(addItemToHistoryMock.mock.calls[1][0]).toEqual({
|
2025-07-18 17:30:28 -07:00
|
|
|
type: 'tool_group',
|
|
|
|
|
tools: [
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
name: 'Shell Command',
|
|
|
|
|
description: 'ls -l',
|
|
|
|
|
status: ToolCallStatus.Success,
|
|
|
|
|
resultDisplay: 'file1.txt\nfile2.txt',
|
|
|
|
|
}),
|
|
|
|
|
],
|
2025-06-15 22:09:30 -04:00
|
|
|
});
|
|
|
|
|
expect(geminiClientMock.addHistory).toHaveBeenCalledTimes(1);
|
2025-05-20 00:21:01 -07:00
|
|
|
});
|
|
|
|
|
|
2025-06-15 22:09:30 -04:00
|
|
|
it('should handle binary output', async () => {
|
|
|
|
|
const { result } = renderProcessorHook();
|
|
|
|
|
const abortController = new AbortController();
|
|
|
|
|
const { isBinary } = await import('../utils/textUtils.js');
|
|
|
|
|
(isBinary as vi.Mock).mockReturnValue(true);
|
2025-05-20 00:21:01 -07:00
|
|
|
|
2025-06-15 22:09:30 -04:00
|
|
|
act(() => {
|
|
|
|
|
result.current.handleShellCommand(
|
|
|
|
|
'cat myimage.png',
|
|
|
|
|
abortController.signal,
|
|
|
|
|
);
|
2025-05-20 00:21:01 -07:00
|
|
|
});
|
|
|
|
|
|
2025-06-15 22:09:30 -04:00
|
|
|
expect(onExecMock).toHaveBeenCalledTimes(1);
|
|
|
|
|
const execPromise = onExecMock.mock.calls[0][0];
|
2025-05-20 00:21:01 -07:00
|
|
|
|
2025-06-15 22:09:30 -04:00
|
|
|
act(() => {
|
|
|
|
|
spawnEmitter.stdout.emit('data', Buffer.from([0x89, 0x50, 0x4e, 0x47]));
|
|
|
|
|
});
|
2025-05-20 00:21:01 -07:00
|
|
|
|
2025-06-15 22:09:30 -04:00
|
|
|
act(() => {
|
|
|
|
|
spawnEmitter.emit('exit', 0, null);
|
2025-05-20 00:21:01 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
2025-06-15 22:09:30 -04:00
|
|
|
await execPromise;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(addItemToHistoryMock).toHaveBeenCalledTimes(2);
|
|
|
|
|
expect(addItemToHistoryMock.mock.calls[1][0]).toEqual({
|
2025-07-18 17:30:28 -07:00
|
|
|
type: 'tool_group',
|
|
|
|
|
tools: [
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
name: 'Shell Command',
|
|
|
|
|
description: 'cat myimage.png',
|
|
|
|
|
status: ToolCallStatus.Success,
|
|
|
|
|
resultDisplay:
|
|
|
|
|
'[Command produced binary output, which is not shown.]',
|
|
|
|
|
}),
|
|
|
|
|
],
|
2025-05-20 00:21:01 -07:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-15 22:09:30 -04:00
|
|
|
it('should handle command failure', async () => {
|
|
|
|
|
const { result } = renderProcessorHook();
|
|
|
|
|
const abortController = new AbortController();
|
2025-05-20 00:21:01 -07:00
|
|
|
|
2025-06-15 22:09:30 -04:00
|
|
|
act(() => {
|
|
|
|
|
result.current.handleShellCommand(
|
|
|
|
|
'a-bad-command',
|
|
|
|
|
abortController.signal,
|
|
|
|
|
);
|
2025-05-20 00:21:01 -07:00
|
|
|
});
|
|
|
|
|
|
2025-06-15 22:09:30 -04:00
|
|
|
const execPromise = onExecMock.mock.calls[0][0];
|
2025-05-20 00:21:01 -07:00
|
|
|
|
2025-06-15 22:09:30 -04:00
|
|
|
act(() => {
|
|
|
|
|
spawnEmitter.stderr.emit('data', Buffer.from('command not found'));
|
|
|
|
|
});
|
2025-05-20 00:21:01 -07:00
|
|
|
|
2025-06-15 22:09:30 -04:00
|
|
|
act(() => {
|
|
|
|
|
spawnEmitter.emit('exit', 127, null);
|
2025-05-20 00:21:01 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
2025-06-15 22:09:30 -04:00
|
|
|
await execPromise;
|
2025-05-20 00:21:01 -07:00
|
|
|
});
|
|
|
|
|
|
2025-06-15 22:09:30 -04:00
|
|
|
expect(addItemToHistoryMock).toHaveBeenCalledTimes(2);
|
|
|
|
|
expect(addItemToHistoryMock.mock.calls[1][0]).toEqual({
|
2025-07-18 17:30:28 -07:00
|
|
|
type: 'tool_group',
|
|
|
|
|
tools: [
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
name: 'Shell Command',
|
|
|
|
|
description: 'a-bad-command',
|
|
|
|
|
status: ToolCallStatus.Error,
|
|
|
|
|
resultDisplay: 'Command exited with code 127.\ncommand not found',
|
|
|
|
|
}),
|
|
|
|
|
],
|
2025-06-15 22:09:30 -04:00
|
|
|
});
|
2025-05-20 00:21:01 -07:00
|
|
|
});
|
|
|
|
|
});
|