diff --git a/integration-tests/extensions-install.test.ts b/integration-tests/extensions-install.test.ts index 90dbf1ab0d..e9f1cdbf49 100644 --- a/integration-tests/extensions-install.test.ts +++ b/integration-tests/extensions-install.test.ts @@ -34,16 +34,20 @@ describe('extension install', () => { writeFileSync(testServerPath, extension); try { const result = await rig.runCommand( - ['extensions', 'install', `${rig.testDir!}`], + ['--debug', 'extensions', 'install', `${rig.testDir!}`], { stdin: 'y\n' }, ); expect(result).toContain('test-extension-install'); - const listResult = await rig.runCommand(['extensions', 'list']); + const listResult = await rig.runCommand([ + '--debug', + 'extensions', + 'list', + ]); expect(listResult).toContain('test-extension-install'); writeFileSync(testServerPath, extensionUpdate); const updateResult = await rig.runCommand( - ['extensions', 'update', `test-extension-install`], + ['--debug', 'extensions', 'update', `test-extension-install`], { stdin: 'y\n' }, ); expect(updateResult).toContain('0.0.2'); diff --git a/integration-tests/extensions-reload.test.ts b/integration-tests/extensions-reload.test.ts index 9d451cedcf..ba9bec55e1 100644 --- a/integration-tests/extensions-reload.test.ts +++ b/integration-tests/extensions-reload.test.ts @@ -66,7 +66,7 @@ describe('extension reloading', () => { } const result = await rig.runCommand( - ['extensions', 'install', `${rig.testDir!}`], + ['--debug', 'extensions', 'install', `${rig.testDir!}`], { stdin: 'y\n' }, ); expect(result).toContain('test-extension'); diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index 5bd9944f63..707774df57 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -32,6 +32,7 @@ import { ValidationRequiredError, type AdminControlsSettings, debugLogger, + isHeadlessMode, } from '@google/gemini-cli-core'; import { loadCliConfig, parseArguments } from './config/config.js'; @@ -296,6 +297,7 @@ export async function main() { const isDebugMode = cliConfig.isDebugMode(argv); const consolePatcher = new ConsolePatcher({ stderr: true, + interactive: isHeadlessMode() ? false : true, debugMode: isDebugMode, onNewMessage: (msg) => { coreEvents.emitConsoleLog(msg.type, msg.content); diff --git a/packages/cli/src/nonInteractiveCli.ts b/packages/cli/src/nonInteractiveCli.ts index 891e3d0ee9..4f9d817204 100644 --- a/packages/cli/src/nonInteractiveCli.ts +++ b/packages/cli/src/nonInteractiveCli.ts @@ -65,6 +65,7 @@ export async function runNonInteractive({ return promptIdContext.run(prompt_id, async () => { const consolePatcher = new ConsolePatcher({ stderr: true, + interactive: false, debugMode: config.getDebugMode(), onNewMessage: (msg) => { coreEvents.emitConsoleLog(msg.type, msg.content); diff --git a/packages/cli/src/ui/utils/ConsolePatcher.test.ts b/packages/cli/src/ui/utils/ConsolePatcher.test.ts new file mode 100644 index 0000000000..8439ca3564 --- /dev/null +++ b/packages/cli/src/ui/utils/ConsolePatcher.test.ts @@ -0,0 +1,236 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable no-console */ + +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { ConsolePatcher } from './ConsolePatcher.js'; + +describe('ConsolePatcher', () => { + let patcher: ConsolePatcher; + const onNewMessage = vi.fn(); + + afterEach(() => { + if (patcher) { + patcher.cleanup(); + } + vi.restoreAllMocks(); + vi.clearAllMocks(); + }); + + it('should patch and restore console methods', () => { + const beforeLog = console.log; + const beforeWarn = console.warn; + const beforeError = console.error; + const beforeDebug = console.debug; + const beforeInfo = console.info; + + patcher = new ConsolePatcher({ onNewMessage, debugMode: false }); + patcher.patch(); + + expect(console.log).not.toBe(beforeLog); + expect(console.warn).not.toBe(beforeWarn); + expect(console.error).not.toBe(beforeError); + expect(console.debug).not.toBe(beforeDebug); + expect(console.info).not.toBe(beforeInfo); + + patcher.cleanup(); + + expect(console.log).toBe(beforeLog); + expect(console.warn).toBe(beforeWarn); + expect(console.error).toBe(beforeError); + expect(console.debug).toBe(beforeDebug); + expect(console.info).toBe(beforeInfo); + }); + + describe('Interactive mode', () => { + it('should ignore log and info when it is not interactive and debugMode is false', () => { + patcher = new ConsolePatcher({ + onNewMessage, + debugMode: false, + interactive: false, + }); + patcher.patch(); + + console.log('test log'); + console.info('test info'); + expect(onNewMessage).not.toHaveBeenCalled(); + }); + + it('should not ignore log and info when it is not interactive and debugMode is true', () => { + patcher = new ConsolePatcher({ + onNewMessage, + debugMode: true, + interactive: false, + }); + patcher.patch(); + + console.log('test log'); + expect(onNewMessage).toHaveBeenCalledWith({ + type: 'log', + content: 'test log', + count: 1, + }); + + console.info('test info'); + expect(onNewMessage).toHaveBeenCalledWith({ + type: 'info', + content: 'test info', + count: 1, + }); + }); + + it('should not ignore log and info when it is interactive', () => { + patcher = new ConsolePatcher({ + onNewMessage, + debugMode: false, + interactive: true, + }); + patcher.patch(); + + console.log('test log'); + expect(onNewMessage).toHaveBeenCalledWith({ + type: 'log', + content: 'test log', + count: 1, + }); + + console.info('test info'); + expect(onNewMessage).toHaveBeenCalledWith({ + type: 'info', + content: 'test info', + count: 1, + }); + }); + }); + + describe('when stderr is false', () => { + it('should call onNewMessage for log, warn, error, and info', () => { + patcher = new ConsolePatcher({ + onNewMessage, + debugMode: false, + stderr: false, + }); + patcher.patch(); + + console.log('test log'); + expect(onNewMessage).toHaveBeenCalledWith({ + type: 'log', + content: 'test log', + count: 1, + }); + + console.warn('test warn'); + expect(onNewMessage).toHaveBeenCalledWith({ + type: 'warn', + content: 'test warn', + count: 1, + }); + + console.error('test error'); + expect(onNewMessage).toHaveBeenCalledWith({ + type: 'error', + content: 'test error', + count: 1, + }); + + console.info('test info'); + expect(onNewMessage).toHaveBeenCalledWith({ + type: 'info', + content: 'test info', + count: 1, + }); + }); + + it('should not call onNewMessage for debug when debugMode is false', () => { + patcher = new ConsolePatcher({ + onNewMessage, + debugMode: false, + stderr: false, + }); + patcher.patch(); + + console.debug('test debug'); + expect(onNewMessage).not.toHaveBeenCalled(); + }); + + it('should call onNewMessage for debug when debugMode is true', () => { + patcher = new ConsolePatcher({ + onNewMessage, + debugMode: true, + stderr: false, + }); + patcher.patch(); + + console.debug('test debug'); + expect(onNewMessage).toHaveBeenCalledWith({ + type: 'debug', + content: 'test debug', + count: 1, + }); + }); + + it('should format multiple arguments using util.format', () => { + patcher = new ConsolePatcher({ + onNewMessage, + debugMode: false, + stderr: false, + }); + patcher.patch(); + + console.log('test %s %d', 'string', 123); + expect(onNewMessage).toHaveBeenCalledWith({ + type: 'log', + content: 'test string 123', + count: 1, + }); + }); + }); + + describe('when stderr is true', () => { + it('should redirect warn and error to originalConsoleError', () => { + const spyError = vi.spyOn(console, 'error').mockImplementation(() => {}); + patcher = new ConsolePatcher({ debugMode: false, stderr: true }); + patcher.patch(); + + console.warn('test warn'); + expect(spyError).toHaveBeenCalledWith('test warn'); + + console.error('test error'); + expect(spyError).toHaveBeenCalledWith('test error'); + }); + + it('should redirect log and info to originalConsoleError when debugMode is true', () => { + const spyError = vi.spyOn(console, 'error').mockImplementation(() => {}); + patcher = new ConsolePatcher({ debugMode: true, stderr: true }); + patcher.patch(); + + console.log('test log'); + expect(spyError).toHaveBeenCalledWith('test log'); + + console.info('test info'); + expect(spyError).toHaveBeenCalledWith('test info'); + }); + + it('should ignore debug when debugMode is false', () => { + const spyError = vi.spyOn(console, 'error').mockImplementation(() => {}); + patcher = new ConsolePatcher({ debugMode: false, stderr: true }); + patcher.patch(); + + console.debug('test debug'); + expect(spyError).not.toHaveBeenCalled(); + }); + + it('should redirect debug to originalConsoleError when debugMode is true', () => { + const spyError = vi.spyOn(console, 'error').mockImplementation(() => {}); + patcher = new ConsolePatcher({ debugMode: true, stderr: true }); + patcher.patch(); + + console.debug('test debug'); + expect(spyError).toHaveBeenCalledWith('test debug'); + }); + }); +}); diff --git a/packages/cli/src/ui/utils/ConsolePatcher.ts b/packages/cli/src/ui/utils/ConsolePatcher.ts index 3674c5614e..ddd26fca0b 100644 --- a/packages/cli/src/ui/utils/ConsolePatcher.ts +++ b/packages/cli/src/ui/utils/ConsolePatcher.ts @@ -13,6 +13,7 @@ interface ConsolePatcherParams { onNewMessage?: (message: Omit) => void; debugMode: boolean; stderr?: boolean; + interactive?: boolean; } export class ConsolePatcher { @@ -49,12 +50,19 @@ export class ConsolePatcher { private patchConsoleMethod = (type: 'log' | 'warn' | 'error' | 'debug' | 'info') => (...args: unknown[]) => { - if (this.params.stderr) { - if (type !== 'debug' || this.params.debugMode) { - this.originalConsoleError(this.formatArgs(args)); + // When it is non interactive mode, do not show info logging unless + // it is debug mode. default to true if it is undefined. + if (this.params.interactive === false) { + if ((type === 'info' || type === 'log') && !this.params.debugMode) { + return; } - } else { - if (type !== 'debug' || this.params.debugMode) { + } + // When it is in the debug mode, redirect console output to stderr + // depending on if it is stderr only mode. + if (type !== 'debug' || this.params.debugMode) { + if (this.params.stderr) { + this.originalConsoleError(this.formatArgs(args)); + } else { this.params.onNewMessage?.({ type, content: this.formatArgs(args),