cc-haha/desktop/src/components/chat/PlanModePermissionDialog.test.tsx
程序员阿江(Relakkes) a94e5b641a fix: unify token usage display across desktop and CLI (#757)
Token counts were rendered with five inconsistent formats across the
chat UI (header "181,117 t", in-progress bare "↓ 2514", background
agents "12,345 tokens", compact summary "1.5k", trace "1.2k"), and the
in-progress count was actually stale: the server never set the status
event's tokens field, so the indicator showed the previous turn's value
(hence "first message shows nothing", "sometimes appears, never moves").

- Add shared desktop lib/formatTokenCount; route header, streaming
  indicator, background agents, compact summary, and trace through it.
- Header shows compact "124.3k tokens" with the exact count on hover.
- Add common.tokens i18n key (en/zh/zh-TW/jp/kr).
- Estimate the in-progress count from streamed chars (÷4, mirroring the
  CLI spinner) via a new streamingResponseChars per-session field, reset
  on each send; drop the dead status.tokens/elapsed fields.
- CLI spinner rows use formatTokens to drop the "1.0k" artifact.
2026-06-12 15:25:56 +08:00

192 lines
5.8 KiB
TypeScript

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(
<PermissionDialog
sessionId="session-1"
requestId="perm-plan"
toolName="ExitPlanMode"
input={{
plan: PLAN,
planFilePath: '/tmp/claude-plan.md',
allowedPrompts: [{ tool: 'Bash', prompt: 'run tests' }],
}}
description="Exit plan mode?"
/>,
)
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(
<PermissionDialog
sessionId="session-1"
requestId="perm-plan"
toolName="ExitPlanMode"
input={{ plan: PLAN, planFilePath: '/tmp/claude-plan.md' }}
/>,
)
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(
<PermissionDialog
sessionId="session-1"
requestId="perm-plan"
toolName="ExitPlanMode"
input={{
plan: PLAN,
allowedPrompts: [{ tool: 'Bash', prompt: 'run tests' }],
}}
/>,
)
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(
<ToolCallBlock
toolName="ExitPlanMode"
input={{ plan: PLAN, planFilePath: '/tmp/claude-plan.md' }}
result={{
isError: false,
content: [
'User has approved your plan. You can now start coding.',
'',
'Your plan has been saved to: /tmp/claude-plan.md',
'',
'## Approved Plan:',
PLAN,
].join('\n'),
}}
/>,
)
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')
})
})