Merge tag 'v0.1.21' of github.com:google-gemini/gemini-cli into chore/sync-gemini-cli-v0.1.21

This commit is contained in:
mingholy.lmh
2025-08-20 22:24:50 +08:00
163 changed files with 8812 additions and 4098 deletions

View File

@@ -25,7 +25,6 @@ vi.mock('../utils/summarizer.js');
import { isCommandAllowed } from '../utils/shell-utils.js';
import { ShellTool } from './shell.js';
import { ToolErrorType } from './tool-error.js';
import { type Config } from '../config/config.js';
import {
type ShellExecutionResult,
@@ -98,22 +97,25 @@ describe('ShellTool', () => {
});
});
describe('validateToolParams', () => {
it('should return null for a valid command', () => {
expect(shellTool.validateToolParams({ command: 'ls -l' })).toBeNull();
describe('build', () => {
it('should return an invocation for a valid command', () => {
const invocation = shellTool.build({ command: 'ls -l' });
expect(invocation).toBeDefined();
});
it('should return an error for an empty command', () => {
expect(shellTool.validateToolParams({ command: ' ' })).toBe(
it('should throw an error for an empty command', () => {
expect(() => shellTool.build({ command: ' ' })).toThrow(
'Command cannot be empty.',
);
});
it('should return an error for a non-existent directory', () => {
it('should throw an error for a non-existent directory', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
expect(
shellTool.validateToolParams({ command: 'ls', directory: 'rel/path' }),
).toBe("Directory 'rel/path' is not a registered workspace directory.");
expect(() =>
shellTool.build({ command: 'ls', directory: 'rel/path' }),
).toThrow(
"Directory 'rel/path' is not a registered workspace directory.",
);
});
});
@@ -139,10 +141,8 @@ describe('ShellTool', () => {
};
it('should wrap command on linux and parse pgrep output', async () => {
const promise = shellTool.execute(
{ command: 'my-command &' },
mockAbortSignal,
);
const invocation = shellTool.build({ command: 'my-command &' });
const promise = invocation.execute(mockAbortSignal);
resolveShellExecution({ pid: 54321 });
vi.mocked(fs.existsSync).mockReturnValue(true);
@@ -164,8 +164,9 @@ describe('ShellTool', () => {
it('should not wrap command on windows', async () => {
vi.mocked(os.platform).mockReturnValue('win32');
const promise = shellTool.execute({ command: 'dir' }, mockAbortSignal);
resolveExecutionPromise({
const invocation = shellTool.build({ command: 'dir' });
const promise = invocation.execute(mockAbortSignal);
resolveShellExecution({
rawOutput: Buffer.from(''),
output: '',
stdout: '',
@@ -187,10 +188,8 @@ describe('ShellTool', () => {
it('should format error messages correctly', async () => {
const error = new Error('wrapped command failed');
const promise = shellTool.execute(
{ command: 'user-command' },
mockAbortSignal,
);
const invocation = shellTool.build({ command: 'user-command' });
const promise = invocation.execute(mockAbortSignal);
resolveShellExecution({
error,
exitCode: 1,
@@ -209,40 +208,19 @@ describe('ShellTool', () => {
expect(result.llmContent).not.toContain('pgrep');
});
it('should return error with error property for invalid parameters', async () => {
const result = await shellTool.execute(
{ command: '' }, // Empty command is invalid
mockAbortSignal,
it('should throw an error for invalid parameters', () => {
expect(() => shellTool.build({ command: '' })).toThrow(
'Command cannot be empty.',
);
expect(result.llmContent).toContain(
'Could not execute command due to invalid parameters:',
);
expect(result.returnDisplay).toBe('Command cannot be empty.');
expect(result.error).toEqual({
message: 'Command cannot be empty.',
type: ToolErrorType.INVALID_TOOL_PARAMS,
});
});
it('should return error with error property for invalid directory', async () => {
it('should throw an error for invalid directory', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
const result = await shellTool.execute(
{ command: 'ls', directory: 'nonexistent' },
mockAbortSignal,
expect(() =>
shellTool.build({ command: 'ls', directory: 'nonexistent' }),
).toThrow(
`Directory 'nonexistent' is not a registered workspace directory.`,
);
expect(result.llmContent).toContain(
'Could not execute command due to invalid parameters:',
);
expect(result.returnDisplay).toBe(
"Directory 'nonexistent' is not a registered workspace directory.",
);
expect(result.error).toEqual({
message:
"Directory 'nonexistent' is not a registered workspace directory.",
type: ToolErrorType.INVALID_TOOL_PARAMS,
});
});
it('should summarize output when configured', async () => {
@@ -253,7 +231,8 @@ describe('ShellTool', () => {
'summarized output',
);
const promise = shellTool.execute({ command: 'ls' }, mockAbortSignal);
const invocation = shellTool.build({ command: 'ls' });
const promise = invocation.execute(mockAbortSignal);
resolveExecutionPromise({
output: 'long output',
rawOutput: Buffer.from('long output'),
@@ -285,9 +264,8 @@ describe('ShellTool', () => {
});
vi.mocked(fs.existsSync).mockReturnValue(true); // Pretend the file exists
await expect(
shellTool.execute({ command: 'a-command' }, mockAbortSignal),
).rejects.toThrow(error);
const invocation = shellTool.build({ command: 'a-command' });
await expect(invocation.execute(mockAbortSignal)).rejects.toThrow(error);
const tmpFile = path.join(os.tmpdir(), 'shell_pgrep_abcdef.tmp');
expect(vi.mocked(fs.unlinkSync)).toHaveBeenCalledWith(tmpFile);
@@ -304,11 +282,8 @@ describe('ShellTool', () => {
});
it('should throttle text output updates', async () => {
const promise = shellTool.execute(
{ command: 'stream' },
mockAbortSignal,
updateOutputMock,
);
const invocation = shellTool.build({ command: 'stream' });
const promise = invocation.execute(mockAbortSignal, updateOutputMock);
// First chunk, should be throttled.
mockShellOutputCallback({
@@ -347,11 +322,8 @@ describe('ShellTool', () => {
});
it('should immediately show binary detection message and throttle progress', async () => {
const promise = shellTool.execute(
{ command: 'cat img' },
mockAbortSignal,
updateOutputMock,
);
const invocation = shellTool.build({ command: 'cat img' });
const promise = invocation.execute(mockAbortSignal, updateOutputMock);
mockShellOutputCallback({ type: 'binary_detected' });
expect(updateOutputMock).toHaveBeenCalledOnce();
@@ -399,8 +371,8 @@ describe('ShellTool', () => {
describe('shouldConfirmExecute', () => {
it('should request confirmation for a new command and whitelist it on "Always"', async () => {
const params = { command: 'npm install' };
const confirmation = await shellTool.shouldConfirmExecute(
params,
const invocation = shellTool.build(params);
const confirmation = await invocation.shouldConfirmExecute(
new AbortController().signal,
);
@@ -413,19 +385,15 @@ describe('ShellTool', () => {
);
// Should now be whitelisted
const secondConfirmation = await shellTool.shouldConfirmExecute(
{ command: 'npm test' },
const secondInvocation = shellTool.build({ command: 'npm test' });
const secondConfirmation = await secondInvocation.shouldConfirmExecute(
new AbortController().signal,
);
expect(secondConfirmation).toBe(false);
});
it('should skip confirmation if validation fails', async () => {
const confirmation = await shellTool.shouldConfirmExecute(
{ command: '' },
new AbortController().signal,
);
expect(confirmation).toBe(false);
it('should throw an error if validation fails', () => {
expect(() => shellTool.build({ command: '' })).toThrow();
});
});
@@ -581,8 +549,8 @@ describe('validateToolParams', () => {
});
});
describe('validateToolParams', () => {
it('should return null for valid directory', () => {
describe('build', () => {
it('should return an invocation for valid directory', () => {
const config = {
getCoreTools: () => undefined,
getExcludeTools: () => undefined,
@@ -591,14 +559,14 @@ describe('validateToolParams', () => {
createMockWorkspaceContext('/root', ['/users/test']),
} as unknown as Config;
const shellTool = new ShellTool(config);
const result = shellTool.validateToolParams({
const invocation = shellTool.build({
command: 'ls',
directory: 'test',
});
expect(result).toBeNull();
expect(invocation).toBeDefined();
});
it('should return error for directory outside workspace', () => {
it('should throw an error for directory outside workspace', () => {
const config = {
getCoreTools: () => undefined,
getExcludeTools: () => undefined,
@@ -607,10 +575,11 @@ describe('validateToolParams', () => {
createMockWorkspaceContext('/root', ['/users/test']),
} as unknown as Config;
const shellTool = new ShellTool(config);
const result = shellTool.validateToolParams({
command: 'ls',
directory: 'test2',
});
expect(result).toContain('is not a registered workspace directory');
expect(() =>
shellTool.build({
command: 'ls',
directory: 'test2',
}),
).toThrow('is not a registered workspace directory');
});
});