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:
@@ -1,230 +1,221 @@
|
||||
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, screen, fireEvent } from '@testing-library/react'
|
||||
import SettingsView from '../SettingsView'
|
||||
import { ExtensionStateContextProvider } from '../../../context/ExtensionStateContext'
|
||||
import { vscode } from '../../../utils/vscode'
|
||||
import * as ExtensionStateContext from '../../../context/ExtensionStateContext'
|
||||
import { ModelInfo } from '../../../../../src/shared/api'
|
||||
|
||||
// Mock dependencies
|
||||
// Mock vscode API
|
||||
jest.mock('../../../utils/vscode', () => ({
|
||||
vscode: {
|
||||
postMessage: jest.fn()
|
||||
}
|
||||
vscode: {
|
||||
postMessage: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock validation functions
|
||||
jest.mock('../../../utils/validate', () => ({
|
||||
validateApiConfiguration: jest.fn(() => undefined),
|
||||
validateModelId: jest.fn(() => undefined)
|
||||
}))
|
||||
|
||||
// Mock ApiOptions component
|
||||
jest.mock('../ApiOptions', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="mock-api-options" />
|
||||
}))
|
||||
|
||||
// Mock VS Code components
|
||||
// Mock VSCode components
|
||||
jest.mock('@vscode/webview-ui-toolkit/react', () => ({
|
||||
VSCodeButton: ({ children, onClick }: any) => (
|
||||
<button onClick={onClick}>{children}</button>
|
||||
),
|
||||
VSCodeCheckbox: ({ children, checked, onChange }: any) => (
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={e => onChange(e)}
|
||||
aria-checked={checked}
|
||||
/>
|
||||
{children}
|
||||
</label>
|
||||
),
|
||||
VSCodeTextArea: ({ children, value, onInput }: any) => (
|
||||
<textarea
|
||||
data-testid="custom-instructions"
|
||||
value={value}
|
||||
readOnly
|
||||
aria-label="Custom Instructions"
|
||||
>{children}</textarea>
|
||||
),
|
||||
VSCodeLink: ({ children, href }: any) => (
|
||||
<a href={href}>{children}</a>
|
||||
)
|
||||
VSCodeButton: ({ children, onClick, appearance }: any) => (
|
||||
appearance === 'icon' ?
|
||||
<button onClick={onClick} className="codicon codicon-close" aria-label="Remove command">
|
||||
<span className="codicon codicon-close" />
|
||||
</button> :
|
||||
<button onClick={onClick} data-appearance={appearance}>{children}</button>
|
||||
),
|
||||
VSCodeCheckbox: ({ children, onChange, checked }: any) => (
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={(e) => onChange({ target: { checked: e.target.checked } })}
|
||||
aria-label={typeof children === 'string' ? children : undefined}
|
||||
/>
|
||||
{children}
|
||||
</label>
|
||||
),
|
||||
VSCodeTextField: ({ value, onInput, placeholder }: any) => (
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => onInput({ target: { value: e.target.value } })}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
),
|
||||
VSCodeTextArea: () => <textarea />,
|
||||
VSCodeLink: () => <a />,
|
||||
VSCodeDropdown: ({ children, value, onChange }: any) => (
|
||||
<select value={value} onChange={onChange}>
|
||||
{children}
|
||||
</select>
|
||||
),
|
||||
VSCodeOption: ({ children, value }: any) => (
|
||||
<option value={value}>{children}</option>
|
||||
),
|
||||
VSCodeRadio: ({ children, value, checked, onChange }: any) => (
|
||||
<input
|
||||
type="radio"
|
||||
value={value}
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
/>
|
||||
),
|
||||
VSCodeRadioGroup: ({ children, value, onChange }: any) => (
|
||||
<div onChange={onChange}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}))
|
||||
|
||||
describe('SettingsView', () => {
|
||||
const mockOnDone = jest.fn()
|
||||
const mockSetAlwaysAllowWrite = jest.fn()
|
||||
const mockSetAlwaysAllowReadOnly = jest.fn()
|
||||
const mockSetCustomInstructions = jest.fn()
|
||||
const mockSetAlwaysAllowExecute = jest.fn()
|
||||
|
||||
let mockState: ExtensionStateContextType
|
||||
|
||||
const mockOpenRouterModels: Record<string, ModelInfo> = {
|
||||
'claude-3-sonnet': {
|
||||
maxTokens: 200000,
|
||||
contextWindow: 200000,
|
||||
supportsImages: true,
|
||||
supportsComputerUse: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0.000008,
|
||||
outputPrice: 0.000024,
|
||||
description: "Anthropic's Claude 3 Sonnet model"
|
||||
}
|
||||
// Mock window.postMessage to trigger state hydration
|
||||
const mockPostMessage = (state: any) => {
|
||||
window.postMessage({
|
||||
type: 'state',
|
||||
state: {
|
||||
version: '1.0.0',
|
||||
clineMessages: [],
|
||||
taskHistory: [],
|
||||
shouldShowAnnouncement: false,
|
||||
allowedCommands: [],
|
||||
alwaysAllowExecute: false,
|
||||
...state
|
||||
}
|
||||
}, '*')
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
|
||||
mockState = {
|
||||
apiConfiguration: {
|
||||
apiProvider: 'anthropic',
|
||||
apiModelId: 'claude-3-sonnet'
|
||||
},
|
||||
version: '1.0.0',
|
||||
customInstructions: 'Test instructions',
|
||||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowWrite: true,
|
||||
alwaysAllowExecute: true,
|
||||
openRouterModels: mockOpenRouterModels,
|
||||
didHydrateState: true,
|
||||
showWelcome: false,
|
||||
theme: 'dark',
|
||||
filePaths: [],
|
||||
taskHistory: [],
|
||||
shouldShowAnnouncement: false,
|
||||
clineMessages: [],
|
||||
uriScheme: 'vscode',
|
||||
|
||||
setAlwaysAllowReadOnly: mockSetAlwaysAllowReadOnly,
|
||||
setAlwaysAllowWrite: mockSetAlwaysAllowWrite,
|
||||
setCustomInstructions: mockSetCustomInstructions,
|
||||
setAlwaysAllowExecute: mockSetAlwaysAllowExecute,
|
||||
setApiConfiguration: jest.fn(),
|
||||
setShowAnnouncement: jest.fn()
|
||||
}
|
||||
|
||||
// Mock the useExtensionState hook
|
||||
jest.spyOn(ExtensionStateContext, 'useExtensionState').mockReturnValue(mockState)
|
||||
const renderSettingsView = () => {
|
||||
const onDone = jest.fn()
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<SettingsView onDone={onDone} />
|
||||
</ExtensionStateContextProvider>
|
||||
)
|
||||
// Hydrate initial state
|
||||
mockPostMessage({})
|
||||
return { onDone }
|
||||
}
|
||||
|
||||
describe('SettingsView - Allowed Commands', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('shows allowed commands section when alwaysAllowExecute is enabled', () => {
|
||||
renderSettingsView()
|
||||
|
||||
// Enable always allow execute
|
||||
const executeCheckbox = screen.getByRole('checkbox', {
|
||||
name: /Always approve allowed execute operations/i
|
||||
})
|
||||
fireEvent.click(executeCheckbox)
|
||||
|
||||
const renderSettingsView = () => {
|
||||
return render(
|
||||
<SettingsView onDone={mockOnDone} />
|
||||
)
|
||||
}
|
||||
// Verify allowed commands section appears
|
||||
expect(screen.getByText(/Allowed Auto-Execute Commands/i)).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText(/Enter command prefix/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
describe('Checkboxes', () => {
|
||||
it('should toggle alwaysAllowWrite checkbox', async () => {
|
||||
mockState.alwaysAllowWrite = false
|
||||
renderSettingsView()
|
||||
|
||||
const writeCheckbox = screen.getByRole('checkbox', {
|
||||
name: /Always approve write operations/i
|
||||
})
|
||||
|
||||
expect(writeCheckbox).not.toBeChecked()
|
||||
await act(async () => {
|
||||
await userEvent.click(writeCheckbox)
|
||||
})
|
||||
expect(mockSetAlwaysAllowWrite).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('should toggle alwaysAllowExecute checkbox', async () => {
|
||||
mockState.alwaysAllowExecute = false
|
||||
renderSettingsView()
|
||||
|
||||
const executeCheckbox = screen.getByRole('checkbox', {
|
||||
name: /Always approve execute operations/i
|
||||
})
|
||||
|
||||
expect(executeCheckbox).not.toBeChecked()
|
||||
await act(async () => {
|
||||
await userEvent.click(executeCheckbox)
|
||||
})
|
||||
expect(mockSetAlwaysAllowExecute).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('should toggle alwaysAllowReadOnly checkbox', async () => {
|
||||
mockState.alwaysAllowReadOnly = false
|
||||
renderSettingsView()
|
||||
|
||||
const readOnlyCheckbox = screen.getByRole('checkbox', {
|
||||
name: /Always approve read-only operations/i
|
||||
})
|
||||
|
||||
expect(readOnlyCheckbox).not.toBeChecked()
|
||||
await act(async () => {
|
||||
await userEvent.click(readOnlyCheckbox)
|
||||
})
|
||||
expect(mockSetAlwaysAllowReadOnly).toHaveBeenCalledWith(true)
|
||||
})
|
||||
it('adds new command to the list', () => {
|
||||
renderSettingsView()
|
||||
|
||||
// Enable always allow execute
|
||||
const executeCheckbox = screen.getByRole('checkbox', {
|
||||
name: /Always approve allowed execute operations/i
|
||||
})
|
||||
fireEvent.click(executeCheckbox)
|
||||
|
||||
describe('Form Submission', () => {
|
||||
it('should send correct messages when form is submitted', async () => {
|
||||
renderSettingsView()
|
||||
// Add a new command
|
||||
const input = screen.getByPlaceholderText(/Enter command prefix/i)
|
||||
fireEvent.change(input, { target: { value: 'npm test' } })
|
||||
|
||||
const addButton = screen.getByText('Add')
|
||||
fireEvent.click(addButton)
|
||||
|
||||
// Submit form
|
||||
const doneButton = screen.getByRole('button', { name: /Done/i })
|
||||
await act(async () => {
|
||||
await userEvent.click(doneButton)
|
||||
})
|
||||
|
||||
// Verify messages were sent in the correct order
|
||||
const calls = (vscode.postMessage as jest.Mock).mock.calls
|
||||
expect(calls).toHaveLength(5)
|
||||
|
||||
expect(calls[0][0]).toEqual({
|
||||
type: 'apiConfiguration',
|
||||
apiConfiguration: {
|
||||
apiProvider: 'anthropic',
|
||||
apiModelId: 'claude-3-sonnet'
|
||||
}
|
||||
})
|
||||
|
||||
expect(calls[1][0]).toEqual({
|
||||
type: 'customInstructions',
|
||||
text: 'Test instructions'
|
||||
})
|
||||
|
||||
expect(calls[2][0]).toEqual({
|
||||
type: 'alwaysAllowReadOnly',
|
||||
bool: true
|
||||
})
|
||||
|
||||
expect(calls[3][0]).toEqual({
|
||||
type: 'alwaysAllowWrite',
|
||||
bool: true
|
||||
})
|
||||
|
||||
expect(calls[4][0]).toEqual({
|
||||
type: 'alwaysAllowExecute',
|
||||
bool: true
|
||||
})
|
||||
|
||||
// Verify onDone was called
|
||||
expect(mockOnDone).toHaveBeenCalled()
|
||||
})
|
||||
// Verify command was added
|
||||
expect(screen.getByText('npm test')).toBeInTheDocument()
|
||||
|
||||
// Verify VSCode message was sent
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: 'allowedCommands',
|
||||
commands: ['npm test']
|
||||
})
|
||||
})
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have accessible form controls', () => {
|
||||
renderSettingsView()
|
||||
|
||||
// Check for proper labels and ARIA attributes
|
||||
const writeCheckbox = screen.getByRole('checkbox', {
|
||||
name: /Always approve write operations/i
|
||||
})
|
||||
expect(writeCheckbox).toHaveAttribute('aria-checked')
|
||||
|
||||
const textarea = screen.getByRole('textbox', {
|
||||
name: /Custom Instructions/i
|
||||
})
|
||||
expect(textarea).toBeInTheDocument()
|
||||
})
|
||||
it('removes command from the list', () => {
|
||||
renderSettingsView()
|
||||
|
||||
// Enable always allow execute
|
||||
const executeCheckbox = screen.getByRole('checkbox', {
|
||||
name: /Always approve allowed execute operations/i
|
||||
})
|
||||
fireEvent.click(executeCheckbox)
|
||||
|
||||
// Add a command
|
||||
const input = screen.getByPlaceholderText(/Enter command prefix/i)
|
||||
fireEvent.change(input, { target: { value: 'npm test' } })
|
||||
const addButton = screen.getByText('Add')
|
||||
fireEvent.click(addButton)
|
||||
|
||||
// Remove the command
|
||||
const removeButton = screen.getByRole('button', { name: 'Remove command' })
|
||||
fireEvent.click(removeButton)
|
||||
|
||||
// Verify command was removed
|
||||
expect(screen.queryByText('npm test')).not.toBeInTheDocument()
|
||||
|
||||
// Verify VSCode message was sent
|
||||
expect(vscode.postMessage).toHaveBeenLastCalledWith({
|
||||
type: 'allowedCommands',
|
||||
commands: []
|
||||
})
|
||||
})
|
||||
|
||||
it('prevents duplicate commands', () => {
|
||||
renderSettingsView()
|
||||
|
||||
// Enable always allow execute
|
||||
const executeCheckbox = screen.getByRole('checkbox', {
|
||||
name: /Always approve allowed execute operations/i
|
||||
})
|
||||
fireEvent.click(executeCheckbox)
|
||||
|
||||
// Add a command twice
|
||||
const input = screen.getByPlaceholderText(/Enter command prefix/i)
|
||||
const addButton = screen.getByText('Add')
|
||||
|
||||
// First addition
|
||||
fireEvent.change(input, { target: { value: 'npm test' } })
|
||||
fireEvent.click(addButton)
|
||||
|
||||
// Second addition attempt
|
||||
fireEvent.change(input, { target: { value: 'npm test' } })
|
||||
fireEvent.click(addButton)
|
||||
|
||||
// Verify command appears only once
|
||||
const commands = screen.getAllByText('npm test')
|
||||
expect(commands).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('saves allowed commands when clicking Done', () => {
|
||||
const { onDone } = renderSettingsView()
|
||||
|
||||
// Enable always allow execute
|
||||
const executeCheckbox = screen.getByRole('checkbox', {
|
||||
name: /Always approve allowed execute operations/i
|
||||
})
|
||||
fireEvent.click(executeCheckbox)
|
||||
|
||||
// Add a command
|
||||
const input = screen.getByPlaceholderText(/Enter command prefix/i)
|
||||
fireEvent.change(input, { target: { value: 'npm test' } })
|
||||
const addButton = screen.getByText('Add')
|
||||
fireEvent.click(addButton)
|
||||
|
||||
// Click Done
|
||||
const doneButton = screen.getByText('Done')
|
||||
fireEvent.click(doneButton)
|
||||
|
||||
// Verify VSCode messages were sent
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'allowedCommands',
|
||||
commands: ['npm test']
|
||||
}))
|
||||
expect(onDone).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user