basant307/AI_Governance_Project
045
1/**2 * @license3 * Copyright 2025 Google LLC4 * SPDX-License-Identifier: Apache-2.05 */6 7import { describe, it, expect, beforeEach, vi, type Mock } from 'vitest';8import { ConfirmationRequiredError, ShellProcessor } from './shellProcessor.js';9import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';10import type { CommandContext } from '../../ui/commands/types.js';11import type { Config } from '@qwen-code/qwen-code-core';12import { ApprovalMode } from '@qwen-code/qwen-code-core';13import os from 'node:os';14import { quote } from 'shell-quote';15import { createPartFromText } from '@google/genai';16import type { PromptPipelineContent } from './types.js';17 18// Helper function to determine the expected escaped string based on the current OS,19// mirroring the logic in the actual `escapeShellArg` implementation.20function getExpectedEscapedArgForPlatform(arg: string): string {21 if (os.platform() === 'win32') {22 // Detect Git Bash / MSYS2 / MinTTY environments (same logic as getShellConfiguration)23 const msystem = process.env['MSYSTEM'];24 const term = process.env['TERM'] || '';25 const isGitBash =26 msystem?.startsWith('MINGW') ||27 msystem?.startsWith('MSYS') ||28 term.includes('msys') ||29 term.includes('cygwin');30 31 if (isGitBash) {32 return quote([arg]);33 }34 35 const comSpec = (process.env['ComSpec'] || 'cmd.exe').toLowerCase();36 const isPowerShell =37 comSpec.endsWith('powershell.exe') || comSpec.endsWith('pwsh.exe');38 39 if (isPowerShell) {40 return `'${arg.replace(/'/g, "''")}'`;41 } else {42 return `"${arg.replace(/"/g, '""')}"`;43 }44 } else {45 return quote([arg]);46 }47}48 49// Helper to create PromptPipelineContent50function createPromptPipelineContent(text: string): PromptPipelineContent {51 return [createPartFromText(text)];52}53 54const mockCheckCommandPermissions = vi.hoisted(() => vi.fn());55const mockShellExecute = vi.hoisted(() => vi.fn());56 57vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {58 const original = await importOriginal<object>();59 return {60 ...original,61 checkCommandPermissions: mockCheckCommandPermissions,62 ShellExecutionService: {63 execute: mockShellExecute,64 },65 };66});67 68const SUCCESS_RESULT = {69 output: 'default shell output',70 exitCode: 0,71 error: null,72 aborted: false,73 signal: null,74};75 76describe('ShellProcessor', () => {77 let context: CommandContext;78 let mockConfig: Partial<Config>;79 80 beforeEach(() => {81 vi.clearAllMocks();82 83 mockConfig = {84 getTargetDir: vi.fn().mockReturnValue('/test/dir'),85 getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),86 getShouldUseNodePtyShell: vi.fn().mockReturnValue(false),87 getShellExecutionConfig: vi.fn().mockReturnValue({}),88 getPermissionsAllow: vi.fn().mockReturnValue([]),89 // Default: no permission manager (tests that need one set it explicitly)90 getPermissionManager: vi.fn().mockReturnValue(null),91 };92 93 context = createMockCommandContext({94 invocation: {95 raw: '/cmd default args',96 name: 'cmd',97 args: 'default args',98 },99 services: {100 config: mockConfig as Config,101 },102 session: {103 sessionShellAllowlist: new Set(),104 },105 });106 107 mockShellExecute.mockReturnValue({108 result: Promise.resolve(SUCCESS_RESULT),109 });110 111 mockCheckCommandPermissions.mockReturnValue({112 allAllowed: true,113 disallowedCommands: [],114 });115 });116 117 it('should throw an error if config is missing', async () => {118 const processor = new ShellProcessor('test-command');119 const prompt: PromptPipelineContent = createPromptPipelineContent('!{ls}');120 const contextWithoutConfig = createMockCommandContext({121 services: {122 config: null,123 },124 });125 126 await expect(127 processor.process(prompt, contextWithoutConfig),128 ).rejects.toThrow(/Security configuration not loaded/);129 });130 131 it('should not change the prompt if no shell injections are present', async () => {132 const processor = new ShellProcessor('test-command');133 const prompt: PromptPipelineContent = createPromptPipelineContent(134 'This is a simple prompt with no injections.',135 );136 const result = await processor.process(prompt, context);137 expect(result).toEqual(prompt);138 expect(mockShellExecute).not.toHaveBeenCalled();139 });140 141 it('should process a single valid shell injection if allowed', async () => {142 const processor = new ShellProcessor('test-command');143 const prompt: PromptPipelineContent = createPromptPipelineContent(144 'The current status is: !{git status}',145 );146 mockCheckCommandPermissions.mockReturnValue({147 allAllowed: true,148 disallowedCommands: [],149 });150 mockShellExecute.mockReturnValue({151 result: Promise.resolve({ ...SUCCESS_RESULT, output: 'On branch main' }),152 });153 154 const result = await processor.process(prompt, context);155 156 expect(mockCheckCommandPermissions).toHaveBeenCalledWith(157 'git status',158 expect.any(Object),159 context.session.sessionShellAllowlist,160 );161 expect(mockShellExecute).toHaveBeenCalledWith(162 'git status',163 expect.any(String),164 expect.any(Function),165 expect.any(Object),166 false,167 expect.any(Object),168 );169 expect(result).toEqual([{ text: 'The current status is: On branch main' }]);170 });171 172 it('should process multiple valid shell injections if all are allowed', async () => {173 const processor = new ShellProcessor('test-command');174 const prompt: PromptPipelineContent = createPromptPipelineContent(175 '!{git status} in !{pwd}',176 );177 mockCheckCommandPermissions.mockReturnValue({178 allAllowed: true,179 disallowedCommands: [],180 });181 182 mockShellExecute183 .mockReturnValueOnce({184 result: Promise.resolve({185 ...SUCCESS_RESULT,186 output: 'On branch main',187 }),188 })189 .mockReturnValueOnce({190 result: Promise.resolve({ ...SUCCESS_RESULT, output: '/usr/home' }),191 });192 193 const result = await processor.process(prompt, context);194 195 expect(mockCheckCommandPermissions).toHaveBeenCalledTimes(2);196 expect(mockShellExecute).toHaveBeenCalledTimes(2);197 expect(result).toEqual([{ text: 'On branch main in /usr/home' }]);198 });199 200 it('should throw ConfirmationRequiredError if a command is not allowed in default mode', async () => {201 const processor = new ShellProcessor('test-command');202 const prompt: PromptPipelineContent = createPromptPipelineContent(203 'Do something dangerous: !{rm -rf /}',204 );205 mockCheckCommandPermissions.mockReturnValue({206 allAllowed: false,207 disallowedCommands: ['rm -rf /'],208 });209 210 await expect(processor.process(prompt, context)).rejects.toThrow(211 ConfirmationRequiredError,212 );213 });214 215 it('should NOT throw ConfirmationRequiredError when a command matches allowedTools', async () => {216 const processor = new ShellProcessor('test-command');217 const prompt: PromptPipelineContent = createPromptPipelineContent(218 'Do something dangerous: !{rm -rf /}',219 );220 mockCheckCommandPermissions.mockReturnValue({221 allAllowed: false,222 disallowedCommands: ['rm -rf /'],223 });224 // Simulate allowedTools being pre-merged into permissionsAllow by Config,225 // so PermissionManager returns 'allow' for this command.226 (mockConfig.getPermissionManager as Mock).mockReturnValue({227 isCommandAllowed: (_cmd: string) => 'allow',228 });229 mockShellExecute.mockReturnValue({230 result: Promise.resolve({ ...SUCCESS_RESULT, output: 'deleted' }),231 });232 233 const result = await processor.process(prompt, context);234 235 expect(mockShellExecute).toHaveBeenCalledWith(236 'rm -rf /',237 expect.any(String),238 expect.any(Function),239 expect.any(Object),240 false,241 expect.any(Object),242 );243 expect(result).toEqual([{ text: 'Do something dangerous: deleted' }]);244 });245 246 it('should NOT throw ConfirmationRequiredError if a command is not allowed but approval mode is YOLO', async () => {247 const processor = new ShellProcessor('test-command');248 const prompt: PromptPipelineContent = createPromptPipelineContent(249 'Do something dangerous: !{rm -rf /}',250 );251 mockCheckCommandPermissions.mockReturnValue({252 allAllowed: false,253 disallowedCommands: ['rm -rf /'],254 });255 // Override the approval mode for this test256 (mockConfig.getApprovalMode as Mock).mockReturnValue(ApprovalMode.YOLO);257 mockShellExecute.mockReturnValue({258 result: Promise.resolve({ ...SUCCESS_RESULT, output: 'deleted' }),259 });260 261 const result = await processor.process(prompt, context);262 263 // It should proceed with execution264 expect(mockShellExecute).toHaveBeenCalledWith(265 'rm -rf /',266 expect.any(String),267 expect.any(Function),268 expect.any(Object),269 false,270 expect.any(Object),271 );272 expect(result).toEqual([{ text: 'Do something dangerous: deleted' }]);273 });274 275 it('should still throw an error for a hard-denied command even in YOLO mode', async () => {276 const processor = new ShellProcessor('test-command');277 const prompt: PromptPipelineContent = createPromptPipelineContent(278 'Do something forbidden: !{reboot}',279 );280 mockCheckCommandPermissions.mockReturnValue({281 allAllowed: false,282 disallowedCommands: ['reboot'],283 isHardDenial: true, // This is the key difference284 blockReason: 'System commands are blocked',285 });286 // Set approval mode to YOLO287 (mockConfig.getApprovalMode as Mock).mockReturnValue(ApprovalMode.YOLO);288 289 await expect(processor.process(prompt, context)).rejects.toThrow(290 /Blocked command: "reboot". Reason: System commands are blocked/,291 );292 293 // Ensure it never tried to execute294 expect(mockShellExecute).not.toHaveBeenCalled();295 });296 297 it('should throw ConfirmationRequiredError with the correct command', async () => {298 const processor = new ShellProcessor('test-command');299 const prompt: PromptPipelineContent = createPromptPipelineContent(300 'Do something dangerous: !{rm -rf /}',301 );302 mockCheckCommandPermissions.mockReturnValue({303 allAllowed: false,304 disallowedCommands: ['rm -rf /'],305 });306 307 try {308 await processor.process(prompt, context);309 // Fail if it doesn't throw310 expect(true).toBe(false);311 } catch (e) {312 expect(e).toBeInstanceOf(ConfirmationRequiredError);313 if (e instanceof ConfirmationRequiredError) {314 expect(e.commandsToConfirm).toEqual(['rm -rf /']);315 }316 }317 318 expect(mockShellExecute).not.toHaveBeenCalled();319 });320 321 it('should throw ConfirmationRequiredError with multiple commands if multiple are disallowed', async () => {322 const processor = new ShellProcessor('test-command');323 const prompt: PromptPipelineContent = createPromptPipelineContent(324 '!{cmd1} and !{cmd2}',325 );326 mockCheckCommandPermissions.mockImplementation((cmd) => {327 if (cmd === 'cmd1') {328 return { allAllowed: false, disallowedCommands: ['cmd1'] };329 }330 if (cmd === 'cmd2') {331 return { allAllowed: false, disallowedCommands: ['cmd2'] };332 }333 return { allAllowed: true, disallowedCommands: [] };334 });335 336 try {337 await processor.process(prompt, context);338 // Fail if it doesn't throw339 expect(true).toBe(false);340 } catch (e) {341 expect(e).toBeInstanceOf(ConfirmationRequiredError);342 if (e instanceof ConfirmationRequiredError) {343 expect(e.commandsToConfirm).toEqual(['cmd1', 'cmd2']);344 }345 }346 });347 348 it('should not execute any commands if at least one requires confirmation', async () => {349 const processor = new ShellProcessor('test-command');350 const prompt: PromptPipelineContent = createPromptPipelineContent(351 'First: !{echo "hello"}, Second: !{rm -rf /}',352 );353 354 mockCheckCommandPermissions.mockImplementation((cmd) => {355 if (cmd.includes('rm')) {356 return { allAllowed: false, disallowedCommands: [cmd] };357 }358 return { allAllowed: true, disallowedCommands: [] };359 });360 361 await expect(processor.process(prompt, context)).rejects.toThrow(362 ConfirmationRequiredError,363 );364 365 // Ensure no commands were executed because the pipeline was halted.366 expect(mockShellExecute).not.toHaveBeenCalled();367 });368 369 it('should only request confirmation for disallowed commands in a mixed prompt', async () => {370 const processor = new ShellProcessor('test-command');371 const prompt: PromptPipelineContent = createPromptPipelineContent(372 'Allowed: !{ls -l}, Disallowed: !{rm -rf /}',373 );374 375 mockCheckCommandPermissions.mockImplementation((cmd) => ({376 allAllowed: !cmd.includes('rm'),377 disallowedCommands: cmd.includes('rm') ? [cmd] : [],378 }));379 380 try {381 await processor.process(prompt, context);382 expect.fail('Should have thrown ConfirmationRequiredError');383 } catch (e) {384 expect(e).toBeInstanceOf(ConfirmationRequiredError);385 if (e instanceof ConfirmationRequiredError) {386 expect(e.commandsToConfirm).toEqual(['rm -rf /']);387 }388 }389 });390 391 it('should execute all commands if they are on the session allowlist', async () => {392 const processor = new ShellProcessor('test-command');393 const prompt: PromptPipelineContent = createPromptPipelineContent(394 'Run !{cmd1} and !{cmd2}',395 );396 397 // Add commands to the session allowlist398 context.session.sessionShellAllowlist = new Set(['cmd1', 'cmd2']);399 400 // checkCommandPermissions should now pass for these401 mockCheckCommandPermissions.mockReturnValue({402 allAllowed: true,403 disallowedCommands: [],404 });405 406 mockShellExecute407 .mockReturnValueOnce({408 result: Promise.resolve({ ...SUCCESS_RESULT, output: 'output1' }),409 })410 .mockReturnValueOnce({411 result: Promise.resolve({ ...SUCCESS_RESULT, output: 'output2' }),412 });413 414 const result = await processor.process(prompt, context);415 416 expect(mockCheckCommandPermissions).toHaveBeenCalledWith(417 'cmd1',418 expect.any(Object),419 context.session.sessionShellAllowlist,420 );421 expect(mockCheckCommandPermissions).toHaveBeenCalledWith(422 'cmd2',423 expect.any(Object),424 context.session.sessionShellAllowlist,425 );426 expect(mockShellExecute).toHaveBeenCalledTimes(2);427 expect(result).toEqual([{ text: 'Run output1 and output2' }]);428 });429 430 it('should trim whitespace from the command inside the injection before interpolation', async () => {431 const processor = new ShellProcessor('test-command');432 const prompt: PromptPipelineContent = createPromptPipelineContent(433 'Files: !{ ls {{args}} -l }',434 );435 436 const rawArgs = context.invocation!.args;437 438 const expectedEscapedArgs = getExpectedEscapedArgForPlatform(rawArgs);439 440 const expectedCommand = `ls ${expectedEscapedArgs} -l`;441 442 mockCheckCommandPermissions.mockReturnValue({443 allAllowed: true,444 disallowedCommands: [],445 });446 mockShellExecute.mockReturnValue({447 result: Promise.resolve({ ...SUCCESS_RESULT, output: 'total 0' }),448 });449 450 await processor.process(prompt, context);451 452 expect(mockCheckCommandPermissions).toHaveBeenCalledWith(453 expectedCommand,454 expect.any(Object),455 context.session.sessionShellAllowlist,456 );457 expect(mockShellExecute).toHaveBeenCalledWith(458 expectedCommand,459 expect.any(String),460 expect.any(Function),461 expect.any(Object),462 false,463 expect.any(Object),464 );465 });466 467 it('should handle an empty command inside the injection gracefully (skips execution)', async () => {468 const processor = new ShellProcessor('test-command');469 const prompt: PromptPipelineContent =470 createPromptPipelineContent('This is weird: !{}');471 472 const result = await processor.process(prompt, context);473 474 expect(mockCheckCommandPermissions).not.toHaveBeenCalled();475 expect(mockShellExecute).not.toHaveBeenCalled();476 477 // It replaces !{} with an empty string.478 expect(result).toEqual([{ text: 'This is weird: ' }]);479 });480 481 describe('Error Reporting', () => {482 it('should append exit code and command name on failure', async () => {483 const processor = new ShellProcessor('test-command');484 const prompt: PromptPipelineContent =485 createPromptPipelineContent('!{cmd}');486 mockShellExecute.mockReturnValue({487 result: Promise.resolve({488 ...SUCCESS_RESULT,489 output: 'some error output',490 stderr: '',491 exitCode: 1,492 }),493 });494 495 const result = await processor.process(prompt, context);496 497 expect(result).toEqual([498 {499 text: "some error output\n[Shell command 'cmd' exited with code 1]",500 },501 ]);502 });503 504 it('should append signal info and command name if terminated by signal', async () => {505 const processor = new ShellProcessor('test-command');506 const prompt: PromptPipelineContent =507 createPromptPipelineContent('!{cmd}');508 mockShellExecute.mockReturnValue({509 result: Promise.resolve({510 ...SUCCESS_RESULT,511 output: 'output',512 stderr: '',513 exitCode: null,514 signal: 'SIGTERM',515 }),516 });517 518 const result = await processor.process(prompt, context);519 520 expect(result).toEqual([521 {522 text: "output\n[Shell command 'cmd' terminated by signal SIGTERM]",523 },524 ]);525 });526 527 it('should throw a detailed error if the shell fails to spawn', async () => {528 const processor = new ShellProcessor('test-command');529 const prompt: PromptPipelineContent =530 createPromptPipelineContent('!{bad-command}');531 const spawnError = new Error('spawn EACCES');532 mockShellExecute.mockReturnValue({533 result: Promise.resolve({534 ...SUCCESS_RESULT,535 stdout: '',536 stderr: '',537 exitCode: null,538 error: spawnError,539 aborted: false,540 }),541 });542 543 await expect(processor.process(prompt, context)).rejects.toThrow(544 "Failed to start shell command in 'test-command': spawn EACCES. Command: bad-command",545 );546 });547 548 it('should report abort status with command name if aborted', async () => {549 const processor = new ShellProcessor('test-command');550 const prompt: PromptPipelineContent = createPromptPipelineContent(551 '!{long-running-command}',552 );553 const spawnError = new Error('Aborted');554 mockShellExecute.mockReturnValue({555 result: Promise.resolve({556 ...SUCCESS_RESULT,557 output: 'partial output',558 stderr: '',559 exitCode: null,560 error: spawnError,561 aborted: true, // Key difference562 }),563 });564 565 const result = await processor.process(prompt, context);566 expect(result).toEqual([567 {568 text: "partial output\n[Shell command 'long-running-command' aborted]",569 },570 ]);571 });572 });573 574 describe('Context-Aware Argument Interpolation ({{args}})', () => {575 const rawArgs = 'user input';576 577 beforeEach(() => {578 // Update context for these tests to use specific arguments579 context.invocation!.args = rawArgs;580 });581 582 it('should perform raw replacement if no shell injections are present (optimization path)', async () => {583 const processor = new ShellProcessor('test-command');584 const prompt: PromptPipelineContent = createPromptPipelineContent(585 'The user said: {{args}}',586 );587 588 const result = await processor.process(prompt, context);589 590 expect(result).toEqual([{ text: `The user said: ${rawArgs}` }]);591 expect(mockShellExecute).not.toHaveBeenCalled();592 });593 594 it('should perform raw replacement outside !{} blocks', async () => {595 const processor = new ShellProcessor('test-command');596 const prompt: PromptPipelineContent = createPromptPipelineContent(597 'Outside: {{args}}. Inside: !{echo "hello"}',598 );599 mockShellExecute.mockReturnValue({600 result: Promise.resolve({ ...SUCCESS_RESULT, output: 'hello' }),601 });602 603 const result = await processor.process(prompt, context);604 605 expect(result).toEqual([{ text: `Outside: ${rawArgs}. Inside: hello` }]);606 });607 608 it('should perform escaped replacement inside !{} blocks', async () => {609 const processor = new ShellProcessor('test-command');610 const prompt: PromptPipelineContent = createPromptPipelineContent(611 'Command: !{grep {{args}} file.txt}',612 );613 mockShellExecute.mockReturnValue({614 result: Promise.resolve({ ...SUCCESS_RESULT, output: 'match found' }),615 });616 617 const result = await processor.process(prompt, context);618 619 const expectedEscapedArgs = getExpectedEscapedArgForPlatform(rawArgs);620 const expectedCommand = `grep ${expectedEscapedArgs} file.txt`;621 622 expect(mockShellExecute).toHaveBeenCalledWith(623 expectedCommand,624 expect.any(String),625 expect.any(Function),626 expect.any(Object),627 false,628 expect.any(Object),629 );630 631 expect(result).toEqual([{ text: 'Command: match found' }]);632 });633 634 it('should handle both raw (outside) and escaped (inside) injection simultaneously', async () => {635 const processor = new ShellProcessor('test-command');636 const prompt: PromptPipelineContent = createPromptPipelineContent(637 'User "({{args}})" requested search: !{search {{args}}}',638 );639 mockShellExecute.mockReturnValue({640 result: Promise.resolve({ ...SUCCESS_RESULT, output: 'results' }),641 });642 643 const result = await processor.process(prompt, context);644 645 const expectedEscapedArgs = getExpectedEscapedArgForPlatform(rawArgs);646 const expectedCommand = `search ${expectedEscapedArgs}`;647 expect(mockShellExecute).toHaveBeenCalledWith(648 expectedCommand,649 expect.any(String),650 expect.any(Function),651 expect.any(Object),652 false,653 expect.any(Object),654 );655 656 expect(result).toEqual([657 { text: `User "(${rawArgs})" requested search: results` },658 ]);659 });660 661 it('should perform security checks on the final, resolved (escaped) command', async () => {662 const processor = new ShellProcessor('test-command');663 const prompt: PromptPipelineContent =664 createPromptPipelineContent('!{rm {{args}}}');665 666 const expectedEscapedArgs = getExpectedEscapedArgForPlatform(rawArgs);667 const expectedResolvedCommand = `rm ${expectedEscapedArgs}`;668 mockCheckCommandPermissions.mockReturnValue({669 allAllowed: false,670 disallowedCommands: [expectedResolvedCommand],671 isHardDenial: false,672 });673 674 await expect(processor.process(prompt, context)).rejects.toThrow(675 ConfirmationRequiredError,676 );677 678 expect(mockCheckCommandPermissions).toHaveBeenCalledWith(679 expectedResolvedCommand,680 expect.any(Object),681 context.session.sessionShellAllowlist,682 );683 });684 685 it('should report the resolved command if a hard denial occurs', async () => {686 const processor = new ShellProcessor('test-command');687 const prompt: PromptPipelineContent =688 createPromptPipelineContent('!{rm {{args}}}');689 const expectedEscapedArgs = getExpectedEscapedArgForPlatform(rawArgs);690 const expectedResolvedCommand = `rm ${expectedEscapedArgs}`;691 mockCheckCommandPermissions.mockReturnValue({692 allAllowed: false,693 disallowedCommands: [expectedResolvedCommand],694 isHardDenial: true,695 blockReason: 'It is forbidden.',696 });697 698 await expect(processor.process(prompt, context)).rejects.toThrow(699 `Blocked command: "${expectedResolvedCommand}". Reason: It is forbidden.`,700 );701 });702 });703 describe('Real-World Escaping Scenarios', () => {704 it('should correctly handle multiline arguments', async () => {705 const processor = new ShellProcessor('test-command');706 const multilineArgs = 'first line\nsecond line';707 context.invocation!.args = multilineArgs;708 const prompt: PromptPipelineContent = createPromptPipelineContent(709 'Commit message: !{git commit -m {{args}}}',710 );711 712 const expectedEscapedArgs =713 getExpectedEscapedArgForPlatform(multilineArgs);714 const expectedCommand = `git commit -m ${expectedEscapedArgs}`;715 716 await processor.process(prompt, context);717 718 expect(mockShellExecute).toHaveBeenCalledWith(719 expectedCommand,720 expect.any(String),721 expect.any(Function),722 expect.any(Object),723 false,724 expect.any(Object),725 );726 });727 728 it.each([729 { name: 'spaces', input: 'file with spaces.txt' },730 { name: 'double quotes', input: 'a "quoted" string' },731 { name: 'single quotes', input: "it's a string" },732 { name: 'command substitution (backticks)', input: '`reboot`' },733 { name: 'command substitution (dollar)', input: '$(reboot)' },734 { name: 'variable expansion', input: '$HOME' },735 { name: 'command chaining (semicolon)', input: 'a; reboot' },736 { name: 'command chaining (ampersand)', input: 'a && reboot' },737 ])('should safely escape args containing $name', async ({ input }) => {738 const processor = new ShellProcessor('test-command');739 context.invocation!.args = input;740 const prompt: PromptPipelineContent =741 createPromptPipelineContent('!{echo {{args}}}');742 743 const expectedEscapedArgs = getExpectedEscapedArgForPlatform(input);744 const expectedCommand = `echo ${expectedEscapedArgs}`;745 746 await processor.process(prompt, context);747 748 expect(mockShellExecute).toHaveBeenCalledWith(749 expectedCommand,750 expect.any(String),751 expect.any(Function),752 expect.any(Object),753 false,754 expect.any(Object),755 );756 });757 });758});759 