/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { describe, it, expect, vi, beforeEach, afterEach, type Mock, } from 'vitest'; import { render, cleanup } from 'ink-testing-library'; import { AppContainer } from './AppContainer.js'; import { type Config, makeFakeConfig, type GeminiClient, type SubagentManager, } from '@qwen-code/qwen-code-core'; import type { LoadedSettings } from '../config/settings.js'; import type { InitializationResult } from '../core/initializer.js'; import { useQuotaAndFallback } from './hooks/useQuotaAndFallback.js'; import { UIStateContext, type UIState } from './contexts/UIStateContext.js'; import { UIActionsContext, type UIActions, } from './contexts/UIActionsContext.js'; import { useContext } from 'react'; // Mock useStdout to capture terminal title writes let mockStdout: { write: ReturnType }; vi.mock('ink', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useStdout: () => ({ stdout: mockStdout }), measureElement: vi.fn(), }; }); // Helper component will read the context values provided by AppContainer // so we can assert against them in our tests. let capturedUIState: UIState; let capturedUIActions: UIActions; function TestContextConsumer() { capturedUIState = useContext(UIStateContext)!; capturedUIActions = useContext(UIActionsContext)!; return null; } vi.mock('./App.js', () => ({ App: TestContextConsumer, })); vi.mock('./hooks/useQuotaAndFallback.js'); vi.mock('./hooks/useHistoryManager.js'); vi.mock('./hooks/useThemeCommand.js'); vi.mock('./auth/useAuth.js'); vi.mock('./hooks/useEditorSettings.js'); vi.mock('./hooks/useSettingsCommand.js'); vi.mock('./hooks/useModelCommand.js'); vi.mock('./hooks/slashCommandProcessor.js'); vi.mock('./hooks/useConsoleMessages.js'); vi.mock('./hooks/useTerminalSize.js', () => ({ useTerminalSize: vi.fn(() => ({ columns: 80, rows: 24 })), })); vi.mock('./hooks/useGeminiStream.js'); vi.mock('./hooks/vim.js'); vi.mock('./hooks/useFocus.js'); vi.mock('./hooks/useBracketedPaste.js'); vi.mock('./hooks/useKeypress.js'); vi.mock('./hooks/useLoadingIndicator.js'); vi.mock('./hooks/useFolderTrust.js'); vi.mock('./hooks/useIdeTrustListener.js'); vi.mock('./hooks/useMessageQueue.js'); vi.mock('./hooks/useAutoAcceptIndicator.js'); vi.mock('./hooks/useWorkspaceMigration.js'); vi.mock('./hooks/useGitBranchName.js'); vi.mock('./contexts/VimModeContext.js'); vi.mock('./contexts/SessionContext.js'); vi.mock('./components/shared/text-buffer.js'); vi.mock('./hooks/useLogger.js'); // Mock external utilities vi.mock('../utils/events.js'); vi.mock('../utils/handleAutoUpdate.js'); vi.mock('./utils/ConsolePatcher.js'); vi.mock('../utils/cleanup.js'); import { useHistory } from './hooks/useHistoryManager.js'; import { useThemeCommand } from './hooks/useThemeCommand.js'; import { useAuthCommand } from './auth/useAuth.js'; import { useEditorSettings } from './hooks/useEditorSettings.js'; import { useSettingsCommand } from './hooks/useSettingsCommand.js'; import { useModelCommand } from './hooks/useModelCommand.js'; import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js'; import { useConsoleMessages } from './hooks/useConsoleMessages.js'; import { useGeminiStream } from './hooks/useGeminiStream.js'; import { useVim } from './hooks/vim.js'; import { useFolderTrust } from './hooks/useFolderTrust.js'; import { useIdeTrustListener } from './hooks/useIdeTrustListener.js'; import { useMessageQueue } from './hooks/useMessageQueue.js'; import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js'; import { useWorkspaceMigration } from './hooks/useWorkspaceMigration.js'; import { useGitBranchName } from './hooks/useGitBranchName.js'; import { useVimMode } from './contexts/VimModeContext.js'; import { useSessionStats } from './contexts/SessionContext.js'; import { useTextBuffer } from './components/shared/text-buffer.js'; import { useLogger } from './hooks/useLogger.js'; import { useLoadingIndicator } from './hooks/useLoadingIndicator.js'; import { measureElement } from 'ink'; import { useTerminalSize } from './hooks/useTerminalSize.js'; import { ShellExecutionService } from '@qwen-code/qwen-code-core'; describe('AppContainer State Management', () => { let mockConfig: Config; let mockSettings: LoadedSettings; let mockInitResult: InitializationResult; // Create typed mocks for all hooks const mockedUseQuotaAndFallback = useQuotaAndFallback as Mock; const mockedUseHistory = useHistory as Mock; const mockedUseThemeCommand = useThemeCommand as Mock; const mockedUseAuthCommand = useAuthCommand as Mock; const mockedUseEditorSettings = useEditorSettings as Mock; const mockedUseSettingsCommand = useSettingsCommand as Mock; const mockedUseModelCommand = useModelCommand as Mock; const mockedUseSlashCommandProcessor = useSlashCommandProcessor as Mock; const mockedUseConsoleMessages = useConsoleMessages as Mock; const mockedUseGeminiStream = useGeminiStream as Mock; const mockedUseVim = useVim as Mock; const mockedUseFolderTrust = useFolderTrust as Mock; const mockedUseIdeTrustListener = useIdeTrustListener as Mock; const mockedUseMessageQueue = useMessageQueue as Mock; const mockedUseAutoAcceptIndicator = useAutoAcceptIndicator as Mock; const mockedUseWorkspaceMigration = useWorkspaceMigration as Mock; const mockedUseGitBranchName = useGitBranchName as Mock; const mockedUseVimMode = useVimMode as Mock; const mockedUseSessionStats = useSessionStats as Mock; const mockedUseTextBuffer = useTextBuffer as Mock; const mockedUseLogger = useLogger as Mock; const mockedUseLoadingIndicator = useLoadingIndicator as Mock; beforeEach(() => { vi.clearAllMocks(); // Initialize mock stdout for terminal title tests mockStdout = { write: vi.fn() }; // Mock computeWindowTitle function to centralize title logic testing vi.mock('../utils/windowTitle.js', async () => ({ computeWindowTitle: vi.fn( (folderName: string) => // Default behavior: return "Gemini - {folderName}" unless CLI_TITLE is set process.env['CLI_TITLE'] || `Gemini - ${folderName}`, ), })); capturedUIState = null!; capturedUIActions = null!; // **Provide a default return value for EVERY mocked hook.** mockedUseQuotaAndFallback.mockReturnValue({ proQuotaRequest: null, handleProQuotaChoice: vi.fn(), }); mockedUseHistory.mockReturnValue({ history: [], addItem: vi.fn(), updateItem: vi.fn(), clearItems: vi.fn(), loadHistory: vi.fn(), }); mockedUseThemeCommand.mockReturnValue({ isThemeDialogOpen: false, openThemeDialog: vi.fn(), handleThemeSelect: vi.fn(), handleThemeHighlight: vi.fn(), }); mockedUseAuthCommand.mockReturnValue({ authState: 'authenticated', setAuthState: vi.fn(), authError: null, onAuthError: vi.fn(), isAuthDialogOpen: false, isAuthenticating: false, handleAuthSelect: vi.fn(), openAuthDialog: vi.fn(), cancelAuthentication: vi.fn(), }); mockedUseEditorSettings.mockReturnValue({ isEditorDialogOpen: false, openEditorDialog: vi.fn(), handleEditorSelect: vi.fn(), exitEditorDialog: vi.fn(), }); mockedUseSettingsCommand.mockReturnValue({ isSettingsDialogOpen: false, openSettingsDialog: vi.fn(), closeSettingsDialog: vi.fn(), }); mockedUseModelCommand.mockReturnValue({ isModelDialogOpen: false, openModelDialog: vi.fn(), closeModelDialog: vi.fn(), }); mockedUseSlashCommandProcessor.mockReturnValue({ handleSlashCommand: vi.fn(), slashCommands: [], pendingHistoryItems: [], commandContext: {}, shellConfirmationRequest: null, confirmationRequest: null, }); mockedUseConsoleMessages.mockReturnValue({ consoleMessages: [], handleNewMessage: vi.fn(), clearConsoleMessages: vi.fn(), }); mockedUseGeminiStream.mockReturnValue({ streamingState: 'idle', submitQuery: vi.fn(), initError: null, pendingHistoryItems: [], thought: null, cancelOngoingRequest: vi.fn(), }); mockedUseVim.mockReturnValue({ handleInput: vi.fn() }); mockedUseFolderTrust.mockReturnValue({ isFolderTrustDialogOpen: false, handleFolderTrustSelect: vi.fn(), isRestarting: false, }); mockedUseIdeTrustListener.mockReturnValue({ needsRestart: false, restartReason: 'NONE', }); mockedUseMessageQueue.mockReturnValue({ messageQueue: [], addMessage: vi.fn(), clearQueue: vi.fn(), getQueuedMessagesText: vi.fn().mockReturnValue(''), }); mockedUseAutoAcceptIndicator.mockReturnValue(false); mockedUseWorkspaceMigration.mockReturnValue({ showWorkspaceMigrationDialog: false, workspaceExtensions: [], onWorkspaceMigrationDialogOpen: vi.fn(), onWorkspaceMigrationDialogClose: vi.fn(), }); mockedUseGitBranchName.mockReturnValue('main'); mockedUseVimMode.mockReturnValue({ isVimEnabled: false, toggleVimEnabled: vi.fn(), }); mockedUseSessionStats.mockReturnValue({ stats: {} }); mockedUseTextBuffer.mockReturnValue({ text: '', setText: vi.fn(), // Add other properties if AppContainer uses them }); mockedUseLogger.mockReturnValue({ getPreviousUserMessages: vi.fn().mockResolvedValue([]), }); mockedUseLoadingIndicator.mockReturnValue({ elapsedTime: '0.0s', currentLoadingPhrase: '', }); // Mock Config mockConfig = makeFakeConfig(); // Mock config's getTargetDir to return consistent workspace directory vi.spyOn(mockConfig, 'getTargetDir').mockReturnValue('/test/workspace'); // Mock GeminiClient to prevent unhandled errors from TaskTool.refreshSubagents const mockGeminiClient: Partial = { initialize: vi.fn().mockResolvedValue(undefined), setTools: vi.fn().mockResolvedValue(undefined), isInitialized: vi.fn().mockReturnValue(false), // Return false to prevent setTools from being called }; vi.spyOn(mockConfig, 'getGeminiClient').mockReturnValue( mockGeminiClient as GeminiClient, ); // Mock SubagentManager to prevent errors during TaskTool initialization const mockSubagentManager: Partial = { listSubagents: vi.fn().mockResolvedValue([]), addChangeListener: vi.fn(), loadSubagent: vi.fn(), createSubagentScope: vi.fn(), }; vi.spyOn(mockConfig, 'getSubagentManager').mockReturnValue( mockSubagentManager as SubagentManager, ); // Mock LoadedSettings mockSettings = { merged: { hideBanner: false, hideFooter: false, hideTips: false, showMemoryUsage: false, theme: 'default', ui: { showStatusInTitle: false, hideWindowTitle: false, }, }, } as unknown as LoadedSettings; // Mock InitializationResult mockInitResult = { themeError: null, authError: null, shouldOpenAuthDialog: false, geminiMdFileCount: 0, } as InitializationResult; }); afterEach(() => { cleanup(); }); describe('Basic Rendering', () => { it('renders without crashing with minimal props', () => { expect(() => { render( , ); }).not.toThrow(); }); it('renders with startup warnings', () => { const startupWarnings = ['Warning 1', 'Warning 2']; expect(() => { render( , ); }).not.toThrow(); }); }); describe('State Initialization', () => { it('initializes with theme error from initialization result', () => { const initResultWithError = { ...mockInitResult, themeError: 'Failed to load theme', }; expect(() => { render( , ); }).not.toThrow(); }); it('handles debug mode state', () => { const debugConfig = makeFakeConfig(); vi.spyOn(debugConfig, 'getDebugMode').mockReturnValue(true); expect(() => { render( , ); }).not.toThrow(); }); }); describe('Context Providers', () => { it('provides AppContext with correct values', () => { const { unmount } = render( , ); // Should render and unmount cleanly expect(() => unmount()).not.toThrow(); }); it('provides UIStateContext with state management', () => { expect(() => { render( , ); }).not.toThrow(); }); it('provides UIActionsContext with action handlers', () => { expect(() => { render( , ); }).not.toThrow(); }); it('provides ConfigContext with config object', () => { expect(() => { render( , ); }).not.toThrow(); }); }); describe('Settings Integration', () => { it('handles settings with all display options disabled', () => { const settingsAllHidden = { merged: { hideBanner: true, hideFooter: true, hideTips: true, showMemoryUsage: false, }, } as unknown as LoadedSettings; expect(() => { render( , ); }).not.toThrow(); }); it('handles settings with memory usage enabled', () => { const settingsWithMemory = { merged: { hideBanner: false, hideFooter: false, hideTips: false, showMemoryUsage: true, }, } as unknown as LoadedSettings; expect(() => { render( , ); }).not.toThrow(); }); }); describe('Version Handling', () => { it.each(['1.0.0', '2.1.3-beta', '3.0.0-nightly'])( 'handles version format: %s', (version) => { expect(() => { render( , ); }).not.toThrow(); }, ); }); describe('Error Handling', () => { it('handles config methods that might throw', () => { const errorConfig = makeFakeConfig(); vi.spyOn(errorConfig, 'getModel').mockImplementation(() => { throw new Error('Config error'); }); // Should still render without crashing - errors should be handled internally expect(() => { render( , ); }).not.toThrow(); }); it('handles undefined settings gracefully', () => { const undefinedSettings = { merged: {}, } as LoadedSettings; expect(() => { render( , ); }).not.toThrow(); }); }); describe('Provider Hierarchy', () => { it('establishes correct provider nesting order', () => { // This tests that all the context providers are properly nested // and that the component tree can be built without circular dependencies const { unmount } = render( , ); expect(() => unmount()).not.toThrow(); }); }); describe('Quota and Fallback Integration', () => { it('passes a null proQuotaRequest to UIStateContext by default', () => { // The default mock from beforeEach already sets proQuotaRequest to null render( , ); // Assert that the context value is as expected expect(capturedUIState.proQuotaRequest).toBeNull(); }); it('passes a valid proQuotaRequest to UIStateContext when provided by the hook', () => { // Arrange: Create a mock request object that a UI dialog would receive const mockRequest = { failedModel: 'gemini-pro', fallbackModel: 'gemini-flash', resolve: vi.fn(), }; mockedUseQuotaAndFallback.mockReturnValue({ proQuotaRequest: mockRequest, handleProQuotaChoice: vi.fn(), }); // Act: Render the container render( , ); // Assert: The mock request is correctly passed through the context expect(capturedUIState.proQuotaRequest).toEqual(mockRequest); }); it('passes the handleProQuotaChoice function to UIActionsContext', () => { // Arrange: Create a mock handler function const mockHandler = vi.fn(); mockedUseQuotaAndFallback.mockReturnValue({ proQuotaRequest: null, handleProQuotaChoice: mockHandler, }); // Act: Render the container render( , ); // Assert: The action in the context is the mock handler we provided expect(capturedUIActions.handleProQuotaChoice).toBe(mockHandler); // You can even verify that the plumbed function is callable capturedUIActions.handleProQuotaChoice('auth'); expect(mockHandler).toHaveBeenCalledWith('auth'); }); }); describe('Terminal Title Update Feature', () => { beforeEach(() => { // Reset mock stdout for each test mockStdout = { write: vi.fn() }; }); it('should not update terminal title when showStatusInTitle is false', () => { // Arrange: Set up mock settings with showStatusInTitle disabled const mockSettingsWithShowStatusFalse = { ...mockSettings, merged: { ...mockSettings.merged, ui: { ...mockSettings.merged.ui, showStatusInTitle: false, hideWindowTitle: false, }, }, } as unknown as LoadedSettings; // Act: Render the container const { unmount } = render( , ); // Assert: Check that no title-related writes occurred const titleWrites = mockStdout.write.mock.calls.filter((call) => call[0].includes('\x1b]2;'), ); expect(titleWrites).toHaveLength(0); unmount(); }); it('should not update terminal title when hideWindowTitle is true', () => { // Arrange: Set up mock settings with hideWindowTitle enabled const mockSettingsWithHideTitleTrue = { ...mockSettings, merged: { ...mockSettings.merged, ui: { ...mockSettings.merged.ui, showStatusInTitle: true, hideWindowTitle: true, }, }, } as unknown as LoadedSettings; // Act: Render the container const { unmount } = render( , ); // Assert: Check that no title-related writes occurred const titleWrites = mockStdout.write.mock.calls.filter((call) => call[0].includes('\x1b]2;'), ); expect(titleWrites).toHaveLength(0); unmount(); }); it('should update terminal title with thought subject when in active state', () => { // Arrange: Set up mock settings with showStatusInTitle enabled const mockSettingsWithTitleEnabled = { ...mockSettings, merged: { ...mockSettings.merged, ui: { ...mockSettings.merged.ui, showStatusInTitle: true, hideWindowTitle: false, }, }, } as unknown as LoadedSettings; // Mock the streaming state and thought const thoughtSubject = 'Processing request'; mockedUseGeminiStream.mockReturnValue({ streamingState: 'responding', submitQuery: vi.fn(), initError: null, pendingHistoryItems: [], thought: { subject: thoughtSubject }, cancelOngoingRequest: vi.fn(), }); // Act: Render the container const { unmount } = render( , ); // Assert: Check that title was updated with thought subject const titleWrites = mockStdout.write.mock.calls.filter((call) => call[0].includes('\x1b]2;'), ); expect(titleWrites).toHaveLength(1); expect(titleWrites[0][0]).toBe( `\x1b]2;${thoughtSubject.padEnd(80, ' ')}\x07`, ); unmount(); }); it('should update terminal title with default text when in Idle state and no thought subject', () => { // Arrange: Set up mock settings with showStatusInTitle enabled const mockSettingsWithTitleEnabled = { ...mockSettings, merged: { ...mockSettings.merged, ui: { ...mockSettings.merged.ui, showStatusInTitle: true, hideWindowTitle: false, }, }, } as unknown as LoadedSettings; // Mock the streaming state as Idle with no thought mockedUseGeminiStream.mockReturnValue({ streamingState: 'idle', submitQuery: vi.fn(), initError: null, pendingHistoryItems: [], thought: null, cancelOngoingRequest: vi.fn(), }); // Act: Render the container const { unmount } = render( , ); // Assert: Check that title was updated with default Idle text const titleWrites = mockStdout.write.mock.calls.filter((call) => call[0].includes('\x1b]2;'), ); expect(titleWrites).toHaveLength(1); expect(titleWrites[0][0]).toBe( `\x1b]2;${'Gemini - workspace'.padEnd(80, ' ')}\x07`, ); unmount(); }); it('should update terminal title when in WaitingForConfirmation state with thought subject', () => { // Arrange: Set up mock settings with showStatusInTitle enabled const mockSettingsWithTitleEnabled = { ...mockSettings, merged: { ...mockSettings.merged, ui: { ...mockSettings.merged.ui, showStatusInTitle: true, hideWindowTitle: false, }, }, } as unknown as LoadedSettings; // Mock the streaming state and thought const thoughtSubject = 'Confirm tool execution'; mockedUseGeminiStream.mockReturnValue({ streamingState: 'waitingForConfirmation', submitQuery: vi.fn(), initError: null, pendingHistoryItems: [], thought: { subject: thoughtSubject }, cancelOngoingRequest: vi.fn(), }); // Act: Render the container const { unmount } = render( , ); // Assert: Check that title was updated with confirmation text const titleWrites = mockStdout.write.mock.calls.filter((call) => call[0].includes('\x1b]2;'), ); expect(titleWrites).toHaveLength(1); expect(titleWrites[0][0]).toBe( `\x1b]2;${thoughtSubject.padEnd(80, ' ')}\x07`, ); unmount(); }); it('should pad title to exactly 80 characters', () => { // Arrange: Set up mock settings with showStatusInTitle enabled const mockSettingsWithTitleEnabled = { ...mockSettings, merged: { ...mockSettings.merged, ui: { ...mockSettings.merged.ui, showStatusInTitle: true, hideWindowTitle: false, }, }, } as unknown as LoadedSettings; // Mock the streaming state and thought with a short subject const shortTitle = 'Short'; mockedUseGeminiStream.mockReturnValue({ streamingState: 'responding', submitQuery: vi.fn(), initError: null, pendingHistoryItems: [], thought: { subject: shortTitle }, cancelOngoingRequest: vi.fn(), }); // Act: Render the container const { unmount } = render( , ); // Assert: Check that title is padded to exactly 80 characters const titleWrites = mockStdout.write.mock.calls.filter((call) => call[0].includes('\x1b]2;'), ); expect(titleWrites).toHaveLength(1); const calledWith = titleWrites[0][0]; const expectedTitle = shortTitle.padEnd(80, ' '); expect(calledWith).toContain(shortTitle); expect(calledWith).toContain('\x1b]2;'); expect(calledWith).toContain('\x07'); expect(calledWith).toBe('\x1b]2;' + expectedTitle + '\x07'); unmount(); }); it('should use correct ANSI escape code format', () => { // Arrange: Set up mock settings with showStatusInTitle enabled const mockSettingsWithTitleEnabled = { ...mockSettings, merged: { ...mockSettings.merged, ui: { ...mockSettings.merged.ui, showStatusInTitle: true, hideWindowTitle: false, }, }, } as unknown as LoadedSettings; // Mock the streaming state and thought const title = 'Test Title'; mockedUseGeminiStream.mockReturnValue({ streamingState: 'responding', submitQuery: vi.fn(), initError: null, pendingHistoryItems: [], thought: { subject: title }, cancelOngoingRequest: vi.fn(), }); // Act: Render the container const { unmount } = render( , ); // Assert: Check that the correct ANSI escape sequence is used const titleWrites = mockStdout.write.mock.calls.filter((call) => call[0].includes('\x1b]2;'), ); expect(titleWrites).toHaveLength(1); const expectedEscapeSequence = `\x1b]2;${title.padEnd(80, ' ')}\x07`; expect(titleWrites[0][0]).toBe(expectedEscapeSequence); unmount(); }); it('should use CLI_TITLE environment variable when set', () => { // Arrange: Set up mock settings with showStatusInTitle enabled const mockSettingsWithTitleEnabled = { ...mockSettings, merged: { ...mockSettings.merged, ui: { ...mockSettings.merged.ui, showStatusInTitle: true, hideWindowTitle: false, }, }, } as unknown as LoadedSettings; // Mock CLI_TITLE environment variable vi.stubEnv('CLI_TITLE', 'Custom Gemini Title'); // Mock the streaming state as Idle with no thought mockedUseGeminiStream.mockReturnValue({ streamingState: 'idle', submitQuery: vi.fn(), initError: null, pendingHistoryItems: [], thought: null, cancelOngoingRequest: vi.fn(), }); // Act: Render the container const { unmount } = render( , ); // Assert: Check that title was updated with CLI_TITLE value const titleWrites = mockStdout.write.mock.calls.filter((call) => call[0].includes('\x1b]2;'), ); expect(titleWrites).toHaveLength(1); expect(titleWrites[0][0]).toBe( `\x1b]2;${'Custom Gemini Title'.padEnd(80, ' ')}\x07`, ); unmount(); }); }); describe('Terminal Height Calculation', () => { const mockedMeasureElement = measureElement as Mock; const mockedUseTerminalSize = useTerminalSize as Mock; it('should prevent terminal height from being less than 1', () => { const resizePtySpy = vi.spyOn(ShellExecutionService, 'resizePty'); // Arrange: Simulate a small terminal and a large footer mockedUseTerminalSize.mockReturnValue({ columns: 80, rows: 5 }); mockedMeasureElement.mockReturnValue({ width: 80, height: 10 }); // Footer is taller than the screen mockedUseGeminiStream.mockReturnValue({ streamingState: 'idle', submitQuery: vi.fn(), initError: null, pendingHistoryItems: [], thought: null, cancelOngoingRequest: vi.fn(), activePtyId: 'some-id', }); render( , ); // Assert: The shell should be resized to a minimum height of 1, not a negative number. // The old code would have tried to set a negative height. expect(resizePtySpy).toHaveBeenCalled(); const lastCall = resizePtySpy.mock.calls[resizePtySpy.mock.calls.length - 1]; // Check the height argument specifically expect(lastCall[2]).toBe(1); }); }); describe('Keyboard Input Handling', () => { it('should block quit command during authentication', () => { mockedUseAuthCommand.mockReturnValue({ authState: 'unauthenticated', setAuthState: vi.fn(), authError: null, onAuthError: vi.fn(), isAuthDialogOpen: false, isAuthenticating: true, handleAuthSelect: vi.fn(), openAuthDialog: vi.fn(), cancelAuthentication: vi.fn(), }); const mockHandleSlashCommand = vi.fn(); mockedUseSlashCommandProcessor.mockReturnValue({ handleSlashCommand: mockHandleSlashCommand, slashCommands: [], pendingHistoryItems: [], commandContext: {}, shellConfirmationRequest: null, confirmationRequest: null, }); render( , ); expect(mockHandleSlashCommand).not.toHaveBeenCalledWith('/quit'); }); it('should prevent exit command when text buffer has content', () => { mockedUseTextBuffer.mockReturnValue({ text: 'some user input', setText: vi.fn(), }); const mockHandleSlashCommand = vi.fn(); mockedUseSlashCommandProcessor.mockReturnValue({ handleSlashCommand: mockHandleSlashCommand, slashCommands: [], pendingHistoryItems: [], commandContext: {}, shellConfirmationRequest: null, confirmationRequest: null, }); render( , ); expect(mockHandleSlashCommand).not.toHaveBeenCalledWith('/quit'); }); it('should require double Ctrl+C to exit when dialogs are open', () => { vi.useFakeTimers(); mockedUseThemeCommand.mockReturnValue({ isThemeDialogOpen: true, openThemeDialog: vi.fn(), handleThemeSelect: vi.fn(), handleThemeHighlight: vi.fn(), }); const mockHandleSlashCommand = vi.fn(); mockedUseSlashCommandProcessor.mockReturnValue({ handleSlashCommand: mockHandleSlashCommand, slashCommands: [], pendingHistoryItems: [], commandContext: {}, shellConfirmationRequest: null, confirmationRequest: null, }); render( , ); expect(mockHandleSlashCommand).not.toHaveBeenCalledWith('/quit'); expect(mockHandleSlashCommand).not.toHaveBeenCalledWith('/quit'); vi.useRealTimers(); }); it('should cancel ongoing request on first Ctrl+C', () => { const mockCancelOngoingRequest = vi.fn(); mockedUseGeminiStream.mockReturnValue({ streamingState: 'responding', submitQuery: vi.fn(), initError: null, pendingHistoryItems: [], thought: null, cancelOngoingRequest: mockCancelOngoingRequest, }); const mockHandleSlashCommand = vi.fn(); mockedUseSlashCommandProcessor.mockReturnValue({ handleSlashCommand: mockHandleSlashCommand, slashCommands: [], pendingHistoryItems: [], commandContext: {}, shellConfirmationRequest: null, confirmationRequest: null, }); render( , ); expect(mockHandleSlashCommand).not.toHaveBeenCalledWith('/quit'); }); it('should reset Ctrl+C state after timeout', () => { vi.useFakeTimers(); const mockHandleSlashCommand = vi.fn(); mockedUseSlashCommandProcessor.mockReturnValue({ handleSlashCommand: mockHandleSlashCommand, slashCommands: [], pendingHistoryItems: [], commandContext: {}, shellConfirmationRequest: null, confirmationRequest: null, }); render( , ); expect(mockHandleSlashCommand).not.toHaveBeenCalledWith('/quit'); vi.advanceTimersByTime(1001); expect(mockHandleSlashCommand).not.toHaveBeenCalledWith('/quit'); vi.useRealTimers(); }); }); describe('Model Dialog Integration', () => { it('should provide isModelDialogOpen in the UIStateContext', () => { mockedUseModelCommand.mockReturnValue({ isModelDialogOpen: true, openModelDialog: vi.fn(), closeModelDialog: vi.fn(), }); render( , ); expect(capturedUIState.isModelDialogOpen).toBe(true); }); it('should provide model dialog actions in the UIActionsContext', () => { const mockCloseModelDialog = vi.fn(); mockedUseModelCommand.mockReturnValue({ isModelDialogOpen: false, openModelDialog: vi.fn(), closeModelDialog: mockCloseModelDialog, }); render( , ); // Verify that the actions are correctly passed through context capturedUIActions.closeModelDialog(); expect(mockCloseModelDialog).toHaveBeenCalled(); }); }); });