import { beforeEach, describe, expect, it, vi } from 'vitest'
import { fireEvent, render, screen } from '@testing-library/react'
const { sendMock } = vi.hoisted(() => ({
sendMock: vi.fn(),
}))
vi.mock('../../api/websocket', () => ({
wsManager: {
connect: vi.fn(),
disconnect: vi.fn(),
onMessage: vi.fn(() => () => {}),
clearHandlers: vi.fn(),
send: sendMock,
},
}))
import { useChatStore } from '../../stores/chatStore'
import { useSettingsStore } from '../../stores/settingsStore'
import { useTabStore } from '../../stores/tabStore'
import { PermissionDialog } from './PermissionDialog'
import { ToolCallBlock } from './ToolCallBlock'
const PLAN = [
'# Release checklist',
'',
'1. Update the desktop plan modal.',
'2. Run `bun run check:desktop`.',
'',
'```bash',
'bun test desktop/src/components/chat/PlanModePermissionDialog.test.tsx',
'```',
].join('\n')
function seedPendingPlanPermission() {
useChatStore.setState({
sessions: {
'session-1': {
messages: [],
chatState: 'permission_pending',
connectionState: 'connected',
streamingText: '',
streamingToolInput: '',
activeToolUseId: null,
activeToolName: null,
activeThinkingId: null,
pendingPermission: {
requestId: 'perm-plan',
toolName: 'ExitPlanMode',
toolUseId: 'toolu-plan',
input: {
plan: PLAN,
planFilePath: '/tmp/claude-plan.md',
allowedPrompts: [{ tool: 'Bash', prompt: 'run tests' }],
},
description: 'Exit plan mode?',
},
pendingComputerUsePermission: null,
tokenUsage: { input_tokens: 0, output_tokens: 0 },
streamingResponseChars: 0,
elapsedSeconds: 0,
statusVerb: '',
slashCommands: [],
agentTaskNotifications: {},
backgroundAgentTasks: {},
elapsedTimer: null,
composerPrefill: null,
composerInsertion: null,
composerDraft: null,
},
},
})
}
describe('plan mode permission UI', () => {
beforeEach(() => {
sendMock.mockReset()
useSettingsStore.setState({ locale: 'en' })
useTabStore.setState({
activeTabId: 'session-1',
tabs: [{ sessionId: 'session-1', title: 'Test', type: 'session' as const, status: 'idle' }],
})
seedPendingPlanPermission()
})
it('renders ExitPlanMode as a plan preview instead of raw tool input', () => {
const { container } = render(
,
)
expect(container.textContent).toContain('Ready to code?')
expect(container.textContent).toContain('Release checklist')
expect(container.textContent).toContain('Update the desktop plan modal.')
expect(container.textContent).toContain('/tmp/claude-plan.md')
expect(container.textContent).toContain('Requested permissions')
expect(container.textContent).toContain('Bash')
expect(container.textContent).toContain('run tests')
expect(screen.getByRole('button', { name: 'Approve plan' })).toBeTruthy()
expect(screen.getByRole('button', { name: 'Keep planning' })).toBeTruthy()
expect(container.textContent).not.toContain('"allowedPrompts"')
})
it('sends typed feedback when the user keeps planning', () => {
render(
,
)
fireEvent.change(screen.getByPlaceholderText('Tell Claude what to change'), {
target: { value: 'Add a rollback step before implementation.' },
})
fireEvent.click(screen.getByRole('button', { name: 'Keep planning' }))
expect(sendMock).toHaveBeenCalledWith('session-1', {
type: 'permission_response',
requestId: 'perm-plan',
allowed: false,
denyMessage: 'Add a rollback step before implementation.',
})
})
it('includes requested prompt permissions when approving the plan', () => {
render(
,
)
fireEvent.click(screen.getByRole('button', { name: 'Approve plan' }))
expect(sendMock).toHaveBeenCalledWith('session-1', {
type: 'permission_response',
requestId: 'perm-plan',
allowed: true,
permissionUpdates: [
{
type: 'addRules',
rules: [{ toolName: 'Bash', ruleContent: 'prompt: run tests' }],
behavior: 'allow',
destination: 'session',
},
],
})
})
it('renders approved ExitPlanMode results as a markdown plan card', () => {
const { container } = render(
,
)
expect(container.textContent).toContain('Plan approved')
expect(container.textContent).toContain('Release checklist')
expect(container.textContent).toContain('Update the desktop plan modal.')
expect(container.textContent).toContain('/tmp/claude-plan.md')
expect(container.textContent).not.toContain('Tool Output')
})
})