Expose a list of allowed auto-execute commands (#31)

This commit is contained in:
Matt Rubens
2024-12-01 15:34:36 -05:00
committed by GitHub
parent 750c24c8a7
commit 6b8f9f7a45
14 changed files with 1085 additions and 719 deletions

View File

@@ -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()
})
})