|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import { |
|
|
describe, |
|
|
it, |
|
|
expect, |
|
|
vi, |
|
|
beforeEach, |
|
|
type MockedFunction, |
|
|
type Mock, |
|
|
} from 'vitest'; |
|
|
import { renderHook, act } from '@testing-library/react'; |
|
|
import { useAutoAcceptIndicator } from './useAutoAcceptIndicator.js'; |
|
|
|
|
|
import { |
|
|
Config, |
|
|
Config as ActualConfigType, |
|
|
ApprovalMode, |
|
|
} from '@google/gemini-cli-core'; |
|
|
import { useInput, type Key as InkKey } from 'ink'; |
|
|
|
|
|
vi.mock('ink'); |
|
|
|
|
|
vi.mock('@google/gemini-cli-core', async () => { |
|
|
const actualServerModule = (await vi.importActual( |
|
|
'@google/gemini-cli-core', |
|
|
)) as Record<string, unknown>; |
|
|
return { |
|
|
...actualServerModule, |
|
|
Config: vi.fn(), |
|
|
}; |
|
|
}); |
|
|
|
|
|
interface MockConfigInstanceShape { |
|
|
getApprovalMode: Mock<() => ApprovalMode>; |
|
|
setApprovalMode: Mock<(value: ApprovalMode) => void>; |
|
|
getCoreTools: Mock<() => string[]>; |
|
|
getToolDiscoveryCommand: Mock<() => string | undefined>; |
|
|
getTargetDir: Mock<() => string>; |
|
|
getApiKey: Mock<() => string>; |
|
|
getModel: Mock<() => string>; |
|
|
getSandbox: Mock<() => boolean | string>; |
|
|
getDebugMode: Mock<() => boolean>; |
|
|
getQuestion: Mock<() => string | undefined>; |
|
|
getFullContext: Mock<() => boolean>; |
|
|
getUserAgent: Mock<() => string>; |
|
|
getUserMemory: Mock<() => string>; |
|
|
getGeminiMdFileCount: Mock<() => number>; |
|
|
getToolRegistry: Mock<() => { discoverTools: Mock<() => void> }>; |
|
|
} |
|
|
|
|
|
type UseInputKey = InkKey; |
|
|
type UseInputHandler = (input: string, key: UseInputKey) => void; |
|
|
|
|
|
describe('useAutoAcceptIndicator', () => { |
|
|
let mockConfigInstance: MockConfigInstanceShape; |
|
|
let capturedUseInputHandler: UseInputHandler; |
|
|
let mockedInkUseInput: MockedFunction<typeof useInput>; |
|
|
|
|
|
beforeEach(() => { |
|
|
vi.resetAllMocks(); |
|
|
|
|
|
( |
|
|
Config as unknown as MockedFunction<() => MockConfigInstanceShape> |
|
|
).mockImplementation(() => { |
|
|
const instanceGetApprovalModeMock = vi.fn(); |
|
|
const instanceSetApprovalModeMock = vi.fn(); |
|
|
|
|
|
const instance: MockConfigInstanceShape = { |
|
|
getApprovalMode: instanceGetApprovalModeMock as Mock< |
|
|
() => ApprovalMode |
|
|
>, |
|
|
setApprovalMode: instanceSetApprovalModeMock as Mock< |
|
|
(value: ApprovalMode) => void |
|
|
>, |
|
|
getCoreTools: vi.fn().mockReturnValue([]) as Mock<() => string[]>, |
|
|
getToolDiscoveryCommand: vi.fn().mockReturnValue(undefined) as Mock< |
|
|
() => string | undefined |
|
|
>, |
|
|
getTargetDir: vi.fn().mockReturnValue('.') as Mock<() => string>, |
|
|
getApiKey: vi.fn().mockReturnValue('test-api-key') as Mock< |
|
|
() => string |
|
|
>, |
|
|
getModel: vi.fn().mockReturnValue('test-model') as Mock<() => string>, |
|
|
getSandbox: vi.fn().mockReturnValue(false) as Mock< |
|
|
() => boolean | string |
|
|
>, |
|
|
getDebugMode: vi.fn().mockReturnValue(false) as Mock<() => boolean>, |
|
|
getQuestion: vi.fn().mockReturnValue(undefined) as Mock< |
|
|
() => string | undefined |
|
|
>, |
|
|
getFullContext: vi.fn().mockReturnValue(false) as Mock<() => boolean>, |
|
|
getUserAgent: vi.fn().mockReturnValue('test-user-agent') as Mock< |
|
|
() => string |
|
|
>, |
|
|
getUserMemory: vi.fn().mockReturnValue('') as Mock<() => string>, |
|
|
getGeminiMdFileCount: vi.fn().mockReturnValue(0) as Mock<() => number>, |
|
|
getToolRegistry: vi |
|
|
.fn() |
|
|
.mockReturnValue({ discoverTools: vi.fn() }) as Mock< |
|
|
() => { discoverTools: Mock<() => void> } |
|
|
>, |
|
|
}; |
|
|
instanceSetApprovalModeMock.mockImplementation((value: ApprovalMode) => { |
|
|
instanceGetApprovalModeMock.mockReturnValue(value); |
|
|
}); |
|
|
return instance; |
|
|
}); |
|
|
|
|
|
mockedInkUseInput = useInput as MockedFunction<typeof useInput>; |
|
|
mockedInkUseInput.mockImplementation((handler: UseInputHandler) => { |
|
|
capturedUseInputHandler = handler; |
|
|
}); |
|
|
|
|
|
|
|
|
mockConfigInstance = new (Config as any)() as MockConfigInstanceShape; |
|
|
}); |
|
|
|
|
|
it('should initialize with ApprovalMode.AUTO_EDIT if config.getApprovalMode returns ApprovalMode.AUTO_EDIT', () => { |
|
|
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.AUTO_EDIT); |
|
|
const { result } = renderHook(() => |
|
|
useAutoAcceptIndicator({ |
|
|
config: mockConfigInstance as unknown as ActualConfigType, |
|
|
}), |
|
|
); |
|
|
expect(result.current).toBe(ApprovalMode.AUTO_EDIT); |
|
|
expect(mockConfigInstance.getApprovalMode).toHaveBeenCalledTimes(1); |
|
|
}); |
|
|
|
|
|
it('should initialize with ApprovalMode.DEFAULT if config.getApprovalMode returns ApprovalMode.DEFAULT', () => { |
|
|
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT); |
|
|
const { result } = renderHook(() => |
|
|
useAutoAcceptIndicator({ |
|
|
config: mockConfigInstance as unknown as ActualConfigType, |
|
|
}), |
|
|
); |
|
|
expect(result.current).toBe(ApprovalMode.DEFAULT); |
|
|
expect(mockConfigInstance.getApprovalMode).toHaveBeenCalledTimes(1); |
|
|
}); |
|
|
|
|
|
it('should initialize with ApprovalMode.YOLO if config.getApprovalMode returns ApprovalMode.YOLO', () => { |
|
|
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.YOLO); |
|
|
const { result } = renderHook(() => |
|
|
useAutoAcceptIndicator({ |
|
|
config: mockConfigInstance as unknown as ActualConfigType, |
|
|
}), |
|
|
); |
|
|
expect(result.current).toBe(ApprovalMode.YOLO); |
|
|
expect(mockConfigInstance.getApprovalMode).toHaveBeenCalledTimes(1); |
|
|
}); |
|
|
|
|
|
it('should toggle the indicator and update config when Shift+Tab or Ctrl+Y is pressed', () => { |
|
|
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT); |
|
|
const { result } = renderHook(() => |
|
|
useAutoAcceptIndicator({ |
|
|
config: mockConfigInstance as unknown as ActualConfigType, |
|
|
}), |
|
|
); |
|
|
expect(result.current).toBe(ApprovalMode.DEFAULT); |
|
|
|
|
|
act(() => { |
|
|
capturedUseInputHandler('', { tab: true, shift: true } as InkKey); |
|
|
}); |
|
|
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith( |
|
|
ApprovalMode.AUTO_EDIT, |
|
|
); |
|
|
expect(result.current).toBe(ApprovalMode.AUTO_EDIT); |
|
|
|
|
|
act(() => { |
|
|
capturedUseInputHandler('y', { ctrl: true } as InkKey); |
|
|
}); |
|
|
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith( |
|
|
ApprovalMode.YOLO, |
|
|
); |
|
|
expect(result.current).toBe(ApprovalMode.YOLO); |
|
|
|
|
|
act(() => { |
|
|
capturedUseInputHandler('y', { ctrl: true } as InkKey); |
|
|
}); |
|
|
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith( |
|
|
ApprovalMode.DEFAULT, |
|
|
); |
|
|
expect(result.current).toBe(ApprovalMode.DEFAULT); |
|
|
|
|
|
act(() => { |
|
|
capturedUseInputHandler('y', { ctrl: true } as InkKey); |
|
|
}); |
|
|
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith( |
|
|
ApprovalMode.YOLO, |
|
|
); |
|
|
expect(result.current).toBe(ApprovalMode.YOLO); |
|
|
|
|
|
act(() => { |
|
|
capturedUseInputHandler('', { tab: true, shift: true } as InkKey); |
|
|
}); |
|
|
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith( |
|
|
ApprovalMode.AUTO_EDIT, |
|
|
); |
|
|
expect(result.current).toBe(ApprovalMode.AUTO_EDIT); |
|
|
|
|
|
act(() => { |
|
|
capturedUseInputHandler('', { tab: true, shift: true } as InkKey); |
|
|
}); |
|
|
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith( |
|
|
ApprovalMode.DEFAULT, |
|
|
); |
|
|
expect(result.current).toBe(ApprovalMode.DEFAULT); |
|
|
}); |
|
|
|
|
|
it('should not toggle if only one key or other keys combinations are pressed', () => { |
|
|
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT); |
|
|
renderHook(() => |
|
|
useAutoAcceptIndicator({ |
|
|
config: mockConfigInstance as unknown as ActualConfigType, |
|
|
}), |
|
|
); |
|
|
|
|
|
act(() => { |
|
|
capturedUseInputHandler('', { tab: true, shift: false } as InkKey); |
|
|
}); |
|
|
expect(mockConfigInstance.setApprovalMode).not.toHaveBeenCalled(); |
|
|
|
|
|
act(() => { |
|
|
capturedUseInputHandler('', { tab: false, shift: true } as InkKey); |
|
|
}); |
|
|
expect(mockConfigInstance.setApprovalMode).not.toHaveBeenCalled(); |
|
|
|
|
|
act(() => { |
|
|
capturedUseInputHandler('a', { tab: false, shift: false } as InkKey); |
|
|
}); |
|
|
expect(mockConfigInstance.setApprovalMode).not.toHaveBeenCalled(); |
|
|
|
|
|
act(() => { |
|
|
capturedUseInputHandler('y', { tab: true } as InkKey); |
|
|
}); |
|
|
expect(mockConfigInstance.setApprovalMode).not.toHaveBeenCalled(); |
|
|
|
|
|
act(() => { |
|
|
capturedUseInputHandler('a', { ctrl: true } as InkKey); |
|
|
}); |
|
|
expect(mockConfigInstance.setApprovalMode).not.toHaveBeenCalled(); |
|
|
|
|
|
act(() => { |
|
|
capturedUseInputHandler('y', { shift: true } as InkKey); |
|
|
}); |
|
|
expect(mockConfigInstance.setApprovalMode).not.toHaveBeenCalled(); |
|
|
|
|
|
act(() => { |
|
|
capturedUseInputHandler('a', { ctrl: true, shift: true } as InkKey); |
|
|
}); |
|
|
expect(mockConfigInstance.setApprovalMode).not.toHaveBeenCalled(); |
|
|
}); |
|
|
|
|
|
it('should update indicator when config value changes externally (useEffect dependency)', () => { |
|
|
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT); |
|
|
const { result, rerender } = renderHook( |
|
|
(props: { config: ActualConfigType }) => useAutoAcceptIndicator(props), |
|
|
{ |
|
|
initialProps: { |
|
|
config: mockConfigInstance as unknown as ActualConfigType, |
|
|
}, |
|
|
}, |
|
|
); |
|
|
expect(result.current).toBe(ApprovalMode.DEFAULT); |
|
|
|
|
|
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.AUTO_EDIT); |
|
|
|
|
|
rerender({ config: mockConfigInstance as unknown as ActualConfigType }); |
|
|
expect(result.current).toBe(ApprovalMode.AUTO_EDIT); |
|
|
expect(mockConfigInstance.getApprovalMode).toHaveBeenCalledTimes(3); |
|
|
}); |
|
|
}); |
|
|
|