mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-31 16:31:08 -07:00
fix(core): improve shell command with redirection detection (#15683)
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
getShellConfiguration,
|
||||
initializeShellParsers,
|
||||
stripShellWrapper,
|
||||
hasRedirection,
|
||||
} from './shell-utils.js';
|
||||
|
||||
const mockPlatform = vi.hoisted(() => vi.fn());
|
||||
@@ -32,6 +33,12 @@ vi.mock('os', () => ({
|
||||
homedir: mockHomedir,
|
||||
}));
|
||||
|
||||
const mockSpawnSync = vi.hoisted(() => vi.fn());
|
||||
vi.mock('node:child_process', () => ({
|
||||
spawnSync: mockSpawnSync,
|
||||
spawn: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockQuote = vi.hoisted(() => vi.fn());
|
||||
vi.mock('shell-quote', () => ({
|
||||
quote: mockQuote,
|
||||
@@ -50,6 +57,12 @@ beforeEach(() => {
|
||||
mockQuote.mockImplementation((args: string[]) =>
|
||||
args.map((arg) => `'${arg}'`).join(' '),
|
||||
);
|
||||
mockSpawnSync.mockReturnValue({
|
||||
stdout: Buffer.from(''),
|
||||
stderr: Buffer.from(''),
|
||||
status: 0,
|
||||
error: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -105,6 +118,64 @@ describe('getCommandRoots', () => {
|
||||
const roots = getCommandRoots('echo ${foo@P}');
|
||||
expect(roots).toEqual([]);
|
||||
});
|
||||
|
||||
it('should include nested command substitutions in redirected statements', () => {
|
||||
const result = getCommandRoots('echo $(cat secret) > output.txt');
|
||||
expect(result).toEqual(['echo', 'cat']);
|
||||
});
|
||||
|
||||
it('should handle parser initialization failures gracefully', async () => {
|
||||
// Reset modules to clear singleton state
|
||||
vi.resetModules();
|
||||
|
||||
// Mock fileUtils to fail Wasm loading
|
||||
vi.doMock('./fileUtils.js', () => ({
|
||||
loadWasmBinary: vi.fn().mockRejectedValue(new Error('Wasm load failed')),
|
||||
}));
|
||||
|
||||
// Re-import shell-utils with mocked dependencies
|
||||
const shellUtils = await import('./shell-utils.js');
|
||||
|
||||
// Should catch the error and not throw
|
||||
await expect(shellUtils.initializeShellParsers()).resolves.not.toThrow();
|
||||
|
||||
// Fallback: splitting commands depends on parser, so if parser fails, it returns empty
|
||||
const roots = shellUtils.getCommandRoots('ls -la');
|
||||
expect(roots).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasRedirection', () => {
|
||||
it('should detect output redirection', () => {
|
||||
expect(hasRedirection('echo hello > world')).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect input redirection', () => {
|
||||
expect(hasRedirection('cat < input')).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect append redirection', () => {
|
||||
expect(hasRedirection('echo hello >> world')).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect heredoc', () => {
|
||||
expect(hasRedirection('cat <<EOF\nhello\nEOF')).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect herestring', () => {
|
||||
expect(hasRedirection('cat <<< "hello"')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for simple commands', () => {
|
||||
expect(hasRedirection('ls -la')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for pipes (pipes are not redirections in this context)', () => {
|
||||
// Note: pipes are often handled separately by splitCommands, but checking here confirms they don't trigger "redirection" flag if we don't want them to.
|
||||
// However, the current implementation checks for 'redirected_statement' nodes.
|
||||
// A pipe is a 'pipeline' node.
|
||||
expect(hasRedirection('echo hello | cat')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describeWindowsOnly('PowerShell integration', () => {
|
||||
@@ -300,3 +371,55 @@ describe('getShellConfiguration', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasRedirection (PowerShell via mock)', () => {
|
||||
beforeEach(() => {
|
||||
mockPlatform.mockReturnValue('win32');
|
||||
process.env['ComSpec'] = 'powershell.exe';
|
||||
});
|
||||
|
||||
const mockPowerShellResult = (
|
||||
commands: Array<{ name: string; text: string }>,
|
||||
hasRedirection: boolean,
|
||||
) => {
|
||||
mockSpawnSync.mockReturnValue({
|
||||
stdout: Buffer.from(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
commands,
|
||||
hasRedirection,
|
||||
}),
|
||||
),
|
||||
stderr: Buffer.from(''),
|
||||
status: 0,
|
||||
error: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
it('should return true when PowerShell parser detects redirection', () => {
|
||||
mockPowerShellResult([{ name: 'echo', text: 'echo hello' }], true);
|
||||
expect(hasRedirection('echo hello > file.txt')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when PowerShell parser does not detect redirection', () => {
|
||||
mockPowerShellResult([{ name: 'echo', text: 'echo hello' }], false);
|
||||
expect(hasRedirection('echo hello')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when quoted redirection chars are used but not actual redirection', () => {
|
||||
mockPowerShellResult(
|
||||
[{ name: 'echo', text: 'echo "-> arrow"' }],
|
||||
false, // Parser says NO redirection
|
||||
);
|
||||
expect(hasRedirection('echo "-> arrow"')).toBe(false);
|
||||
});
|
||||
|
||||
it('should fallback to regex if parsing fails (simulating safety)', () => {
|
||||
mockSpawnSync.mockReturnValue({
|
||||
stdout: Buffer.from('invalid json'),
|
||||
status: 0,
|
||||
});
|
||||
// Fallback regex sees '>' in arrow
|
||||
expect(hasRedirection('echo "-> arrow"')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user