2025-07-27 02:00:26 -04:00
|
|
|
/**
|
|
|
|
|
* @license
|
|
|
|
|
* Copyright 2025 Google LLC
|
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
*/
|
|
|
|
|
|
2025-08-18 10:34:51 -04:00
|
|
|
import { describe, it, expect, beforeEach, vi, type Mock } from 'vitest';
|
2025-07-27 02:00:26 -04:00
|
|
|
import { ConfirmationRequiredError, ShellProcessor } from './shellProcessor.js';
|
|
|
|
|
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
|
2025-08-26 00:04:53 +02:00
|
|
|
import type { CommandContext } from '../../ui/commands/types.js';
|
|
|
|
|
import type { Config } from '@google/gemini-cli-core';
|
2026-01-06 23:28:06 -05:00
|
|
|
import {
|
|
|
|
|
ApprovalMode,
|
|
|
|
|
getShellConfiguration,
|
|
|
|
|
PolicyDecision,
|
2026-03-09 14:57:45 -07:00
|
|
|
NoopSandboxManager,
|
2026-01-06 23:28:06 -05:00
|
|
|
} from '@google/gemini-cli-core';
|
2025-08-17 00:02:54 -04:00
|
|
|
import { quote } from 'shell-quote';
|
2025-08-27 23:22:21 -04:00
|
|
|
import { createPartFromText } from '@google/genai';
|
|
|
|
|
import type { PromptPipelineContent } from './types.js';
|
2025-08-17 00:02:54 -04:00
|
|
|
|
|
|
|
|
// Helper function to determine the expected escaped string based on the current OS,
|
2025-08-27 23:22:21 -04:00
|
|
|
// mirroring the logic in the actual `escapeShellArg` implementation.
|
2025-08-17 00:02:54 -04:00
|
|
|
function getExpectedEscapedArgForPlatform(arg: string): string {
|
2025-10-16 17:25:30 -07:00
|
|
|
const { shell } = getShellConfiguration();
|
2025-08-17 00:02:54 -04:00
|
|
|
|
2025-10-16 17:25:30 -07:00
|
|
|
switch (shell) {
|
|
|
|
|
case 'powershell':
|
2025-08-17 00:02:54 -04:00
|
|
|
return `'${arg.replace(/'/g, "''")}'`;
|
2025-10-16 17:25:30 -07:00
|
|
|
case 'cmd':
|
2025-08-17 00:02:54 -04:00
|
|
|
return `"${arg.replace(/"/g, '""')}"`;
|
2025-10-16 17:25:30 -07:00
|
|
|
case 'bash':
|
|
|
|
|
default:
|
|
|
|
|
return quote([arg]);
|
2025-08-17 00:02:54 -04:00
|
|
|
}
|
|
|
|
|
}
|
2025-07-27 02:00:26 -04:00
|
|
|
|
2025-08-27 23:22:21 -04:00
|
|
|
// Helper to create PromptPipelineContent
|
|
|
|
|
function createPromptPipelineContent(text: string): PromptPipelineContent {
|
|
|
|
|
return [createPartFromText(text)];
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-27 02:00:26 -04:00
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
2025-08-17 00:02:54 -04:00
|
|
|
const SUCCESS_RESULT = {
|
2025-08-19 16:03:51 -07:00
|
|
|
output: 'default shell output',
|
2025-08-17 00:02:54 -04:00
|
|
|
exitCode: 0,
|
|
|
|
|
error: null,
|
|
|
|
|
aborted: false,
|
|
|
|
|
signal: null,
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-27 02:00:26 -04:00
|
|
|
describe('ShellProcessor', () => {
|
|
|
|
|
let context: CommandContext;
|
|
|
|
|
let mockConfig: Partial<Config>;
|
2026-01-06 23:28:06 -05:00
|
|
|
let mockPolicyEngineCheck: Mock;
|
2025-07-27 02:00:26 -04:00
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks();
|
|
|
|
|
|
2026-01-06 23:28:06 -05:00
|
|
|
mockPolicyEngineCheck = vi.fn().mockResolvedValue({
|
|
|
|
|
decision: PolicyDecision.ALLOW,
|
|
|
|
|
});
|
|
|
|
|
|
2025-07-27 02:00:26 -04:00
|
|
|
mockConfig = {
|
|
|
|
|
getTargetDir: vi.fn().mockReturnValue('/test/dir'),
|
2025-08-18 10:34:51 -04:00
|
|
|
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
2025-10-08 13:28:19 -07:00
|
|
|
getEnableInteractiveShell: vi.fn().mockReturnValue(false),
|
2026-03-09 14:57:45 -07:00
|
|
|
getShellExecutionConfig: vi.fn().mockReturnValue({
|
|
|
|
|
sandboxManager: new NoopSandboxManager(),
|
|
|
|
|
sanitizationConfig: {
|
|
|
|
|
allowedEnvironmentVariables: [],
|
|
|
|
|
blockedEnvironmentVariables: [],
|
|
|
|
|
enableEnvironmentVariableRedaction: false,
|
|
|
|
|
},
|
|
|
|
|
}),
|
2026-01-06 23:28:06 -05:00
|
|
|
getPolicyEngine: vi.fn().mockReturnValue({
|
|
|
|
|
check: mockPolicyEngineCheck,
|
|
|
|
|
}),
|
2025-07-27 02:00:26 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
context = createMockCommandContext({
|
2025-08-17 00:02:54 -04:00
|
|
|
invocation: {
|
|
|
|
|
raw: '/cmd default args',
|
|
|
|
|
name: 'cmd',
|
|
|
|
|
args: 'default args',
|
|
|
|
|
},
|
2025-07-27 02:00:26 -04:00
|
|
|
services: {
|
|
|
|
|
config: mockConfig as Config,
|
|
|
|
|
},
|
|
|
|
|
session: {
|
|
|
|
|
sessionShellAllowlist: new Set(),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
mockShellExecute.mockReturnValue({
|
2025-08-17 00:02:54 -04:00
|
|
|
result: Promise.resolve(SUCCESS_RESULT),
|
2025-07-27 02:00:26 -04:00
|
|
|
});
|
2025-08-17 00:02:54 -04:00
|
|
|
|
2025-07-27 02:00:26 -04:00
|
|
|
mockCheckCommandPermissions.mockReturnValue({
|
|
|
|
|
allAllowed: true,
|
|
|
|
|
disallowedCommands: [],
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2025-08-17 00:02:54 -04:00
|
|
|
it('should throw an error if config is missing', async () => {
|
|
|
|
|
const processor = new ShellProcessor('test-command');
|
2025-08-27 23:22:21 -04:00
|
|
|
const prompt: PromptPipelineContent = createPromptPipelineContent('!{ls}');
|
2025-08-17 00:02:54 -04:00
|
|
|
const contextWithoutConfig = createMockCommandContext({
|
|
|
|
|
services: {
|
|
|
|
|
config: null,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await expect(
|
|
|
|
|
processor.process(prompt, contextWithoutConfig),
|
|
|
|
|
).rejects.toThrow(/Security configuration not loaded/);
|
|
|
|
|
});
|
|
|
|
|
|
2025-07-27 02:00:26 -04:00
|
|
|
it('should not change the prompt if no shell injections are present', async () => {
|
|
|
|
|
const processor = new ShellProcessor('test-command');
|
2025-08-27 23:22:21 -04:00
|
|
|
const prompt: PromptPipelineContent = createPromptPipelineContent(
|
|
|
|
|
'This is a simple prompt with no injections.',
|
|
|
|
|
);
|
2025-07-27 02:00:26 -04:00
|
|
|
const result = await processor.process(prompt, context);
|
2025-08-27 23:22:21 -04:00
|
|
|
expect(result).toEqual(prompt);
|
2025-07-27 02:00:26 -04:00
|
|
|
expect(mockShellExecute).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should process a single valid shell injection if allowed', async () => {
|
|
|
|
|
const processor = new ShellProcessor('test-command');
|
2025-08-27 23:22:21 -04:00
|
|
|
const prompt: PromptPipelineContent = createPromptPipelineContent(
|
|
|
|
|
'The current status is: !{git status}',
|
|
|
|
|
);
|
2026-01-06 23:28:06 -05:00
|
|
|
mockPolicyEngineCheck.mockResolvedValue({
|
|
|
|
|
decision: PolicyDecision.ALLOW,
|
2025-07-27 02:00:26 -04:00
|
|
|
});
|
|
|
|
|
mockShellExecute.mockReturnValue({
|
2025-08-19 16:03:51 -07:00
|
|
|
result: Promise.resolve({ ...SUCCESS_RESULT, output: 'On branch main' }),
|
2025-07-27 02:00:26 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await processor.process(prompt, context);
|
|
|
|
|
|
2026-01-06 23:28:06 -05:00
|
|
|
expect(mockPolicyEngineCheck).toHaveBeenCalledWith(
|
|
|
|
|
{
|
|
|
|
|
name: 'run_shell_command',
|
|
|
|
|
args: { command: 'git status' },
|
|
|
|
|
},
|
|
|
|
|
undefined,
|
2025-07-27 02:00:26 -04:00
|
|
|
);
|
|
|
|
|
expect(mockShellExecute).toHaveBeenCalledWith(
|
|
|
|
|
'git status',
|
|
|
|
|
expect.any(String),
|
|
|
|
|
expect.any(Function),
|
|
|
|
|
expect.any(Object),
|
2025-08-19 16:03:51 -07:00
|
|
|
false,
|
2025-09-11 13:27:27 -07:00
|
|
|
expect.any(Object),
|
2025-07-27 02:00:26 -04:00
|
|
|
);
|
2025-08-27 23:22:21 -04:00
|
|
|
expect(result).toEqual([{ text: 'The current status is: On branch main' }]);
|
2025-07-27 02:00:26 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should process multiple valid shell injections if all are allowed', async () => {
|
|
|
|
|
const processor = new ShellProcessor('test-command');
|
2025-08-27 23:22:21 -04:00
|
|
|
const prompt: PromptPipelineContent = createPromptPipelineContent(
|
|
|
|
|
'!{git status} in !{pwd}',
|
|
|
|
|
);
|
2026-01-06 23:28:06 -05:00
|
|
|
mockPolicyEngineCheck.mockResolvedValue({
|
|
|
|
|
decision: PolicyDecision.ALLOW,
|
2025-07-27 02:00:26 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
mockShellExecute
|
|
|
|
|
.mockReturnValueOnce({
|
2025-08-17 00:02:54 -04:00
|
|
|
result: Promise.resolve({
|
|
|
|
|
...SUCCESS_RESULT,
|
2025-08-19 16:03:51 -07:00
|
|
|
output: 'On branch main',
|
2025-08-17 00:02:54 -04:00
|
|
|
}),
|
2025-07-27 02:00:26 -04:00
|
|
|
})
|
|
|
|
|
.mockReturnValueOnce({
|
2025-08-19 16:03:51 -07:00
|
|
|
result: Promise.resolve({ ...SUCCESS_RESULT, output: '/usr/home' }),
|
2025-07-27 02:00:26 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await processor.process(prompt, context);
|
|
|
|
|
|
2026-01-06 23:28:06 -05:00
|
|
|
expect(mockPolicyEngineCheck).toHaveBeenCalledTimes(2);
|
2025-07-27 02:00:26 -04:00
|
|
|
expect(mockShellExecute).toHaveBeenCalledTimes(2);
|
2025-08-27 23:22:21 -04:00
|
|
|
expect(result).toEqual([{ text: 'On branch main in /usr/home' }]);
|
2025-07-27 02:00:26 -04:00
|
|
|
});
|
|
|
|
|
|
2025-08-18 10:34:51 -04:00
|
|
|
it('should throw ConfirmationRequiredError if a command is not allowed in default mode', async () => {
|
2025-07-27 02:00:26 -04:00
|
|
|
const processor = new ShellProcessor('test-command');
|
2025-08-27 23:22:21 -04:00
|
|
|
const prompt: PromptPipelineContent = createPromptPipelineContent(
|
|
|
|
|
'Do something dangerous: !{rm -rf /}',
|
|
|
|
|
);
|
2026-01-06 23:28:06 -05:00
|
|
|
mockPolicyEngineCheck.mockResolvedValue({
|
|
|
|
|
decision: PolicyDecision.ASK_USER,
|
2025-07-27 02:00:26 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await expect(processor.process(prompt, context)).rejects.toThrow(
|
|
|
|
|
ConfirmationRequiredError,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2025-08-18 10:34:51 -04:00
|
|
|
it('should NOT throw ConfirmationRequiredError if a command is not allowed but approval mode is YOLO', async () => {
|
|
|
|
|
const processor = new ShellProcessor('test-command');
|
2025-08-27 23:22:21 -04:00
|
|
|
const prompt: PromptPipelineContent = createPromptPipelineContent(
|
|
|
|
|
'Do something dangerous: !{rm -rf /}',
|
|
|
|
|
);
|
2026-01-06 23:28:06 -05:00
|
|
|
// In YOLO mode, PolicyEngine returns ALLOW
|
|
|
|
|
mockPolicyEngineCheck.mockResolvedValue({
|
|
|
|
|
decision: PolicyDecision.ALLOW,
|
2025-08-18 10:34:51 -04:00
|
|
|
});
|
2026-01-06 23:28:06 -05:00
|
|
|
// Override the approval mode for this test (though PolicyEngine mock handles the decision)
|
2025-08-18 10:34:51 -04:00
|
|
|
(mockConfig.getApprovalMode as Mock).mockReturnValue(ApprovalMode.YOLO);
|
|
|
|
|
mockShellExecute.mockReturnValue({
|
2025-08-19 16:03:51 -07:00
|
|
|
result: Promise.resolve({ ...SUCCESS_RESULT, output: 'deleted' }),
|
2025-08-18 10:34:51 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await processor.process(prompt, context);
|
|
|
|
|
|
|
|
|
|
// It should proceed with execution
|
|
|
|
|
expect(mockShellExecute).toHaveBeenCalledWith(
|
|
|
|
|
'rm -rf /',
|
|
|
|
|
expect.any(String),
|
|
|
|
|
expect.any(Function),
|
|
|
|
|
expect.any(Object),
|
2025-08-19 16:03:51 -07:00
|
|
|
false,
|
2025-09-11 13:27:27 -07:00
|
|
|
expect.any(Object),
|
2025-08-18 10:34:51 -04:00
|
|
|
);
|
2025-08-27 23:22:21 -04:00
|
|
|
expect(result).toEqual([{ text: 'Do something dangerous: deleted' }]);
|
2025-08-18 10:34:51 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should still throw an error for a hard-denied command even in YOLO mode', async () => {
|
|
|
|
|
const processor = new ShellProcessor('test-command');
|
2025-08-27 23:22:21 -04:00
|
|
|
const prompt: PromptPipelineContent = createPromptPipelineContent(
|
|
|
|
|
'Do something forbidden: !{reboot}',
|
|
|
|
|
);
|
2026-01-06 23:28:06 -05:00
|
|
|
mockPolicyEngineCheck.mockResolvedValue({
|
|
|
|
|
decision: PolicyDecision.DENY,
|
2025-08-18 10:34:51 -04:00
|
|
|
});
|
|
|
|
|
// Set approval mode to YOLO
|
|
|
|
|
(mockConfig.getApprovalMode as Mock).mockReturnValue(ApprovalMode.YOLO);
|
|
|
|
|
|
|
|
|
|
await expect(processor.process(prompt, context)).rejects.toThrow(
|
2026-01-06 23:28:06 -05:00
|
|
|
/Blocked command: "reboot". Reason: Blocked by policy/,
|
2025-08-18 10:34:51 -04:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Ensure it never tried to execute
|
|
|
|
|
expect(mockShellExecute).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
2025-07-27 02:00:26 -04:00
|
|
|
it('should throw ConfirmationRequiredError with the correct command', async () => {
|
|
|
|
|
const processor = new ShellProcessor('test-command');
|
2025-08-27 23:22:21 -04:00
|
|
|
const prompt: PromptPipelineContent = createPromptPipelineContent(
|
|
|
|
|
'Do something dangerous: !{rm -rf /}',
|
|
|
|
|
);
|
2026-01-06 23:28:06 -05:00
|
|
|
mockPolicyEngineCheck.mockResolvedValue({
|
|
|
|
|
decision: PolicyDecision.ASK_USER,
|
2025-07-27 02:00:26 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await processor.process(prompt, context);
|
|
|
|
|
// Fail if it doesn't throw
|
|
|
|
|
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');
|
2025-08-27 23:22:21 -04:00
|
|
|
const prompt: PromptPipelineContent = createPromptPipelineContent(
|
|
|
|
|
'!{cmd1} and !{cmd2}',
|
|
|
|
|
);
|
2026-01-06 23:28:06 -05:00
|
|
|
mockPolicyEngineCheck.mockImplementation(async (toolCall) => {
|
|
|
|
|
const cmd = toolCall.args.command;
|
|
|
|
|
if (cmd === 'cmd1' || cmd === 'cmd2') {
|
|
|
|
|
return { decision: PolicyDecision.ASK_USER };
|
2025-07-27 02:00:26 -04:00
|
|
|
}
|
2026-01-06 23:28:06 -05:00
|
|
|
return { decision: PolicyDecision.ALLOW };
|
2025-07-27 02:00:26 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await processor.process(prompt, context);
|
|
|
|
|
// Fail if it doesn't throw
|
|
|
|
|
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');
|
2025-08-27 23:22:21 -04:00
|
|
|
const prompt: PromptPipelineContent = createPromptPipelineContent(
|
|
|
|
|
'First: !{echo "hello"}, Second: !{rm -rf /}',
|
|
|
|
|
);
|
2025-07-27 02:00:26 -04:00
|
|
|
|
2026-01-06 23:28:06 -05:00
|
|
|
mockPolicyEngineCheck.mockImplementation(async (toolCall) => {
|
|
|
|
|
const cmd = toolCall.args.command;
|
2025-07-27 02:00:26 -04:00
|
|
|
if (cmd.includes('rm')) {
|
2026-01-06 23:28:06 -05:00
|
|
|
return { decision: PolicyDecision.ASK_USER };
|
2025-07-27 02:00:26 -04:00
|
|
|
}
|
2026-01-06 23:28:06 -05:00
|
|
|
return { decision: PolicyDecision.ALLOW };
|
2025-07-27 02:00:26 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await expect(processor.process(prompt, context)).rejects.toThrow(
|
|
|
|
|
ConfirmationRequiredError,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Ensure no commands were executed because the pipeline was halted.
|
|
|
|
|
expect(mockShellExecute).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should only request confirmation for disallowed commands in a mixed prompt', async () => {
|
|
|
|
|
const processor = new ShellProcessor('test-command');
|
2025-08-27 23:22:21 -04:00
|
|
|
const prompt: PromptPipelineContent = createPromptPipelineContent(
|
|
|
|
|
'Allowed: !{ls -l}, Disallowed: !{rm -rf /}',
|
|
|
|
|
);
|
2025-07-27 02:00:26 -04:00
|
|
|
|
2026-01-06 23:28:06 -05:00
|
|
|
mockPolicyEngineCheck.mockImplementation(async (toolCall) => {
|
|
|
|
|
const cmd = toolCall.args.command;
|
|
|
|
|
if (cmd.includes('rm')) {
|
|
|
|
|
return { decision: PolicyDecision.ASK_USER };
|
|
|
|
|
}
|
|
|
|
|
return { decision: PolicyDecision.ALLOW };
|
|
|
|
|
});
|
2025-07-27 02:00:26 -04:00
|
|
|
|
|
|
|
|
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');
|
2025-08-27 23:22:21 -04:00
|
|
|
const prompt: PromptPipelineContent = createPromptPipelineContent(
|
|
|
|
|
'Run !{cmd1} and !{cmd2}',
|
|
|
|
|
);
|
2025-07-27 02:00:26 -04:00
|
|
|
|
2026-01-06 23:28:06 -05:00
|
|
|
// Add commands to the session allowlist (conceptually, in this test we just mock the engine allowing them)
|
2025-07-27 02:00:26 -04:00
|
|
|
context.session.sessionShellAllowlist = new Set(['cmd1', 'cmd2']);
|
|
|
|
|
|
|
|
|
|
// checkCommandPermissions should now pass for these
|
2026-01-06 23:28:06 -05:00
|
|
|
mockPolicyEngineCheck.mockResolvedValue({
|
|
|
|
|
decision: PolicyDecision.ALLOW,
|
2025-07-27 02:00:26 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
mockShellExecute
|
2025-08-17 00:02:54 -04:00
|
|
|
.mockReturnValueOnce({
|
2025-08-19 16:03:51 -07:00
|
|
|
result: Promise.resolve({ ...SUCCESS_RESULT, output: 'output1' }),
|
2025-08-17 00:02:54 -04:00
|
|
|
})
|
|
|
|
|
.mockReturnValueOnce({
|
2025-08-19 16:03:51 -07:00
|
|
|
result: Promise.resolve({ ...SUCCESS_RESULT, output: 'output2' }),
|
2025-08-17 00:02:54 -04:00
|
|
|
});
|
2025-07-27 02:00:26 -04:00
|
|
|
|
|
|
|
|
const result = await processor.process(prompt, context);
|
|
|
|
|
|
2026-01-06 23:28:06 -05:00
|
|
|
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 () => {
|
|
|
|
|
// 1. Initial State: Command NOT allowed
|
|
|
|
|
const processor = new ShellProcessor('test-command');
|
|
|
|
|
const prompt: PromptPipelineContent =
|
|
|
|
|
createPromptPipelineContent('!{echo "once"}');
|
|
|
|
|
|
|
|
|
|
// Policy Engine says ASK_USER
|
|
|
|
|
mockPolicyEngineCheck.mockResolvedValue({
|
|
|
|
|
decision: PolicyDecision.ASK_USER,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 2. First Attempt: processing should fail with ConfirmationRequiredError
|
|
|
|
|
try {
|
|
|
|
|
await processor.process(prompt, context);
|
|
|
|
|
expect.fail('Should have thrown ConfirmationRequiredError');
|
|
|
|
|
} catch (e) {
|
|
|
|
|
expect(e).toBeInstanceOf(ConfirmationRequiredError);
|
|
|
|
|
expect(mockPolicyEngineCheck).toHaveBeenCalledTimes(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. User Approves: Add to session allowlist (simulating UI action)
|
|
|
|
|
context.session.sessionShellAllowlist.add('echo "once"');
|
|
|
|
|
|
|
|
|
|
// 4. Retry: calling process() again with the same context
|
|
|
|
|
// Reset mocks to ensure we track new calls cleanly
|
|
|
|
|
mockPolicyEngineCheck.mockClear();
|
|
|
|
|
|
|
|
|
|
// Mock successful execution
|
|
|
|
|
mockShellExecute.mockReturnValue({
|
|
|
|
|
result: Promise.resolve({ ...SUCCESS_RESULT, output: 'once' }),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await processor.process(prompt, context);
|
|
|
|
|
|
|
|
|
|
// 5. Verify Success AND Policy Engine Bypass
|
|
|
|
|
expect(mockPolicyEngineCheck).not.toHaveBeenCalled();
|
|
|
|
|
expect(mockShellExecute).toHaveBeenCalledWith(
|
|
|
|
|
'echo "once"',
|
|
|
|
|
expect.any(String),
|
|
|
|
|
expect.any(Function),
|
2025-07-27 02:00:26 -04:00
|
|
|
expect.any(Object),
|
2026-01-06 23:28:06 -05:00
|
|
|
false,
|
2025-07-27 02:00:26 -04:00
|
|
|
expect.any(Object),
|
|
|
|
|
);
|
2026-01-06 23:28:06 -05:00
|
|
|
expect(result).toEqual([{ text: 'once' }]);
|
2025-07-27 02:00:26 -04:00
|
|
|
});
|
|
|
|
|
|
2025-08-17 00:02:54 -04:00
|
|
|
it('should trim whitespace from the command inside the injection before interpolation', async () => {
|
2025-07-27 02:00:26 -04:00
|
|
|
const processor = new ShellProcessor('test-command');
|
2025-08-27 23:22:21 -04:00
|
|
|
const prompt: PromptPipelineContent = createPromptPipelineContent(
|
|
|
|
|
'Files: !{ ls {{args}} -l }',
|
|
|
|
|
);
|
2025-08-17 00:02:54 -04:00
|
|
|
|
|
|
|
|
const rawArgs = context.invocation!.args;
|
|
|
|
|
|
|
|
|
|
const expectedEscapedArgs = getExpectedEscapedArgForPlatform(rawArgs);
|
|
|
|
|
|
|
|
|
|
const expectedCommand = `ls ${expectedEscapedArgs} -l`;
|
|
|
|
|
|
2026-01-06 23:28:06 -05:00
|
|
|
mockPolicyEngineCheck.mockResolvedValue({
|
|
|
|
|
decision: PolicyDecision.ALLOW,
|
2025-07-27 02:00:26 -04:00
|
|
|
});
|
|
|
|
|
mockShellExecute.mockReturnValue({
|
2025-08-19 16:03:51 -07:00
|
|
|
result: Promise.resolve({ ...SUCCESS_RESULT, output: 'total 0' }),
|
2025-07-27 02:00:26 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await processor.process(prompt, context);
|
|
|
|
|
|
2026-01-06 23:28:06 -05:00
|
|
|
expect(mockPolicyEngineCheck).toHaveBeenCalledWith(
|
|
|
|
|
{ name: 'run_shell_command', args: { command: expectedCommand } },
|
|
|
|
|
undefined,
|
2025-07-27 02:00:26 -04:00
|
|
|
);
|
|
|
|
|
expect(mockShellExecute).toHaveBeenCalledWith(
|
2025-08-17 00:02:54 -04:00
|
|
|
expectedCommand,
|
2025-07-27 02:00:26 -04:00
|
|
|
expect.any(String),
|
|
|
|
|
expect.any(Function),
|
|
|
|
|
expect.any(Object),
|
2025-08-19 16:03:51 -07:00
|
|
|
false,
|
2025-09-11 13:27:27 -07:00
|
|
|
expect.any(Object),
|
2025-07-27 02:00:26 -04:00
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2025-08-17 00:02:54 -04:00
|
|
|
it('should handle an empty command inside the injection gracefully (skips execution)', async () => {
|
2025-07-27 02:00:26 -04:00
|
|
|
const processor = new ShellProcessor('test-command');
|
2025-08-27 23:22:21 -04:00
|
|
|
const prompt: PromptPipelineContent =
|
|
|
|
|
createPromptPipelineContent('This is weird: !{}');
|
2025-08-17 00:02:54 -04:00
|
|
|
|
|
|
|
|
const result = await processor.process(prompt, context);
|
|
|
|
|
|
2026-01-06 23:28:06 -05:00
|
|
|
expect(mockPolicyEngineCheck).not.toHaveBeenCalled();
|
2025-08-17 00:02:54 -04:00
|
|
|
expect(mockShellExecute).not.toHaveBeenCalled();
|
|
|
|
|
|
|
|
|
|
// It replaces !{} with an empty string.
|
2025-08-27 23:22:21 -04:00
|
|
|
expect(result).toEqual([{ text: 'This is weird: ' }]);
|
2025-08-17 00:02:54 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('Error Reporting', () => {
|
2025-08-19 16:03:51 -07:00
|
|
|
it('should append exit code and command name on failure', async () => {
|
2025-08-17 00:02:54 -04:00
|
|
|
const processor = new ShellProcessor('test-command');
|
2025-08-27 23:22:21 -04:00
|
|
|
const prompt: PromptPipelineContent =
|
|
|
|
|
createPromptPipelineContent('!{cmd}');
|
2025-08-17 00:02:54 -04:00
|
|
|
mockShellExecute.mockReturnValue({
|
|
|
|
|
result: Promise.resolve({
|
|
|
|
|
...SUCCESS_RESULT,
|
2025-08-19 16:03:51 -07:00
|
|
|
output: 'some error output',
|
2025-08-17 00:02:54 -04:00
|
|
|
stderr: '',
|
|
|
|
|
exitCode: 1,
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await processor.process(prompt, context);
|
|
|
|
|
|
2025-08-27 23:22:21 -04:00
|
|
|
expect(result).toEqual([
|
|
|
|
|
{
|
|
|
|
|
text: "some error output\n[Shell command 'cmd' exited with code 1]",
|
|
|
|
|
},
|
|
|
|
|
]);
|
2025-08-17 00:02:54 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should append signal info and command name if terminated by signal', async () => {
|
|
|
|
|
const processor = new ShellProcessor('test-command');
|
2025-08-27 23:22:21 -04:00
|
|
|
const prompt: PromptPipelineContent =
|
|
|
|
|
createPromptPipelineContent('!{cmd}');
|
2025-08-17 00:02:54 -04:00
|
|
|
mockShellExecute.mockReturnValue({
|
|
|
|
|
result: Promise.resolve({
|
|
|
|
|
...SUCCESS_RESULT,
|
2025-08-19 16:03:51 -07:00
|
|
|
output: 'output',
|
2025-08-17 00:02:54 -04:00
|
|
|
stderr: '',
|
|
|
|
|
exitCode: null,
|
|
|
|
|
signal: 'SIGTERM',
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await processor.process(prompt, context);
|
|
|
|
|
|
2025-08-27 23:22:21 -04:00
|
|
|
expect(result).toEqual([
|
|
|
|
|
{
|
|
|
|
|
text: "output\n[Shell command 'cmd' terminated by signal SIGTERM]",
|
|
|
|
|
},
|
|
|
|
|
]);
|
2025-08-17 00:02:54 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should throw a detailed error if the shell fails to spawn', async () => {
|
|
|
|
|
const processor = new ShellProcessor('test-command');
|
2025-08-27 23:22:21 -04:00
|
|
|
const prompt: PromptPipelineContent =
|
|
|
|
|
createPromptPipelineContent('!{bad-command}');
|
2025-08-17 00:02:54 -04:00
|
|
|
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');
|
2025-08-27 23:22:21 -04:00
|
|
|
const prompt: PromptPipelineContent = createPromptPipelineContent(
|
|
|
|
|
'!{long-running-command}',
|
|
|
|
|
);
|
2025-08-17 00:02:54 -04:00
|
|
|
const spawnError = new Error('Aborted');
|
|
|
|
|
mockShellExecute.mockReturnValue({
|
|
|
|
|
result: Promise.resolve({
|
|
|
|
|
...SUCCESS_RESULT,
|
2025-08-19 16:03:51 -07:00
|
|
|
output: 'partial output',
|
2025-08-17 00:02:54 -04:00
|
|
|
stderr: '',
|
|
|
|
|
exitCode: null,
|
|
|
|
|
error: spawnError,
|
|
|
|
|
aborted: true, // Key difference
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await processor.process(prompt, context);
|
2025-08-27 23:22:21 -04:00
|
|
|
expect(result).toEqual([
|
|
|
|
|
{
|
|
|
|
|
text: "partial output\n[Shell command 'long-running-command' aborted]",
|
|
|
|
|
},
|
|
|
|
|
]);
|
2025-08-17 00:02:54 -04:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('Context-Aware Argument Interpolation ({{args}})', () => {
|
|
|
|
|
const rawArgs = 'user input';
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
// Update context for these tests to use specific arguments
|
|
|
|
|
context.invocation!.args = rawArgs;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should perform raw replacement if no shell injections are present (optimization path)', async () => {
|
|
|
|
|
const processor = new ShellProcessor('test-command');
|
2025-08-27 23:22:21 -04:00
|
|
|
const prompt: PromptPipelineContent = createPromptPipelineContent(
|
|
|
|
|
'The user said: {{args}}',
|
|
|
|
|
);
|
2025-08-17 00:02:54 -04:00
|
|
|
|
|
|
|
|
const result = await processor.process(prompt, context);
|
|
|
|
|
|
2025-08-27 23:22:21 -04:00
|
|
|
expect(result).toEqual([{ text: `The user said: ${rawArgs}` }]);
|
2025-08-17 00:02:54 -04:00
|
|
|
expect(mockShellExecute).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should perform raw replacement outside !{} blocks', async () => {
|
|
|
|
|
const processor = new ShellProcessor('test-command');
|
2025-08-27 23:22:21 -04:00
|
|
|
const prompt: PromptPipelineContent = createPromptPipelineContent(
|
|
|
|
|
'Outside: {{args}}. Inside: !{echo "hello"}',
|
|
|
|
|
);
|
2025-08-17 00:02:54 -04:00
|
|
|
mockShellExecute.mockReturnValue({
|
2025-08-19 16:03:51 -07:00
|
|
|
result: Promise.resolve({ ...SUCCESS_RESULT, output: 'hello' }),
|
2025-08-17 00:02:54 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await processor.process(prompt, context);
|
|
|
|
|
|
2025-08-27 23:22:21 -04:00
|
|
|
expect(result).toEqual([{ text: `Outside: ${rawArgs}. Inside: hello` }]);
|
2025-08-17 00:02:54 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should perform escaped replacement inside !{} blocks', async () => {
|
|
|
|
|
const processor = new ShellProcessor('test-command');
|
2025-08-27 23:22:21 -04:00
|
|
|
const prompt: PromptPipelineContent = createPromptPipelineContent(
|
|
|
|
|
'Command: !{grep {{args}} file.txt}',
|
|
|
|
|
);
|
2025-08-17 00:02:54 -04:00
|
|
|
mockShellExecute.mockReturnValue({
|
2025-08-19 16:03:51 -07:00
|
|
|
result: Promise.resolve({ ...SUCCESS_RESULT, output: 'match found' }),
|
2025-08-17 00:02:54 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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),
|
2025-08-19 16:03:51 -07:00
|
|
|
false,
|
2025-09-11 13:27:27 -07:00
|
|
|
expect.any(Object),
|
2025-08-17 00:02:54 -04:00
|
|
|
);
|
|
|
|
|
|
2025-08-27 23:22:21 -04:00
|
|
|
expect(result).toEqual([{ text: 'Command: match found' }]);
|
2025-08-17 00:02:54 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle both raw (outside) and escaped (inside) injection simultaneously', async () => {
|
|
|
|
|
const processor = new ShellProcessor('test-command');
|
2025-08-27 23:22:21 -04:00
|
|
|
const prompt: PromptPipelineContent = createPromptPipelineContent(
|
|
|
|
|
'User "({{args}})" requested search: !{search {{args}}}',
|
|
|
|
|
);
|
2025-08-17 00:02:54 -04:00
|
|
|
mockShellExecute.mockReturnValue({
|
2025-08-19 16:03:51 -07:00
|
|
|
result: Promise.resolve({ ...SUCCESS_RESULT, output: 'results' }),
|
2025-08-17 00:02:54 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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),
|
2025-08-19 16:03:51 -07:00
|
|
|
false,
|
2025-09-11 13:27:27 -07:00
|
|
|
expect.any(Object),
|
2025-08-17 00:02:54 -04:00
|
|
|
);
|
|
|
|
|
|
2025-08-27 23:22:21 -04:00
|
|
|
expect(result).toEqual([
|
|
|
|
|
{ text: `User "(${rawArgs})" requested search: results` },
|
|
|
|
|
]);
|
2025-08-17 00:02:54 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should perform security checks on the final, resolved (escaped) command', async () => {
|
|
|
|
|
const processor = new ShellProcessor('test-command');
|
2025-08-27 23:22:21 -04:00
|
|
|
const prompt: PromptPipelineContent =
|
|
|
|
|
createPromptPipelineContent('!{rm {{args}}}');
|
2025-08-17 00:02:54 -04:00
|
|
|
|
|
|
|
|
const expectedEscapedArgs = getExpectedEscapedArgForPlatform(rawArgs);
|
|
|
|
|
const expectedResolvedCommand = `rm ${expectedEscapedArgs}`;
|
2026-01-06 23:28:06 -05:00
|
|
|
mockPolicyEngineCheck.mockResolvedValue({
|
|
|
|
|
decision: PolicyDecision.ASK_USER,
|
2025-08-17 00:02:54 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await expect(processor.process(prompt, context)).rejects.toThrow(
|
|
|
|
|
ConfirmationRequiredError,
|
|
|
|
|
);
|
|
|
|
|
|
2026-01-06 23:28:06 -05:00
|
|
|
expect(mockPolicyEngineCheck).toHaveBeenCalledWith(
|
|
|
|
|
{
|
|
|
|
|
name: 'run_shell_command',
|
|
|
|
|
args: { command: expectedResolvedCommand },
|
|
|
|
|
},
|
|
|
|
|
undefined,
|
2025-08-17 00:02:54 -04:00
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should report the resolved command if a hard denial occurs', async () => {
|
|
|
|
|
const processor = new ShellProcessor('test-command');
|
2025-08-27 23:22:21 -04:00
|
|
|
const prompt: PromptPipelineContent =
|
|
|
|
|
createPromptPipelineContent('!{rm {{args}}}');
|
2025-08-17 00:02:54 -04:00
|
|
|
const expectedEscapedArgs = getExpectedEscapedArgForPlatform(rawArgs);
|
|
|
|
|
const expectedResolvedCommand = `rm ${expectedEscapedArgs}`;
|
2026-01-06 23:28:06 -05:00
|
|
|
mockPolicyEngineCheck.mockResolvedValue({
|
|
|
|
|
decision: PolicyDecision.DENY,
|
2025-08-17 00:02:54 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await expect(processor.process(prompt, context)).rejects.toThrow(
|
2026-01-06 23:28:06 -05:00
|
|
|
`Blocked command: "${expectedResolvedCommand}". Reason: Blocked by policy.`,
|
2025-08-17 00:02:54 -04:00
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
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;
|
2025-08-27 23:22:21 -04:00
|
|
|
const prompt: PromptPipelineContent = createPromptPipelineContent(
|
|
|
|
|
'Commit message: !{git commit -m {{args}}}',
|
|
|
|
|
);
|
2025-08-17 00:02:54 -04:00
|
|
|
|
|
|
|
|
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),
|
2025-08-19 16:03:51 -07:00
|
|
|
false,
|
2025-09-11 13:27:27 -07:00
|
|
|
expect.any(Object),
|
2025-08-17 00:02:54 -04:00
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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;
|
2025-08-27 23:22:21 -04:00
|
|
|
const prompt: PromptPipelineContent =
|
|
|
|
|
createPromptPipelineContent('!{echo {{args}}}');
|
2025-08-17 00:02:54 -04:00
|
|
|
|
|
|
|
|
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),
|
2025-08-19 16:03:51 -07:00
|
|
|
false,
|
2025-09-11 13:27:27 -07:00
|
|
|
expect.any(Object),
|
2025-08-17 00:02:54 -04:00
|
|
|
);
|
|
|
|
|
});
|
2025-07-27 02:00:26 -04:00
|
|
|
});
|
|
|
|
|
});
|