Skip to content

Commit 1c03872

Browse files
cynthialong0-0ProthamD
authored andcommitted
fix(cli): skip console log/info in headless mode (google-gemini#22739)
1 parent bf91c3e commit 1c03872

6 files changed

Lines changed: 260 additions & 9 deletions

File tree

integration-tests/extensions-install.test.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,20 @@ describe('extension install', () => {
3434
writeFileSync(testServerPath, extension);
3535
try {
3636
const result = await rig.runCommand(
37-
['extensions', 'install', `${rig.testDir!}`],
37+
['--debug', 'extensions', 'install', `${rig.testDir!}`],
3838
{ stdin: 'y\n' },
3939
);
4040
expect(result).toContain('test-extension-install');
4141

42-
const listResult = await rig.runCommand(['extensions', 'list']);
42+
const listResult = await rig.runCommand([
43+
'--debug',
44+
'extensions',
45+
'list',
46+
]);
4347
expect(listResult).toContain('test-extension-install');
4448
writeFileSync(testServerPath, extensionUpdate);
4549
const updateResult = await rig.runCommand(
46-
['extensions', 'update', `test-extension-install`],
50+
['--debug', 'extensions', 'update', `test-extension-install`],
4751
{ stdin: 'y\n' },
4852
);
4953
expect(updateResult).toContain('0.0.2');

integration-tests/extensions-reload.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ describe('extension reloading', () => {
6666
}
6767

6868
const result = await rig.runCommand(
69-
['extensions', 'install', `${rig.testDir!}`],
69+
['--debug', 'extensions', 'install', `${rig.testDir!}`],
7070
{ stdin: 'y\n' },
7171
);
7272
expect(result).toContain('test-extension');

packages/cli/src/gemini.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
ValidationRequiredError,
3333
type AdminControlsSettings,
3434
debugLogger,
35+
isHeadlessMode,
3536
} from '@google/gemini-cli-core';
3637

3738
import { loadCliConfig, parseArguments } from './config/config.js';
@@ -296,6 +297,7 @@ export async function main() {
296297
const isDebugMode = cliConfig.isDebugMode(argv);
297298
const consolePatcher = new ConsolePatcher({
298299
stderr: true,
300+
interactive: isHeadlessMode() ? false : true,
299301
debugMode: isDebugMode,
300302
onNewMessage: (msg) => {
301303
coreEvents.emitConsoleLog(msg.type, msg.content);

packages/cli/src/nonInteractiveCli.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export async function runNonInteractive({
6565
return promptIdContext.run(prompt_id, async () => {
6666
const consolePatcher = new ConsolePatcher({
6767
stderr: true,
68+
interactive: false,
6869
debugMode: config.getDebugMode(),
6970
onNewMessage: (msg) => {
7071
coreEvents.emitConsoleLog(msg.type, msg.content);
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
/* eslint-disable no-console */
8+
9+
import { describe, it, expect, vi, afterEach } from 'vitest';
10+
import { ConsolePatcher } from './ConsolePatcher.js';
11+
12+
describe('ConsolePatcher', () => {
13+
let patcher: ConsolePatcher;
14+
const onNewMessage = vi.fn();
15+
16+
afterEach(() => {
17+
if (patcher) {
18+
patcher.cleanup();
19+
}
20+
vi.restoreAllMocks();
21+
vi.clearAllMocks();
22+
});
23+
24+
it('should patch and restore console methods', () => {
25+
const beforeLog = console.log;
26+
const beforeWarn = console.warn;
27+
const beforeError = console.error;
28+
const beforeDebug = console.debug;
29+
const beforeInfo = console.info;
30+
31+
patcher = new ConsolePatcher({ onNewMessage, debugMode: false });
32+
patcher.patch();
33+
34+
expect(console.log).not.toBe(beforeLog);
35+
expect(console.warn).not.toBe(beforeWarn);
36+
expect(console.error).not.toBe(beforeError);
37+
expect(console.debug).not.toBe(beforeDebug);
38+
expect(console.info).not.toBe(beforeInfo);
39+
40+
patcher.cleanup();
41+
42+
expect(console.log).toBe(beforeLog);
43+
expect(console.warn).toBe(beforeWarn);
44+
expect(console.error).toBe(beforeError);
45+
expect(console.debug).toBe(beforeDebug);
46+
expect(console.info).toBe(beforeInfo);
47+
});
48+
49+
describe('Interactive mode', () => {
50+
it('should ignore log and info when it is not interactive and debugMode is false', () => {
51+
patcher = new ConsolePatcher({
52+
onNewMessage,
53+
debugMode: false,
54+
interactive: false,
55+
});
56+
patcher.patch();
57+
58+
console.log('test log');
59+
console.info('test info');
60+
expect(onNewMessage).not.toHaveBeenCalled();
61+
});
62+
63+
it('should not ignore log and info when it is not interactive and debugMode is true', () => {
64+
patcher = new ConsolePatcher({
65+
onNewMessage,
66+
debugMode: true,
67+
interactive: false,
68+
});
69+
patcher.patch();
70+
71+
console.log('test log');
72+
expect(onNewMessage).toHaveBeenCalledWith({
73+
type: 'log',
74+
content: 'test log',
75+
count: 1,
76+
});
77+
78+
console.info('test info');
79+
expect(onNewMessage).toHaveBeenCalledWith({
80+
type: 'info',
81+
content: 'test info',
82+
count: 1,
83+
});
84+
});
85+
86+
it('should not ignore log and info when it is interactive', () => {
87+
patcher = new ConsolePatcher({
88+
onNewMessage,
89+
debugMode: false,
90+
interactive: true,
91+
});
92+
patcher.patch();
93+
94+
console.log('test log');
95+
expect(onNewMessage).toHaveBeenCalledWith({
96+
type: 'log',
97+
content: 'test log',
98+
count: 1,
99+
});
100+
101+
console.info('test info');
102+
expect(onNewMessage).toHaveBeenCalledWith({
103+
type: 'info',
104+
content: 'test info',
105+
count: 1,
106+
});
107+
});
108+
});
109+
110+
describe('when stderr is false', () => {
111+
it('should call onNewMessage for log, warn, error, and info', () => {
112+
patcher = new ConsolePatcher({
113+
onNewMessage,
114+
debugMode: false,
115+
stderr: false,
116+
});
117+
patcher.patch();
118+
119+
console.log('test log');
120+
expect(onNewMessage).toHaveBeenCalledWith({
121+
type: 'log',
122+
content: 'test log',
123+
count: 1,
124+
});
125+
126+
console.warn('test warn');
127+
expect(onNewMessage).toHaveBeenCalledWith({
128+
type: 'warn',
129+
content: 'test warn',
130+
count: 1,
131+
});
132+
133+
console.error('test error');
134+
expect(onNewMessage).toHaveBeenCalledWith({
135+
type: 'error',
136+
content: 'test error',
137+
count: 1,
138+
});
139+
140+
console.info('test info');
141+
expect(onNewMessage).toHaveBeenCalledWith({
142+
type: 'info',
143+
content: 'test info',
144+
count: 1,
145+
});
146+
});
147+
148+
it('should not call onNewMessage for debug when debugMode is false', () => {
149+
patcher = new ConsolePatcher({
150+
onNewMessage,
151+
debugMode: false,
152+
stderr: false,
153+
});
154+
patcher.patch();
155+
156+
console.debug('test debug');
157+
expect(onNewMessage).not.toHaveBeenCalled();
158+
});
159+
160+
it('should call onNewMessage for debug when debugMode is true', () => {
161+
patcher = new ConsolePatcher({
162+
onNewMessage,
163+
debugMode: true,
164+
stderr: false,
165+
});
166+
patcher.patch();
167+
168+
console.debug('test debug');
169+
expect(onNewMessage).toHaveBeenCalledWith({
170+
type: 'debug',
171+
content: 'test debug',
172+
count: 1,
173+
});
174+
});
175+
176+
it('should format multiple arguments using util.format', () => {
177+
patcher = new ConsolePatcher({
178+
onNewMessage,
179+
debugMode: false,
180+
stderr: false,
181+
});
182+
patcher.patch();
183+
184+
console.log('test %s %d', 'string', 123);
185+
expect(onNewMessage).toHaveBeenCalledWith({
186+
type: 'log',
187+
content: 'test string 123',
188+
count: 1,
189+
});
190+
});
191+
});
192+
193+
describe('when stderr is true', () => {
194+
it('should redirect warn and error to originalConsoleError', () => {
195+
const spyError = vi.spyOn(console, 'error').mockImplementation(() => {});
196+
patcher = new ConsolePatcher({ debugMode: false, stderr: true });
197+
patcher.patch();
198+
199+
console.warn('test warn');
200+
expect(spyError).toHaveBeenCalledWith('test warn');
201+
202+
console.error('test error');
203+
expect(spyError).toHaveBeenCalledWith('test error');
204+
});
205+
206+
it('should redirect log and info to originalConsoleError when debugMode is true', () => {
207+
const spyError = vi.spyOn(console, 'error').mockImplementation(() => {});
208+
patcher = new ConsolePatcher({ debugMode: true, stderr: true });
209+
patcher.patch();
210+
211+
console.log('test log');
212+
expect(spyError).toHaveBeenCalledWith('test log');
213+
214+
console.info('test info');
215+
expect(spyError).toHaveBeenCalledWith('test info');
216+
});
217+
218+
it('should ignore debug when debugMode is false', () => {
219+
const spyError = vi.spyOn(console, 'error').mockImplementation(() => {});
220+
patcher = new ConsolePatcher({ debugMode: false, stderr: true });
221+
patcher.patch();
222+
223+
console.debug('test debug');
224+
expect(spyError).not.toHaveBeenCalled();
225+
});
226+
227+
it('should redirect debug to originalConsoleError when debugMode is true', () => {
228+
const spyError = vi.spyOn(console, 'error').mockImplementation(() => {});
229+
patcher = new ConsolePatcher({ debugMode: true, stderr: true });
230+
patcher.patch();
231+
232+
console.debug('test debug');
233+
expect(spyError).toHaveBeenCalledWith('test debug');
234+
});
235+
});
236+
});

packages/cli/src/ui/utils/ConsolePatcher.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ interface ConsolePatcherParams {
1313
onNewMessage?: (message: Omit<ConsoleMessageItem, 'id'>) => void;
1414
debugMode: boolean;
1515
stderr?: boolean;
16+
interactive?: boolean;
1617
}
1718

1819
export class ConsolePatcher {
@@ -49,12 +50,19 @@ export class ConsolePatcher {
4950
private patchConsoleMethod =
5051
(type: 'log' | 'warn' | 'error' | 'debug' | 'info') =>
5152
(...args: unknown[]) => {
52-
if (this.params.stderr) {
53-
if (type !== 'debug' || this.params.debugMode) {
54-
this.originalConsoleError(this.formatArgs(args));
53+
// When it is non interactive mode, do not show info logging unless
54+
// it is debug mode. default to true if it is undefined.
55+
if (this.params.interactive === false) {
56+
if ((type === 'info' || type === 'log') && !this.params.debugMode) {
57+
return;
5558
}
56-
} else {
57-
if (type !== 'debug' || this.params.debugMode) {
59+
}
60+
// When it is in the debug mode, redirect console output to stderr
61+
// depending on if it is stderr only mode.
62+
if (type !== 'debug' || this.params.debugMode) {
63+
if (this.params.stderr) {
64+
this.originalConsoleError(this.formatArgs(args));
65+
} else {
5866
this.params.onNewMessage?.({
5967
type,
6068
content: this.formatArgs(args),

0 commit comments

Comments
 (0)