mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-15 12:53:31 +08:00
fix(desktop): complete active-turn permission guard
Close open permission overlays when a turn starts, reject stale permission-mode writes in the chat store, and add the missing localized tooltip strings and transition regressions.\n\nFollow-up-to: #999\nTested: bun run check:desktop\nConfidence: high\nScope-risk: narrow
This commit is contained in:
parent
745112a136
commit
a6964f1932
@ -1,4 +1,4 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import '@testing-library/jest-dom'
|
||||
|
||||
@ -45,15 +45,43 @@ vi.mock('../../i18n', () => ({
|
||||
}))
|
||||
|
||||
import { PermissionModeSelector } from './PermissionModeSelector'
|
||||
import { useChatStore } from '../../stores/chatStore'
|
||||
import { useChatStore, type PerSessionState } from '../../stores/chatStore'
|
||||
import { useSettingsStore } from '../../stores/settingsStore'
|
||||
import { useSessionStore } from '../../stores/sessionStore'
|
||||
import { useTabStore } from '../../stores/tabStore'
|
||||
|
||||
const initialSetSessionPermissionMode = useChatStore.getState().setSessionPermissionMode
|
||||
|
||||
function makeChatSession(chatState: PerSessionState['chatState']): PerSessionState {
|
||||
return {
|
||||
messages: [],
|
||||
chatState,
|
||||
connectionState: 'connected',
|
||||
streamingText: '',
|
||||
streamingToolInput: '',
|
||||
activeToolUseId: null,
|
||||
activeToolName: null,
|
||||
activeThinkingId: null,
|
||||
pendingPermission: null,
|
||||
pendingComputerUsePermission: null,
|
||||
tokenUsage: { input_tokens: 0, output_tokens: 0 },
|
||||
streamingResponseChars: 0,
|
||||
elapsedSeconds: 0,
|
||||
statusVerb: '',
|
||||
slashCommands: [],
|
||||
agentTaskNotifications: {},
|
||||
elapsedTimer: null,
|
||||
}
|
||||
}
|
||||
|
||||
describe('PermissionModeSelector', () => {
|
||||
beforeEach(() => {
|
||||
viewportMocks.isMobile = false
|
||||
useSettingsStore.setState({ permissionMode: 'default' })
|
||||
useChatStore.setState({
|
||||
sessions: {},
|
||||
setSessionPermissionMode: initialSetSessionPermissionMode,
|
||||
})
|
||||
useSessionStore.setState({ sessions: [], activeSessionId: null })
|
||||
useTabStore.setState({ activeTabId: null, tabs: [] })
|
||||
})
|
||||
@ -166,9 +194,9 @@ describe('PermissionModeSelector', () => {
|
||||
useChatStore.setState({
|
||||
setSessionPermissionMode,
|
||||
sessions: {
|
||||
'current-tab': { chatState: 'thinking' },
|
||||
'current-tab': makeChatSession('thinking'),
|
||||
},
|
||||
} as Partial<ReturnType<typeof useChatStore.getState>>)
|
||||
})
|
||||
useSessionStore.setState({
|
||||
activeSessionId: 'current-tab',
|
||||
sessions: [
|
||||
@ -202,4 +230,61 @@ describe('PermissionModeSelector', () => {
|
||||
expect(screen.queryByRole('menu')).not.toBeInTheDocument()
|
||||
expect(setSessionPermissionMode).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('closes an open permission menu when the session turn starts', () => {
|
||||
useChatStore.setState({
|
||||
sessions: {
|
||||
'current-tab': makeChatSession('idle'),
|
||||
},
|
||||
})
|
||||
useTabStore.setState({
|
||||
activeTabId: 'current-tab',
|
||||
tabs: [{ sessionId: 'current-tab', title: 'Current', type: 'session', status: 'idle' }],
|
||||
})
|
||||
|
||||
render(<PermissionModeSelector />)
|
||||
|
||||
const trigger = screen.getByRole('button', { name: 'Ask permissions' })
|
||||
fireEvent.click(trigger)
|
||||
expect(screen.getByRole('menuitem', { name: /Auto accept edits/ })).toBeInTheDocument()
|
||||
|
||||
act(() => {
|
||||
useChatStore.setState({
|
||||
sessions: {
|
||||
'current-tab': makeChatSession('thinking'),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
expect(trigger).toBeDisabled()
|
||||
expect(screen.queryByRole('menuitem', { name: /Auto accept edits/ })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('closes an open bypass confirmation when the session turn starts', () => {
|
||||
useChatStore.setState({
|
||||
sessions: {
|
||||
'current-tab': makeChatSession('idle'),
|
||||
},
|
||||
})
|
||||
useTabStore.setState({
|
||||
activeTabId: 'current-tab',
|
||||
tabs: [{ sessionId: 'current-tab', title: 'Current', type: 'session', status: 'idle' }],
|
||||
})
|
||||
|
||||
render(<PermissionModeSelector />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Ask permissions' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: /Bypass permissions/ }))
|
||||
expect(screen.getByRole('dialog', { name: 'Enable bypass mode' })).toBeInTheDocument()
|
||||
|
||||
act(() => {
|
||||
useChatStore.setState({
|
||||
sessions: {
|
||||
'current-tab': makeChatSession('tool_executing'),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
expect(screen.queryByRole('dialog', { name: 'Enable bypass mode' })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -106,6 +106,13 @@ export function PermissionModeSelector({ workDir: workDirProp, compact = false,
|
||||
: 'bottom-full mb-2'
|
||||
const menuId = 'permission-mode-menu'
|
||||
|
||||
useEffect(() => {
|
||||
if (isTurnActive) {
|
||||
setOpen(false)
|
||||
setConfirmDialog(false)
|
||||
}
|
||||
}, [isTurnActive])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
@ -136,6 +143,10 @@ export function PermissionModeSelector({ workDir: workDirProp, compact = false,
|
||||
key={item.value}
|
||||
role="menuitem"
|
||||
onClick={() => {
|
||||
if (isTurnActive) {
|
||||
setOpen(false)
|
||||
return
|
||||
}
|
||||
if (item.value === 'bypassPermissions') {
|
||||
setOpen(false)
|
||||
setConfirmDialog(true)
|
||||
@ -265,6 +276,10 @@ export function PermissionModeSelector({ workDir: workDirProp, compact = false,
|
||||
{
|
||||
label: t('permMode.enableBypassBtn'),
|
||||
onClick: () => {
|
||||
if (isTurnActive) {
|
||||
setConfirmDialog(false)
|
||||
return
|
||||
}
|
||||
if (isControlled) {
|
||||
onChange?.('bypassPermissions')
|
||||
} else if (activeTabId) {
|
||||
|
||||
@ -1638,6 +1638,7 @@ export const en = {
|
||||
'permMode.permShell': 'Execute arbitrary shell commands',
|
||||
'permMode.permPackages': 'Install or remove packages',
|
||||
'permMode.enableBypassBtn': 'Enable bypass',
|
||||
'permMode.disabledDuringTurn': 'Cannot switch permissions while session is active',
|
||||
|
||||
// Mode labels (compact, for chips)
|
||||
'permMode.label.default': 'Ask permissions',
|
||||
|
||||
@ -1640,6 +1640,7 @@ export const jp: Record<TranslationKey, string> = {
|
||||
'permMode.permShell': '任意のシェルコマンドの実行',
|
||||
'permMode.permPackages': 'パッケージのインストールまたは削除',
|
||||
'permMode.enableBypassBtn': 'バイパスを有効化',
|
||||
'permMode.disabledDuringTurn': 'セッションの実行中は権限を切り替えられません',
|
||||
|
||||
// Mode labels (compact, for chips)
|
||||
'permMode.label.default': '権限を確認',
|
||||
|
||||
@ -1640,6 +1640,7 @@ export const kr: Record<TranslationKey, string> = {
|
||||
'permMode.permShell': '임의의 셸 명령 실행',
|
||||
'permMode.permPackages': '패키지 설치 또는 제거',
|
||||
'permMode.enableBypassBtn': '우회 사용',
|
||||
'permMode.disabledDuringTurn': '세션이 진행 중일 때는 권한을 변경할 수 없습니다',
|
||||
|
||||
// Mode labels (compact, for chips)
|
||||
'permMode.label.default': '권한 확인',
|
||||
|
||||
@ -1640,6 +1640,7 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'permMode.permShell': '執行任意 Shell 命令',
|
||||
'permMode.permPackages': '安裝或移除軟體包',
|
||||
'permMode.enableBypassBtn': '啟用跳過',
|
||||
'permMode.disabledDuringTurn': '工作階段進行中,無法切換權限',
|
||||
|
||||
// Mode labels (compact, for chips)
|
||||
'permMode.label.default': '詢問許可權',
|
||||
|
||||
@ -1640,6 +1640,7 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'permMode.permShell': '执行任意 Shell 命令',
|
||||
'permMode.permPackages': '安装或移除软件包',
|
||||
'permMode.enableBypassBtn': '启用跳过',
|
||||
'permMode.disabledDuringTurn': '会话进行中,无法切换权限',
|
||||
|
||||
// Mode labels (compact, for chips)
|
||||
'permMode.label.default': '询问权限',
|
||||
|
||||
@ -2835,6 +2835,21 @@ describe('chatStore history mapping', () => {
|
||||
expect(updateSessionPermissionModeMock).toHaveBeenCalledWith('session-1', 'acceptEdits')
|
||||
})
|
||||
|
||||
it('does not send permission mode updates while the session turn is active', () => {
|
||||
useChatStore.setState({
|
||||
sessions: {
|
||||
'session-1': makeSession({ chatState: 'thinking' }),
|
||||
},
|
||||
})
|
||||
|
||||
useChatStore.getState().setSessionPermissionMode('session-1', 'acceptEdits')
|
||||
|
||||
expect(sendMock).not.toHaveBeenCalledWith('session-1', {
|
||||
type: 'set_permission_mode',
|
||||
mode: 'acceptEdits',
|
||||
})
|
||||
})
|
||||
|
||||
it('mirrors CLI permission-mode broadcasts locally without echoing back to the server', () => {
|
||||
sendMock.mockReset()
|
||||
updateSessionPermissionModeMock.mockReset()
|
||||
|
||||
@ -1296,7 +1296,8 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
||||
},
|
||||
|
||||
setSessionPermissionMode: (sessionId, mode) => {
|
||||
if (!get().sessions[sessionId]) return
|
||||
const session = get().sessions[sessionId]
|
||||
if (!session || session.chatState !== 'idle') return
|
||||
wsManager.send(sessionId, { type: 'set_permission_mode', mode })
|
||||
},
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user