CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes45downloads
shellProcessor.test.ts759 linesDownload Raw Back to prompt-processors
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 
basant307/AI_Governance_Project · CoolFace