Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/cli/src/gemini.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ import { setupTerminalAndTheme } from './utils/terminalTheme.js';
import { runDeferredCommand } from './deferred.js';
import { cleanupBackgroundLogs } from './utils/logCleanup.js';
import { SlashCommandConflictHandler } from './services/SlashCommandConflictHandler.js';
import { initializeConsoleStore } from './ui/hooks/useConsoleMessages.js';

export function validateDnsResolutionOrder(
order: string | undefined,
Expand Down Expand Up @@ -293,6 +294,7 @@ export async function main() {
process.exit(ExitCodes.FATAL_INPUT_ERROR);
}

initializeConsoleStore();
const isDebugMode = cliConfig.isDebugMode(argv);
const consolePatcher = new ConsolePatcher({
stderr: true,
Expand Down
25 changes: 5 additions & 20 deletions packages/cli/src/ui/components/DetailedMessagesDisplay.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,7 @@ vi.mock('./shared/ScrollableList.js', () => ({

describe('DetailedMessagesDisplay', () => {
beforeEach(() => {
vi.mocked(useConsoleMessages).mockReturnValue({
consoleMessages: [],
clearConsoleMessages: vi.fn(),
});
vi.mocked(useConsoleMessages).mockReturnValue([]);
});
it('renders nothing when messages are empty', async () => {
const { lastFrame, unmount } = await renderWithProviders(
Expand All @@ -58,10 +55,7 @@ describe('DetailedMessagesDisplay', () => {
{ type: 'error', content: 'Error message', count: 1 },
{ type: 'debug', content: 'Debug message', count: 1 },
];
vi.mocked(useConsoleMessages).mockReturnValue({
consoleMessages: messages,
clearConsoleMessages: vi.fn(),
});
vi.mocked(useConsoleMessages).mockReturnValue(messages);

const { lastFrame, unmount } = await renderWithProviders(
<DetailedMessagesDisplay maxHeight={20} width={80} hasFocus={true} />,
Expand All @@ -79,10 +73,7 @@ describe('DetailedMessagesDisplay', () => {
const messages: ConsoleMessageItem[] = [
{ type: 'error', content: 'Error message', count: 1 },
];
vi.mocked(useConsoleMessages).mockReturnValue({
consoleMessages: messages,
clearConsoleMessages: vi.fn(),
});
vi.mocked(useConsoleMessages).mockReturnValue(messages);

const { lastFrame, unmount } = await renderWithProviders(
<DetailedMessagesDisplay maxHeight={20} width={80} hasFocus={true} />,
Expand All @@ -98,10 +89,7 @@ describe('DetailedMessagesDisplay', () => {
const messages: ConsoleMessageItem[] = [
{ type: 'error', content: 'Error message', count: 1 },
];
vi.mocked(useConsoleMessages).mockReturnValue({
consoleMessages: messages,
clearConsoleMessages: vi.fn(),
});
vi.mocked(useConsoleMessages).mockReturnValue(messages);

const { lastFrame, unmount } = await renderWithProviders(
<DetailedMessagesDisplay maxHeight={20} width={80} hasFocus={true} />,
Expand All @@ -117,10 +105,7 @@ describe('DetailedMessagesDisplay', () => {
const messages: ConsoleMessageItem[] = [
{ type: 'log', content: 'Repeated message', count: 5 },
];
vi.mocked(useConsoleMessages).mockReturnValue({
consoleMessages: messages,
clearConsoleMessages: vi.fn(),
});
vi.mocked(useConsoleMessages).mockReturnValue(messages);

const { lastFrame, unmount } = await renderWithProviders(
<DetailedMessagesDisplay maxHeight={10} width={80} hasFocus={false} />,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const DetailedMessagesDisplay: React.FC<
> = ({ maxHeight, width, hasFocus }) => {
const scrollableListRef = useRef<ScrollableListRef<ConsoleMessageItem>>(null);

const { consoleMessages } = useConsoleMessages();
const consoleMessages = useConsoleMessages();
const config = useConfig();

const messages = useMemo(() => {
Expand Down
184 changes: 112 additions & 72 deletions packages/cli/src/ui/hooks/useConsoleMessages.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,76 +7,93 @@
import { act, useCallback } from 'react';
import { vi } from 'vitest';
import { render } from '../../test-utils/render.js';
import { useConsoleMessages } from './useConsoleMessages.js';
import { CoreEvent, type ConsoleLogPayload } from '@google/gemini-cli-core';

// Mock coreEvents
let consoleLogHandler: ((payload: ConsoleLogPayload) => void) | undefined;
import {
useConsoleMessages,
useErrorCount,
initializeConsoleStore,
} from './useConsoleMessages.js';
import { coreEvents } from '@google/gemini-cli-core';

vi.mock('@google/gemini-cli-core', async (importOriginal) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const actual = (await importOriginal()) as any;
const actual = await importOriginal();
const handlers = new Map<string, (payload: unknown) => void>();

return {
...actual,
...(actual as Record<string, unknown>),
coreEvents: {
on: vi.fn((event, handler) => {
if (event === CoreEvent.ConsoleLog) {
consoleLogHandler = handler;
}
...((actual as Record<string, unknown>)['coreEvents'] as Record<
string,
unknown
>),
on: vi.fn((event: string, handler: (payload: unknown) => void) => {
handlers.set(event, handler);
}),
off: vi.fn((event) => {
if (event === CoreEvent.ConsoleLog) {
consoleLogHandler = undefined;
}
off: vi.fn((event: string) => {
handlers.delete(event);
}),
emitConsoleLog: vi.fn(),
// Helper for testing to trigger the handlers
_trigger: (event: string, payload: unknown) => {
handlers.get(event)?.(payload);
},
},
};
});

describe('useConsoleMessages', () => {
let unmounts: Array<() => void> = [];

beforeEach(() => {
vi.useFakeTimers();
consoleLogHandler = undefined;
initializeConsoleStore();
});

afterEach(() => {
for (const unmount of unmounts) {
try {
unmount();
} catch (_e) {
// Ignore unmount errors
}
}
unmounts = [];
vi.runOnlyPendingTimers();
vi.useRealTimers();
vi.restoreAllMocks();
});

const useTestableConsoleMessages = () => {
const { ...rest } = useConsoleMessages();
const consoleMessages = useConsoleMessages();
const log = useCallback((content: string) => {
if (consoleLogHandler) {
consoleLogHandler({ type: 'log', content });
}
// @ts-expect-error - internal testing helper
coreEvents._trigger('console-log', { type: 'log', content });
}, []);
const error = useCallback((content: string) => {
if (consoleLogHandler) {
consoleLogHandler({ type: 'error', content });
}
// @ts-expect-error - internal testing helper
coreEvents._trigger('console-log', { type: 'error', content });
}, []);
const clearConsoleMessages = useCallback(() => {
initializeConsoleStore();
}, []);
return {
...rest,
consoleMessages,
log,
error,
clearConsoleMessages: rest.clearConsoleMessages,
clearConsoleMessages,
};
};

const renderConsoleMessagesHook = async () => {
let hookResult: ReturnType<typeof useTestableConsoleMessages>;
let hookResult: ReturnType<typeof useTestableConsoleMessages> | undefined;
function TestComponent() {
hookResult = useTestableConsoleMessages();
return null;
}
const { unmount } = await render(<TestComponent />);
unmounts.push(unmount);
return {
result: {
get current() {
return hookResult;
return hookResult!;
},
},
unmount,
Expand All @@ -93,10 +110,7 @@ describe('useConsoleMessages', () => {

act(() => {
result.current.log('Test message');
});

await act(async () => {
await vi.advanceTimersByTimeAsync(60);
vi.runAllTimers();
});

expect(result.current.consoleMessages).toEqual([
Expand All @@ -111,10 +125,7 @@ describe('useConsoleMessages', () => {
result.current.log('Test message');
result.current.log('Test message');
result.current.log('Test message');
});

await act(async () => {
await vi.advanceTimersByTimeAsync(60);
vi.runAllTimers();
});

expect(result.current.consoleMessages).toEqual([
Expand All @@ -128,64 +139,93 @@ describe('useConsoleMessages', () => {
act(() => {
result.current.log('First message');
result.current.error('Second message');
});

await act(async () => {
await vi.advanceTimersByTimeAsync(60);
vi.runAllTimers();
});

expect(result.current.consoleMessages).toEqual([
{ type: 'log', content: 'First message', count: 1 },
{ type: 'error', content: 'Second message', count: 1 },
]);
});
});

it('should clear all messages when clearConsoleMessages is called', async () => {
const { result } = await renderConsoleMessagesHook();

act(() => {
result.current.log('A message');
});
describe('useErrorCount', () => {
let unmounts: Array<() => void> = [];

await act(async () => {
await vi.advanceTimersByTimeAsync(60);
});
beforeEach(() => {
vi.useFakeTimers();
initializeConsoleStore();
});

expect(result.current.consoleMessages).toHaveLength(1);
afterEach(() => {
for (const unmount of unmounts) {
try {
unmount();
} catch (_e) {
// Ignore unmount errors
}
}
unmounts = [];
vi.runOnlyPendingTimers();
vi.useRealTimers();
vi.restoreAllMocks();
});

act(() => {
result.current.clearConsoleMessages();
});
const renderErrorCountHook = async () => {
let hookResult: ReturnType<typeof useErrorCount>;
function TestComponent() {
hookResult = useErrorCount();
return null;
}
const { unmount } = await render(<TestComponent />);
unmounts.push(unmount);
return {
result: {
get current() {
return hookResult;
},
},
unmount,
};
};

expect(result.current.consoleMessages).toHaveLength(0);
it('should initialize with an error count of 0', async () => {
const { result } = await renderErrorCountHook();
expect(result.current.errorCount).toBe(0);
});

it('should clear the pending timeout when clearConsoleMessages is called', async () => {
const { result } = await renderConsoleMessagesHook();
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout');

it('should increment error count when an error is logged', async () => {
const { result } = await renderErrorCountHook();
act(() => {
result.current.log('A message');
// @ts-expect-error - internal testing helper
coreEvents._trigger('console-log', { type: 'error', content: 'error' });
vi.runAllTimers();
});
expect(result.current.errorCount).toBe(1);
});

it('should not increment error count for non-error logs', async () => {
const { result } = await renderErrorCountHook();
act(() => {
result.current.clearConsoleMessages();
// @ts-expect-error - internal testing helper
coreEvents._trigger('console-log', { type: 'log', content: 'log' });
vi.runAllTimers();
});

expect(clearTimeoutSpy).toHaveBeenCalled();
// clearTimeoutSpy.mockRestore() is handled by afterEach restoreAllMocks
expect(result.current.errorCount).toBe(0);
});

it('should clean up the timeout on unmount', async () => {
const { result, unmount } = await renderConsoleMessagesHook();
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout');

it('should clear the error count', async () => {
const { result } = await renderErrorCountHook();
act(() => {
result.current.log('A message');
// @ts-expect-error - internal testing helper
coreEvents._trigger('console-log', { type: 'error', content: 'error' });
vi.runAllTimers();
});
expect(result.current.errorCount).toBe(1);

unmount();

expect(clearTimeoutSpy).toHaveBeenCalled();
act(() => {
result.current.clearErrorCount();
});
expect(result.current.errorCount).toBe(0);
});
});
Loading
Loading