mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-21 12:51:17 -05:00
Expose a list of allowed auto-execute commands (#31)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react"
|
||||
import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextArea, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import { memo, useEffect, useState } from "react"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { validateApiConfiguration, validateModelId } from "../../utils/validate"
|
||||
@@ -26,9 +26,13 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
alwaysAllowBrowser,
|
||||
setAlwaysAllowBrowser,
|
||||
openRouterModels,
|
||||
setAllowedCommands,
|
||||
allowedCommands,
|
||||
} = useExtensionState()
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
|
||||
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
|
||||
const [commandInput, setCommandInput] = useState("")
|
||||
|
||||
const handleSubmit = () => {
|
||||
const apiValidationResult = validateApiConfiguration(apiConfiguration)
|
||||
const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels)
|
||||
@@ -42,6 +46,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
vscode.postMessage({ type: "alwaysAllowWrite", bool: alwaysAllowWrite })
|
||||
vscode.postMessage({ type: "alwaysAllowExecute", bool: alwaysAllowExecute })
|
||||
vscode.postMessage({ type: "alwaysAllowBrowser", bool: alwaysAllowBrowser })
|
||||
vscode.postMessage({ type: "allowedCommands", commands: allowedCommands ?? [] })
|
||||
onDone()
|
||||
}
|
||||
}
|
||||
@@ -51,22 +56,31 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
setModelIdErrorMessage(undefined)
|
||||
}, [apiConfiguration])
|
||||
|
||||
// validate as soon as the component is mounted
|
||||
/*
|
||||
useEffect will use stale values of variables if they are not included in the dependency array. so trying to use useEffect with a dependency array of only one value for example will use any other variables' old values. In most cases you don't want this, and should opt to use react-use hooks.
|
||||
|
||||
// Initial validation on mount
|
||||
useEffect(() => {
|
||||
// uses someVar and anotherVar
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [someVar])
|
||||
|
||||
If we only want to run code once on mount we can use react-use's useEffectOnce or useMount
|
||||
*/
|
||||
const apiValidationResult = validateApiConfiguration(apiConfiguration)
|
||||
const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels)
|
||||
setApiErrorMessage(apiValidationResult)
|
||||
setModelIdErrorMessage(modelIdValidationResult)
|
||||
}, [apiConfiguration, openRouterModels])
|
||||
|
||||
const handleResetState = () => {
|
||||
vscode.postMessage({ type: "resetState" })
|
||||
}
|
||||
|
||||
const handleAddCommand = () => {
|
||||
const currentCommands = allowedCommands ?? []
|
||||
if (commandInput && !currentCommands.includes(commandInput)) {
|
||||
const newCommands = [...currentCommands, commandInput]
|
||||
setAllowedCommands(newCommands)
|
||||
setCommandInput("")
|
||||
vscode.postMessage({
|
||||
type: "allowedCommands",
|
||||
commands: newCommands
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -149,27 +163,12 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: "5px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
padding: "8px",
|
||||
border: "1px solid var(--vscode-errorBorder)",
|
||||
borderRadius: "4px",
|
||||
color: "var(--vscode-errorForeground)",
|
||||
}}>
|
||||
When enabled, Cline will automatically write to files and create directories
|
||||
without requiring you to click the Approve button.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 5 }}>
|
||||
<VSCodeCheckbox
|
||||
checked={alwaysAllowExecute}
|
||||
onChange={(e: any) => setAlwaysAllowExecute(e.target.checked)}>
|
||||
<span style={{ fontWeight: "500" }}>Always approve execute operations</span>
|
||||
</VSCodeCheckbox>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: "5px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
When enabled, Cline will automatically CLI commands without requiring
|
||||
you to click the Approve button.
|
||||
⚠️ WARNING: When enabled, Cline will automatically create and edit files without requiring approval. This is potentially very dangerous and could lead to unwanted system modifications or security risks. Enable only if you fully trust the AI and understand the risks.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -183,13 +182,90 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: "5px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
padding: "8px",
|
||||
backgroundColor: "var(--vscode-errorBackground)",
|
||||
border: "1px solid var(--vscode-errorBorder)",
|
||||
borderRadius: "4px",
|
||||
color: "var(--vscode-errorForeground)",
|
||||
}}>
|
||||
When enabled, Cline will automatically perform browser actions without requiring
|
||||
you to click the Approve button.
|
||||
⚠️ WARNING: When enabled, Cline will automatically perform browser actions without requiring approval. This is potentially very dangerous and could lead to unwanted system modifications or security risks. Enable only if you fully trust the AI and understand the risks.<br/><br/>NOTE: The checkbox only applies when the model supports computer use.
|
||||
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 5 }}>
|
||||
<VSCodeCheckbox
|
||||
checked={alwaysAllowExecute}
|
||||
onChange={(e: any) => setAlwaysAllowExecute(e.target.checked)}>
|
||||
<span style={{ fontWeight: "500" }}>Always approve allowed execute operations</span>
|
||||
</VSCodeCheckbox>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: "5px",
|
||||
padding: "8px",
|
||||
backgroundColor: "var(--vscode-errorBackground)",
|
||||
border: "1px solid var(--vscode-errorBorder)",
|
||||
borderRadius: "4px",
|
||||
color: "var(--vscode-errorForeground)",
|
||||
}}>
|
||||
⚠️ WARNING: When enabled, Cline will automatically execute allowed terminal commands without requiring approval. This is potentially very dangerous and could lead to unwanted system modifications or security risks. Enable only if you fully trust the AI and understand the risks.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{alwaysAllowExecute && (
|
||||
<div style={{ marginBottom: 5 }}>
|
||||
<div style={{ marginBottom: "10px" }}>
|
||||
<span style={{ fontWeight: "500" }}>Allowed Auto-Execute Commands</span>
|
||||
<p style={{
|
||||
fontSize: "12px",
|
||||
marginTop: "5px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
Command prefixes that can be auto-executed when "Always approve execute operations" is enabled.
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'flex', gap: '5px', marginTop: '10px' }}>
|
||||
<VSCodeTextField
|
||||
value={commandInput}
|
||||
onInput={(e: any) => setCommandInput(e.target.value)}
|
||||
placeholder="Enter command prefix (e.g., 'git ')"
|
||||
style={{ flexGrow: 1 }}
|
||||
/>
|
||||
<VSCodeButton onClick={handleAddCommand}>
|
||||
Add
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '10px' }}>
|
||||
{(allowedCommands ?? []).map((cmd, index) => (
|
||||
<div key={index} style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '5px',
|
||||
marginBottom: '5px'
|
||||
}}>
|
||||
<span>{cmd}</span>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={() => {
|
||||
const newCommands = (allowedCommands ?? []).filter((_, i) => i !== index)
|
||||
setAllowedCommands(newCommands)
|
||||
vscode.postMessage({
|
||||
type: "allowedCommands",
|
||||
commands: newCommands
|
||||
})
|
||||
}}
|
||||
>
|
||||
<span className="codicon codicon-close" />
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{IS_DEV && (
|
||||
<>
|
||||
<div style={{ marginTop: "10px", marginBottom: "4px" }}>Debug</div>
|
||||
@@ -218,8 +294,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
}}>
|
||||
<p style={{ wordWrap: "break-word", margin: 0, padding: 0 }}>
|
||||
If you have any questions or feedback, feel free to open an issue at{" "}
|
||||
<VSCodeLink href="https://github.com/cline/cline" style={{ display: "inline" }}>
|
||||
https://github.com/cline/cline
|
||||
<VSCodeLink href="https://github.com/RooVetGit/Roo-Cline" style={{ display: "inline" }}>
|
||||
https://github.com/RooVetGit/Roo-Cline
|
||||
</VSCodeLink>
|
||||
</p>
|
||||
<p style={{ fontStyle: "italic", margin: "10px 0 0 0", padding: 0 }}>v{version}</p>
|
||||
|
||||
@@ -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