mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 12:21:13 -05:00
Expose a list of allowed auto-execute commands (#31)
This commit is contained in:
@@ -34,18 +34,8 @@ interface ChatViewProps {
|
||||
|
||||
export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
|
||||
|
||||
const ALLOWED_AUTO_EXECUTE_COMMANDS = [
|
||||
'npm',
|
||||
'npx',
|
||||
'tsc',
|
||||
'git log',
|
||||
'git diff',
|
||||
'git show',
|
||||
'ls'
|
||||
] as const
|
||||
|
||||
const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryView }: ChatViewProps) => {
|
||||
const { version, clineMessages: messages, taskHistory, apiConfiguration, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute } = useExtensionState()
|
||||
const { version, clineMessages: messages, taskHistory, apiConfiguration, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, allowedCommands } = useExtensionState()
|
||||
|
||||
//const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined) : undefined
|
||||
const task = useMemo(() => messages.at(0), [messages]) // leaving this less safe version here since if the first message is not a task, then the extension is in a bad state and needs to be debugged (see Cline.abort)
|
||||
@@ -712,19 +702,14 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
if (lastMessage?.type === "ask" && lastMessage.text) {
|
||||
const command = lastMessage.text
|
||||
|
||||
// Check for command chaining characters
|
||||
if (command.includes('&&') ||
|
||||
command.includes(';') ||
|
||||
command.includes('||') ||
|
||||
command.includes('|') ||
|
||||
command.includes('$(') ||
|
||||
command.includes('`')) {
|
||||
return false
|
||||
}
|
||||
const trimmedCommand = command.trim().toLowerCase()
|
||||
return ALLOWED_AUTO_EXECUTE_COMMANDS.some(prefix =>
|
||||
trimmedCommand.startsWith(prefix.toLowerCase())
|
||||
)
|
||||
// Split command by chaining operators
|
||||
const commands = command.split(/&&|\|\||;|\||\$\(|`/).map(cmd => cmd.trim())
|
||||
|
||||
// Check if all individual commands are allowed
|
||||
return commands.every((cmd) => {
|
||||
const trimmedCommand = cmd.toLowerCase()
|
||||
return allowedCommands?.some((prefix) => trimmedCommand.startsWith(prefix.toLowerCase()))
|
||||
})
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -737,7 +722,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
) {
|
||||
handlePrimaryButtonClick()
|
||||
}
|
||||
}, [clineAsk, enableButtons, handlePrimaryButtonClick, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, messages])
|
||||
}, [clineAsk, enableButtons, handlePrimaryButtonClick, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, messages, allowedCommands])
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -1,309 +1,549 @@
|
||||
import { render, screen, act } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { ExtensionStateContextType } from '../../../context/ExtensionStateContext'
|
||||
import React from 'react'
|
||||
import { render, waitFor } from '@testing-library/react'
|
||||
import ChatView from '../ChatView'
|
||||
import { ExtensionStateContextProvider } from '../../../context/ExtensionStateContext'
|
||||
import { vscode } from '../../../utils/vscode'
|
||||
import * as ExtensionStateContext from '../../../context/ExtensionStateContext'
|
||||
|
||||
// Mock vscode
|
||||
// Define minimal types needed for testing
|
||||
interface ClineMessage {
|
||||
type: 'say' | 'ask';
|
||||
say?: string;
|
||||
ask?: string;
|
||||
ts: number;
|
||||
text?: string;
|
||||
partial?: boolean;
|
||||
}
|
||||
|
||||
interface ExtensionState {
|
||||
version: string;
|
||||
clineMessages: ClineMessage[];
|
||||
taskHistory: any[];
|
||||
shouldShowAnnouncement: boolean;
|
||||
allowedCommands: string[];
|
||||
alwaysAllowExecute: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// Mock vscode API
|
||||
jest.mock('../../../utils/vscode', () => ({
|
||||
vscode: {
|
||||
postMessage: jest.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
// Mock all components that use problematic dependencies
|
||||
jest.mock('../../common/CodeBlock', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="mock-code-block" />
|
||||
}))
|
||||
|
||||
jest.mock('../../common/MarkdownBlock', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="mock-markdown-block" />
|
||||
vscode: {
|
||||
postMessage: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock components that use ESM dependencies
|
||||
jest.mock('../BrowserSessionRow', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="mock-browser-session-row" />
|
||||
__esModule: true,
|
||||
default: function MockBrowserSessionRow({ messages }: { messages: ClineMessage[] }) {
|
||||
return <div data-testid="browser-session">{JSON.stringify(messages)}</div>
|
||||
}
|
||||
}))
|
||||
|
||||
// Update ChatRow mock to capture props
|
||||
let chatRowProps = null
|
||||
jest.mock('../ChatRow', () => ({
|
||||
__esModule: true,
|
||||
default: function MockChatRow({ message }: { message: ClineMessage }) {
|
||||
return <div data-testid="chat-row">{JSON.stringify(message)}</div>
|
||||
}
|
||||
}))
|
||||
|
||||
interface ChatTextAreaProps {
|
||||
onSend: (value: string) => void;
|
||||
inputValue?: string;
|
||||
textAreaDisabled?: boolean;
|
||||
placeholderText?: string;
|
||||
selectedImages?: string[];
|
||||
shouldDisableImages?: boolean;
|
||||
}
|
||||
|
||||
jest.mock('../ChatTextArea', () => {
|
||||
const mockReact = require('react')
|
||||
return {
|
||||
__esModule: true,
|
||||
default: (props: any) => {
|
||||
chatRowProps = props
|
||||
return <div data-testid="mock-chat-row" />
|
||||
}
|
||||
}))
|
||||
|
||||
// Mock Virtuoso component
|
||||
jest.mock('react-virtuoso', () => ({
|
||||
Virtuoso: ({ children }: any) => (
|
||||
<div data-testid="mock-virtuoso">{children}</div>
|
||||
)
|
||||
}))
|
||||
|
||||
// Mock VS Code components
|
||||
jest.mock('@vscode/webview-ui-toolkit/react', () => ({
|
||||
VSCodeButton: ({ children, onClick }: any) => (
|
||||
<button onClick={onClick}>{children}</button>
|
||||
),
|
||||
VSCodeProgressRing: () => <div data-testid="progress-ring" />
|
||||
}))
|
||||
|
||||
describe('ChatView', () => {
|
||||
const mockShowHistoryView = jest.fn()
|
||||
const mockHideAnnouncement = jest.fn()
|
||||
|
||||
let mockState: ExtensionStateContextType
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
|
||||
mockState = {
|
||||
clineMessages: [],
|
||||
apiConfiguration: {
|
||||
apiProvider: 'anthropic',
|
||||
apiModelId: 'claude-3-sonnet'
|
||||
},
|
||||
version: '1.0.0',
|
||||
customInstructions: '',
|
||||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowWrite: true,
|
||||
alwaysAllowExecute: true,
|
||||
alwaysAllowBrowser: true,
|
||||
openRouterModels: {},
|
||||
didHydrateState: true,
|
||||
showWelcome: false,
|
||||
theme: 'dark',
|
||||
filePaths: [],
|
||||
taskHistory: [],
|
||||
shouldShowAnnouncement: false,
|
||||
uriScheme: 'vscode',
|
||||
|
||||
setApiConfiguration: jest.fn(),
|
||||
setShowAnnouncement: jest.fn(),
|
||||
setCustomInstructions: jest.fn(),
|
||||
setAlwaysAllowReadOnly: jest.fn(),
|
||||
setAlwaysAllowWrite: jest.fn(),
|
||||
setAlwaysAllowExecute: jest.fn(),
|
||||
setAlwaysAllowBrowser: jest.fn()
|
||||
}
|
||||
|
||||
// Mock the useExtensionState hook
|
||||
jest.spyOn(ExtensionStateContext, 'useExtensionState').mockReturnValue(mockState)
|
||||
})
|
||||
|
||||
const renderChatView = () => {
|
||||
return render(
|
||||
<ChatView
|
||||
isHidden={false}
|
||||
showAnnouncement={false}
|
||||
hideAnnouncement={mockHideAnnouncement}
|
||||
showHistoryView={mockShowHistoryView}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
describe('Always Allow Logic', () => {
|
||||
it('should auto-approve read-only tool actions when alwaysAllowReadOnly is true', () => {
|
||||
mockState.clineMessages = [
|
||||
{
|
||||
type: 'ask',
|
||||
ask: 'tool',
|
||||
text: JSON.stringify({ tool: 'readFile' }),
|
||||
ts: Date.now(),
|
||||
}
|
||||
]
|
||||
renderChatView()
|
||||
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: 'askResponse',
|
||||
askResponse: 'yesButtonClicked'
|
||||
})
|
||||
})
|
||||
|
||||
it('should auto-approve all file listing tool types when alwaysAllowReadOnly is true', () => {
|
||||
const fileListingTools = [
|
||||
'readFile', 'listFiles', 'listFilesTopLevel',
|
||||
'listFilesRecursive', 'listCodeDefinitionNames', 'searchFiles'
|
||||
]
|
||||
|
||||
fileListingTools.forEach(tool => {
|
||||
jest.clearAllMocks()
|
||||
mockState.clineMessages = [
|
||||
{
|
||||
type: 'ask',
|
||||
ask: 'tool',
|
||||
text: JSON.stringify({ tool }),
|
||||
ts: Date.now(),
|
||||
}
|
||||
]
|
||||
renderChatView()
|
||||
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: 'askResponse',
|
||||
askResponse: 'yesButtonClicked'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should auto-approve write tool actions when alwaysAllowWrite is true', () => {
|
||||
mockState.clineMessages = [
|
||||
{
|
||||
type: 'ask',
|
||||
ask: 'tool',
|
||||
text: JSON.stringify({ tool: 'editedExistingFile' }),
|
||||
ts: Date.now(),
|
||||
}
|
||||
]
|
||||
renderChatView()
|
||||
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: 'askResponse',
|
||||
askResponse: 'yesButtonClicked'
|
||||
})
|
||||
})
|
||||
|
||||
it('should auto-approve allowed execute commands when alwaysAllowExecute is true', () => {
|
||||
mockState.clineMessages = [
|
||||
{
|
||||
type: 'ask',
|
||||
ask: 'command',
|
||||
text: 'npm install',
|
||||
ts: Date.now(),
|
||||
}
|
||||
]
|
||||
renderChatView()
|
||||
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: 'askResponse',
|
||||
askResponse: 'yesButtonClicked'
|
||||
})
|
||||
})
|
||||
|
||||
it('should not auto-approve disallowed execute commands even when alwaysAllowExecute is true', () => {
|
||||
mockState.clineMessages = [
|
||||
{
|
||||
type: 'ask',
|
||||
ask: 'command',
|
||||
text: 'rm -rf /',
|
||||
ts: Date.now(),
|
||||
}
|
||||
]
|
||||
renderChatView()
|
||||
|
||||
expect(vscode.postMessage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not auto-approve commands with chaining characters when alwaysAllowExecute is true', () => {
|
||||
mockState.clineMessages = [
|
||||
{
|
||||
type: 'ask',
|
||||
ask: 'command',
|
||||
text: 'npm install && rm -rf /',
|
||||
ts: Date.now(),
|
||||
}
|
||||
]
|
||||
renderChatView()
|
||||
|
||||
expect(vscode.postMessage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should auto-approve browser actions when alwaysAllowBrowser is true', () => {
|
||||
mockState.clineMessages = [
|
||||
{
|
||||
type: 'ask',
|
||||
ask: 'browser_action_launch',
|
||||
ts: Date.now(),
|
||||
}
|
||||
]
|
||||
renderChatView()
|
||||
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: 'askResponse',
|
||||
askResponse: 'yesButtonClicked'
|
||||
})
|
||||
})
|
||||
|
||||
it('should not auto-approve when corresponding alwaysAllow flag is false', () => {
|
||||
mockState.alwaysAllowReadOnly = false
|
||||
mockState.clineMessages = [
|
||||
{
|
||||
type: 'ask',
|
||||
ask: 'tool',
|
||||
text: JSON.stringify({ tool: 'readFile' }),
|
||||
ts: Date.now(),
|
||||
}
|
||||
]
|
||||
renderChatView()
|
||||
|
||||
expect(vscode.postMessage).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Streaming State', () => {
|
||||
it('should show cancel button while streaming and trigger cancel on click', async () => {
|
||||
mockState.clineMessages = [
|
||||
{
|
||||
type: 'say',
|
||||
say: 'task',
|
||||
ts: Date.now(),
|
||||
},
|
||||
{
|
||||
type: 'say',
|
||||
say: 'text',
|
||||
partial: true,
|
||||
ts: Date.now(),
|
||||
}
|
||||
]
|
||||
renderChatView()
|
||||
|
||||
const cancelButton = screen.getByText('Cancel')
|
||||
await userEvent.click(cancelButton)
|
||||
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: 'cancelTask'
|
||||
})
|
||||
})
|
||||
|
||||
it('should show terminate button when task is paused and trigger terminate on click', async () => {
|
||||
mockState.clineMessages = [
|
||||
{
|
||||
type: 'ask',
|
||||
ask: 'resume_task',
|
||||
ts: Date.now(),
|
||||
}
|
||||
]
|
||||
renderChatView()
|
||||
|
||||
const terminateButton = screen.getByText('Terminate')
|
||||
await userEvent.click(terminateButton)
|
||||
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: 'clearTask'
|
||||
})
|
||||
})
|
||||
|
||||
it('should show retry button when API error occurs and trigger retry on click', async () => {
|
||||
mockState.clineMessages = [
|
||||
{
|
||||
type: 'ask',
|
||||
ask: 'api_req_failed',
|
||||
ts: Date.now(),
|
||||
}
|
||||
]
|
||||
renderChatView()
|
||||
|
||||
const retryButton = screen.getByText('Retry')
|
||||
await userEvent.click(retryButton)
|
||||
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: 'askResponse',
|
||||
askResponse: 'yesButtonClicked'
|
||||
})
|
||||
})
|
||||
default: mockReact.forwardRef(function MockChatTextArea(props: ChatTextAreaProps, ref: React.ForwardedRef<HTMLInputElement>) {
|
||||
return (
|
||||
<div data-testid="chat-textarea">
|
||||
<input ref={ref} type="text" onChange={(e) => props.onSend(e.target.value)} />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('../TaskHeader', () => ({
|
||||
__esModule: true,
|
||||
default: function MockTaskHeader({ task }: { task: ClineMessage }) {
|
||||
return <div data-testid="task-header">{JSON.stringify(task)}</div>
|
||||
}
|
||||
}))
|
||||
|
||||
// Mock VSCode components
|
||||
jest.mock('@vscode/webview-ui-toolkit/react', () => ({
|
||||
VSCodeButton: function MockVSCodeButton({
|
||||
children,
|
||||
onClick,
|
||||
appearance
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
appearance?: string;
|
||||
}) {
|
||||
return <button onClick={onClick} data-appearance={appearance}>{children}</button>
|
||||
},
|
||||
VSCodeTextField: function MockVSCodeTextField({
|
||||
value,
|
||||
onInput,
|
||||
placeholder
|
||||
}: {
|
||||
value?: string;
|
||||
onInput?: (e: { target: { value: string } }) => void;
|
||||
placeholder?: string;
|
||||
}) {
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => onInput?.({ target: { value: e.target.value } })}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
)
|
||||
},
|
||||
VSCodeLink: function MockVSCodeLink({
|
||||
children,
|
||||
href
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
href?: string;
|
||||
}) {
|
||||
return <a href={href}>{children}</a>
|
||||
}
|
||||
}))
|
||||
|
||||
// Mock window.postMessage to trigger state hydration
|
||||
const mockPostMessage = (state: Partial<ExtensionState>) => {
|
||||
window.postMessage({
|
||||
type: 'state',
|
||||
state: {
|
||||
version: '1.0.0',
|
||||
clineMessages: [],
|
||||
taskHistory: [],
|
||||
shouldShowAnnouncement: false,
|
||||
allowedCommands: [],
|
||||
alwaysAllowExecute: false,
|
||||
...state
|
||||
}
|
||||
}, '*')
|
||||
}
|
||||
|
||||
describe('ChatView - Auto Approval Tests', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('auto-approves browser actions when alwaysAllowBrowser is enabled', async () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ChatView
|
||||
isHidden={false}
|
||||
showAnnouncement={false}
|
||||
hideAnnouncement={() => {}}
|
||||
showHistoryView={() => {}}
|
||||
/>
|
||||
</ExtensionStateContextProvider>
|
||||
)
|
||||
|
||||
// First hydrate state with initial task
|
||||
mockPostMessage({
|
||||
alwaysAllowBrowser: true,
|
||||
clineMessages: [
|
||||
{
|
||||
type: 'say',
|
||||
say: 'task',
|
||||
ts: Date.now() - 2000,
|
||||
text: 'Initial task'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// Then send the browser action ask message
|
||||
mockPostMessage({
|
||||
alwaysAllowBrowser: true,
|
||||
clineMessages: [
|
||||
{
|
||||
type: 'say',
|
||||
say: 'task',
|
||||
ts: Date.now() - 2000,
|
||||
text: 'Initial task'
|
||||
},
|
||||
{
|
||||
type: 'ask',
|
||||
ask: 'browser_action_launch',
|
||||
ts: Date.now(),
|
||||
text: JSON.stringify({ action: 'launch', url: 'http://example.com' }),
|
||||
partial: false
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// Wait for the auto-approval message
|
||||
await waitFor(() => {
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: 'askResponse',
|
||||
askResponse: 'yesButtonClicked'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('auto-approves read-only tools when alwaysAllowReadOnly is enabled', async () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ChatView
|
||||
isHidden={false}
|
||||
showAnnouncement={false}
|
||||
hideAnnouncement={() => {}}
|
||||
showHistoryView={() => {}}
|
||||
/>
|
||||
</ExtensionStateContextProvider>
|
||||
)
|
||||
|
||||
// First hydrate state with initial task
|
||||
mockPostMessage({
|
||||
alwaysAllowReadOnly: true,
|
||||
clineMessages: [
|
||||
{
|
||||
type: 'say',
|
||||
say: 'task',
|
||||
ts: Date.now() - 2000,
|
||||
text: 'Initial task'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// Then send the read-only tool ask message
|
||||
mockPostMessage({
|
||||
alwaysAllowReadOnly: true,
|
||||
clineMessages: [
|
||||
{
|
||||
type: 'say',
|
||||
say: 'task',
|
||||
ts: Date.now() - 2000,
|
||||
text: 'Initial task'
|
||||
},
|
||||
{
|
||||
type: 'ask',
|
||||
ask: 'tool',
|
||||
ts: Date.now(),
|
||||
text: JSON.stringify({ tool: 'readFile', path: 'test.txt' }),
|
||||
partial: false
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// Wait for the auto-approval message
|
||||
await waitFor(() => {
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: 'askResponse',
|
||||
askResponse: 'yesButtonClicked'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('auto-approves write tools when alwaysAllowWrite is enabled', async () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ChatView
|
||||
isHidden={false}
|
||||
showAnnouncement={false}
|
||||
hideAnnouncement={() => {}}
|
||||
showHistoryView={() => {}}
|
||||
/>
|
||||
</ExtensionStateContextProvider>
|
||||
)
|
||||
|
||||
// First hydrate state with initial task
|
||||
mockPostMessage({
|
||||
alwaysAllowWrite: true,
|
||||
clineMessages: [
|
||||
{
|
||||
type: 'say',
|
||||
say: 'task',
|
||||
ts: Date.now() - 2000,
|
||||
text: 'Initial task'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// Then send the write tool ask message
|
||||
mockPostMessage({
|
||||
alwaysAllowWrite: true,
|
||||
clineMessages: [
|
||||
{
|
||||
type: 'say',
|
||||
say: 'task',
|
||||
ts: Date.now() - 2000,
|
||||
text: 'Initial task'
|
||||
},
|
||||
{
|
||||
type: 'ask',
|
||||
ask: 'tool',
|
||||
ts: Date.now(),
|
||||
text: JSON.stringify({ tool: 'editedExistingFile', path: 'test.txt' }),
|
||||
partial: false
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// Wait for the auto-approval message
|
||||
await waitFor(() => {
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: 'askResponse',
|
||||
askResponse: 'yesButtonClicked'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('auto-approves allowed commands when alwaysAllowExecute is enabled', async () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ChatView
|
||||
isHidden={false}
|
||||
showAnnouncement={false}
|
||||
hideAnnouncement={() => {}}
|
||||
showHistoryView={() => {}}
|
||||
/>
|
||||
</ExtensionStateContextProvider>
|
||||
)
|
||||
|
||||
// First hydrate state with initial task
|
||||
mockPostMessage({
|
||||
alwaysAllowExecute: true,
|
||||
allowedCommands: ['npm test'],
|
||||
clineMessages: [
|
||||
{
|
||||
type: 'say',
|
||||
say: 'task',
|
||||
ts: Date.now() - 2000,
|
||||
text: 'Initial task'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// Then send the command ask message
|
||||
mockPostMessage({
|
||||
alwaysAllowExecute: true,
|
||||
allowedCommands: ['npm test'],
|
||||
clineMessages: [
|
||||
{
|
||||
type: 'say',
|
||||
say: 'task',
|
||||
ts: Date.now() - 2000,
|
||||
text: 'Initial task'
|
||||
},
|
||||
{
|
||||
type: 'ask',
|
||||
ask: 'command',
|
||||
ts: Date.now(),
|
||||
text: 'npm test',
|
||||
partial: false
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// Wait for the auto-approval message
|
||||
await waitFor(() => {
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: 'askResponse',
|
||||
askResponse: 'yesButtonClicked'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('does not auto-approve disallowed commands even when alwaysAllowExecute is enabled', () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ChatView
|
||||
isHidden={false}
|
||||
showAnnouncement={false}
|
||||
hideAnnouncement={() => {}}
|
||||
showHistoryView={() => {}}
|
||||
/>
|
||||
</ExtensionStateContextProvider>
|
||||
)
|
||||
|
||||
// First hydrate state with initial task
|
||||
mockPostMessage({
|
||||
alwaysAllowExecute: true,
|
||||
allowedCommands: ['npm test'],
|
||||
clineMessages: [
|
||||
{
|
||||
type: 'say',
|
||||
say: 'task',
|
||||
ts: Date.now() - 2000,
|
||||
text: 'Initial task'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// Then send the disallowed command ask message
|
||||
mockPostMessage({
|
||||
alwaysAllowExecute: true,
|
||||
allowedCommands: ['npm test'],
|
||||
clineMessages: [
|
||||
{
|
||||
type: 'say',
|
||||
say: 'task',
|
||||
ts: Date.now() - 2000,
|
||||
text: 'Initial task'
|
||||
},
|
||||
{
|
||||
type: 'ask',
|
||||
ask: 'command',
|
||||
ts: Date.now(),
|
||||
text: 'rm -rf /',
|
||||
partial: false
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// Verify no auto-approval message was sent
|
||||
expect(vscode.postMessage).not.toHaveBeenCalledWith({
|
||||
type: 'askResponse',
|
||||
askResponse: 'yesButtonClicked'
|
||||
})
|
||||
})
|
||||
|
||||
describe('Command Chaining Tests', () => {
|
||||
it('auto-approves chained commands when all parts are allowed', async () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ChatView
|
||||
isHidden={false}
|
||||
showAnnouncement={false}
|
||||
hideAnnouncement={() => {}}
|
||||
showHistoryView={() => {}}
|
||||
/>
|
||||
</ExtensionStateContextProvider>
|
||||
)
|
||||
|
||||
// Test various allowed command chaining scenarios
|
||||
const allowedChainedCommands = [
|
||||
'npm test && npm run build',
|
||||
'npm test; npm run build',
|
||||
'npm test || npm run build',
|
||||
'npm test | npm run build'
|
||||
]
|
||||
|
||||
for (const command of allowedChainedCommands) {
|
||||
jest.clearAllMocks()
|
||||
|
||||
// First hydrate state with initial task
|
||||
mockPostMessage({
|
||||
alwaysAllowExecute: true,
|
||||
allowedCommands: ['npm test', 'npm run build'],
|
||||
clineMessages: [
|
||||
{
|
||||
type: 'say',
|
||||
say: 'task',
|
||||
ts: Date.now() - 2000,
|
||||
text: 'Initial task'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// Then send the chained command ask message
|
||||
mockPostMessage({
|
||||
alwaysAllowExecute: true,
|
||||
allowedCommands: ['npm test', 'npm run build'],
|
||||
clineMessages: [
|
||||
{
|
||||
type: 'say',
|
||||
say: 'task',
|
||||
ts: Date.now() - 2000,
|
||||
text: 'Initial task'
|
||||
},
|
||||
{
|
||||
type: 'ask',
|
||||
ask: 'command',
|
||||
ts: Date.now(),
|
||||
text: command,
|
||||
partial: false
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// Wait for the auto-approval message
|
||||
await waitFor(() => {
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: 'askResponse',
|
||||
askResponse: 'yesButtonClicked'
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('does not auto-approve chained commands when any part is disallowed', () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ChatView
|
||||
isHidden={false}
|
||||
showAnnouncement={false}
|
||||
hideAnnouncement={() => {}}
|
||||
showHistoryView={() => {}}
|
||||
/>
|
||||
</ExtensionStateContextProvider>
|
||||
)
|
||||
|
||||
// Test various command chaining scenarios with disallowed parts
|
||||
const disallowedChainedCommands = [
|
||||
'npm test && rm -rf /',
|
||||
'npm test; rm -rf /',
|
||||
'npm test || rm -rf /',
|
||||
'npm test | rm -rf /',
|
||||
'npm test $(echo dangerous)',
|
||||
'npm test `echo dangerous`'
|
||||
]
|
||||
|
||||
disallowedChainedCommands.forEach(command => {
|
||||
// First hydrate state with initial task
|
||||
mockPostMessage({
|
||||
alwaysAllowExecute: true,
|
||||
allowedCommands: ['npm test'],
|
||||
clineMessages: [
|
||||
{
|
||||
type: 'say',
|
||||
say: 'task',
|
||||
ts: Date.now() - 2000,
|
||||
text: 'Initial task'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// Then send the chained command ask message
|
||||
mockPostMessage({
|
||||
alwaysAllowExecute: true,
|
||||
allowedCommands: ['npm test'],
|
||||
clineMessages: [
|
||||
{
|
||||
type: 'say',
|
||||
say: 'task',
|
||||
ts: Date.now() - 2000,
|
||||
text: 'Initial task'
|
||||
},
|
||||
{
|
||||
type: 'ask',
|
||||
ask: 'command',
|
||||
ts: Date.now(),
|
||||
text: command,
|
||||
partial: false
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// Verify no auto-approval message was sent for chained commands with disallowed parts
|
||||
expect(vscode.postMessage).not.toHaveBeenCalledWith({
|
||||
type: 'askResponse',
|
||||
askResponse: 'yesButtonClicked'
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user