| |
| |
| |
| |
| |
|
|
| import { describe, it, expect, beforeEach, vi, type Mock } from 'vitest'; |
| import { ConfirmationRequiredError, ShellProcessor } from './shellProcessor.js'; |
| import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; |
| import type { CommandContext } from '../../ui/commands/types.js'; |
| import type { Config } from '@google/gemini-cli-core'; |
| import { |
| ApprovalMode, |
| getShellConfiguration, |
| PolicyDecision, |
| NoopSandboxManager, |
| } from '@google/gemini-cli-core'; |
| import { quote } from 'shell-quote'; |
| import { createPartFromText } from '@google/genai'; |
| import type { PromptPipelineContent } from './types.js'; |
|
|
| |
| |
| function getExpectedEscapedArgForPlatform(arg: string): string { |
| const { shell } = getShellConfiguration(); |
|
|
| switch (shell) { |
| case 'powershell': |
| return `'${arg.replace(/'/g, "''")}'`; |
| case 'cmd': |
| return `"${arg.replace(/"/g, '""')}"`; |
| case 'bash': |
| default: |
| return quote([arg]); |
| } |
| } |
|
|
| |
| function createPromptPipelineContent(text: string): PromptPipelineContent { |
| return [createPartFromText(text)]; |
| } |
|
|
| const mockCheckCommandPermissions = vi.hoisted(() => vi.fn()); |
| const mockShellExecute = vi.hoisted(() => vi.fn()); |
|
|
| vi.mock('@google/gemini-cli-core', async (importOriginal) => { |
| const original = await importOriginal<object>(); |
| return { |
| ...original, |
| checkCommandPermissions: mockCheckCommandPermissions, |
| ShellExecutionService: { |
| execute: mockShellExecute, |
| }, |
| }; |
| }); |
|
|
| const SUCCESS_RESULT = { |
| output: 'default shell output', |
| exitCode: 0, |
| error: null, |
| aborted: false, |
| signal: null, |
| }; |
|
|
| describe('ShellProcessor', () => { |
| let context: CommandContext; |
| let mockConfig: Partial<Config>; |
| let mockPolicyEngineCheck: Mock; |
|
|
| beforeEach(() => { |
| vi.clearAllMocks(); |
|
|
| mockPolicyEngineCheck = vi.fn().mockResolvedValue({ |
| decision: PolicyDecision.ALLOW, |
| }); |
|
|
| mockConfig = { |
| getTargetDir: vi.fn().mockReturnValue('/test/dir'), |
| getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT), |
| getEnableInteractiveShell: vi.fn().mockReturnValue(false), |
| getShellExecutionConfig: vi.fn().mockReturnValue({ |
| sandboxManager: new NoopSandboxManager(), |
| sanitizationConfig: { |
| allowedEnvironmentVariables: [], |
| blockedEnvironmentVariables: [], |
| enableEnvironmentVariableRedaction: false, |
| }, |
| }), |
| getPolicyEngine: vi.fn().mockReturnValue({ |
| check: mockPolicyEngineCheck, |
| }), |
| getExperimentalGemma: vi.fn().mockReturnValue(false), |
| get config() { |
| return this as unknown as Config; |
| }, |
| }; |
|
|
| context = createMockCommandContext({ |
| invocation: { |
| raw: '/cmd default args', |
| name: 'cmd', |
| args: 'default args', |
| }, |
| services: { |
| agentContext: mockConfig as Config, |
| }, |
| session: { |
| sessionShellAllowlist: new Set(), |
| }, |
| }); |
|
|
| mockShellExecute.mockReturnValue({ |
| result: Promise.resolve(SUCCESS_RESULT), |
| }); |
|
|
| mockCheckCommandPermissions.mockReturnValue({ |
| allAllowed: true, |
| disallowedCommands: [], |
| }); |
| }); |
|
|
| it('should throw an error if config is missing', async () => { |
| const processor = new ShellProcessor('test-command'); |
| const prompt: PromptPipelineContent = createPromptPipelineContent('!{ls}'); |
| const contextWithoutConfig = createMockCommandContext({ |
| services: { |
| agentContext: null, |
| }, |
| }); |
|
|
| await expect( |
| processor.process(prompt, contextWithoutConfig), |
| ).rejects.toThrow(/Security configuration not loaded/); |
| }); |
|
|
| it('should not change the prompt if no shell injections are present', async () => { |
| const processor = new ShellProcessor('test-command'); |
| const prompt: PromptPipelineContent = createPromptPipelineContent( |
| 'This is a simple prompt with no injections.', |
| ); |
| const result = await processor.process(prompt, context); |
| expect(result).toEqual(prompt); |
| expect(mockShellExecute).not.toHaveBeenCalled(); |
| }); |
|
|
| it('should process a single valid shell injection if allowed', async () => { |
| const processor = new ShellProcessor('test-command'); |
| const prompt: PromptPipelineContent = createPromptPipelineContent( |
| 'The current status is: !{git status}', |
| ); |
| mockPolicyEngineCheck.mockResolvedValue({ |
| decision: PolicyDecision.ALLOW, |
| }); |
| mockShellExecute.mockReturnValue({ |
| result: Promise.resolve({ ...SUCCESS_RESULT, output: 'On branch main' }), |
| }); |
|
|
| const result = await processor.process(prompt, context); |
|
|
| expect(mockPolicyEngineCheck).toHaveBeenCalledWith( |
| { |
| name: 'run_shell_command', |
| args: { command: 'git status' }, |
| }, |
| undefined, |
| ); |
| expect(mockShellExecute).toHaveBeenCalledWith( |
| 'git status', |
| expect.any(String), |
| expect.any(Function), |
| expect.any(Object), |
| false, |
| expect.any(Object), |
| ); |
| expect(result).toEqual([{ text: 'The current status is: On branch main' }]); |
| }); |
|
|
| it('should process multiple valid shell injections if all are allowed', async () => { |
| const processor = new ShellProcessor('test-command'); |
| const prompt: PromptPipelineContent = createPromptPipelineContent( |
| '!{git status} in !{pwd}', |
| ); |
| mockPolicyEngineCheck.mockResolvedValue({ |
| decision: PolicyDecision.ALLOW, |
| }); |
|
|
| mockShellExecute |
| .mockReturnValueOnce({ |
| result: Promise.resolve({ |
| ...SUCCESS_RESULT, |
| output: 'On branch main', |
| }), |
| }) |
| .mockReturnValueOnce({ |
| result: Promise.resolve({ ...SUCCESS_RESULT, output: '/usr/home' }), |
| }); |
|
|
| const result = await processor.process(prompt, context); |
|
|
| expect(mockPolicyEngineCheck).toHaveBeenCalledTimes(2); |
| expect(mockShellExecute).toHaveBeenCalledTimes(2); |
| expect(result).toEqual([{ text: 'On branch main in /usr/home' }]); |
| }); |
|
|
| it('should throw ConfirmationRequiredError if a command is not allowed in default mode', async () => { |
| const processor = new ShellProcessor('test-command'); |
| const prompt: PromptPipelineContent = createPromptPipelineContent( |
| 'Do something dangerous: !{rm -rf /}', |
| ); |
| mockPolicyEngineCheck.mockResolvedValue({ |
| decision: PolicyDecision.ASK_USER, |
| }); |
|
|
| await expect(processor.process(prompt, context)).rejects.toThrow( |
| ConfirmationRequiredError, |
| ); |
| }); |
|
|
| it('should NOT throw ConfirmationRequiredError if a command is not allowed but approval mode is YOLO', async () => { |
| const processor = new ShellProcessor('test-command'); |
| const prompt: PromptPipelineContent = createPromptPipelineContent( |
| 'Do something dangerous: !{rm -rf /}', |
| ); |
| |
| mockPolicyEngineCheck.mockResolvedValue({ |
| decision: PolicyDecision.ALLOW, |
| }); |
| |
| (mockConfig.getApprovalMode as Mock).mockReturnValue(ApprovalMode.YOLO); |
| mockShellExecute.mockReturnValue({ |
| result: Promise.resolve({ ...SUCCESS_RESULT, output: 'deleted' }), |
| }); |
|
|
| const result = await processor.process(prompt, context); |
|
|
| |
| expect(mockShellExecute).toHaveBeenCalledWith( |
| 'rm -rf /', |
| expect.any(String), |
| expect.any(Function), |
| expect.any(Object), |
| false, |
| expect.any(Object), |
| ); |
| expect(result).toEqual([{ text: 'Do something dangerous: deleted' }]); |
| }); |
|
|
| it('should still throw an error for a hard-denied command even in YOLO mode', async () => { |
| const processor = new ShellProcessor('test-command'); |
| const prompt: PromptPipelineContent = createPromptPipelineContent( |
| 'Do something forbidden: !{reboot}', |
| ); |
| mockPolicyEngineCheck.mockResolvedValue({ |
| decision: PolicyDecision.DENY, |
| }); |
| |
| (mockConfig.getApprovalMode as Mock).mockReturnValue(ApprovalMode.YOLO); |
|
|
| await expect(processor.process(prompt, context)).rejects.toThrow( |
| /Blocked command: "reboot". Reason: Blocked by policy/, |
| ); |
|
|
| |
| expect(mockShellExecute).not.toHaveBeenCalled(); |
| }); |
|
|
| it('should throw ConfirmationRequiredError with the correct command', async () => { |
| const processor = new ShellProcessor('test-command'); |
| const prompt: PromptPipelineContent = createPromptPipelineContent( |
| 'Do something dangerous: !{rm -rf /}', |
| ); |
| mockPolicyEngineCheck.mockResolvedValue({ |
| decision: PolicyDecision.ASK_USER, |
| }); |
|
|
| try { |
| await processor.process(prompt, context); |
| |
| expect(true).toBe(false); |
| } catch (e) { |
| expect(e).toBeInstanceOf(ConfirmationRequiredError); |
| if (e instanceof ConfirmationRequiredError) { |
| expect(e.commandsToConfirm).toEqual(['rm -rf /']); |
| } |
| } |
|
|
| expect(mockShellExecute).not.toHaveBeenCalled(); |
| }); |
|
|
| it('should throw ConfirmationRequiredError with multiple commands if multiple are disallowed', async () => { |
| const processor = new ShellProcessor('test-command'); |
| const prompt: PromptPipelineContent = createPromptPipelineContent( |
| '!{cmd1} and !{cmd2}', |
| ); |
| mockPolicyEngineCheck.mockImplementation(async (toolCall) => { |
| const cmd = toolCall.args.command; |
| if (cmd === 'cmd1' || cmd === 'cmd2') { |
| return { decision: PolicyDecision.ASK_USER }; |
| } |
| return { decision: PolicyDecision.ALLOW }; |
| }); |
|
|
| try { |
| await processor.process(prompt, context); |
| |
| expect(true).toBe(false); |
| } catch (e) { |
| expect(e).toBeInstanceOf(ConfirmationRequiredError); |
| if (e instanceof ConfirmationRequiredError) { |
| expect(e.commandsToConfirm).toEqual(['cmd1', 'cmd2']); |
| } |
| } |
| }); |
|
|
| it('should not execute any commands if at least one requires confirmation', async () => { |
| const processor = new ShellProcessor('test-command'); |
| const prompt: PromptPipelineContent = createPromptPipelineContent( |
| 'First: !{echo "hello"}, Second: !{rm -rf /}', |
| ); |
|
|
| mockPolicyEngineCheck.mockImplementation(async (toolCall) => { |
| const cmd = toolCall.args.command; |
| if (cmd.includes('rm')) { |
| return { decision: PolicyDecision.ASK_USER }; |
| } |
| return { decision: PolicyDecision.ALLOW }; |
| }); |
|
|
| await expect(processor.process(prompt, context)).rejects.toThrow( |
| ConfirmationRequiredError, |
| ); |
|
|
| |
| expect(mockShellExecute).not.toHaveBeenCalled(); |
| }); |
|
|
| it('should only request confirmation for disallowed commands in a mixed prompt', async () => { |
| const processor = new ShellProcessor('test-command'); |
| const prompt: PromptPipelineContent = createPromptPipelineContent( |
| 'Allowed: !{ls -l}, Disallowed: !{rm -rf /}', |
| ); |
|
|
| mockPolicyEngineCheck.mockImplementation(async (toolCall) => { |
| const cmd = toolCall.args.command; |
| if (cmd.includes('rm')) { |
| return { decision: PolicyDecision.ASK_USER }; |
| } |
| return { decision: PolicyDecision.ALLOW }; |
| }); |
|
|
| try { |
| await processor.process(prompt, context); |
| expect.fail('Should have thrown ConfirmationRequiredError'); |
| } catch (e) { |
| expect(e).toBeInstanceOf(ConfirmationRequiredError); |
| if (e instanceof ConfirmationRequiredError) { |
| expect(e.commandsToConfirm).toEqual(['rm -rf /']); |
| } |
| } |
| }); |
|
|
| it('should execute all commands if they are on the session allowlist', async () => { |
| const processor = new ShellProcessor('test-command'); |
| const prompt: PromptPipelineContent = createPromptPipelineContent( |
| 'Run !{cmd1} and !{cmd2}', |
| ); |
|
|
| |
| context.session.sessionShellAllowlist = new Set(['cmd1', 'cmd2']); |
|
|
| |
| mockPolicyEngineCheck.mockResolvedValue({ |
| decision: PolicyDecision.ALLOW, |
| }); |
|
|
| mockShellExecute |
| .mockReturnValueOnce({ |
| result: Promise.resolve({ ...SUCCESS_RESULT, output: 'output1' }), |
| }) |
| .mockReturnValueOnce({ |
| result: Promise.resolve({ ...SUCCESS_RESULT, output: 'output2' }), |
| }); |
|
|
| const result = await processor.process(prompt, context); |
|
|
| expect(mockPolicyEngineCheck).not.toHaveBeenCalled(); |
| expect(mockShellExecute).toHaveBeenCalledTimes(2); |
| expect(result).toEqual([{ text: 'Run output1 and output2' }]); |
| }); |
|
|
| it('should support the full confirmation flow (Ask -> Approve -> Retry)', async () => { |
| |
| const processor = new ShellProcessor('test-command'); |
| const prompt: PromptPipelineContent = |
| createPromptPipelineContent('!{echo "once"}'); |
|
|
| |
| mockPolicyEngineCheck.mockResolvedValue({ |
| decision: PolicyDecision.ASK_USER, |
| }); |
|
|
| |
| try { |
| await processor.process(prompt, context); |
| expect.fail('Should have thrown ConfirmationRequiredError'); |
| } catch (e) { |
| expect(e).toBeInstanceOf(ConfirmationRequiredError); |
| expect(mockPolicyEngineCheck).toHaveBeenCalledTimes(1); |
| } |
|
|
| |
| context.session.sessionShellAllowlist.add('echo "once"'); |
|
|
| |
| |
| mockPolicyEngineCheck.mockClear(); |
|
|
| |
| mockShellExecute.mockReturnValue({ |
| result: Promise.resolve({ ...SUCCESS_RESULT, output: 'once' }), |
| }); |
|
|
| const result = await processor.process(prompt, context); |
|
|
| |
| expect(mockPolicyEngineCheck).not.toHaveBeenCalled(); |
| expect(mockShellExecute).toHaveBeenCalledWith( |
| 'echo "once"', |
| expect.any(String), |
| expect.any(Function), |
| expect.any(Object), |
| false, |
| expect.any(Object), |
| ); |
| expect(result).toEqual([{ text: 'once' }]); |
| }); |
|
|
| it('should trim whitespace from the command inside the injection before interpolation', async () => { |
| const processor = new ShellProcessor('test-command'); |
| const prompt: PromptPipelineContent = createPromptPipelineContent( |
| 'Files: !{ ls {{args}} -l }', |
| ); |
|
|
| const rawArgs = context.invocation!.args; |
|
|
| const expectedEscapedArgs = getExpectedEscapedArgForPlatform(rawArgs); |
|
|
| const expectedCommand = `ls ${expectedEscapedArgs} -l`; |
|
|
| mockPolicyEngineCheck.mockResolvedValue({ |
| decision: PolicyDecision.ALLOW, |
| }); |
| mockShellExecute.mockReturnValue({ |
| result: Promise.resolve({ ...SUCCESS_RESULT, output: 'total 0' }), |
| }); |
|
|
| await processor.process(prompt, context); |
|
|
| expect(mockPolicyEngineCheck).toHaveBeenCalledWith( |
| { name: 'run_shell_command', args: { command: expectedCommand } }, |
| undefined, |
| ); |
| expect(mockShellExecute).toHaveBeenCalledWith( |
| expectedCommand, |
| expect.any(String), |
| expect.any(Function), |
| expect.any(Object), |
| false, |
| expect.any(Object), |
| ); |
| }); |
|
|
| it('should handle an empty command inside the injection gracefully (skips execution)', async () => { |
| const processor = new ShellProcessor('test-command'); |
| const prompt: PromptPipelineContent = |
| createPromptPipelineContent('This is weird: !{}'); |
|
|
| const result = await processor.process(prompt, context); |
|
|
| expect(mockPolicyEngineCheck).not.toHaveBeenCalled(); |
| expect(mockShellExecute).not.toHaveBeenCalled(); |
|
|
| |
| expect(result).toEqual([{ text: 'This is weird: ' }]); |
| }); |
|
|
| describe('Error Reporting', () => { |
| it('should append exit code and command name on failure', async () => { |
| const processor = new ShellProcessor('test-command'); |
| const prompt: PromptPipelineContent = |
| createPromptPipelineContent('!{cmd}'); |
| mockShellExecute.mockReturnValue({ |
| result: Promise.resolve({ |
| ...SUCCESS_RESULT, |
| output: 'some error output', |
| stderr: '', |
| exitCode: 1, |
| }), |
| }); |
|
|
| const result = await processor.process(prompt, context); |
|
|
| expect(result).toEqual([ |
| { |
| text: "some error output\n[Shell command 'cmd' exited with code 1]", |
| }, |
| ]); |
| }); |
|
|
| it('should append signal info and command name if terminated by signal', async () => { |
| const processor = new ShellProcessor('test-command'); |
| const prompt: PromptPipelineContent = |
| createPromptPipelineContent('!{cmd}'); |
| mockShellExecute.mockReturnValue({ |
| result: Promise.resolve({ |
| ...SUCCESS_RESULT, |
| output: 'output', |
| stderr: '', |
| exitCode: null, |
| signal: 'SIGTERM', |
| }), |
| }); |
|
|
| const result = await processor.process(prompt, context); |
|
|
| expect(result).toEqual([ |
| { |
| text: "output\n[Shell command 'cmd' terminated by signal SIGTERM]", |
| }, |
| ]); |
| }); |
|
|
| it('should throw a detailed error if the shell fails to spawn', async () => { |
| const processor = new ShellProcessor('test-command'); |
| const prompt: PromptPipelineContent = |
| createPromptPipelineContent('!{bad-command}'); |
| const spawnError = new Error('spawn EACCES'); |
| mockShellExecute.mockReturnValue({ |
| result: Promise.resolve({ |
| ...SUCCESS_RESULT, |
| stdout: '', |
| stderr: '', |
| exitCode: null, |
| error: spawnError, |
| aborted: false, |
| }), |
| }); |
|
|
| await expect(processor.process(prompt, context)).rejects.toThrow( |
| "Failed to start shell command in 'test-command': spawn EACCES. Command: bad-command", |
| ); |
| }); |
|
|
| it('should report abort status with command name if aborted', async () => { |
| const processor = new ShellProcessor('test-command'); |
| const prompt: PromptPipelineContent = createPromptPipelineContent( |
| '!{long-running-command}', |
| ); |
| const spawnError = new Error('Aborted'); |
| mockShellExecute.mockReturnValue({ |
| result: Promise.resolve({ |
| ...SUCCESS_RESULT, |
| output: 'partial output', |
| stderr: '', |
| exitCode: null, |
| error: spawnError, |
| aborted: true, |
| }), |
| }); |
|
|
| const result = await processor.process(prompt, context); |
| expect(result).toEqual([ |
| { |
| text: "partial output\n[Shell command 'long-running-command' aborted]", |
| }, |
| ]); |
| }); |
| }); |
|
|
| describe('Context-Aware Argument Interpolation ({{args}})', () => { |
| const rawArgs = 'user input'; |
|
|
| beforeEach(() => { |
| |
| context.invocation!.args = rawArgs; |
| }); |
|
|
| it('should perform raw replacement if no shell injections are present (optimization path)', async () => { |
| const processor = new ShellProcessor('test-command'); |
| const prompt: PromptPipelineContent = createPromptPipelineContent( |
| 'The user said: {{args}}', |
| ); |
|
|
| const result = await processor.process(prompt, context); |
|
|
| expect(result).toEqual([{ text: `The user said: ${rawArgs}` }]); |
| expect(mockShellExecute).not.toHaveBeenCalled(); |
| }); |
|
|
| it('should perform raw replacement outside !{} blocks', async () => { |
| const processor = new ShellProcessor('test-command'); |
| const prompt: PromptPipelineContent = createPromptPipelineContent( |
| 'Outside: {{args}}. Inside: !{echo "hello"}', |
| ); |
| mockShellExecute.mockReturnValue({ |
| result: Promise.resolve({ ...SUCCESS_RESULT, output: 'hello' }), |
| }); |
|
|
| const result = await processor.process(prompt, context); |
|
|
| expect(result).toEqual([{ text: `Outside: ${rawArgs}. Inside: hello` }]); |
| }); |
|
|
| it('should perform escaped replacement inside !{} blocks', async () => { |
| const processor = new ShellProcessor('test-command'); |
| const prompt: PromptPipelineContent = createPromptPipelineContent( |
| 'Command: !{grep {{args}} file.txt}', |
| ); |
| mockShellExecute.mockReturnValue({ |
| result: Promise.resolve({ ...SUCCESS_RESULT, output: 'match found' }), |
| }); |
|
|
| const result = await processor.process(prompt, context); |
|
|
| const expectedEscapedArgs = getExpectedEscapedArgForPlatform(rawArgs); |
| const expectedCommand = `grep ${expectedEscapedArgs} file.txt`; |
|
|
| expect(mockShellExecute).toHaveBeenCalledWith( |
| expectedCommand, |
| expect.any(String), |
| expect.any(Function), |
| expect.any(Object), |
| false, |
| expect.any(Object), |
| ); |
|
|
| expect(result).toEqual([{ text: 'Command: match found' }]); |
| }); |
|
|
| it('should handle both raw (outside) and escaped (inside) injection simultaneously', async () => { |
| const processor = new ShellProcessor('test-command'); |
| const prompt: PromptPipelineContent = createPromptPipelineContent( |
| 'User "({{args}})" requested search: !{search {{args}}}', |
| ); |
| mockShellExecute.mockReturnValue({ |
| result: Promise.resolve({ ...SUCCESS_RESULT, output: 'results' }), |
| }); |
|
|
| const result = await processor.process(prompt, context); |
|
|
| const expectedEscapedArgs = getExpectedEscapedArgForPlatform(rawArgs); |
| const expectedCommand = `search ${expectedEscapedArgs}`; |
| expect(mockShellExecute).toHaveBeenCalledWith( |
| expectedCommand, |
| expect.any(String), |
| expect.any(Function), |
| expect.any(Object), |
| false, |
| expect.any(Object), |
| ); |
|
|
| expect(result).toEqual([ |
| { text: `User "(${rawArgs})" requested search: results` }, |
| ]); |
| }); |
|
|
| it('should perform security checks on the final, resolved (escaped) command', async () => { |
| const processor = new ShellProcessor('test-command'); |
| const prompt: PromptPipelineContent = |
| createPromptPipelineContent('!{rm {{args}}}'); |
|
|
| const expectedEscapedArgs = getExpectedEscapedArgForPlatform(rawArgs); |
| const expectedResolvedCommand = `rm ${expectedEscapedArgs}`; |
| mockPolicyEngineCheck.mockResolvedValue({ |
| decision: PolicyDecision.ASK_USER, |
| }); |
|
|
| await expect(processor.process(prompt, context)).rejects.toThrow( |
| ConfirmationRequiredError, |
| ); |
|
|
| expect(mockPolicyEngineCheck).toHaveBeenCalledWith( |
| { |
| name: 'run_shell_command', |
| args: { command: expectedResolvedCommand }, |
| }, |
| undefined, |
| ); |
| }); |
|
|
| it('should report the resolved command if a hard denial occurs', async () => { |
| const processor = new ShellProcessor('test-command'); |
| const prompt: PromptPipelineContent = |
| createPromptPipelineContent('!{rm {{args}}}'); |
| const expectedEscapedArgs = getExpectedEscapedArgForPlatform(rawArgs); |
| const expectedResolvedCommand = `rm ${expectedEscapedArgs}`; |
| mockPolicyEngineCheck.mockResolvedValue({ |
| decision: PolicyDecision.DENY, |
| }); |
|
|
| await expect(processor.process(prompt, context)).rejects.toThrow( |
| `Blocked command: "${expectedResolvedCommand}". Reason: Blocked by policy.`, |
| ); |
| }); |
| }); |
| describe('Real-World Escaping Scenarios', () => { |
| it('should correctly handle multiline arguments', async () => { |
| const processor = new ShellProcessor('test-command'); |
| const multilineArgs = 'first line\nsecond line'; |
| context.invocation!.args = multilineArgs; |
| const prompt: PromptPipelineContent = createPromptPipelineContent( |
| 'Commit message: !{git commit -m {{args}}}', |
| ); |
|
|
| const expectedEscapedArgs = |
| getExpectedEscapedArgForPlatform(multilineArgs); |
| const expectedCommand = `git commit -m ${expectedEscapedArgs}`; |
|
|
| await processor.process(prompt, context); |
|
|
| expect(mockShellExecute).toHaveBeenCalledWith( |
| expectedCommand, |
| expect.any(String), |
| expect.any(Function), |
| expect.any(Object), |
| false, |
| expect.any(Object), |
| ); |
| }); |
|
|
| it.each([ |
| { name: 'spaces', input: 'file with spaces.txt' }, |
| { name: 'double quotes', input: 'a "quoted" string' }, |
| { name: 'single quotes', input: "it's a string" }, |
| { name: 'command substitution (backticks)', input: '`reboot`' }, |
| { name: 'command substitution (dollar)', input: '$(reboot)' }, |
| { name: 'variable expansion', input: '$HOME' }, |
| { name: 'command chaining (semicolon)', input: 'a; reboot' }, |
| { name: 'command chaining (ampersand)', input: 'a && reboot' }, |
| ])('should safely escape args containing $name', async ({ input }) => { |
| const processor = new ShellProcessor('test-command'); |
| context.invocation!.args = input; |
| const prompt: PromptPipelineContent = |
| createPromptPipelineContent('!{echo {{args}}}'); |
|
|
| const expectedEscapedArgs = getExpectedEscapedArgForPlatform(input); |
| const expectedCommand = `echo ${expectedEscapedArgs}`; |
|
|
| await processor.process(prompt, context); |
|
|
| expect(mockShellExecute).toHaveBeenCalledWith( |
| expectedCommand, |
| expect.any(String), |
| expect.any(Function), |
| expect.any(Object), |
| false, |
| expect.any(Object), |
| ); |
| }); |
| }); |
| }); |
|
|