Skip to content

Commit 44cdb3e

Browse files
authored
fix(cli): resolve missing F12 logs via global console store (#24235)
1 parent 9cf4104 commit 44cdb3e

5 files changed

Lines changed: 269 additions & 285 deletions

File tree

packages/cli/src/gemini.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ import { setupTerminalAndTheme } from './utils/terminalTheme.js';
9393
import { runDeferredCommand } from './deferred.js';
9494
import { cleanupBackgroundLogs } from './utils/logCleanup.js';
9595
import { SlashCommandConflictHandler } from './services/SlashCommandConflictHandler.js';
96+
import { initializeConsoleStore } from './ui/hooks/useConsoleMessages.js';
9697

9798
export function validateDnsResolutionOrder(
9899
order: string | undefined,
@@ -294,6 +295,7 @@ export async function main() {
294295
process.exit(ExitCodes.FATAL_INPUT_ERROR);
295296
}
296297

298+
initializeConsoleStore();
297299
const isDebugMode = cliConfig.isDebugMode(argv);
298300
const consolePatcher = new ConsolePatcher({
299301
stderr: true,

packages/cli/src/ui/components/DetailedMessagesDisplay.test.tsx

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,7 @@ vi.mock('./shared/ScrollableList.js', () => ({
3535

3636
describe('DetailedMessagesDisplay', () => {
3737
beforeEach(() => {
38-
vi.mocked(useConsoleMessages).mockReturnValue({
39-
consoleMessages: [],
40-
clearConsoleMessages: vi.fn(),
41-
});
38+
vi.mocked(useConsoleMessages).mockReturnValue([]);
4239
});
4340
it('renders nothing when messages are empty', async () => {
4441
const { lastFrame, unmount } = await renderWithProviders(
@@ -58,10 +55,7 @@ describe('DetailedMessagesDisplay', () => {
5855
{ type: 'error', content: 'Error message', count: 1 },
5956
{ type: 'debug', content: 'Debug message', count: 1 },
6057
];
61-
vi.mocked(useConsoleMessages).mockReturnValue({
62-
consoleMessages: messages,
63-
clearConsoleMessages: vi.fn(),
64-
});
58+
vi.mocked(useConsoleMessages).mockReturnValue(messages);
6559

6660
const { lastFrame, unmount } = await renderWithProviders(
6761
<DetailedMessagesDisplay maxHeight={20} width={80} hasFocus={true} />,
@@ -79,10 +73,7 @@ describe('DetailedMessagesDisplay', () => {
7973
const messages: ConsoleMessageItem[] = [
8074
{ type: 'error', content: 'Error message', count: 1 },
8175
];
82-
vi.mocked(useConsoleMessages).mockReturnValue({
83-
consoleMessages: messages,
84-
clearConsoleMessages: vi.fn(),
85-
});
76+
vi.mocked(useConsoleMessages).mockReturnValue(messages);
8677

8778
const { lastFrame, unmount } = await renderWithProviders(
8879
<DetailedMessagesDisplay maxHeight={20} width={80} hasFocus={true} />,
@@ -98,10 +89,7 @@ describe('DetailedMessagesDisplay', () => {
9889
const messages: ConsoleMessageItem[] = [
9990
{ type: 'error', content: 'Error message', count: 1 },
10091
];
101-
vi.mocked(useConsoleMessages).mockReturnValue({
102-
consoleMessages: messages,
103-
clearConsoleMessages: vi.fn(),
104-
});
92+
vi.mocked(useConsoleMessages).mockReturnValue(messages);
10593

10694
const { lastFrame, unmount } = await renderWithProviders(
10795
<DetailedMessagesDisplay maxHeight={20} width={80} hasFocus={true} />,
@@ -117,10 +105,7 @@ describe('DetailedMessagesDisplay', () => {
117105
const messages: ConsoleMessageItem[] = [
118106
{ type: 'log', content: 'Repeated message', count: 5 },
119107
];
120-
vi.mocked(useConsoleMessages).mockReturnValue({
121-
consoleMessages: messages,
122-
clearConsoleMessages: vi.fn(),
123-
});
108+
vi.mocked(useConsoleMessages).mockReturnValue(messages);
124109

125110
const { lastFrame, unmount } = await renderWithProviders(
126111
<DetailedMessagesDisplay maxHeight={10} width={80} hasFocus={false} />,

packages/cli/src/ui/components/DetailedMessagesDisplay.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export const DetailedMessagesDisplay: React.FC<
2929
> = ({ maxHeight, width, hasFocus }) => {
3030
const scrollableListRef = useRef<ScrollableListRef<ConsoleMessageItem>>(null);
3131

32-
const { consoleMessages } = useConsoleMessages();
32+
const consoleMessages = useConsoleMessages();
3333
const config = useConfig();
3434

3535
const messages = useMemo(() => {

packages/cli/src/ui/hooks/useConsoleMessages.test.tsx

Lines changed: 112 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -7,76 +7,93 @@
77
import { act, useCallback } from 'react';
88
import { vi } from 'vitest';
99
import { render } from '../../test-utils/render.js';
10-
import { useConsoleMessages } from './useConsoleMessages.js';
11-
import { CoreEvent, type ConsoleLogPayload } from '@google/gemini-cli-core';
12-
13-
// Mock coreEvents
14-
let consoleLogHandler: ((payload: ConsoleLogPayload) => void) | undefined;
10+
import {
11+
useConsoleMessages,
12+
useErrorCount,
13+
initializeConsoleStore,
14+
} from './useConsoleMessages.js';
15+
import { coreEvents } from '@google/gemini-cli-core';
1516

1617
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
17-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
18-
const actual = (await importOriginal()) as any;
18+
const actual = await importOriginal();
19+
const handlers = new Map<string, (payload: unknown) => void>();
20+
1921
return {
20-
...actual,
22+
...(actual as Record<string, unknown>),
2123
coreEvents: {
22-
on: vi.fn((event, handler) => {
23-
if (event === CoreEvent.ConsoleLog) {
24-
consoleLogHandler = handler;
25-
}
24+
...((actual as Record<string, unknown>)['coreEvents'] as Record<
25+
string,
26+
unknown
27+
>),
28+
on: vi.fn((event: string, handler: (payload: unknown) => void) => {
29+
handlers.set(event, handler);
2630
}),
27-
off: vi.fn((event) => {
28-
if (event === CoreEvent.ConsoleLog) {
29-
consoleLogHandler = undefined;
30-
}
31+
off: vi.fn((event: string) => {
32+
handlers.delete(event);
3133
}),
32-
emitConsoleLog: vi.fn(),
34+
// Helper for testing to trigger the handlers
35+
_trigger: (event: string, payload: unknown) => {
36+
handlers.get(event)?.(payload);
37+
},
3338
},
3439
};
3540
});
3641

3742
describe('useConsoleMessages', () => {
43+
let unmounts: Array<() => void> = [];
44+
3845
beforeEach(() => {
3946
vi.useFakeTimers();
40-
consoleLogHandler = undefined;
47+
initializeConsoleStore();
4148
});
4249

4350
afterEach(() => {
51+
for (const unmount of unmounts) {
52+
try {
53+
unmount();
54+
} catch (_e) {
55+
// Ignore unmount errors
56+
}
57+
}
58+
unmounts = [];
4459
vi.runOnlyPendingTimers();
4560
vi.useRealTimers();
4661
vi.restoreAllMocks();
4762
});
4863

4964
const useTestableConsoleMessages = () => {
50-
const { ...rest } = useConsoleMessages();
65+
const consoleMessages = useConsoleMessages();
5166
const log = useCallback((content: string) => {
52-
if (consoleLogHandler) {
53-
consoleLogHandler({ type: 'log', content });
54-
}
67+
// @ts-expect-error - internal testing helper
68+
coreEvents._trigger('console-log', { type: 'log', content });
5569
}, []);
5670
const error = useCallback((content: string) => {
57-
if (consoleLogHandler) {
58-
consoleLogHandler({ type: 'error', content });
59-
}
71+
// @ts-expect-error - internal testing helper
72+
coreEvents._trigger('console-log', { type: 'error', content });
73+
}, []);
74+
const clearConsoleMessages = useCallback(() => {
75+
initializeConsoleStore();
6076
}, []);
6177
return {
62-
...rest,
78+
consoleMessages,
6379
log,
6480
error,
65-
clearConsoleMessages: rest.clearConsoleMessages,
81+
clearConsoleMessages,
6682
};
6783
};
6884

6985
const renderConsoleMessagesHook = async () => {
70-
let hookResult: ReturnType<typeof useTestableConsoleMessages>;
86+
let hookResult: ReturnType<typeof useTestableConsoleMessages> | undefined;
7187
function TestComponent() {
7288
hookResult = useTestableConsoleMessages();
7389
return null;
7490
}
7591
const { unmount } = await render(<TestComponent />);
92+
unmounts.push(unmount);
7693
return {
7794
result: {
7895
get current() {
79-
return hookResult;
96+
return hookResult!;
8097
},
8198
},
8299
unmount,
@@ -93,10 +110,7 @@ describe('useConsoleMessages', () => {
93110

94111
act(() => {
95112
result.current.log('Test message');
96-
});
97-
98-
await act(async () => {
99-
await vi.advanceTimersByTimeAsync(60);
113+
vi.runAllTimers();
100114
});
101115

102116
expect(result.current.consoleMessages).toEqual([
@@ -111,10 +125,7 @@ describe('useConsoleMessages', () => {
111125
result.current.log('Test message');
112126
result.current.log('Test message');
113127
result.current.log('Test message');
114-
});
115-
116-
await act(async () => {
117-
await vi.advanceTimersByTimeAsync(60);
128+
vi.runAllTimers();
118129
});
119130

120131
expect(result.current.consoleMessages).toEqual([
@@ -128,64 +139,93 @@ describe('useConsoleMessages', () => {
128139
act(() => {
129140
result.current.log('First message');
130141
result.current.error('Second message');
131-
});
132-
133-
await act(async () => {
134-
await vi.advanceTimersByTimeAsync(60);
142+
vi.runAllTimers();
135143
});
136144

137145
expect(result.current.consoleMessages).toEqual([
138146
{ type: 'log', content: 'First message', count: 1 },
139147
{ type: 'error', content: 'Second message', count: 1 },
140148
]);
141149
});
150+
});
142151

143-
it('should clear all messages when clearConsoleMessages is called', async () => {
144-
const { result } = await renderConsoleMessagesHook();
145-
146-
act(() => {
147-
result.current.log('A message');
148-
});
152+
describe('useErrorCount', () => {
153+
let unmounts: Array<() => void> = [];
149154

150-
await act(async () => {
151-
await vi.advanceTimersByTimeAsync(60);
152-
});
155+
beforeEach(() => {
156+
vi.useFakeTimers();
157+
initializeConsoleStore();
158+
});
153159

154-
expect(result.current.consoleMessages).toHaveLength(1);
160+
afterEach(() => {
161+
for (const unmount of unmounts) {
162+
try {
163+
unmount();
164+
} catch (_e) {
165+
// Ignore unmount errors
166+
}
167+
}
168+
unmounts = [];
169+
vi.runOnlyPendingTimers();
170+
vi.useRealTimers();
171+
vi.restoreAllMocks();
172+
});
155173

156-
act(() => {
157-
result.current.clearConsoleMessages();
158-
});
174+
const renderErrorCountHook = async () => {
175+
let hookResult: ReturnType<typeof useErrorCount>;
176+
function TestComponent() {
177+
hookResult = useErrorCount();
178+
return null;
179+
}
180+
const { unmount } = await render(<TestComponent />);
181+
unmounts.push(unmount);
182+
return {
183+
result: {
184+
get current() {
185+
return hookResult;
186+
},
187+
},
188+
unmount,
189+
};
190+
};
159191

160-
expect(result.current.consoleMessages).toHaveLength(0);
192+
it('should initialize with an error count of 0', async () => {
193+
const { result } = await renderErrorCountHook();
194+
expect(result.current.errorCount).toBe(0);
161195
});
162196

163-
it('should clear the pending timeout when clearConsoleMessages is called', async () => {
164-
const { result } = await renderConsoleMessagesHook();
165-
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout');
166-
197+
it('should increment error count when an error is logged', async () => {
198+
const { result } = await renderErrorCountHook();
167199
act(() => {
168-
result.current.log('A message');
200+
// @ts-expect-error - internal testing helper
201+
coreEvents._trigger('console-log', { type: 'error', content: 'error' });
202+
vi.runAllTimers();
169203
});
204+
expect(result.current.errorCount).toBe(1);
205+
});
170206

207+
it('should not increment error count for non-error logs', async () => {
208+
const { result } = await renderErrorCountHook();
171209
act(() => {
172-
result.current.clearConsoleMessages();
210+
// @ts-expect-error - internal testing helper
211+
coreEvents._trigger('console-log', { type: 'log', content: 'log' });
212+
vi.runAllTimers();
173213
});
174-
175-
expect(clearTimeoutSpy).toHaveBeenCalled();
176-
// clearTimeoutSpy.mockRestore() is handled by afterEach restoreAllMocks
214+
expect(result.current.errorCount).toBe(0);
177215
});
178216

179-
it('should clean up the timeout on unmount', async () => {
180-
const { result, unmount } = await renderConsoleMessagesHook();
181-
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout');
182-
217+
it('should clear the error count', async () => {
218+
const { result } = await renderErrorCountHook();
183219
act(() => {
184-
result.current.log('A message');
220+
// @ts-expect-error - internal testing helper
221+
coreEvents._trigger('console-log', { type: 'error', content: 'error' });
222+
vi.runAllTimers();
185223
});
224+
expect(result.current.errorCount).toBe(1);
186225

187-
unmount();
188-
189-
expect(clearTimeoutSpy).toHaveBeenCalled();
226+
act(() => {
227+
result.current.clearErrorCount();
228+
});
229+
expect(result.current.errorCount).toBe(0);
190230
});
191231
});

0 commit comments

Comments
 (0)