Migrate console to coreEvents.emitFeedback or debugLogger (#15219)

This commit is contained in:
Adib234
2025-12-29 15:46:10 -05:00
committed by GitHub
parent dcd2449b1a
commit 10ae84869a
66 changed files with 564 additions and 425 deletions
+9
View File
@@ -391,6 +391,15 @@ When working in the `/docs` directory, follow the guidelines in this section:
Only write high-value comments if at all. Avoid talking to the user through Only write high-value comments if at all. Avoid talking to the user through
comments. comments.
## Logging and Error Handling
- **Avoid Console Statements:** Do not use `console.log`, `console.error`, or
similar methods directly.
- **Non-User-Facing Logs:** For developer-facing debug messages, use
`debugLogger` (from `@google/gemini-cli-core`).
- **User-Facing Feedback:** To surface errors or warnings to the user, use
`coreEvents.emitFeedback` (from `@google/gemini-cli-core`).
## General requirements ## General requirements
- If there is something you do not understand or is ambiguous, seek confirmation - If there is something you do not understand or is ambiguous, seek confirmation
+1
View File
@@ -164,6 +164,7 @@ export default tseslint.config(
'prefer-arrow-callback': 'error', 'prefer-arrow-callback': 'error',
'prefer-const': ['error', { destructuring: 'all' }], 'prefer-const': ['error', { destructuring: 'all' }],
radix: 'error', radix: 'error',
'no-console': 'error',
'default-case': 'error', 'default-case': 'error',
'@typescript-eslint/await-thenable': ['error'], '@typescript-eslint/await-thenable': ['error'],
'@typescript-eslint/no-floating-promises': ['error'], '@typescript-eslint/no-floating-promises': ['error'],
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import { debugLogger } from '@google/gemini-cli-core';
import { ExtensionsCommand } from './extensions.js'; import { ExtensionsCommand } from './extensions.js';
import { InitCommand } from './init.js'; import { InitCommand } from './init.js';
import { RestoreCommand } from './restore.js'; import { RestoreCommand } from './restore.js';
@@ -20,7 +21,7 @@ class CommandRegistry {
register(command: Command) { register(command: Command) {
if (this.commands.has(command.name)) { if (this.commands.has(command.name)) {
console.warn(`Command ${command.name} already registered. Skipping.`); debugLogger.warn(`Command ${command.name} already registered. Skipping.`);
return; return;
} }
@@ -9,6 +9,7 @@ import * as fs from 'node:fs';
import * as path from 'node:path'; import * as path from 'node:path';
import * as os from 'node:os'; import * as os from 'node:os';
import { loadSettings, USER_SETTINGS_PATH } from './settings.js'; import { loadSettings, USER_SETTINGS_PATH } from './settings.js';
import { debugLogger } from '@google/gemini-cli-core';
const mocks = vi.hoisted(() => { const mocks = vi.hoisted(() => {
const suffix = Math.random().toString(36).slice(2); const suffix = Math.random().toString(36).slice(2);
@@ -75,7 +76,7 @@ describe('loadSettings', () => {
fs.rmSync(mockWorkspaceDir, { recursive: true, force: true }); fs.rmSync(mockWorkspaceDir, { recursive: true, force: true });
} }
} catch (e) { } catch (e) {
console.error('Failed to cleanup temp dirs', e); debugLogger.error('Failed to cleanup temp dirs', e);
} }
vi.restoreAllMocks(); vi.restoreAllMocks();
}); });
+2 -2
View File
@@ -25,7 +25,7 @@ import { loadConfig, loadEnvironment, setTargetDir } from '../config/config.js';
import { loadSettings } from '../config/settings.js'; import { loadSettings } from '../config/settings.js';
import { loadExtensions } from '../config/extension.js'; import { loadExtensions } from '../config/extension.js';
import { commandRegistry } from '../commands/command-registry.js'; import { commandRegistry } from '../commands/command-registry.js';
import { SimpleExtensionLoader } from '@google/gemini-cli-core'; import { debugLogger, SimpleExtensionLoader } from '@google/gemini-cli-core';
import type { Command, CommandArgument } from '../commands/types.js'; import type { Command, CommandArgument } from '../commands/types.js';
import { GitService } from '@google/gemini-cli-core'; import { GitService } from '@google/gemini-cli-core';
@@ -239,7 +239,7 @@ export async function createApp() {
): CommandResponse | undefined => { ): CommandResponse | undefined => {
const commandName = command.name; const commandName = command.name;
if (visited.includes(commandName)) { if (visited.includes(commandName)) {
console.warn( debugLogger.warn(
`Command ${commandName} already inserted in the response, skipping`, `Command ${commandName} already inserted in the response, skipping`,
); );
return undefined; return undefined;
+5 -5
View File
@@ -29,6 +29,7 @@ import {
type Config, type Config,
type ResumedSessionData, type ResumedSessionData,
debugLogger, debugLogger,
coreEvents,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import { act } from 'react'; import { act } from 'react';
import { type InitializationResult } from './core/initializer.js'; import { type InitializationResult } from './core/initializer.js';
@@ -819,9 +820,7 @@ describe('gemini.tsx main function kitty protocol', () => {
.mockImplementation((code) => { .mockImplementation((code) => {
throw new MockProcessExitError(code); throw new MockProcessExitError(code);
}); });
const consoleErrorSpy = vi const emitFeedbackSpy = vi.spyOn(coreEvents, 'emitFeedback');
.spyOn(console, 'error')
.mockImplementation(() => {});
vi.mocked(loadSettings).mockReturnValue({ vi.mocked(loadSettings).mockReturnValue({
merged: { advanced: {}, security: { auth: {} }, ui: { theme: 'test' } }, merged: { advanced: {}, security: { auth: {} }, ui: { theme: 'test' } },
@@ -875,12 +874,13 @@ describe('gemini.tsx main function kitty protocol', () => {
if (!(e instanceof MockProcessExitError)) throw e; if (!(e instanceof MockProcessExitError)) throw e;
} }
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(emitFeedbackSpy).toHaveBeenCalledWith(
'error',
expect.stringContaining('Error resuming session: Session not found'), expect.stringContaining('Error resuming session: Session not found'),
); );
expect(processExitSpy).toHaveBeenCalledWith(42); expect(processExitSpy).toHaveBeenCalledWith(42);
processExitSpy.mockRestore(); processExitSpy.mockRestore();
consoleErrorSpy.mockRestore(); emitFeedbackSpy.mockRestore();
}); });
it.skip('should log error when cleanupExpiredSessions fails', async () => { it.skip('should log error when cleanupExpiredSessions fails', async () => {
+22 -8
View File
@@ -41,6 +41,7 @@ import {
type ResumedSessionData, type ResumedSessionData,
type OutputPayload, type OutputPayload,
type ConsoleLogPayload, type ConsoleLogPayload,
type UserFeedbackPayload,
sessionId, sessionId,
logUserPrompt, logUserPrompt,
AuthType, AuthType,
@@ -597,7 +598,8 @@ export async function main() {
// Use the existing session ID to continue recording to the same session // Use the existing session ID to continue recording to the same session
config.setSessionId(resumedSessionData.conversation.sessionId); config.setSessionId(resumedSessionData.conversation.sessionId);
} catch (error) { } catch (error) {
console.error( coreEvents.emitFeedback(
'error',
`Error resuming session: ${error instanceof Error ? error.message : 'Unknown error'}`, `Error resuming session: ${error instanceof Error ? error.message : 'Unknown error'}`,
); );
await runExitCleanup(); await runExitCleanup();
@@ -719,13 +721,25 @@ export function initializeOutputListenersAndFlush() {
} }
}); });
coreEvents.on(CoreEvent.ConsoleLog, (payload: ConsoleLogPayload) => { if (coreEvents.listenerCount(CoreEvent.ConsoleLog) === 0) {
if (payload.type === 'error' || payload.type === 'warn') { coreEvents.on(CoreEvent.ConsoleLog, (payload: ConsoleLogPayload) => {
writeToStderr(payload.content); if (payload.type === 'error' || payload.type === 'warn') {
} else { writeToStderr(payload.content);
writeToStdout(payload.content); } else {
} writeToStdout(payload.content);
}); }
});
}
if (coreEvents.listenerCount(CoreEvent.UserFeedback) === 0) {
coreEvents.on(CoreEvent.UserFeedback, (payload: UserFeedbackPayload) => {
if (payload.severity === 'error' || payload.severity === 'warning') {
writeToStderr(payload.message);
} else {
writeToStdout(payload.message);
}
});
}
} }
coreEvents.drainBacklogs(); coreEvents.drainBacklogs();
} }
+5 -12
View File
@@ -44,6 +44,7 @@ const mockCoreEvents = vi.hoisted(() => ({
off: vi.fn(), off: vi.fn(),
drainBacklogs: vi.fn(), drainBacklogs: vi.fn(),
emit: vi.fn(), emit: vi.fn(),
emitFeedback: vi.fn(),
})); }));
vi.mock('@google/gemini-cli-core', async (importOriginal) => { vi.mock('@google/gemini-cli-core', async (importOriginal) => {
@@ -785,11 +786,6 @@ describe('runNonInteractive', () => {
throw testError; throw testError;
}); });
// Mock console.error to capture JSON error output
const consoleErrorJsonSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
let thrownError: Error | null = null; let thrownError: Error | null = null;
try { try {
await runNonInteractive({ await runNonInteractive({
@@ -807,7 +803,8 @@ describe('runNonInteractive', () => {
// Should throw because of mocked process.exit // Should throw because of mocked process.exit
expect(thrownError?.message).toBe('process.exit(1) called'); expect(thrownError?.message).toBe('process.exit(1) called');
expect(consoleErrorJsonSpy).toHaveBeenCalledWith( expect(mockCoreEvents.emitFeedback).toHaveBeenCalledWith(
'error',
JSON.stringify( JSON.stringify(
{ {
session_id: 'test-session-id', session_id: 'test-session-id',
@@ -831,11 +828,6 @@ describe('runNonInteractive', () => {
throw fatalError; throw fatalError;
}); });
// Mock console.error to capture JSON error output
const consoleErrorJsonSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
let thrownError: Error | null = null; let thrownError: Error | null = null;
try { try {
await runNonInteractive({ await runNonInteractive({
@@ -853,7 +845,8 @@ describe('runNonInteractive', () => {
// Should throw because of mocked process.exit with custom exit code // Should throw because of mocked process.exit with custom exit code
expect(thrownError?.message).toBe('process.exit(42) called'); expect(thrownError?.message).toBe('process.exit(42) called');
expect(consoleErrorJsonSpy).toHaveBeenCalledWith( expect(mockCoreEvents.emitFeedback).toHaveBeenCalledWith(
'error',
JSON.stringify( JSON.stringify(
{ {
session_id: 'test-session-id', session_id: 'test-session-id',
+11 -6
View File
@@ -10,7 +10,7 @@ import toml from '@iarna/toml';
import { glob } from 'glob'; import { glob } from 'glob';
import { z } from 'zod'; import { z } from 'zod';
import type { Config } from '@google/gemini-cli-core'; import type { Config } from '@google/gemini-cli-core';
import { Storage } from '@google/gemini-cli-core'; import { Storage, coreEvents } from '@google/gemini-cli-core';
import type { ICommandLoader } from './types.js'; import type { ICommandLoader } from './types.js';
import type { import type {
CommandContext, CommandContext,
@@ -126,7 +126,8 @@ export class FileCommandLoader implements ICommandLoader {
!signal.aborted && !signal.aborted &&
(error as { code?: string })?.code !== 'ENOENT' (error as { code?: string })?.code !== 'ENOENT'
) { ) {
console.error( coreEvents.emitFeedback(
'error',
`[FileCommandLoader] Error loading commands from ${dirInfo.path}:`, `[FileCommandLoader] Error loading commands from ${dirInfo.path}:`,
error, error,
); );
@@ -189,7 +190,8 @@ export class FileCommandLoader implements ICommandLoader {
try { try {
fileContent = await fs.readFile(filePath, 'utf-8'); fileContent = await fs.readFile(filePath, 'utf-8');
} catch (error: unknown) { } catch (error: unknown) {
console.error( coreEvents.emitFeedback(
'error',
`[FileCommandLoader] Failed to read file ${filePath}:`, `[FileCommandLoader] Failed to read file ${filePath}:`,
error instanceof Error ? error.message : String(error), error instanceof Error ? error.message : String(error),
); );
@@ -200,7 +202,8 @@ export class FileCommandLoader implements ICommandLoader {
try { try {
parsed = toml.parse(fileContent); parsed = toml.parse(fileContent);
} catch (error: unknown) { } catch (error: unknown) {
console.error( coreEvents.emitFeedback(
'error',
`[FileCommandLoader] Failed to parse TOML file ${filePath}:`, `[FileCommandLoader] Failed to parse TOML file ${filePath}:`,
error instanceof Error ? error.message : String(error), error instanceof Error ? error.message : String(error),
); );
@@ -210,7 +213,8 @@ export class FileCommandLoader implements ICommandLoader {
const validationResult = TomlCommandDefSchema.safeParse(parsed); const validationResult = TomlCommandDefSchema.safeParse(parsed);
if (!validationResult.success) { if (!validationResult.success) {
console.error( coreEvents.emitFeedback(
'error',
`[FileCommandLoader] Skipping invalid command file: ${filePath}. Validation errors:`, `[FileCommandLoader] Skipping invalid command file: ${filePath}. Validation errors:`,
validationResult.error.flatten(), validationResult.error.flatten(),
); );
@@ -278,7 +282,8 @@ export class FileCommandLoader implements ICommandLoader {
_args: string, _args: string,
): Promise<SlashCommandActionReturn> => { ): Promise<SlashCommandActionReturn> => {
if (!context.invocation) { if (!context.invocation) {
console.error( coreEvents.emitFeedback(
'error',
`[FileCommandLoader] Critical error: Command '${baseCommandName}' was executed without invocation context.`, `[FileCommandLoader] Critical error: Command '${baseCommandName}' was executed without invocation context.`,
); );
return { return {
+1 -9
View File
@@ -842,16 +842,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
const handleClearScreen = useCallback(() => { const handleClearScreen = useCallback(() => {
historyManager.clearItems(); historyManager.clearItems();
clearConsoleMessagesState(); clearConsoleMessagesState();
if (!isAlternateBuffer) {
console.clear();
}
refreshStatic(); refreshStatic();
}, [ }, [historyManager, clearConsoleMessagesState, refreshStatic]);
historyManager,
clearConsoleMessagesState,
refreshStatic,
isAlternateBuffer,
]);
const { handleInput: vimHandleInput } = useVim(buffer, handleFinalSubmit); const { handleInput: vimHandleInput } = useVim(buffer, handleFinalSubmit);
@@ -4,14 +4,30 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import { describe, it, expect, vi, afterEach } from 'vitest'; import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
import { render } from 'ink-testing-library'; import { render } from '../test-utils/render.js';
import { act } from 'react'; import { act } from 'react';
import { IdeIntegrationNudge } from './IdeIntegrationNudge.js'; import { IdeIntegrationNudge } from './IdeIntegrationNudge.js';
import { KeypressProvider } from './contexts/KeypressContext.js'; import { KeypressProvider } from './contexts/KeypressContext.js';
import { debugLogger } from '@google/gemini-cli-core';
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
// Mock debugLogger
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
debugLogger: {
log: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
};
});
describe('IdeIntegrationNudge', () => { describe('IdeIntegrationNudge', () => {
const defaultProps = { const defaultProps = {
ide: { ide: {
@@ -21,24 +37,20 @@ describe('IdeIntegrationNudge', () => {
onComplete: vi.fn(), onComplete: vi.fn(),
}; };
const originalError = console.error;
afterEach(() => { afterEach(() => {
console.error = originalError;
vi.restoreAllMocks(); vi.restoreAllMocks();
vi.unstubAllEnvs(); vi.unstubAllEnvs();
}); });
beforeEach(() => { beforeEach(() => {
console.error = (...args) => { vi.mocked(debugLogger.warn).mockImplementation((...args) => {
if ( if (
typeof args[0] === 'string' && typeof args[0] === 'string' &&
/was not wrapped in act/.test(args[0]) /was not wrapped in act/.test(args[0])
) { ) {
return; return;
} }
originalError.call(console, ...args); });
};
vi.stubEnv('GEMINI_CLI_IDE_SERVER_PORT', ''); vi.stubEnv('GEMINI_CLI_IDE_SERVER_PORT', '');
vi.stubEnv('GEMINI_CLI_IDE_WORKSPACE_PATH', ''); vi.stubEnv('GEMINI_CLI_IDE_WORKSPACE_PATH', '');
}); });
@@ -5,12 +5,27 @@
*/ */
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render } from 'ink-testing-library'; import { render } from '../../test-utils/render.js';
import { act } from 'react'; import { act } from 'react';
import { AuthInProgress } from './AuthInProgress.js'; import { AuthInProgress } from './AuthInProgress.js';
import { useKeypress, type Key } from '../hooks/useKeypress.js'; import { useKeypress, type Key } from '../hooks/useKeypress.js';
import { debugLogger } from '@google/gemini-cli-core';
// Mock dependencies // Mock dependencies
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
debugLogger: {
log: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
};
});
vi.mock('../hooks/useKeypress.js', () => ({ vi.mock('../hooks/useKeypress.js', () => ({
useKeypress: vi.fn(), useKeypress: vi.fn(),
})); }));
@@ -22,24 +37,20 @@ vi.mock('../components/CliSpinner.js', () => ({
describe('AuthInProgress', () => { describe('AuthInProgress', () => {
const onTimeout = vi.fn(); const onTimeout = vi.fn();
const originalError = console.error;
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
vi.useFakeTimers(); vi.useFakeTimers();
console.error = (...args) => { vi.mocked(debugLogger.error).mockImplementation((...args) => {
if ( if (
typeof args[0] === 'string' && typeof args[0] === 'string' &&
args[0].includes('was not wrapped in act') args[0].includes('was not wrapped in act')
) { ) {
return; return;
} }
originalError.call(console, ...args); });
};
}); });
afterEach(() => { afterEach(() => {
console.error = originalError;
vi.useRealTimers(); vi.useRealTimers();
}); });
@@ -12,6 +12,7 @@ import type { LoadedSettings } from '../../config/settings.js';
import { KeypressProvider } from '../contexts/KeypressContext.js'; import { KeypressProvider } from '../contexts/KeypressContext.js';
import { act } from 'react'; import { act } from 'react';
import { waitFor } from '../../test-utils/async.js'; import { waitFor } from '../../test-utils/async.js';
import { debugLogger } from '@google/gemini-cli-core';
vi.mock('@google/gemini-cli-core', async (importOriginal) => { vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual = const actual =
@@ -101,7 +102,7 @@ describe('EditorSettingsDialog', () => {
await waitFor(() => { await waitFor(() => {
const frame = lastFrame() || ''; const frame = lastFrame() || '';
if (!frame.includes('> Apply To')) { if (!frame.includes('> Apply To')) {
console.log( debugLogger.debug(
'Waiting for scope focus. Current frame:', 'Waiting for scope focus. Current frame:',
JSON.stringify(frame), JSON.stringify(frame),
); );
@@ -166,7 +167,7 @@ describe('EditorSettingsDialog', () => {
const frame = lastFrame() || ''; const frame = lastFrame() || '';
if (!frame.includes('(Also modified')) { if (!frame.includes('(Also modified')) {
console.log( debugLogger.debug(
'Modified message test failure. Frame:', 'Modified message test failure. Frame:',
JSON.stringify(frame), JSON.stringify(frame),
); );
@@ -24,6 +24,7 @@ import {
EDITOR_DISPLAY_NAMES, EDITOR_DISPLAY_NAMES,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import { useKeypress } from '../hooks/useKeypress.js'; import { useKeypress } from '../hooks/useKeypress.js';
import { coreEvents } from '@google/gemini-cli-core';
interface EditorDialogProps { interface EditorDialogProps {
onSelect: ( onSelect: (
@@ -68,7 +69,10 @@ export function EditorSettingsDialog({
) )
: 0; : 0;
if (editorIndex === -1) { if (editorIndex === -1) {
console.error(`Editor is not supported: ${currentPreference}`); coreEvents.emitFeedback(
'error',
`Editor is not supported: ${currentPreference}`,
);
editorIndex = 0; editorIndex = 0;
} }
@@ -8,6 +8,7 @@ import { vi, describe, it, expect, beforeEach } from 'vitest';
import * as processUtils from '../../utils/processUtils.js'; import * as processUtils from '../../utils/processUtils.js';
import { renderWithProviders } from '../../test-utils/render.js'; import { renderWithProviders } from '../../test-utils/render.js';
import { IdeTrustChangeDialog } from './IdeTrustChangeDialog.js'; import { IdeTrustChangeDialog } from './IdeTrustChangeDialog.js';
import { debugLogger } from '@google/gemini-cli-core';
describe('IdeTrustChangeDialog', () => { describe('IdeTrustChangeDialog', () => {
beforeEach(() => { beforeEach(() => {
@@ -39,8 +40,8 @@ describe('IdeTrustChangeDialog', () => {
}); });
it('renders a generic message and logs an error for NONE reason', () => { it('renders a generic message and logs an error for NONE reason', () => {
const consoleErrorSpy = vi const debugLoggerWarnSpy = vi
.spyOn(console, 'error') .spyOn(debugLogger, 'warn')
.mockImplementation(() => {}); .mockImplementation(() => {});
const { lastFrame } = renderWithProviders( const { lastFrame } = renderWithProviders(
<IdeTrustChangeDialog reason="NONE" />, <IdeTrustChangeDialog reason="NONE" />,
@@ -48,7 +49,7 @@ describe('IdeTrustChangeDialog', () => {
const frameText = lastFrame(); const frameText = lastFrame();
expect(frameText).toContain('Workspace trust has changed.'); expect(frameText).toContain('Workspace trust has changed.');
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(debugLoggerWarnSpy).toHaveBeenCalledWith(
'IdeTrustChangeDialog rendered with unexpected reason "NONE"', 'IdeTrustChangeDialog rendered with unexpected reason "NONE"',
); );
}); });
@@ -9,6 +9,7 @@ import { theme } from '../semantic-colors.js';
import { useKeypress } from '../hooks/useKeypress.js'; import { useKeypress } from '../hooks/useKeypress.js';
import { relaunchApp } from '../../utils/processUtils.js'; import { relaunchApp } from '../../utils/processUtils.js';
import { type RestartReason } from '../hooks/useIdeTrustListener.js'; import { type RestartReason } from '../hooks/useIdeTrustListener.js';
import { debugLogger } from '@google/gemini-cli-core';
interface IdeTrustChangeDialogProps { interface IdeTrustChangeDialogProps {
reason: RestartReason; reason: RestartReason;
@@ -28,7 +29,7 @@ export const IdeTrustChangeDialog = ({ reason }: IdeTrustChangeDialogProps) => {
let message = 'Workspace trust has changed.'; let message = 'Workspace trust has changed.';
if (reason === 'NONE') { if (reason === 'NONE') {
// This should not happen, but provides a fallback and a debug log. // This should not happen, but provides a fallback and a debug log.
console.error( debugLogger.warn(
'IdeTrustChangeDialog rendered with unexpected reason "NONE"', 'IdeTrustChangeDialog rendered with unexpected reason "NONE"',
); );
} else if (reason === 'CONNECTION_CHANGE') { } else if (reason === 'CONNECTION_CHANGE') {
@@ -15,7 +15,7 @@ import {
calculateTransformedLine, calculateTransformedLine,
} from './shared/text-buffer.js'; } from './shared/text-buffer.js';
import type { Config } from '@google/gemini-cli-core'; import type { Config } from '@google/gemini-cli-core';
import { ApprovalMode } from '@google/gemini-cli-core'; import { ApprovalMode, debugLogger } from '@google/gemini-cli-core';
import * as path from 'node:path'; import * as path from 'node:path';
import type { CommandContext, SlashCommand } from '../commands/types.js'; import type { CommandContext, SlashCommand } from '../commands/types.js';
import { CommandKind } from '../commands/types.js'; import { CommandKind } from '../commands/types.js';
@@ -577,8 +577,8 @@ describe('InputPrompt', () => {
}); });
it('should handle errors during clipboard operations', async () => { it('should handle errors during clipboard operations', async () => {
const consoleErrorSpy = vi const debugLoggerErrorSpy = vi
.spyOn(console, 'error') .spyOn(debugLogger, 'error')
.mockImplementation(() => {}); .mockImplementation(() => {});
vi.mocked(clipboardUtils.clipboardHasImage).mockRejectedValue( vi.mocked(clipboardUtils.clipboardHasImage).mockRejectedValue(
new Error('Clipboard error'), new Error('Clipboard error'),
@@ -592,14 +592,14 @@ describe('InputPrompt', () => {
stdin.write('\x16'); // Ctrl+V stdin.write('\x16'); // Ctrl+V
}); });
await waitFor(() => { await waitFor(() => {
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
'Error handling clipboard image:', 'Error handling clipboard image:',
expect.any(Error), expect.any(Error),
); );
}); });
expect(mockBuffer.setText).not.toHaveBeenCalled(); expect(mockBuffer.setText).not.toHaveBeenCalled();
consoleErrorSpy.mockRestore(); debugLoggerErrorSpy.mockRestore();
unmount(); unmount();
}); });
}); });
@@ -24,7 +24,7 @@ import { useKeypress } from '../hooks/useKeypress.js';
import { keyMatchers, Command } from '../keyMatchers.js'; import { keyMatchers, Command } from '../keyMatchers.js';
import type { CommandContext, SlashCommand } from '../commands/types.js'; import type { CommandContext, SlashCommand } from '../commands/types.js';
import type { Config } from '@google/gemini-cli-core'; import type { Config } from '@google/gemini-cli-core';
import { ApprovalMode } from '@google/gemini-cli-core'; import { ApprovalMode, debugLogger } from '@google/gemini-cli-core';
import { import {
parseInputForHighlighting, parseInputForHighlighting,
parseSegmentsFromTokens, parseSegmentsFromTokens,
@@ -354,7 +354,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
const offset = buffer.getOffset(); const offset = buffer.getOffset();
buffer.replaceRangeByOffset(offset, offset, textToInsert); buffer.replaceRangeByOffset(offset, offset, textToInsert);
} catch (error) { } catch (error) {
console.error('Error handling clipboard image:', error); debugLogger.error('Error handling clipboard image:', error);
} }
}, [buffer, config]); }, [buffer, config]);
@@ -12,7 +12,7 @@ import { theme } from '../semantic-colors.js';
import { StreamingState } from '../types.js'; import { StreamingState } from '../types.js';
import { UpdateNotification } from './UpdateNotification.js'; import { UpdateNotification } from './UpdateNotification.js';
import { GEMINI_DIR, Storage } from '@google/gemini-cli-core'; import { GEMINI_DIR, Storage, debugLogger } from '@google/gemini-cli-core';
import * as fs from 'node:fs/promises'; import * as fs from 'node:fs/promises';
import os from 'node:os'; import os from 'node:os';
@@ -66,7 +66,7 @@ export const Notifications = () => {
}); });
await fs.writeFile(screenReaderNudgeFilePath, 'true'); await fs.writeFile(screenReaderNudgeFilePath, 'true');
} catch (error) { } catch (error) {
console.error('Error storing screen reader nudge', error); debugLogger.error('Error storing screen reader nudge', error);
} }
} }
}; };
@@ -42,7 +42,7 @@ import {
type SettingsValue, type SettingsValue,
TOGGLE_TYPES, TOGGLE_TYPES,
} from '../../config/settingsSchema.js'; } from '../../config/settingsSchema.js';
import { debugLogger } from '@google/gemini-cli-core'; import { coreEvents, debugLogger } from '@google/gemini-cli-core';
import { keyMatchers, Command } from '../keyMatchers.js'; import { keyMatchers, Command } from '../keyMatchers.js';
import type { Config } from '@google/gemini-cli-core'; import type { Config } from '@google/gemini-cli-core';
import { useUIState } from '../contexts/UIStateContext.js'; import { useUIState } from '../contexts/UIStateContext.js';
@@ -254,7 +254,11 @@ export function SettingsDialog({
if (key === 'general.vimMode' && newValue !== vimEnabled) { if (key === 'general.vimMode' && newValue !== vimEnabled) {
// Call toggleVimEnabled to sync the VimModeContext local state // Call toggleVimEnabled to sync the VimModeContext local state
toggleVimEnabled().catch((error) => { toggleVimEnabled().catch((error) => {
console.error('Failed to toggle vim mode:', error); coreEvents.emitFeedback(
'error',
'Failed to toggle vim mode:',
error,
);
}); });
} }
@@ -10,6 +10,7 @@ import stringWidth from 'string-width';
import { theme } from '../../semantic-colors.js'; import { theme } from '../../semantic-colors.js';
import { toCodePoints } from '../../utils/textUtils.js'; import { toCodePoints } from '../../utils/textUtils.js';
import { useOverflowActions } from '../../contexts/OverflowContext.js'; import { useOverflowActions } from '../../contexts/OverflowContext.js';
import { debugLogger } from '@google/gemini-cli-core';
let enableDebugLog = false; let enableDebugLog = false;
@@ -28,7 +29,7 @@ function debugReportError(message: string, element: React.ReactNode) {
if (!enableDebugLog) return; if (!enableDebugLog) return;
if (!React.isValidElement(element)) { if (!React.isValidElement(element)) {
console.error( debugLogger.warn(
message, message,
`Invalid element: '${String(element)}' typeof=${typeof element}`, `Invalid element: '${String(element)}' typeof=${typeof element}`,
); );
@@ -44,10 +45,13 @@ function debugReportError(message: string, element: React.ReactNode) {
const lineNumber = elementWithSource._source?.lineNumber; const lineNumber = elementWithSource._source?.lineNumber;
sourceMessage = fileName ? `${fileName}:${lineNumber}` : '<Unknown file>'; sourceMessage = fileName ? `${fileName}:${lineNumber}` : '<Unknown file>';
} catch (error) { } catch (error) {
console.error('Error while trying to get file name:', error); debugLogger.warn('Error while trying to get file name:', error);
} }
console.error(message, `${String(element.type)}. Source: ${sourceMessage}`); debugLogger.warn(
message,
`${String(element.type)}. Source: ${sourceMessage}`,
);
} }
interface MaxSizedBoxProps { interface MaxSizedBoxProps {
children?: React.ReactNode; children?: React.ReactNode;
@@ -10,7 +10,12 @@ import os from 'node:os';
import pathMod from 'node:path'; import pathMod from 'node:path';
import * as path from 'node:path'; import * as path from 'node:path';
import { useState, useCallback, useEffect, useMemo, useReducer } from 'react'; import { useState, useCallback, useEffect, useMemo, useReducer } from 'react';
import { coreEvents, CoreEvent, unescapePath } from '@google/gemini-cli-core'; import {
coreEvents,
CoreEvent,
debugLogger,
unescapePath,
} from '@google/gemini-cli-core';
import { import {
toCodePoints, toCodePoints,
cpLen, cpLen,
@@ -1411,7 +1416,7 @@ function textBufferReducerLogic(
break; break;
default: { default: {
const exhaustiveCheck: never = dir; const exhaustiveCheck: never = dir;
console.error( debugLogger.error(
`Unknown visual movement direction: ${exhaustiveCheck}`, `Unknown visual movement direction: ${exhaustiveCheck}`,
); );
return state; return state;
@@ -1751,7 +1756,7 @@ function textBufferReducerLogic(
default: { default: {
const exhaustiveCheck: never = action; const exhaustiveCheck: never = action;
console.error(`Unknown action encountered: ${exhaustiveCheck}`); debugLogger.error(`Unknown action encountered: ${exhaustiveCheck}`);
return state; return state;
} }
} }
@@ -2173,7 +2178,11 @@ export function useTextBuffer({
newText = newText.replace(/\r\n?/g, '\n'); newText = newText.replace(/\r\n?/g, '\n');
dispatch({ type: 'set_text', payload: newText, pushToUndo: false }); dispatch({ type: 'set_text', payload: newText, pushToUndo: false });
} catch (err) { } catch (err) {
console.error('[useTextBuffer] external editor error', err); coreEvents.emitFeedback(
'error',
'[useTextBuffer] external editor error',
err,
);
} finally { } finally {
coreEvents.emit(CoreEvent.ExternalEditorClosed); coreEvents.emit(CoreEvent.ExternalEditorClosed);
if (wasRaw) setRawMode?.(true); if (wasRaw) setRawMode?.(true);
@@ -502,7 +502,7 @@ export async function handleAtCommand({
const errorMessages = resourceReadDisplays const errorMessages = resourceReadDisplays
.filter((d) => d.status === ToolCallStatus.Error) .filter((d) => d.status === ToolCallStatus.Error)
.map((d) => d.resultDisplay); .map((d) => d.resultDisplay);
console.error(errorMessages); debugLogger.error(errorMessages);
const errorMsg = `Exiting due to an error processing the @ command: ${firstError.resultDisplay}`; const errorMsg = `Exiting due to an error processing the @ command: ${firstError.resultDisplay}`;
return { processedQuery: null, error: errorMsg }; return { processedQuery: null, error: errorMsg };
} }
@@ -244,7 +244,6 @@ describe('useSlashCommandProcessor', () => {
}); });
expect(mockClearItems).toHaveBeenCalled(); expect(mockClearItems).toHaveBeenCalled();
expect(console.clear).not.toHaveBeenCalled();
}); });
it('should call console.clear if alternate buffer is not active', async () => { it('should call console.clear if alternate buffer is not active', async () => {
@@ -262,7 +261,6 @@ describe('useSlashCommandProcessor', () => {
}); });
expect(mockClearItems).toHaveBeenCalled(); expect(mockClearItems).toHaveBeenCalled();
expect(console.clear).toHaveBeenCalled();
}); });
}); });
@@ -50,7 +50,6 @@ import {
type ExtensionUpdateStatus, type ExtensionUpdateStatus,
} from '../state/extensions.js'; } from '../state/extensions.js';
import { appEvents } from '../../utils/events.js'; import { appEvents } from '../../utils/events.js';
import { useAlternateBuffer } from './useAlternateBuffer.js';
import { import {
LogoutConfirmationDialog, LogoutConfirmationDialog,
LogoutChoice, LogoutChoice,
@@ -96,7 +95,6 @@ export const useSlashCommandProcessor = (
const [commands, setCommands] = useState<readonly SlashCommand[] | undefined>( const [commands, setCommands] = useState<readonly SlashCommand[] | undefined>(
undefined, undefined,
); );
const alternateBuffer = useAlternateBuffer();
const [reloadTrigger, setReloadTrigger] = useState(0); const [reloadTrigger, setReloadTrigger] = useState(0);
const reloadCommands = useCallback(() => { const reloadCommands = useCallback(() => {
@@ -212,9 +210,6 @@ export const useSlashCommandProcessor = (
addItem, addItem,
clear: () => { clear: () => {
clearItems(); clearItems();
if (!alternateBuffer) {
console.clear();
}
refreshStatic(); refreshStatic();
setBannerVisible(false); setBannerVisible(false);
}, },
@@ -238,7 +233,6 @@ export const useSlashCommandProcessor = (
}, },
}), }),
[ [
alternateBuffer,
config, config,
settings, settings,
gitService, gitService,
@@ -8,7 +8,10 @@ import { useEffect } from 'react';
import type { Config } from '@google/gemini-cli-core'; import type { Config } from '@google/gemini-cli-core';
import { loadTrustedFolders } from '../../config/trustedFolders.js'; import { loadTrustedFolders } from '../../config/trustedFolders.js';
import { expandHomeDir } from '../utils/directoryUtils.js'; import { expandHomeDir } from '../utils/directoryUtils.js';
import { refreshServerHierarchicalMemory } from '@google/gemini-cli-core'; import {
debugLogger,
refreshServerHierarchicalMemory,
} from '@google/gemini-cli-core';
import { MultiFolderTrustDialog } from '../components/MultiFolderTrustDialog.js'; import { MultiFolderTrustDialog } from '../components/MultiFolderTrustDialog.js';
import type { UseHistoryManagerReturn } from './useHistoryManager.js'; import type { UseHistoryManagerReturn } from './useHistoryManager.js';
import { MessageType, type HistoryItem } from '../types.js'; import { MessageType, type HistoryItem } from '../types.js';
@@ -133,7 +136,7 @@ export function useIncludeDirsTrust(
} }
if (undefinedTrustDirs.length > 0) { if (undefinedTrustDirs.length > 0) {
console.log( debugLogger.log(
'Creating custom dialog with undecidedDirs:', 'Creating custom dialog with undecidedDirs:',
undefinedTrustDirs, undefinedTrustDirs,
); );
@@ -7,6 +7,7 @@
import { useReducer, useRef, useEffect, useCallback } from 'react'; import { useReducer, useRef, useEffect, useCallback } from 'react';
import { useKeypress, type Key } from './useKeypress.js'; import { useKeypress, type Key } from './useKeypress.js';
import { keyMatchers, Command } from '../keyMatchers.js'; import { keyMatchers, Command } from '../keyMatchers.js';
import { debugLogger } from '@google/gemini-cli-core';
export interface SelectionListItem<T> { export interface SelectionListItem<T> {
key: string; key: string;
@@ -198,7 +199,7 @@ function selectionListReducer(
default: { default: {
const exhaustiveCheck: never = action; const exhaustiveCheck: never = action;
console.error(`Unknown selection list action: ${exhaustiveCheck}`); debugLogger.warn(`Unknown selection list action: ${exhaustiveCheck}`);
return state; return state;
} }
} }
@@ -19,6 +19,7 @@ import type {
ConversationRecord, ConversationRecord,
MessageRecord, MessageRecord,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import { coreEvents } from '@google/gemini-cli-core';
// Mock modules // Mock modules
vi.mock('fs/promises'); vi.mock('fs/promises');
@@ -52,6 +53,7 @@ describe('useSessionBrowser', () => {
beforeEach(() => { beforeEach(() => {
vi.resetAllMocks(); vi.resetAllMocks();
vi.spyOn(coreEvents, 'emitFeedback').mockImplementation(() => {});
mockedPath.join.mockImplementation((...args) => args.join('/')); mockedPath.join.mockImplementation((...args) => args.join('/'));
vi.mocked(mockConfig.storage.getProjectTempDir).mockReturnValue( vi.mocked(mockConfig.storage.getProjectTempDir).mockReturnValue(
MOCKED_PROJECT_TEMP_DIR, MOCKED_PROJECT_TEMP_DIR,
@@ -100,9 +102,6 @@ describe('useSessionBrowser', () => {
fileName: MOCKED_FILENAME, fileName: MOCKED_FILENAME,
} as SessionInfo; } as SessionInfo;
mockedFs.readFile.mockRejectedValue(new Error('File not found')); mockedFs.readFile.mockRejectedValue(new Error('File not found'));
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
const { result } = renderHook(() => const { result } = renderHook(() =>
useSessionBrowser(mockConfig, mockOnLoadHistory), useSessionBrowser(mockConfig, mockOnLoadHistory),
@@ -112,9 +111,12 @@ describe('useSessionBrowser', () => {
await result.current.handleResumeSession(mockSession); await result.current.handleResumeSession(mockSession);
}); });
expect(consoleErrorSpy).toHaveBeenCalled(); expect(coreEvents.emitFeedback).toHaveBeenCalledWith(
'error',
'Error resuming session:',
expect.any(Error),
);
expect(result.current.isSessionBrowserOpen).toBe(false); expect(result.current.isSessionBrowserOpen).toBe(false);
consoleErrorSpy.mockRestore();
}); });
it('should handle JSON parse error', async () => { it('should handle JSON parse error', async () => {
@@ -124,9 +126,6 @@ describe('useSessionBrowser', () => {
fileName: MOCKED_FILENAME, fileName: MOCKED_FILENAME,
} as SessionInfo; } as SessionInfo;
mockedFs.readFile.mockResolvedValue('invalid json'); mockedFs.readFile.mockResolvedValue('invalid json');
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
const { result } = renderHook(() => const { result } = renderHook(() =>
useSessionBrowser(mockConfig, mockOnLoadHistory), useSessionBrowser(mockConfig, mockOnLoadHistory),
@@ -136,9 +135,12 @@ describe('useSessionBrowser', () => {
await result.current.handleResumeSession(mockSession); await result.current.handleResumeSession(mockSession);
}); });
expect(consoleErrorSpy).toHaveBeenCalled(); expect(coreEvents.emitFeedback).toHaveBeenCalledWith(
'error',
'Error resuming session:',
expect.any(Error),
);
expect(result.current.isSessionBrowserOpen).toBe(false); expect(result.current.isSessionBrowserOpen).toBe(false);
consoleErrorSpy.mockRestore();
}); });
}); });
@@ -14,7 +14,7 @@ import type {
ResumedSessionData, ResumedSessionData,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import type { Part } from '@google/genai'; import type { Part } from '@google/genai';
import { partListUnionToString } from '@google/gemini-cli-core'; import { partListUnionToString, coreEvents } from '@google/gemini-cli-core';
import type { SessionInfo } from '../../utils/sessionUtils.js'; import type { SessionInfo } from '../../utils/sessionUtils.js';
import { MessageType, ToolCallStatus } from '../types.js'; import { MessageType, ToolCallStatus } from '../types.js';
@@ -79,7 +79,7 @@ export const useSessionBrowser = (
resumedSessionData, resumedSessionData,
); );
} catch (error) { } catch (error) {
console.error('Error resuming session:', error); coreEvents.emitFeedback('error', 'Error resuming session:', error);
setIsSessionBrowserOpen(false); setIsSessionBrowserOpen(false);
} }
}, },
@@ -103,7 +103,7 @@ export const useSessionBrowser = (
chatRecordingService.deleteSession(session.file); chatRecordingService.deleteSession(session.file);
} }
} catch (error) { } catch (error) {
console.error('Error deleting session:', error); coreEvents.emitFeedback('error', 'Error deleting session:', error);
throw error; throw error;
} }
}, },
+3 -3
View File
@@ -7,7 +7,7 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import * as fs from 'node:fs/promises'; import * as fs from 'node:fs/promises';
import * as path from 'node:path'; import * as path from 'node:path';
import { isNodeError, Storage } from '@google/gemini-cli-core'; import { debugLogger, isNodeError, Storage } from '@google/gemini-cli-core';
const MAX_HISTORY_LENGTH = 100; const MAX_HISTORY_LENGTH = 100;
@@ -52,7 +52,7 @@ async function readHistoryFile(filePath: string): Promise<string[]> {
return result; return result;
} catch (err) { } catch (err) {
if (isNodeError(err) && err.code === 'ENOENT') return []; if (isNodeError(err) && err.code === 'ENOENT') return [];
console.error('Error reading history:', err); debugLogger.error('Error reading history:', err);
return []; return [];
} }
} }
@@ -65,7 +65,7 @@ async function writeHistoryFile(
await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, history.join('\n')); await fs.writeFile(filePath, history.join('\n'));
} catch (error) { } catch (error) {
console.error('Error writing shell history:', error); debugLogger.error('Error writing shell history:', error);
} }
} }
@@ -4,6 +4,8 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
/* eslint-disable no-console */
import util from 'node:util'; import util from 'node:util';
import type { ConsoleMessageItem } from '../types.js'; import type { ConsoleMessageItem } from '../types.js';
@@ -8,6 +8,7 @@ import React from 'react';
import { Text } from 'ink'; import { Text } from 'ink';
import { theme } from '../semantic-colors.js'; import { theme } from '../semantic-colors.js';
import stringWidth from 'string-width'; import stringWidth from 'string-width';
import { debugLogger } from '@google/gemini-cli-core';
// Constants for Markdown parsing // Constants for Markdown parsing
const BOLD_MARKER_LENGTH = 2; // For "**" const BOLD_MARKER_LENGTH = 2; // For "**"
@@ -144,7 +145,7 @@ const RenderInlineInternal: React.FC<RenderInlineProps> = ({
); );
} }
} catch (e) { } catch (e) {
console.error('Error parsing inline markdown part:', fullMatch, e); debugLogger.warn('Error parsing inline markdown part:', fullMatch, e);
renderedNode = null; renderedNode = null;
} }
+128 -38
View File
@@ -4,9 +4,22 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import { vi, type MockInstance } from 'vitest'; import {
vi,
type MockInstance,
describe,
it,
expect,
beforeEach,
afterEach,
} from 'vitest';
import type { Config } from '@google/gemini-cli-core'; import type { Config } from '@google/gemini-cli-core';
import { OutputFormat, FatalInputError } from '@google/gemini-cli-core'; import {
OutputFormat,
FatalInputError,
debugLogger,
coreEvents,
} from '@google/gemini-cli-core';
import { import {
getErrorMessage, getErrorMessage,
handleError, handleError,
@@ -14,6 +27,12 @@ import {
handleCancellationError, handleCancellationError,
handleMaxTurnsExceededError, handleMaxTurnsExceededError,
} from './errors.js'; } from './errors.js';
import { runSyncCleanup } from './cleanup.js';
// Mock the cleanup module
vi.mock('./cleanup.js', () => ({
runSyncCleanup: vi.fn(),
}));
// Mock the core modules // Mock the core modules
vi.mock('@google/gemini-cli-core', async (importOriginal) => { vi.mock('@google/gemini-cli-core', async (importOriginal) => {
@@ -63,6 +82,9 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
JsonStreamEventType: { JsonStreamEventType: {
RESULT: 'result', RESULT: 'result',
}, },
coreEvents: {
emitFeedback: vi.fn(),
},
FatalToolExecutionError: class extends Error { FatalToolExecutionError: class extends Error {
constructor(message: string) { constructor(message: string) {
super(message); super(message);
@@ -85,7 +107,10 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
describe('errors', () => { describe('errors', () => {
let mockConfig: Config; let mockConfig: Config;
let processExitSpy: MockInstance; let processExitSpy: MockInstance;
let consoleErrorSpy: MockInstance; let debugLoggerErrorSpy: MockInstance;
let debugLoggerWarnSpy: MockInstance;
let coreEventsEmitFeedbackSpy: MockInstance;
let runSyncCleanupSpy: MockInstance;
const TEST_SESSION_ID = 'test-session-123'; const TEST_SESSION_ID = 'test-session-123';
@@ -93,8 +118,19 @@ describe('errors', () => {
// Reset mocks // Reset mocks
vi.clearAllMocks(); vi.clearAllMocks();
// Mock console.error // Mock debugLogger
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); debugLoggerErrorSpy = vi
.spyOn(debugLogger, 'error')
.mockImplementation(() => {});
debugLoggerWarnSpy = vi
.spyOn(debugLogger, 'warn')
.mockImplementation(() => {});
// Mock coreEvents
coreEventsEmitFeedbackSpy = vi.mocked(coreEvents.emitFeedback);
// Mock runSyncCleanup
runSyncCleanupSpy = vi.mocked(runSyncCleanup);
// Mock process.exit to throw instead of actually exiting // Mock process.exit to throw instead of actually exiting
processExitSpy = vi.spyOn(process, 'exit').mockImplementation((code) => { processExitSpy = vi.spyOn(process, 'exit').mockImplementation((code) => {
@@ -110,7 +146,8 @@ describe('errors', () => {
}); });
afterEach(() => { afterEach(() => {
consoleErrorSpy.mockRestore(); debugLoggerErrorSpy.mockRestore();
debugLoggerWarnSpy.mockRestore();
processExitSpy.mockRestore(); processExitSpy.mockRestore();
}); });
@@ -141,14 +178,14 @@ describe('errors', () => {
).mockReturnValue(OutputFormat.TEXT); ).mockReturnValue(OutputFormat.TEXT);
}); });
it('should log error message and re-throw', () => { it('should re-throw without logging to debugLogger', () => {
const testError = new Error('Test error'); const testError = new Error('Test error');
expect(() => { expect(() => {
handleError(testError, mockConfig); handleError(testError, mockConfig);
}).toThrow(testError); }).toThrow(testError);
expect(consoleErrorSpy).toHaveBeenCalledWith('API Error: Test error'); expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
}); });
it('should handle non-Error objects', () => { it('should handle non-Error objects', () => {
@@ -157,8 +194,6 @@ describe('errors', () => {
expect(() => { expect(() => {
handleError(testError, mockConfig); handleError(testError, mockConfig);
}).toThrow(testError); }).toThrow(testError);
expect(consoleErrorSpy).toHaveBeenCalledWith('API Error: String error');
}); });
}); });
@@ -169,14 +204,16 @@ describe('errors', () => {
).mockReturnValue(OutputFormat.JSON); ).mockReturnValue(OutputFormat.JSON);
}); });
it('should format error as JSON and exit with default code', () => { it('should format error as JSON, emit feedback exactly once, and exit with default code', () => {
const testError = new Error('Test error'); const testError = new Error('Test error');
expect(() => { expect(() => {
handleError(testError, mockConfig); handleError(testError, mockConfig);
}).toThrow('process.exit called with code: 1'); }).toThrow('process.exit called with code: 1');
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledTimes(1);
expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledWith(
'error',
JSON.stringify( JSON.stringify(
{ {
session_id: TEST_SESSION_ID, session_id: TEST_SESSION_ID,
@@ -190,16 +227,20 @@ describe('errors', () => {
2, 2,
), ),
); );
expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
expect(runSyncCleanupSpy).toHaveBeenCalled();
}); });
it('should use custom error code when provided', () => { it('should use custom error code when provided and only surface once', () => {
const testError = new Error('Test error'); const testError = new Error('Test error');
expect(() => { expect(() => {
handleError(testError, mockConfig, 42); handleError(testError, mockConfig, 42);
}).toThrow('process.exit called with code: 42'); }).toThrow('process.exit called with code: 42');
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledTimes(1);
expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledWith(
'error',
JSON.stringify( JSON.stringify(
{ {
session_id: TEST_SESSION_ID, session_id: TEST_SESSION_ID,
@@ -213,16 +254,19 @@ describe('errors', () => {
2, 2,
), ),
); );
expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
}); });
it('should extract exitCode from FatalError instances', () => { it('should extract exitCode from FatalError instances and only surface once', () => {
const fatalError = new FatalInputError('Fatal error'); const fatalError = new FatalInputError('Fatal error');
expect(() => { expect(() => {
handleError(fatalError, mockConfig); handleError(fatalError, mockConfig);
}).toThrow('process.exit called with code: 42'); }).toThrow('process.exit called with code: 42');
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledTimes(1);
expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledWith(
'error',
JSON.stringify( JSON.stringify(
{ {
session_id: TEST_SESSION_ID, session_id: TEST_SESSION_ID,
@@ -236,6 +280,7 @@ describe('errors', () => {
2, 2,
), ),
); );
expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
}); });
it('should handle error with code property', () => { it('should handle error with code property', () => {
@@ -259,7 +304,8 @@ describe('errors', () => {
handleError(errorWithStatus, mockConfig); handleError(errorWithStatus, mockConfig);
}).toThrow('process.exit called with code: 1'); // string codes become 1 }).toThrow('process.exit called with code: 1'); // string codes become 1
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledWith(
'error',
JSON.stringify( JSON.stringify(
{ {
session_id: TEST_SESSION_ID, session_id: TEST_SESSION_ID,
@@ -283,12 +329,14 @@ describe('errors', () => {
).mockReturnValue(OutputFormat.STREAM_JSON); ).mockReturnValue(OutputFormat.STREAM_JSON);
}); });
it('should emit result event and exit', () => { it('should emit result event, run cleanup, and exit', () => {
const testError = new Error('Test error'); const testError = new Error('Test error');
expect(() => { expect(() => {
handleError(testError, mockConfig); handleError(testError, mockConfig);
}).toThrow('process.exit called with code: 1'); }).toThrow('process.exit called with code: 1');
expect(runSyncCleanupSpy).toHaveBeenCalled();
}); });
it('should extract exitCode from FatalError instances', () => { it('should extract exitCode from FatalError instances', () => {
@@ -312,10 +360,10 @@ describe('errors', () => {
).mockReturnValue(OutputFormat.TEXT); ).mockReturnValue(OutputFormat.TEXT);
}); });
it('should log error message to stderr', () => { it('should log error message to stderr (via debugLogger) for non-fatal', () => {
handleToolError(toolName, toolError, mockConfig); handleToolError(toolName, toolError, mockConfig);
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(debugLoggerWarnSpy).toHaveBeenCalledWith(
'Error executing tool test-tool: Tool failed', 'Error executing tool test-tool: Tool failed',
); );
}); });
@@ -329,10 +377,24 @@ describe('errors', () => {
'Custom display message', 'Custom display message',
); );
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(debugLoggerWarnSpy).toHaveBeenCalledWith(
'Error executing tool test-tool: Custom display message', 'Error executing tool test-tool: Custom display message',
); );
}); });
it('should emit feedback exactly once for fatal errors and not use debugLogger', () => {
expect(() => {
handleToolError(toolName, toolError, mockConfig, 'no_space_left');
}).toThrow('process.exit called with code: 54');
expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledTimes(1);
expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledWith(
'error',
'Error executing tool test-tool: Tool failed',
);
expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
expect(runSyncCleanupSpy).toHaveBeenCalled();
});
}); });
describe('in JSON mode', () => { describe('in JSON mode', () => {
@@ -351,29 +413,32 @@ describe('errors', () => {
'invalid_tool_params', 'invalid_tool_params',
); );
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(debugLoggerWarnSpy).toHaveBeenCalledWith(
'Error executing tool test-tool: Tool failed', 'Error executing tool test-tool: Tool failed',
); );
// Should not exit for non-fatal errors // Should not exit for non-fatal errors
expect(processExitSpy).not.toHaveBeenCalled(); expect(processExitSpy).not.toHaveBeenCalled();
expect(coreEventsEmitFeedbackSpy).not.toHaveBeenCalled();
}); });
it('should not exit for file not found errors', () => { it('should not exit for file not found errors', () => {
handleToolError(toolName, toolError, mockConfig, 'file_not_found'); handleToolError(toolName, toolError, mockConfig, 'file_not_found');
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(debugLoggerWarnSpy).toHaveBeenCalledWith(
'Error executing tool test-tool: Tool failed', 'Error executing tool test-tool: Tool failed',
); );
expect(processExitSpy).not.toHaveBeenCalled(); expect(processExitSpy).not.toHaveBeenCalled();
expect(coreEventsEmitFeedbackSpy).not.toHaveBeenCalled();
}); });
it('should not exit for permission denied errors', () => { it('should not exit for permission denied errors', () => {
handleToolError(toolName, toolError, mockConfig, 'permission_denied'); handleToolError(toolName, toolError, mockConfig, 'permission_denied');
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(debugLoggerWarnSpy).toHaveBeenCalledWith(
'Error executing tool test-tool: Tool failed', 'Error executing tool test-tool: Tool failed',
); );
expect(processExitSpy).not.toHaveBeenCalled(); expect(processExitSpy).not.toHaveBeenCalled();
expect(coreEventsEmitFeedbackSpy).not.toHaveBeenCalled();
}); });
it('should not exit for path not in workspace errors', () => { it('should not exit for path not in workspace errors', () => {
@@ -384,10 +449,11 @@ describe('errors', () => {
'path_not_in_workspace', 'path_not_in_workspace',
); );
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(debugLoggerWarnSpy).toHaveBeenCalledWith(
'Error executing tool test-tool: Tool failed', 'Error executing tool test-tool: Tool failed',
); );
expect(processExitSpy).not.toHaveBeenCalled(); expect(processExitSpy).not.toHaveBeenCalled();
expect(coreEventsEmitFeedbackSpy).not.toHaveBeenCalled();
}); });
it('should prefer resultDisplay over error message', () => { it('should prefer resultDisplay over error message', () => {
@@ -399,7 +465,7 @@ describe('errors', () => {
'Display message', 'Display message',
); );
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(debugLoggerWarnSpy).toHaveBeenCalledWith(
'Error executing tool test-tool: Display message', 'Error executing tool test-tool: Display message',
); );
expect(processExitSpy).not.toHaveBeenCalled(); expect(processExitSpy).not.toHaveBeenCalled();
@@ -407,12 +473,14 @@ describe('errors', () => {
}); });
describe('fatal errors', () => { describe('fatal errors', () => {
it('should exit immediately for NO_SPACE_LEFT errors', () => { it('should exit immediately for NO_SPACE_LEFT errors and only surface once', () => {
expect(() => { expect(() => {
handleToolError(toolName, toolError, mockConfig, 'no_space_left'); handleToolError(toolName, toolError, mockConfig, 'no_space_left');
}).toThrow('process.exit called with code: 54'); }).toThrow('process.exit called with code: 54');
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledTimes(1);
expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledWith(
'error',
JSON.stringify( JSON.stringify(
{ {
session_id: TEST_SESSION_ID, session_id: TEST_SESSION_ID,
@@ -426,6 +494,8 @@ describe('errors', () => {
2, 2,
), ),
); );
expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
expect(runSyncCleanupSpy).toHaveBeenCalled();
}); });
}); });
}); });
@@ -437,15 +507,17 @@ describe('errors', () => {
).mockReturnValue(OutputFormat.STREAM_JSON); ).mockReturnValue(OutputFormat.STREAM_JSON);
}); });
it('should emit result event and exit for fatal errors', () => { it('should emit result event, run cleanup, and exit for fatal errors', () => {
expect(() => { expect(() => {
handleToolError(toolName, toolError, mockConfig, 'no_space_left'); handleToolError(toolName, toolError, mockConfig, 'no_space_left');
}).toThrow('process.exit called with code: 54'); }).toThrow('process.exit called with code: 54');
expect(runSyncCleanupSpy).toHaveBeenCalled();
expect(coreEventsEmitFeedbackSpy).not.toHaveBeenCalled(); // Stream mode uses emitEvent
}); });
it('should log to stderr and not exit for non-fatal errors', () => { it('should log to stderr and not exit for non-fatal errors', () => {
handleToolError(toolName, toolError, mockConfig, 'invalid_tool_params'); handleToolError(toolName, toolError, mockConfig, 'invalid_tool_params');
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(debugLoggerWarnSpy).toHaveBeenCalledWith(
'Error executing tool test-tool: Tool failed', 'Error executing tool test-tool: Tool failed',
); );
expect(processExitSpy).not.toHaveBeenCalled(); expect(processExitSpy).not.toHaveBeenCalled();
@@ -461,12 +533,18 @@ describe('errors', () => {
).mockReturnValue(OutputFormat.TEXT); ).mockReturnValue(OutputFormat.TEXT);
}); });
it('should log cancellation message and exit with 130', () => { it('should emit feedback exactly once, run cleanup, and exit with 130', () => {
expect(() => { expect(() => {
handleCancellationError(mockConfig); handleCancellationError(mockConfig);
}).toThrow('process.exit called with code: 130'); }).toThrow('process.exit called with code: 130');
expect(consoleErrorSpy).toHaveBeenCalledWith('Operation cancelled.'); expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledTimes(1);
expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledWith(
'error',
'Operation cancelled.',
);
expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
expect(runSyncCleanupSpy).toHaveBeenCalled();
}); });
}); });
@@ -477,12 +555,14 @@ describe('errors', () => {
).mockReturnValue(OutputFormat.JSON); ).mockReturnValue(OutputFormat.JSON);
}); });
it('should format cancellation as JSON and exit with 130', () => { it('should format cancellation as JSON, emit feedback once, and exit with 130', () => {
expect(() => { expect(() => {
handleCancellationError(mockConfig); handleCancellationError(mockConfig);
}).toThrow('process.exit called with code: 130'); }).toThrow('process.exit called with code: 130');
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledTimes(1);
expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledWith(
'error',
JSON.stringify( JSON.stringify(
{ {
session_id: TEST_SESSION_ID, session_id: TEST_SESSION_ID,
@@ -496,6 +576,7 @@ describe('errors', () => {
2, 2,
), ),
); );
expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
}); });
}); });
@@ -510,6 +591,7 @@ describe('errors', () => {
expect(() => { expect(() => {
handleCancellationError(mockConfig); handleCancellationError(mockConfig);
}).toThrow('process.exit called with code: 130'); }).toThrow('process.exit called with code: 130');
expect(coreEventsEmitFeedbackSpy).not.toHaveBeenCalled();
}); });
}); });
}); });
@@ -522,14 +604,18 @@ describe('errors', () => {
).mockReturnValue(OutputFormat.TEXT); ).mockReturnValue(OutputFormat.TEXT);
}); });
it('should log max turns message and exit with 53', () => { it('should emit feedback exactly once, run cleanup, and exit with 53', () => {
expect(() => { expect(() => {
handleMaxTurnsExceededError(mockConfig); handleMaxTurnsExceededError(mockConfig);
}).toThrow('process.exit called with code: 53'); }).toThrow('process.exit called with code: 53');
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledTimes(1);
expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledWith(
'error',
'Reached max session turns for this session. Increase the number of turns by specifying maxSessionTurns in settings.json.', 'Reached max session turns for this session. Increase the number of turns by specifying maxSessionTurns in settings.json.',
); );
expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
expect(runSyncCleanupSpy).toHaveBeenCalled();
}); });
}); });
@@ -540,12 +626,14 @@ describe('errors', () => {
).mockReturnValue(OutputFormat.JSON); ).mockReturnValue(OutputFormat.JSON);
}); });
it('should format max turns error as JSON and exit with 53', () => { it('should format max turns error as JSON, emit feedback once, and exit with 53', () => {
expect(() => { expect(() => {
handleMaxTurnsExceededError(mockConfig); handleMaxTurnsExceededError(mockConfig);
}).toThrow('process.exit called with code: 53'); }).toThrow('process.exit called with code: 53');
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledTimes(1);
expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledWith(
'error',
JSON.stringify( JSON.stringify(
{ {
session_id: TEST_SESSION_ID, session_id: TEST_SESSION_ID,
@@ -560,6 +648,7 @@ describe('errors', () => {
2, 2,
), ),
); );
expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
}); });
}); });
@@ -574,6 +663,7 @@ describe('errors', () => {
expect(() => { expect(() => {
handleMaxTurnsExceededError(mockConfig); handleMaxTurnsExceededError(mockConfig);
}).toThrow('process.exit called with code: 53'); }).toThrow('process.exit called with code: 53');
expect(coreEventsEmitFeedbackSpy).not.toHaveBeenCalled();
}); });
}); });
}); });
+10 -9
View File
@@ -16,6 +16,8 @@ import {
FatalCancellationError, FatalCancellationError,
FatalToolExecutionError, FatalToolExecutionError,
isFatalToolError, isFatalToolError,
debugLogger,
coreEvents,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import { runSyncCleanup } from './cleanup.js'; import { runSyncCleanup } from './cleanup.js';
@@ -103,11 +105,10 @@ export function handleError(
config.getSessionId(), config.getSessionId(),
); );
console.error(formattedError); coreEvents.emitFeedback('error', formattedError);
runSyncCleanup(); runSyncCleanup();
process.exit(getNumericExitCode(errorCode)); process.exit(getNumericExitCode(errorCode));
} else { } else {
console.error(errorMessage);
throw error; throw error;
} }
} }
@@ -155,16 +156,16 @@ export function handleToolError(
errorType ?? toolExecutionError.exitCode, errorType ?? toolExecutionError.exitCode,
config.getSessionId(), config.getSessionId(),
); );
console.error(formattedError); coreEvents.emitFeedback('error', formattedError);
} else { } else {
console.error(errorMessage); coreEvents.emitFeedback('error', errorMessage);
} }
runSyncCleanup(); runSyncCleanup();
process.exit(toolExecutionError.exitCode); process.exit(toolExecutionError.exitCode);
} }
// Non-fatal: log and continue // Non-fatal: log and continue
console.error(errorMessage); debugLogger.warn(errorMessage);
} }
/** /**
@@ -196,11 +197,11 @@ export function handleCancellationError(config: Config): never {
config.getSessionId(), config.getSessionId(),
); );
console.error(formattedError); coreEvents.emitFeedback('error', formattedError);
runSyncCleanup(); runSyncCleanup();
process.exit(cancellationError.exitCode); process.exit(cancellationError.exitCode);
} else { } else {
console.error(cancellationError.message); coreEvents.emitFeedback('error', cancellationError.message);
runSyncCleanup(); runSyncCleanup();
process.exit(cancellationError.exitCode); process.exit(cancellationError.exitCode);
} }
@@ -237,11 +238,11 @@ export function handleMaxTurnsExceededError(config: Config): never {
config.getSessionId(), config.getSessionId(),
); );
console.error(formattedError); coreEvents.emitFeedback('error', formattedError);
runSyncCleanup(); runSyncCleanup();
process.exit(maxTurnsError.exitCode); process.exit(maxTurnsError.exitCode);
} else { } else {
console.error(maxTurnsError.message); coreEvents.emitFeedback('error', maxTurnsError.message);
runSyncCleanup(); runSyncCleanup();
process.exit(maxTurnsError.exitCode); process.exit(maxTurnsError.exitCode);
} }
+19 -9
View File
@@ -18,6 +18,19 @@ import { RELAUNCH_EXIT_CODE } from './processUtils.js';
import type { ChildProcess } from 'node:child_process'; import type { ChildProcess } from 'node:child_process';
import { spawn } from 'node:child_process'; import { spawn } from 'node:child_process';
const mocks = vi.hoisted(() => ({
writeToStderr: vi.fn(),
}));
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
writeToStderr: mocks.writeToStderr,
};
});
vi.mock('node:child_process', async (importOriginal) => { vi.mock('node:child_process', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:child_process')>(); const actual = await importOriginal<typeof import('node:child_process')>();
return { return {
@@ -33,23 +46,21 @@ import { relaunchAppInChildProcess, relaunchOnExitCode } from './relaunch.js';
describe('relaunchOnExitCode', () => { describe('relaunchOnExitCode', () => {
let processExitSpy: MockInstance; let processExitSpy: MockInstance;
let consoleErrorSpy: MockInstance;
let stdinResumeSpy: MockInstance; let stdinResumeSpy: MockInstance;
beforeEach(() => { beforeEach(() => {
processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => { processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('PROCESS_EXIT_CALLED'); throw new Error('PROCESS_EXIT_CALLED');
}); });
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
stdinResumeSpy = vi stdinResumeSpy = vi
.spyOn(process.stdin, 'resume') .spyOn(process.stdin, 'resume')
.mockImplementation(() => process.stdin); .mockImplementation(() => process.stdin);
vi.clearAllMocks(); vi.clearAllMocks();
mocks.writeToStderr.mockClear();
}); });
afterEach(() => { afterEach(() => {
processExitSpy.mockRestore(); processExitSpy.mockRestore();
consoleErrorSpy.mockRestore();
stdinResumeSpy.mockRestore(); stdinResumeSpy.mockRestore();
}); });
@@ -90,9 +101,10 @@ describe('relaunchOnExitCode', () => {
); );
expect(runner).toHaveBeenCalledTimes(1); expect(runner).toHaveBeenCalledTimes(1);
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(mocks.writeToStderr).toHaveBeenCalledWith(
'Fatal error: Failed to relaunch the CLI process.', expect.stringContaining(
error, 'Fatal error: Failed to relaunch the CLI process.',
),
); );
expect(stdinResumeSpy).toHaveBeenCalled(); expect(stdinResumeSpy).toHaveBeenCalled();
expect(processExitSpy).toHaveBeenCalledWith(1); expect(processExitSpy).toHaveBeenCalledWith(1);
@@ -101,7 +113,6 @@ describe('relaunchOnExitCode', () => {
describe('relaunchAppInChildProcess', () => { describe('relaunchAppInChildProcess', () => {
let processExitSpy: MockInstance; let processExitSpy: MockInstance;
let consoleErrorSpy: MockInstance;
let stdinPauseSpy: MockInstance; let stdinPauseSpy: MockInstance;
let stdinResumeSpy: MockInstance; let stdinResumeSpy: MockInstance;
@@ -113,6 +124,7 @@ describe('relaunchAppInChildProcess', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
mocks.writeToStderr.mockClear();
process.env = { ...originalEnv }; process.env = { ...originalEnv };
delete process.env['GEMINI_CLI_NO_RELAUNCH']; delete process.env['GEMINI_CLI_NO_RELAUNCH'];
@@ -124,7 +136,6 @@ describe('relaunchAppInChildProcess', () => {
processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => { processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('PROCESS_EXIT_CALLED'); throw new Error('PROCESS_EXIT_CALLED');
}); });
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
stdinPauseSpy = vi stdinPauseSpy = vi
.spyOn(process.stdin, 'pause') .spyOn(process.stdin, 'pause')
.mockImplementation(() => process.stdin); .mockImplementation(() => process.stdin);
@@ -140,7 +151,6 @@ describe('relaunchAppInChildProcess', () => {
process.execPath = originalExecPath; process.execPath = originalExecPath;
processExitSpy.mockRestore(); processExitSpy.mockRestore();
consoleErrorSpy.mockRestore();
stdinPauseSpy.mockRestore(); stdinPauseSpy.mockRestore();
stdinResumeSpy.mockRestore(); stdinResumeSpy.mockRestore();
}); });
+6 -1
View File
@@ -6,6 +6,7 @@
import { spawn } from 'node:child_process'; import { spawn } from 'node:child_process';
import { RELAUNCH_EXIT_CODE } from './processUtils.js'; import { RELAUNCH_EXIT_CODE } from './processUtils.js';
import { writeToStderr } from '@google/gemini-cli-core';
export async function relaunchOnExitCode(runner: () => Promise<number>) { export async function relaunchOnExitCode(runner: () => Promise<number>) {
while (true) { while (true) {
@@ -17,7 +18,11 @@ export async function relaunchOnExitCode(runner: () => Promise<number>) {
} }
} catch (error) { } catch (error) {
process.stdin.resume(); process.stdin.resume();
console.error('Fatal error: Failed to relaunch the CLI process.', error); const errorMessage =
error instanceof Error ? (error.stack ?? error.message) : String(error);
writeToStderr(
`Fatal error: Failed to relaunch the CLI process.\n${errorMessage}\n`,
);
process.exit(1); process.exit(1);
} }
} }
+16 -6
View File
@@ -251,7 +251,9 @@ export async function start_sandbox(
} }
// stop if image is missing // stop if image is missing
if (!(await ensureSandboxImageIsPresent(config.command, image))) { if (
!(await ensureSandboxImageIsPresent(config.command, image, cliConfig))
) {
const remedy = const remedy =
image === LOCAL_DEV_SANDBOX_IMAGE_NAME image === LOCAL_DEV_SANDBOX_IMAGE_NAME
? 'Try running `npm run build:all` or `npm run build:sandbox` under the gemini-cli repo to build it locally, or check the image name and your network connection.' ? 'Try running `npm run build:all` or `npm run build:sandbox` under the gemini-cli repo to build it locally, or check the image name and your network connection.'
@@ -718,8 +720,12 @@ async function imageExists(sandbox: string, image: string): Promise<boolean> {
}); });
} }
async function pullImage(sandbox: string, image: string): Promise<boolean> { async function pullImage(
console.info(`Attempting to pull image ${image} using ${sandbox}...`); sandbox: string,
image: string,
cliConfig?: Config,
): Promise<boolean> {
debugLogger.debug(`Attempting to pull image ${image} using ${sandbox}...`);
return new Promise((resolve) => { return new Promise((resolve) => {
const args = ['pull', image]; const args = ['pull', image];
const pullProcess = spawn(sandbox, args, { stdio: 'pipe' }); const pullProcess = spawn(sandbox, args, { stdio: 'pipe' });
@@ -727,11 +733,14 @@ async function pullImage(sandbox: string, image: string): Promise<boolean> {
let stderrData = ''; let stderrData = '';
const onStdoutData = (data: Buffer) => { const onStdoutData = (data: Buffer) => {
console.info(data.toString().trim()); // Show pull progress if (cliConfig?.getDebugMode() || process.env['DEBUG']) {
debugLogger.log(data.toString().trim()); // Show pull progress
}
}; };
const onStderrData = (data: Buffer) => { const onStderrData = (data: Buffer) => {
stderrData += data.toString(); stderrData += data.toString();
// eslint-disable-next-line no-console
console.error(data.toString().trim()); // Show pull errors/info from the command itself console.error(data.toString().trim()); // Show pull errors/info from the command itself
}; };
@@ -745,7 +754,7 @@ async function pullImage(sandbox: string, image: string): Promise<boolean> {
const onClose = (code: number | null) => { const onClose = (code: number | null) => {
if (code === 0) { if (code === 0) {
console.info(`Successfully pulled image ${image}.`); debugLogger.log(`Successfully pulled image ${image}.`);
cleanup(); cleanup();
resolve(true); resolve(true);
} else { } else {
@@ -788,6 +797,7 @@ async function pullImage(sandbox: string, image: string): Promise<boolean> {
async function ensureSandboxImageIsPresent( async function ensureSandboxImageIsPresent(
sandbox: string, sandbox: string,
image: string, image: string,
cliConfig?: Config,
): Promise<boolean> { ): Promise<boolean> {
debugLogger.log(`Checking for sandbox image: ${image}`); debugLogger.log(`Checking for sandbox image: ${image}`);
if (await imageExists(sandbox, image)) { if (await imageExists(sandbox, image)) {
@@ -801,7 +811,7 @@ async function ensureSandboxImageIsPresent(
return false; return false;
} }
if (await pullImage(sandbox, image)) { if (await pullImage(sandbox, image, cliConfig)) {
// After attempting to pull, check again to be certain // After attempting to pull, check again to be certain
if (await imageExists(sandbox, image)) { if (await imageExists(sandbox, image)) {
debugLogger.log(`Sandbox image ${image} is now available after pulling.`); debugLogger.log(`Sandbox image ${image} is now available after pulling.`);
@@ -7,7 +7,11 @@
import { describe, it, expect, vi } from 'vitest'; import { describe, it, expect, vi } from 'vitest';
import { cleanupExpiredSessions } from './sessionCleanup.js'; import { cleanupExpiredSessions } from './sessionCleanup.js';
import type { Settings } from '../config/settings.js'; import type { Settings } from '../config/settings.js';
import { SESSION_FILE_PREFIX, type Config } from '@google/gemini-cli-core'; import {
SESSION_FILE_PREFIX,
type Config,
debugLogger,
} from '@google/gemini-cli-core';
// Create a mock config for integration testing // Create a mock config for integration testing
function createTestConfig(): Config { function createTestConfig(): Config {
@@ -112,7 +116,7 @@ describe('Session Cleanup Integration', () => {
}); });
it('should validate configuration and fail gracefully', async () => { it('should validate configuration and fail gracefully', async () => {
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const errorSpy = vi.spyOn(debugLogger, 'warn').mockImplementation(() => {});
const config = createTestConfig(); const config = createTestConfig();
const settings: Settings = { const settings: Settings = {
+18 -109
View File
@@ -100,6 +100,8 @@ function createTestSessions(): SessionInfo[] {
describe('Session Cleanup', () => { describe('Session Cleanup', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
vi.spyOn(debugLogger, 'error').mockImplementation(() => {});
vi.spyOn(debugLogger, 'warn').mockImplementation(() => {});
// By default, return all test sessions as valid // By default, return all test sessions as valid
const sessions = createTestSessions(); const sessions = createTestSessions();
mockGetAllSessionFiles.mockResolvedValue( mockGetAllSessionFiles.mockResolvedValue(
@@ -154,20 +156,16 @@ describe('Session Cleanup', () => {
}, },
}; };
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const result = await cleanupExpiredSessions(config, settings); const result = await cleanupExpiredSessions(config, settings);
expect(result.disabled).toBe(true); expect(result.disabled).toBe(true);
expect(result.scanned).toBe(0); expect(result.scanned).toBe(0);
expect(result.deleted).toBe(0); expect(result.deleted).toBe(0);
expect(errorSpy).toHaveBeenCalledWith( expect(debugLogger.warn).toHaveBeenCalledWith(
expect.stringContaining( expect.stringContaining(
'Session cleanup disabled: Error: Invalid retention period format', 'Session cleanup disabled: Error: Invalid retention period format',
), ),
); );
errorSpy.mockRestore();
}); });
it('should delete sessions older than maxAge', async () => { it('should delete sessions older than maxAge', async () => {
@@ -338,8 +336,6 @@ describe('Session Cleanup', () => {
}, },
}; };
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
// Mock getSessionFiles to throw an error // Mock getSessionFiles to throw an error
mockGetAllSessionFiles.mockRejectedValue( mockGetAllSessionFiles.mockRejectedValue(
new Error('Directory access failed'), new Error('Directory access failed'),
@@ -349,11 +345,9 @@ describe('Session Cleanup', () => {
expect(result.disabled).toBe(false); expect(result.disabled).toBe(false);
expect(result.failed).toBe(1); expect(result.failed).toBe(1);
expect(errorSpy).toHaveBeenCalledWith( expect(debugLogger.warn).toHaveBeenCalledWith(
'Session cleanup failed: Directory access failed', 'Session cleanup failed: Directory access failed',
); );
errorSpy.mockRestore();
}); });
it('should respect minRetention configuration', async () => { it('should respect minRetention configuration', async () => {
@@ -979,21 +973,17 @@ describe('Session Cleanup', () => {
}, },
}; };
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const result = await cleanupExpiredSessions(config, settings); const result = await cleanupExpiredSessions(config, settings);
expect(result.disabled).toBe(true); expect(result.disabled).toBe(true);
expect(result.scanned).toBe(0); expect(result.scanned).toBe(0);
expect(errorSpy).toHaveBeenCalledWith( expect(debugLogger.warn).toHaveBeenCalledWith(
expect.stringContaining( expect.stringContaining(
input === '0d' input === '0d'
? 'Invalid retention period: 0d. Value must be greater than 0' ? 'Invalid retention period: 0d. Value must be greater than 0'
: `Invalid retention period format: ${input}`, : `Invalid retention period format: ${input}`,
), ),
); );
errorSpy.mockRestore();
}); });
// Test special case - empty string // Test special case - empty string
@@ -1010,18 +1000,14 @@ describe('Session Cleanup', () => {
}, },
}; };
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const result = await cleanupExpiredSessions(config, settings); const result = await cleanupExpiredSessions(config, settings);
expect(result.disabled).toBe(true); expect(result.disabled).toBe(true);
expect(result.scanned).toBe(0); expect(result.scanned).toBe(0);
// Empty string means no valid retention method specified // Empty string means no valid retention method specified
expect(errorSpy).toHaveBeenCalledWith( expect(debugLogger.warn).toHaveBeenCalledWith(
expect.stringContaining('Either maxAge or maxCount must be specified'), expect.stringContaining('Either maxAge or maxCount must be specified'),
); );
errorSpy.mockRestore();
}); });
// Test edge cases // Test edge cases
@@ -1082,17 +1068,13 @@ describe('Session Cleanup', () => {
}, },
}; };
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const result = await cleanupExpiredSessions(config, settings); const result = await cleanupExpiredSessions(config, settings);
expect(result.disabled).toBe(true); expect(result.disabled).toBe(true);
expect(result.scanned).toBe(0); expect(result.scanned).toBe(0);
expect(errorSpy).toHaveBeenCalledWith( expect(debugLogger.warn).toHaveBeenCalledWith(
expect.stringContaining('Either maxAge or maxCount must be specified'), expect.stringContaining('Either maxAge or maxCount must be specified'),
); );
errorSpy.mockRestore();
}); });
it('should validate maxCount range', async () => { it('should validate maxCount range', async () => {
@@ -1108,17 +1090,13 @@ describe('Session Cleanup', () => {
}, },
}; };
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const result = await cleanupExpiredSessions(config, settings); const result = await cleanupExpiredSessions(config, settings);
expect(result.disabled).toBe(true); expect(result.disabled).toBe(true);
expect(result.scanned).toBe(0); expect(result.scanned).toBe(0);
expect(errorSpy).toHaveBeenCalledWith( expect(debugLogger.warn).toHaveBeenCalledWith(
expect.stringContaining('maxCount must be at least 1'), expect.stringContaining('maxCount must be at least 1'),
); );
errorSpy.mockRestore();
}); });
describe('maxAge format validation', () => { describe('maxAge format validation', () => {
@@ -1135,21 +1113,14 @@ describe('Session Cleanup', () => {
}, },
}; };
const errorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
const result = await cleanupExpiredSessions(config, settings); const result = await cleanupExpiredSessions(config, settings);
expect(result.disabled).toBe(true); expect(result.disabled).toBe(true);
expect(result.scanned).toBe(0); expect(result.scanned).toBe(0);
expect(errorSpy).toHaveBeenCalledWith( expect(debugLogger.warn).toHaveBeenCalledWith(
expect.stringContaining('Invalid retention period format: 30'), expect.stringContaining('Invalid retention period format: 30'),
); );
errorSpy.mockRestore();
}); });
it('should reject invalid maxAge format - invalid unit', async () => { it('should reject invalid maxAge format - invalid unit', async () => {
const config = createMockConfig({ const config = createMockConfig({
getDebugMode: vi.fn().mockReturnValue(true), getDebugMode: vi.fn().mockReturnValue(true),
@@ -1163,21 +1134,14 @@ describe('Session Cleanup', () => {
}, },
}; };
const errorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
const result = await cleanupExpiredSessions(config, settings); const result = await cleanupExpiredSessions(config, settings);
expect(result.disabled).toBe(true); expect(result.disabled).toBe(true);
expect(result.scanned).toBe(0); expect(result.scanned).toBe(0);
expect(errorSpy).toHaveBeenCalledWith( expect(debugLogger.warn).toHaveBeenCalledWith(
expect.stringContaining('Invalid retention period format: 30x'), expect.stringContaining('Invalid retention period format: 30x'),
); );
errorSpy.mockRestore();
}); });
it('should reject invalid maxAge format - no number', async () => { it('should reject invalid maxAge format - no number', async () => {
const config = createMockConfig({ const config = createMockConfig({
getDebugMode: vi.fn().mockReturnValue(true), getDebugMode: vi.fn().mockReturnValue(true),
@@ -1191,21 +1155,14 @@ describe('Session Cleanup', () => {
}, },
}; };
const errorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
const result = await cleanupExpiredSessions(config, settings); const result = await cleanupExpiredSessions(config, settings);
expect(result.disabled).toBe(true); expect(result.disabled).toBe(true);
expect(result.scanned).toBe(0); expect(result.scanned).toBe(0);
expect(errorSpy).toHaveBeenCalledWith( expect(debugLogger.warn).toHaveBeenCalledWith(
expect.stringContaining('Invalid retention period format: d'), expect.stringContaining('Invalid retention period format: d'),
); );
errorSpy.mockRestore();
}); });
it('should reject invalid maxAge format - decimal number', async () => { it('should reject invalid maxAge format - decimal number', async () => {
const config = createMockConfig({ const config = createMockConfig({
getDebugMode: vi.fn().mockReturnValue(true), getDebugMode: vi.fn().mockReturnValue(true),
@@ -1219,21 +1176,14 @@ describe('Session Cleanup', () => {
}, },
}; };
const errorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
const result = await cleanupExpiredSessions(config, settings); const result = await cleanupExpiredSessions(config, settings);
expect(result.disabled).toBe(true); expect(result.disabled).toBe(true);
expect(result.scanned).toBe(0); expect(result.scanned).toBe(0);
expect(errorSpy).toHaveBeenCalledWith( expect(debugLogger.warn).toHaveBeenCalledWith(
expect.stringContaining('Invalid retention period format: 1.5d'), expect.stringContaining('Invalid retention period format: 1.5d'),
); );
errorSpy.mockRestore();
}); });
it('should reject invalid maxAge format - negative number', async () => { it('should reject invalid maxAge format - negative number', async () => {
const config = createMockConfig({ const config = createMockConfig({
getDebugMode: vi.fn().mockReturnValue(true), getDebugMode: vi.fn().mockReturnValue(true),
@@ -1247,21 +1197,14 @@ describe('Session Cleanup', () => {
}, },
}; };
const errorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
const result = await cleanupExpiredSessions(config, settings); const result = await cleanupExpiredSessions(config, settings);
expect(result.disabled).toBe(true); expect(result.disabled).toBe(true);
expect(result.scanned).toBe(0); expect(result.scanned).toBe(0);
expect(errorSpy).toHaveBeenCalledWith( expect(debugLogger.warn).toHaveBeenCalledWith(
expect.stringContaining('Invalid retention period format: -5d'), expect.stringContaining('Invalid retention period format: -5d'),
); );
errorSpy.mockRestore();
}); });
it('should accept valid maxAge format - hours', async () => { it('should accept valid maxAge format - hours', async () => {
const config = createMockConfig(); const config = createMockConfig();
const settings: Settings = { const settings: Settings = {
@@ -1362,23 +1305,16 @@ describe('Session Cleanup', () => {
}, },
}; };
const errorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
const result = await cleanupExpiredSessions(config, settings); const result = await cleanupExpiredSessions(config, settings);
expect(result.disabled).toBe(true); expect(result.disabled).toBe(true);
expect(result.scanned).toBe(0); expect(result.scanned).toBe(0);
expect(errorSpy).toHaveBeenCalledWith( expect(debugLogger.warn).toHaveBeenCalledWith(
expect.stringContaining( expect.stringContaining(
'maxAge cannot be less than minRetention (1d)', 'maxAge cannot be less than minRetention (1d)',
), ),
); );
errorSpy.mockRestore();
}); });
it('should reject maxAge less than custom minRetention', async () => { it('should reject maxAge less than custom minRetention', async () => {
const config = createMockConfig({ const config = createMockConfig({
getDebugMode: vi.fn().mockReturnValue(true), getDebugMode: vi.fn().mockReturnValue(true),
@@ -1393,23 +1329,16 @@ describe('Session Cleanup', () => {
}, },
}; };
const errorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
const result = await cleanupExpiredSessions(config, settings); const result = await cleanupExpiredSessions(config, settings);
expect(result.disabled).toBe(true); expect(result.disabled).toBe(true);
expect(result.scanned).toBe(0); expect(result.scanned).toBe(0);
expect(errorSpy).toHaveBeenCalledWith( expect(debugLogger.warn).toHaveBeenCalledWith(
expect.stringContaining( expect.stringContaining(
'maxAge cannot be less than minRetention (3d)', 'maxAge cannot be less than minRetention (3d)',
), ),
); );
errorSpy.mockRestore();
}); });
it('should accept maxAge equal to minRetention', async () => { it('should accept maxAge equal to minRetention', async () => {
const config = createMockConfig(); const config = createMockConfig();
const settings: Settings = { const settings: Settings = {
@@ -1537,21 +1466,14 @@ describe('Session Cleanup', () => {
}, },
}; };
const errorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
const result = await cleanupExpiredSessions(config, settings); const result = await cleanupExpiredSessions(config, settings);
expect(result.disabled).toBe(true); expect(result.disabled).toBe(true);
expect(result.scanned).toBe(0); expect(result.scanned).toBe(0);
expect(errorSpy).toHaveBeenCalledWith( expect(debugLogger.warn).toHaveBeenCalledWith(
expect.stringContaining('maxCount must be at least 1'), expect.stringContaining('maxCount must be at least 1'),
); );
errorSpy.mockRestore();
}); });
it('should accept valid maxCount in normal range', async () => { it('should accept valid maxCount in normal range', async () => {
const config = createMockConfig(); const config = createMockConfig();
const settings: Settings = { const settings: Settings = {
@@ -1611,22 +1533,15 @@ describe('Session Cleanup', () => {
}, },
}; };
const errorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
const result = await cleanupExpiredSessions(config, settings); const result = await cleanupExpiredSessions(config, settings);
expect(result.disabled).toBe(true); expect(result.disabled).toBe(true);
expect(result.scanned).toBe(0); expect(result.scanned).toBe(0);
// Should fail on first validation error (maxAge format) // Should fail on first validation error (maxAge format)
expect(errorSpy).toHaveBeenCalledWith( expect(debugLogger.warn).toHaveBeenCalledWith(
expect.stringContaining('Invalid retention period format'), expect.stringContaining('Invalid retention period format'),
); );
errorSpy.mockRestore();
}); });
it('should reject if maxAge is invalid even when maxCount is valid', async () => { it('should reject if maxAge is invalid even when maxCount is valid', async () => {
const config = createMockConfig({ const config = createMockConfig({
getDebugMode: vi.fn().mockReturnValue(true), getDebugMode: vi.fn().mockReturnValue(true),
@@ -1642,20 +1557,14 @@ describe('Session Cleanup', () => {
}; };
// The validation logic rejects invalid maxAge format even if maxCount is valid // The validation logic rejects invalid maxAge format even if maxCount is valid
const errorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
const result = await cleanupExpiredSessions(config, settings); const result = await cleanupExpiredSessions(config, settings);
// Should reject due to invalid maxAge format // Should reject due to invalid maxAge format
expect(result.disabled).toBe(true); expect(result.disabled).toBe(true);
expect(result.scanned).toBe(0); expect(result.scanned).toBe(0);
expect(errorSpy).toHaveBeenCalledWith( expect(debugLogger.warn).toHaveBeenCalledWith(
expect.stringContaining('Invalid retention period format'), expect.stringContaining('Invalid retention period format'),
); );
errorSpy.mockRestore();
}); });
}); });
+4 -4
View File
@@ -62,7 +62,7 @@ export async function cleanupExpiredSessions(
); );
if (validationErrorMessage) { if (validationErrorMessage) {
// Log validation errors to console for visibility // Log validation errors to console for visibility
console.error(`Session cleanup disabled: ${validationErrorMessage}`); debugLogger.warn(`Session cleanup disabled: ${validationErrorMessage}`);
return { ...result, disabled: true }; return { ...result, disabled: true };
} }
@@ -114,7 +114,7 @@ export async function cleanupExpiredSessions(
: sessionToDelete.sessionInfo.id; : sessionToDelete.sessionInfo.id;
const errorMessage = const errorMessage =
error instanceof Error ? error.message : 'Unknown error'; error instanceof Error ? error.message : 'Unknown error';
console.error( debugLogger.warn(
`Failed to delete session ${sessionId}: ${errorMessage}`, `Failed to delete session ${sessionId}: ${errorMessage}`,
); );
result.failed++; result.failed++;
@@ -133,7 +133,7 @@ export async function cleanupExpiredSessions(
// Global error handler - don't let cleanup failures break startup // Global error handler - don't let cleanup failures break startup
const errorMessage = const errorMessage =
error instanceof Error ? error.message : 'Unknown error'; error instanceof Error ? error.message : 'Unknown error';
console.error(`Session cleanup failed: ${errorMessage}`); debugLogger.warn(`Session cleanup failed: ${errorMessage}`);
result.failed++; result.failed++;
} }
@@ -273,7 +273,7 @@ function validateRetentionConfig(
} catch (error) { } catch (error) {
// If minRetention format is invalid, fall back to default // If minRetention format is invalid, fall back to default
if (config.getDebugMode()) { if (config.getDebugMode()) {
console.error(`Failed to parse minRetention: ${error}`); debugLogger.warn(`Failed to parse minRetention: ${error}`);
} }
minRetentionMs = parseRetentionPeriod(DEFAULT_MIN_RETENTION); minRetentionMs = parseRetentionPeriod(DEFAULT_MIN_RETENTION);
} }
+39 -43
View File
@@ -10,6 +10,11 @@ import { ChatRecordingService } from '@google/gemini-cli-core';
import { listSessions, deleteSession } from './sessions.js'; import { listSessions, deleteSession } from './sessions.js';
import { SessionSelector, type SessionInfo } from './sessionUtils.js'; import { SessionSelector, type SessionInfo } from './sessionUtils.js';
const mocks = vi.hoisted(() => ({
writeToStdout: vi.fn(),
writeToStderr: vi.fn(),
}));
// Mock the SessionSelector and ChatRecordingService // Mock the SessionSelector and ChatRecordingService
vi.mock('./sessionUtils.js', () => ({ vi.mock('./sessionUtils.js', () => ({
SessionSelector: vi.fn(), SessionSelector: vi.fn(),
@@ -22,13 +27,14 @@ vi.mock('@google/gemini-cli-core', async () => {
...actual, ...actual,
ChatRecordingService: vi.fn(), ChatRecordingService: vi.fn(),
generateSummary: vi.fn().mockResolvedValue(undefined), generateSummary: vi.fn().mockResolvedValue(undefined),
writeToStdout: mocks.writeToStdout,
writeToStderr: mocks.writeToStderr,
}; };
}); });
describe('listSessions', () => { describe('listSessions', () => {
let mockConfig: Config; let mockConfig: Config;
let mockListSessions: ReturnType<typeof vi.fn>; let mockListSessions: ReturnType<typeof vi.fn>;
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => { beforeEach(() => {
// Create mock config // Create mock config
@@ -49,14 +55,12 @@ describe('listSessions', () => {
listSessions: mockListSessions, listSessions: mockListSessions,
}) as unknown as InstanceType<typeof SessionSelector>, }) as unknown as InstanceType<typeof SessionSelector>,
); );
// Spy on console.log
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
}); });
afterEach(() => { afterEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
consoleLogSpy.mockRestore(); mocks.writeToStdout.mockClear();
mocks.writeToStderr.mockClear();
}); });
it('should display message when no previous sessions were found', async () => { it('should display message when no previous sessions were found', async () => {
@@ -68,7 +72,7 @@ describe('listSessions', () => {
// Assert // Assert
expect(mockListSessions).toHaveBeenCalledOnce(); expect(mockListSessions).toHaveBeenCalledOnce();
expect(consoleLogSpy).toHaveBeenCalledWith( expect(mocks.writeToStdout).toHaveBeenCalledWith(
'No previous sessions found for this project.', 'No previous sessions found for this project.',
); );
}); });
@@ -127,32 +131,32 @@ describe('listSessions', () => {
expect(mockListSessions).toHaveBeenCalledOnce(); expect(mockListSessions).toHaveBeenCalledOnce();
// Check that the header was displayed // Check that the header was displayed
expect(consoleLogSpy).toHaveBeenCalledWith( expect(mocks.writeToStdout).toHaveBeenCalledWith(
'\nAvailable sessions for this project (3):\n', '\nAvailable sessions for this project (3):\n',
); );
// Check that each session was logged // Check that each session was logged
expect(consoleLogSpy).toHaveBeenCalledWith( expect(mocks.writeToStdout).toHaveBeenCalledWith(
expect.stringContaining('1. First user message'), expect.stringContaining('1. First user message'),
); );
expect(consoleLogSpy).toHaveBeenCalledWith( expect(mocks.writeToStdout).toHaveBeenCalledWith(
expect.stringContaining('[session-1]'), expect.stringContaining('[session-1]'),
); );
expect(consoleLogSpy).toHaveBeenCalledWith( expect(mocks.writeToStdout).toHaveBeenCalledWith(
expect.stringContaining('2. Second user message'), expect.stringContaining('2. Second user message'),
); );
expect(consoleLogSpy).toHaveBeenCalledWith( expect(mocks.writeToStdout).toHaveBeenCalledWith(
expect.stringContaining('[session-2]'), expect.stringContaining('[session-2]'),
); );
expect(consoleLogSpy).toHaveBeenCalledWith( expect(mocks.writeToStdout).toHaveBeenCalledWith(
expect.stringContaining('3. Current session'), expect.stringContaining('3. Current session'),
); );
expect(consoleLogSpy).toHaveBeenCalledWith( expect(mocks.writeToStdout).toHaveBeenCalledWith(
expect.stringContaining(', current)'), expect.stringContaining(', current)'),
); );
expect(consoleLogSpy).toHaveBeenCalledWith( expect(mocks.writeToStdout).toHaveBeenCalledWith(
expect.stringContaining('[current-session-id]'), expect.stringContaining('[current-session-id]'),
); );
}); });
@@ -209,7 +213,7 @@ describe('listSessions', () => {
// Assert // Assert
// Get all the session log calls (skip the header) // Get all the session log calls (skip the header)
const sessionCalls = consoleLogSpy.mock.calls.filter( const sessionCalls = mocks.writeToStdout.mock.calls.filter(
(call): call is [string] => (call): call is [string] =>
typeof call[0] === 'string' && typeof call[0] === 'string' &&
call[0].includes('[session-') && call[0].includes('[session-') &&
@@ -246,13 +250,13 @@ describe('listSessions', () => {
await listSessions(mockConfig); await listSessions(mockConfig);
// Assert // Assert
expect(consoleLogSpy).toHaveBeenCalledWith( expect(mocks.writeToStdout).toHaveBeenCalledWith(
expect.stringContaining('1. Test message'), expect.stringContaining('1. Test message'),
); );
expect(consoleLogSpy).toHaveBeenCalledWith( expect(mocks.writeToStdout).toHaveBeenCalledWith(
expect.stringContaining('some time ago'), expect.stringContaining('some time ago'),
); );
expect(consoleLogSpy).toHaveBeenCalledWith( expect(mocks.writeToStdout).toHaveBeenCalledWith(
expect.stringContaining('[abc123def456]'), expect.stringContaining('[abc123def456]'),
); );
}); });
@@ -281,13 +285,13 @@ describe('listSessions', () => {
await listSessions(mockConfig); await listSessions(mockConfig);
// Assert // Assert
expect(consoleLogSpy).toHaveBeenCalledWith( expect(mocks.writeToStdout).toHaveBeenCalledWith(
'\nAvailable sessions for this project (1):\n', '\nAvailable sessions for this project (1):\n',
); );
expect(consoleLogSpy).toHaveBeenCalledWith( expect(mocks.writeToStdout).toHaveBeenCalledWith(
expect.stringContaining('1. Only session'), expect.stringContaining('1. Only session'),
); );
expect(consoleLogSpy).toHaveBeenCalledWith( expect(mocks.writeToStdout).toHaveBeenCalledWith(
expect.stringContaining(', current)'), expect.stringContaining(', current)'),
); );
}); });
@@ -318,10 +322,10 @@ describe('listSessions', () => {
await listSessions(mockConfig); await listSessions(mockConfig);
// Assert: Should show the summary (displayName), not the first user message // Assert: Should show the summary (displayName), not the first user message
expect(consoleLogSpy).toHaveBeenCalledWith( expect(mocks.writeToStdout).toHaveBeenCalledWith(
expect.stringContaining('1. Add dark mode to the app'), expect.stringContaining('1. Add dark mode to the app'),
); );
expect(consoleLogSpy).not.toHaveBeenCalledWith( expect(mocks.writeToStdout).not.toHaveBeenCalledWith(
expect.stringContaining('How do I add dark mode to my React application'), expect.stringContaining('How do I add dark mode to my React application'),
); );
}); });
@@ -331,8 +335,6 @@ describe('deleteSession', () => {
let mockConfig: Config; let mockConfig: Config;
let mockListSessions: ReturnType<typeof vi.fn>; let mockListSessions: ReturnType<typeof vi.fn>;
let mockDeleteSession: ReturnType<typeof vi.fn>; let mockDeleteSession: ReturnType<typeof vi.fn>;
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => { beforeEach(() => {
// Create mock config // Create mock config
@@ -362,16 +364,10 @@ describe('deleteSession', () => {
deleteSession: mockDeleteSession, deleteSession: mockDeleteSession,
}) as unknown as InstanceType<typeof ChatRecordingService>, }) as unknown as InstanceType<typeof ChatRecordingService>,
); );
// Spy on console methods
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
}); });
afterEach(() => { afterEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
consoleLogSpy.mockRestore();
consoleErrorSpy.mockRestore();
}); });
it('should display error when no sessions are found', async () => { it('should display error when no sessions are found', async () => {
@@ -383,7 +379,7 @@ describe('deleteSession', () => {
// Assert // Assert
expect(mockListSessions).toHaveBeenCalledOnce(); expect(mockListSessions).toHaveBeenCalledOnce();
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(mocks.writeToStderr).toHaveBeenCalledWith(
'No sessions found for this project.', 'No sessions found for this project.',
); );
expect(mockDeleteSession).not.toHaveBeenCalled(); expect(mockDeleteSession).not.toHaveBeenCalled();
@@ -416,10 +412,10 @@ describe('deleteSession', () => {
// Assert // Assert
expect(mockListSessions).toHaveBeenCalledOnce(); expect(mockListSessions).toHaveBeenCalledOnce();
expect(mockDeleteSession).toHaveBeenCalledWith('session-file-123'); expect(mockDeleteSession).toHaveBeenCalledWith('session-file-123');
expect(consoleLogSpy).toHaveBeenCalledWith( expect(mocks.writeToStdout).toHaveBeenCalledWith(
'Deleted session 1: Test session (some time ago)', 'Deleted session 1: Test session (some time ago)',
); );
expect(consoleErrorSpy).not.toHaveBeenCalled(); expect(mocks.writeToStderr).not.toHaveBeenCalled();
}); });
it('should delete session by index', async () => { it('should delete session by index', async () => {
@@ -463,7 +459,7 @@ describe('deleteSession', () => {
// Assert // Assert
expect(mockListSessions).toHaveBeenCalledOnce(); expect(mockListSessions).toHaveBeenCalledOnce();
expect(mockDeleteSession).toHaveBeenCalledWith('session-file-2'); expect(mockDeleteSession).toHaveBeenCalledWith('session-file-2');
expect(consoleLogSpy).toHaveBeenCalledWith( expect(mocks.writeToStdout).toHaveBeenCalledWith(
'Deleted session 2: Second session (some time ago)', 'Deleted session 2: Second session (some time ago)',
); );
}); });
@@ -492,7 +488,7 @@ describe('deleteSession', () => {
await deleteSession(mockConfig, 'invalid-id'); await deleteSession(mockConfig, 'invalid-id');
// Assert // Assert
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(mocks.writeToStderr).toHaveBeenCalledWith(
'Invalid session identifier "invalid-id". Use --list-sessions to see available sessions.', 'Invalid session identifier "invalid-id". Use --list-sessions to see available sessions.',
); );
expect(mockDeleteSession).not.toHaveBeenCalled(); expect(mockDeleteSession).not.toHaveBeenCalled();
@@ -522,7 +518,7 @@ describe('deleteSession', () => {
await deleteSession(mockConfig, '999'); await deleteSession(mockConfig, '999');
// Assert // Assert
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(mocks.writeToStderr).toHaveBeenCalledWith(
'Invalid session identifier "999". Use --list-sessions to see available sessions.', 'Invalid session identifier "999". Use --list-sessions to see available sessions.',
); );
expect(mockDeleteSession).not.toHaveBeenCalled(); expect(mockDeleteSession).not.toHaveBeenCalled();
@@ -552,7 +548,7 @@ describe('deleteSession', () => {
await deleteSession(mockConfig, '0'); await deleteSession(mockConfig, '0');
// Assert // Assert
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(mocks.writeToStderr).toHaveBeenCalledWith(
'Invalid session identifier "0". Use --list-sessions to see available sessions.', 'Invalid session identifier "0". Use --list-sessions to see available sessions.',
); );
expect(mockDeleteSession).not.toHaveBeenCalled(); expect(mockDeleteSession).not.toHaveBeenCalled();
@@ -582,7 +578,7 @@ describe('deleteSession', () => {
await deleteSession(mockConfig, '1'); await deleteSession(mockConfig, '1');
// Assert // Assert
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(mocks.writeToStderr).toHaveBeenCalledWith(
'Cannot delete the current active session.', 'Cannot delete the current active session.',
); );
expect(mockDeleteSession).not.toHaveBeenCalled(); expect(mockDeleteSession).not.toHaveBeenCalled();
@@ -612,7 +608,7 @@ describe('deleteSession', () => {
await deleteSession(mockConfig, 'current-session-id'); await deleteSession(mockConfig, 'current-session-id');
// Assert // Assert
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(mocks.writeToStderr).toHaveBeenCalledWith(
'Cannot delete the current active session.', 'Cannot delete the current active session.',
); );
expect(mockDeleteSession).not.toHaveBeenCalled(); expect(mockDeleteSession).not.toHaveBeenCalled();
@@ -646,7 +642,7 @@ describe('deleteSession', () => {
// Assert // Assert
expect(mockDeleteSession).toHaveBeenCalledWith('session-file-1'); expect(mockDeleteSession).toHaveBeenCalledWith('session-file-1');
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(mocks.writeToStderr).toHaveBeenCalledWith(
'Failed to delete session: File deletion failed', 'Failed to delete session: File deletion failed',
); );
}); });
@@ -679,7 +675,7 @@ describe('deleteSession', () => {
await deleteSession(mockConfig, '1'); await deleteSession(mockConfig, '1');
// Assert // Assert
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(mocks.writeToStderr).toHaveBeenCalledWith(
'Failed to delete session: Unknown error', 'Failed to delete session: Unknown error',
); );
}); });
@@ -737,7 +733,7 @@ describe('deleteSession', () => {
// Assert // Assert
expect(mockDeleteSession).toHaveBeenCalledWith('session-file-1'); expect(mockDeleteSession).toHaveBeenCalledWith('session-file-1');
expect(consoleLogSpy).toHaveBeenCalledWith( expect(mocks.writeToStdout).toHaveBeenCalledWith(
expect.stringContaining('Oldest session'), expect.stringContaining('Oldest session'),
); );
}); });
+13 -9
View File
@@ -7,6 +7,8 @@
import { import {
ChatRecordingService, ChatRecordingService,
generateSummary, generateSummary,
writeToStderr,
writeToStdout,
type Config, type Config,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import { import {
@@ -23,11 +25,13 @@ export async function listSessions(config: Config): Promise<void> {
const sessions = await sessionSelector.listSessions(); const sessions = await sessionSelector.listSessions();
if (sessions.length === 0) { if (sessions.length === 0) {
console.log('No previous sessions found for this project.'); writeToStdout('No previous sessions found for this project.');
return; return;
} }
console.log(`\nAvailable sessions for this project (${sessions.length}):\n`); writeToStdout(
`\nAvailable sessions for this project (${sessions.length}):\n`,
);
sessions sessions
.sort( .sort(
@@ -41,8 +45,8 @@ export async function listSessions(config: Config): Promise<void> {
session.displayName.length > 100 session.displayName.length > 100
? session.displayName.slice(0, 97) + '...' ? session.displayName.slice(0, 97) + '...'
: session.displayName; : session.displayName;
console.log( writeToStdout(
` ${index + 1}. ${title} (${time}${current}) [${session.id}]`, ` ${index + 1}. ${title} (${time}${current}) [${session.id}]\n`,
); );
}); });
} }
@@ -55,7 +59,7 @@ export async function deleteSession(
const sessions = await sessionSelector.listSessions(); const sessions = await sessionSelector.listSessions();
if (sessions.length === 0) { if (sessions.length === 0) {
console.error('No sessions found for this project.'); writeToStderr('No sessions found for this project.');
return; return;
} }
@@ -76,7 +80,7 @@ export async function deleteSession(
// Parse session index // Parse session index
const index = parseInt(sessionIndex, 10); const index = parseInt(sessionIndex, 10);
if (isNaN(index) || index < 1 || index > sessions.length) { if (isNaN(index) || index < 1 || index > sessions.length) {
console.error( writeToStderr(
`Invalid session identifier "${sessionIndex}". Use --list-sessions to see available sessions.`, `Invalid session identifier "${sessionIndex}". Use --list-sessions to see available sessions.`,
); );
return; return;
@@ -86,7 +90,7 @@ export async function deleteSession(
// Prevent deleting the current session // Prevent deleting the current session
if (sessionToDelete.isCurrentSession) { if (sessionToDelete.isCurrentSession) {
console.error('Cannot delete the current active session.'); writeToStderr('Cannot delete the current active session.');
return; return;
} }
@@ -96,11 +100,11 @@ export async function deleteSession(
chatRecordingService.deleteSession(sessionToDelete.file); chatRecordingService.deleteSession(sessionToDelete.file);
const time = formatRelativeTime(sessionToDelete.lastUpdated); const time = formatRelativeTime(sessionToDelete.lastUpdated);
console.log( writeToStdout(
`Deleted session ${sessionToDelete.index}: ${sessionToDelete.firstUserMessage} (${time})`, `Deleted session ${sessionToDelete.index}: ${sessionToDelete.firstUserMessage} (${time})`,
); );
} catch (error) { } catch (error) {
console.error( writeToStderr(
`Failed to delete session: ${error instanceof Error ? error.message : 'Unknown error'}`, `Failed to delete session: ${error instanceof Error ? error.message : 'Unknown error'}`,
); );
} }
@@ -21,6 +21,7 @@ import {
makeFakeConfig, makeFakeConfig,
debugLogger, debugLogger,
ExitCodes, ExitCodes,
coreEvents,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import type { Config } from '@google/gemini-cli-core'; import type { Config } from '@google/gemini-cli-core';
import * as auth from './config/auth.js'; import * as auth from './config/auth.js';
@@ -36,8 +37,8 @@ describe('validateNonInterActiveAuth', () => {
let originalEnvGeminiApiKey: string | undefined; let originalEnvGeminiApiKey: string | undefined;
let originalEnvVertexAi: string | undefined; let originalEnvVertexAi: string | undefined;
let originalEnvGcp: string | undefined; let originalEnvGcp: string | undefined;
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
let debugLoggerErrorSpy: ReturnType<typeof vi.spyOn>; let debugLoggerErrorSpy: ReturnType<typeof vi.spyOn>;
let coreEventsEmitFeedbackSpy: MockInstance;
let processExitSpy: MockInstance; let processExitSpy: MockInstance;
let refreshAuthMock: Mock; let refreshAuthMock: Mock;
let mockSettings: LoadedSettings; let mockSettings: LoadedSettings;
@@ -49,10 +50,12 @@ describe('validateNonInterActiveAuth', () => {
delete process.env['GEMINI_API_KEY']; delete process.env['GEMINI_API_KEY'];
delete process.env['GOOGLE_GENAI_USE_VERTEXAI']; delete process.env['GOOGLE_GENAI_USE_VERTEXAI'];
delete process.env['GOOGLE_GENAI_USE_GCA']; delete process.env['GOOGLE_GENAI_USE_GCA'];
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
debugLoggerErrorSpy = vi debugLoggerErrorSpy = vi
.spyOn(debugLogger, 'error') .spyOn(debugLogger, 'error')
.mockImplementation(() => {}); .mockImplementation(() => {});
coreEventsEmitFeedbackSpy = vi
.spyOn(coreEvents, 'emitFeedback')
.mockImplementation(() => {});
processExitSpy = vi processExitSpy = vi
.spyOn(process, 'exit') .spyOn(process, 'exit')
.mockImplementation((code?: string | number | null | undefined) => { .mockImplementation((code?: string | number | null | undefined) => {
@@ -302,6 +305,7 @@ describe('validateNonInterActiveAuth', () => {
expect(validateAuthMethodSpy).not.toHaveBeenCalled(); expect(validateAuthMethodSpy).not.toHaveBeenCalled();
expect(debugLoggerErrorSpy).not.toHaveBeenCalled(); expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
expect(coreEventsEmitFeedbackSpy).not.toHaveBeenCalled();
expect(processExitSpy).not.toHaveBeenCalled(); expect(processExitSpy).not.toHaveBeenCalled();
// We still expect refreshAuth to be called with the (invalid) type // We still expect refreshAuth to be called with the (invalid) type
expect(refreshAuthMock).toHaveBeenCalledWith('invalid-auth-type'); expect(refreshAuthMock).toHaveBeenCalledWith('invalid-auth-type');
@@ -404,7 +408,8 @@ describe('validateNonInterActiveAuth', () => {
expect(thrown?.message).toBe( expect(thrown?.message).toBe(
`process.exit(${ExitCodes.FATAL_AUTHENTICATION_ERROR}) called`, `process.exit(${ExitCodes.FATAL_AUTHENTICATION_ERROR}) called`,
); );
const errorArg = consoleErrorSpy.mock.calls[0]?.[0] as string; // Checking coreEventsEmitFeedbackSpy arguments
const errorArg = coreEventsEmitFeedbackSpy.mock.calls[0]?.[1] as string;
const payload = JSON.parse(errorArg); const payload = JSON.parse(errorArg);
expect(payload.error.type).toBe('Error'); expect(payload.error.type).toBe('Error');
expect(payload.error.code).toBe(ExitCodes.FATAL_AUTHENTICATION_ERROR); expect(payload.error.code).toBe(ExitCodes.FATAL_AUTHENTICATION_ERROR);
@@ -439,7 +444,8 @@ describe('validateNonInterActiveAuth', () => {
`process.exit(${ExitCodes.FATAL_AUTHENTICATION_ERROR}) called`, `process.exit(${ExitCodes.FATAL_AUTHENTICATION_ERROR}) called`,
); );
{ {
const errorArg = consoleErrorSpy.mock.calls[0]?.[0] as string; // Checking coreEventsEmitFeedbackSpy arguments
const errorArg = coreEventsEmitFeedbackSpy.mock.calls[0]?.[1] as string;
const payload = JSON.parse(errorArg); const payload = JSON.parse(errorArg);
expect(payload.error.type).toBe('Error'); expect(payload.error.type).toBe('Error');
expect(payload.error.code).toBe(ExitCodes.FATAL_AUTHENTICATION_ERROR); expect(payload.error.code).toBe(ExitCodes.FATAL_AUTHENTICATION_ERROR);
@@ -477,7 +483,8 @@ describe('validateNonInterActiveAuth', () => {
`process.exit(${ExitCodes.FATAL_AUTHENTICATION_ERROR}) called`, `process.exit(${ExitCodes.FATAL_AUTHENTICATION_ERROR}) called`,
); );
{ {
const errorArg = consoleErrorSpy.mock.calls[0]?.[0] as string; // Checking coreEventsEmitFeedbackSpy arguments
const errorArg = coreEventsEmitFeedbackSpy.mock.calls[0]?.[1] as string;
const payload = JSON.parse(errorArg); const payload = JSON.parse(errorArg);
expect(payload.error.type).toBe('Error'); expect(payload.error.type).toBe('Error');
expect(payload.error.code).toBe(ExitCodes.FATAL_AUTHENTICATION_ERROR); expect(payload.error.code).toBe(ExitCodes.FATAL_AUTHENTICATION_ERROR);
@@ -14,6 +14,7 @@ import {
type HookPolicyDecision, type HookPolicyDecision,
} from './types.js'; } from './types.js';
import { safeJsonStringify } from '../utils/safeJsonStringify.js'; import { safeJsonStringify } from '../utils/safeJsonStringify.js';
import { debugLogger } from '../utils/debugLogger.js';
export class MessageBus extends EventEmitter { export class MessageBus extends EventEmitter {
constructor( constructor(
@@ -45,7 +46,7 @@ export class MessageBus extends EventEmitter {
async publish(message: Message): Promise<void> { async publish(message: Message): Promise<void> {
if (this.debug) { if (this.debug) {
console.debug(`[MESSAGE_BUS] publish: ${safeJsonStringify(message)}`); debugLogger.debug(`[MESSAGE_BUS] publish: ${safeJsonStringify(message)}`);
} }
try { try {
if (!this.isValidMessage(message)) { if (!this.isValidMessage(message)) {
+1 -1
View File
@@ -30,7 +30,7 @@ const logger = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
debug: (...args: any[]) => debugLogger.debug('[DEBUG] [IDEClient]', ...args), debug: (...args: any[]) => debugLogger.debug('[DEBUG] [IDEClient]', ...args),
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
error: (...args: any[]) => console.error('[ERROR] [IDEClient]', ...args), error: (...args: any[]) => debugLogger.error('[ERROR] [IDEClient]', ...args),
}; };
export type DiffUpdateResult = export type DiffUpdateResult =
+4 -1
View File
@@ -418,7 +418,10 @@ export class OAuthUtils {
return payload.exp * 1000; // Convert seconds to milliseconds return payload.exp * 1000; // Convert seconds to milliseconds
} }
} catch (e) { } catch (e) {
console.error('Failed to parse ID token for expiry time with error:', e); debugLogger.error(
'Failed to parse ID token for expiry time with error:',
e,
);
} }
// Return undefined if try block fails or 'exp' is missing/invalid // Return undefined if try block fails or 'exp' is missing/invalid
@@ -324,7 +324,11 @@ export class KeychainTokenStorage
.filter((cred) => cred.account.startsWith(SECRET_PREFIX)) .filter((cred) => cred.account.startsWith(SECRET_PREFIX))
.map((cred) => cred.account.substring(SECRET_PREFIX.length)); .map((cred) => cred.account.substring(SECRET_PREFIX.length));
} catch (error) { } catch (error) {
console.error('Failed to list secrets from keychain:', error); coreEvents.emitFeedback(
'error',
'Failed to list secrets from keychain',
error,
);
return []; return [];
} }
} }
+2 -1
View File
@@ -28,6 +28,7 @@ import {
} from '../confirmation-bus/types.js'; } from '../confirmation-bus/types.js';
import { type MessageBus } from '../confirmation-bus/message-bus.js'; import { type MessageBus } from '../confirmation-bus/message-bus.js';
import { coreEvents } from '../utils/events.js'; import { coreEvents } from '../utils/events.js';
import { debugLogger } from '../utils/debugLogger.js';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
@@ -312,7 +313,7 @@ export function createPolicyUpdater(
existingData = toml.parse(fileContent) as { rule?: TomlRule[] }; existingData = toml.parse(fileContent) as { rule?: TomlRule[] };
} catch (error) { } catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
console.warn( debugLogger.warn(
`Failed to parse ${policyFile}, overwriting with new policy.`, `Failed to parse ${policyFile}, overwriting with new policy.`,
error, error,
); );
@@ -14,6 +14,14 @@ import type {
} from '../routingStrategy.js'; } from '../routingStrategy.js';
import type { Config } from '../../config/config.js'; import type { Config } from '../../config/config.js';
import type { BaseLlmClient } from '../../core/baseLlmClient.js'; import type { BaseLlmClient } from '../../core/baseLlmClient.js';
import { debugLogger } from '../../utils/debugLogger.js';
import { coreEvents } from '../../utils/events.js';
vi.mock('../../utils/debugLogger.js', () => ({
debugLogger: {
warn: vi.fn(),
},
}));
describe('CompositeStrategy', () => { describe('CompositeStrategy', () => {
let mockContext: RoutingContext; let mockContext: RoutingContext;
@@ -22,6 +30,7 @@ describe('CompositeStrategy', () => {
let mockStrategy1: RoutingStrategy; let mockStrategy1: RoutingStrategy;
let mockStrategy2: RoutingStrategy; let mockStrategy2: RoutingStrategy;
let mockTerminalStrategy: TerminalStrategy; let mockTerminalStrategy: TerminalStrategy;
let emitFeedbackSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
@@ -30,6 +39,8 @@ describe('CompositeStrategy', () => {
mockConfig = {} as Config; mockConfig = {} as Config;
mockBaseLlmClient = {} as BaseLlmClient; mockBaseLlmClient = {} as BaseLlmClient;
emitFeedbackSpy = vi.spyOn(coreEvents, 'emitFeedback');
mockStrategy1 = { mockStrategy1 = {
name: 'strategy1', name: 'strategy1',
route: vi.fn().mockResolvedValue(null), route: vi.fn().mockResolvedValue(null),
@@ -112,9 +123,6 @@ describe('CompositeStrategy', () => {
}); });
it('should handle errors in non-terminal strategies and continue', async () => { it('should handle errors in non-terminal strategies and continue', async () => {
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
vi.spyOn(mockStrategy1, 'route').mockRejectedValue( vi.spyOn(mockStrategy1, 'route').mockRejectedValue(
new Error('Strategy 1 failed'), new Error('Strategy 1 failed'),
); );
@@ -130,18 +138,14 @@ describe('CompositeStrategy', () => {
mockBaseLlmClient, mockBaseLlmClient,
); );
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(debugLogger.warn).toHaveBeenCalledWith(
"[Routing] Strategy 'strategy1' failed. Continuing to next strategy. Error:", "[Routing] Strategy 'strategy1' failed. Continuing to next strategy. Error:",
expect.any(Error), expect.any(Error),
); );
expect(result.model).toBe('terminal-model'); expect(result.model).toBe('terminal-model');
consoleErrorSpy.mockRestore();
}); });
it('should re-throw an error from the terminal strategy', async () => { it('should re-throw an error from the terminal strategy', async () => {
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
const terminalError = new Error('Terminal strategy failed'); const terminalError = new Error('Terminal strategy failed');
vi.spyOn(mockTerminalStrategy, 'route').mockRejectedValue(terminalError); vi.spyOn(mockTerminalStrategy, 'route').mockRejectedValue(terminalError);
@@ -151,11 +155,11 @@ describe('CompositeStrategy', () => {
composite.route(mockContext, mockConfig, mockBaseLlmClient), composite.route(mockContext, mockConfig, mockBaseLlmClient),
).rejects.toThrow(terminalError); ).rejects.toThrow(terminalError);
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(emitFeedbackSpy).toHaveBeenCalledWith(
'error',
"[Routing] Critical Error: Terminal strategy 'terminal' failed. Routing cannot proceed. Error:", "[Routing] Critical Error: Terminal strategy 'terminal' failed. Routing cannot proceed. Error:",
terminalError, terminalError,
); );
consoleErrorSpy.mockRestore();
}); });
it('should correctly finalize the decision metadata', async () => { it('should correctly finalize the decision metadata', async () => {
@@ -6,6 +6,8 @@
import type { Config } from '../../config/config.js'; import type { Config } from '../../config/config.js';
import type { BaseLlmClient } from '../../core/baseLlmClient.js'; import type { BaseLlmClient } from '../../core/baseLlmClient.js';
import { debugLogger } from '../../utils/debugLogger.js';
import { coreEvents } from '../../utils/events.js';
import type { import type {
RoutingContext, RoutingContext,
RoutingDecision, RoutingDecision,
@@ -59,7 +61,7 @@ export class CompositeStrategy implements TerminalStrategy {
return this.finalizeDecision(decision, startTime); return this.finalizeDecision(decision, startTime);
} }
} catch (error) { } catch (error) {
console.error( debugLogger.warn(
`[Routing] Strategy '${strategy.name}' failed. Continuing to next strategy. Error:`, `[Routing] Strategy '${strategy.name}' failed. Continuing to next strategy. Error:`,
error, error,
); );
@@ -76,7 +78,8 @@ export class CompositeStrategy implements TerminalStrategy {
return this.finalizeDecision(decision, startTime); return this.finalizeDecision(decision, startTime);
} catch (error) { } catch (error) {
console.error( coreEvents.emitFeedback(
'error',
`[Routing] Critical Error: Terminal strategy '${terminalStrategy.name}' failed. Routing cannot proceed. Error:`, `[Routing] Critical Error: Terminal strategy '${terminalStrategy.name}' failed. Routing cannot proceed. Error:`,
error, error,
); );
@@ -277,7 +277,7 @@ export class ClearcutLogger {
this.enqueueHelper(event); this.enqueueHelper(event);
} catch (error) { } catch (error) {
if (this.config?.getDebugMode()) { if (this.config?.getDebugMode()) {
console.error('ClearcutLogger: Failed to enqueue log event.', error); debugLogger.warn('ClearcutLogger: Failed to enqueue log event.', error);
} }
} }
} }
@@ -301,9 +301,7 @@ export class ClearcutLogger {
this.enqueueHelper(event); this.enqueueHelper(event);
}); });
} catch (error) { } catch (error) {
if (this.config?.getDebugMode()) { debugLogger.warn('ClearcutLogger: Failed to enqueue log event.', error);
console.error('ClearcutLogger: Failed to enqueue log event.', error);
}
} }
} }
@@ -435,7 +433,7 @@ export class ClearcutLogger {
}; };
} else { } else {
if (this.config?.getDebugMode()) { if (this.config?.getDebugMode()) {
console.error( debugLogger.warn(
`Error flushing log events: HTTP ${response.status}: ${response.statusText}`, `Error flushing log events: HTTP ${response.status}: ${response.statusText}`,
); );
} }
@@ -445,7 +443,7 @@ export class ClearcutLogger {
} }
} catch (e: unknown) { } catch (e: unknown) {
if (this.config?.getDebugMode()) { if (this.config?.getDebugMode()) {
console.error('Error flushing log events:', e as Error); debugLogger.warn('Error flushing log events:', e as Error);
} }
// Re-queue failed events for retry // Re-queue failed events for retry
+3 -4
View File
@@ -157,7 +157,6 @@ export async function initializeTelemetry(
) { ) {
const message = `Telemetry credentials have changed (from ${activeTelemetryEmail} to ${credentials.client_email}), but telemetry cannot be re-initialized in this process. Please restart the CLI to use the new account for telemetry.`; const message = `Telemetry credentials have changed (from ${activeTelemetryEmail} to ${credentials.client_email}), but telemetry cannot be re-initialized in this process. Please restart the CLI to use the new account for telemetry.`;
debugLogger.error(message); debugLogger.error(message);
console.error(message);
} }
return; return;
} }
@@ -309,7 +308,7 @@ export async function initializeTelemetry(
initializeMetrics(config); initializeMetrics(config);
void flushTelemetryBuffer(); void flushTelemetryBuffer();
} catch (error) { } catch (error) {
console.error('Error starting OpenTelemetry SDK:', error); debugLogger.error('Error starting OpenTelemetry SDK:', error);
} }
// Note: We don't use process.on('exit') here because that callback is synchronous // Note: We don't use process.on('exit') here because that callback is synchronous
@@ -343,7 +342,7 @@ export async function flushTelemetry(config: Config): Promise<void> {
debugLogger.log('OpenTelemetry SDK flushed successfully.'); debugLogger.log('OpenTelemetry SDK flushed successfully.');
} }
} catch (error) { } catch (error) {
console.error('Error flushing SDK:', error); debugLogger.error('Error flushing SDK:', error);
} }
} }
@@ -361,7 +360,7 @@ export async function shutdownTelemetry(
debugLogger.log('OpenTelemetry SDK shut down successfully.'); debugLogger.log('OpenTelemetry SDK shut down successfully.');
} }
} catch (error) { } catch (error) {
console.error('Error shutting down SDK:', error); debugLogger.error('Error shutting down SDK:', error);
} finally { } finally {
telemetryInitialized = false; telemetryInitialized = false;
sdk = undefined; sdk = undefined;
+1 -1
View File
@@ -1159,7 +1159,7 @@ describe('EditTool', () => {
result.returnDisplay.diffStat?.model_removed_lines, result.returnDisplay.diffStat?.model_removed_lines,
); );
} else if (result.error) { } else if (result.error) {
console.error(`Edit failed for ${file.path}:`, result.error); throw result.error;
} }
} }
-7
View File
@@ -277,9 +277,6 @@ class MemoryToolInvocation extends BaseToolInvocation<
} catch (error) { } catch (error) {
const errorMessage = const errorMessage =
error instanceof Error ? error.message : String(error); error instanceof Error ? error.message : String(error);
console.warn(
`[MemoryTool] Error executing save_memory for fact "${fact}": ${errorMessage}`,
);
return { return {
llmContent: JSON.stringify({ llmContent: JSON.stringify({
success: false, success: false,
@@ -367,10 +364,6 @@ export class MemoryTool
await fsAdapter.writeFile(memoryFilePath, newContent, 'utf-8'); await fsAdapter.writeFile(memoryFilePath, newContent, 'utf-8');
} catch (error) { } catch (error) {
console.error(
`[MemoryTool] Error adding memory entry to ${memoryFilePath}:`,
error,
);
throw new Error( throw new Error(
`[MemoryTool] Failed to add memory entry: ${error instanceof Error ? error.message : String(error)}`, `[MemoryTool] Failed to add memory entry: ${error instanceof Error ? error.message : String(error)}`,
); );
+1 -1
View File
@@ -729,7 +729,7 @@ describe('SmartEditTool', () => {
result.returnDisplay.diffStat?.model_removed_lines, result.returnDisplay.diffStat?.model_removed_lines,
); );
} else if (result.error) { } else if (result.error) {
console.error(`Edit failed for ${file.path}:`, result.error); throw result.error;
} }
} }
+1 -1
View File
@@ -416,7 +416,7 @@ export class ToolRegistry {
); );
} }
} catch (e) { } catch (e) {
console.error(`Tool discovery command "${discoveryCmd}" failed:`, e); debugLogger.error(`Tool discovery command "${discoveryCmd}" failed:`, e);
throw e; throw e;
} }
} }
+2 -1
View File
@@ -14,6 +14,7 @@ import { ToolErrorType } from './tool-error.js';
import { getErrorMessage } from '../utils/errors.js'; import { getErrorMessage } from '../utils/errors.js';
import { type Config } from '../config/config.js'; import { type Config } from '../config/config.js';
import { getResponseText } from '../utils/partUtils.js'; import { getResponseText } from '../utils/partUtils.js';
import { debugLogger } from '../utils/debugLogger.js';
interface GroundingChunkWeb { interface GroundingChunkWeb {
uri?: string; uri?: string;
@@ -167,7 +168,7 @@ class WebSearchToolInvocation extends BaseToolInvocation<
const errorMessage = `Error during web search for query "${ const errorMessage = `Error during web search for query "${
this.params.query this.params.query
}": ${getErrorMessage(error)}`; }": ${getErrorMessage(error)}`;
console.error(errorMessage, error); debugLogger.warn(errorMessage, error);
return { return {
llmContent: `Error: ${errorMessage}`, llmContent: `Error: ${errorMessage}`,
returnDisplay: `Error performing web search.`, returnDisplay: `Error performing web search.`,
+2 -1
View File
@@ -44,6 +44,7 @@ import { FileOperation } from '../telemetry/metrics.js';
import { getSpecificMimeType } from '../utils/fileUtils.js'; import { getSpecificMimeType } from '../utils/fileUtils.js';
import { getLanguageFromFilePath } from '../utils/language-detection.js'; import { getLanguageFromFilePath } from '../utils/language-detection.js';
import type { MessageBus } from '../confirmation-bus/message-bus.js'; import type { MessageBus } from '../confirmation-bus/message-bus.js';
import { debugLogger } from '../utils/debugLogger.js';
/** /**
* Parameters for the WriteFile tool * Parameters for the WriteFile tool
@@ -377,7 +378,7 @@ class WriteFileToolInvocation extends BaseToolInvocation<
// Include stack trace in debug mode for better troubleshooting // Include stack trace in debug mode for better troubleshooting
if (this.config.getDebugMode() && error.stack) { if (this.config.getDebugMode() && error.stack) {
console.error('Write file error stack:', error.stack); debugLogger.error('Write file error stack:', error.stack);
} }
} else if (error instanceof Error) { } else if (error instanceof Error) {
errorMsg = `Error writing to file: ${error.message}`; errorMsg = `Error writing to file: ${error.message}`;
+1
View File
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
/* eslint-disable no-console */
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import * as util from 'node:util'; import * as util from 'node:util';
+5 -4
View File
@@ -22,6 +22,7 @@ import {
} from '../utils/messageInspectors.js'; } from '../utils/messageInspectors.js';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import { promptIdContext } from './promptIdContext.js'; import { promptIdContext } from './promptIdContext.js';
import { debugLogger } from './debugLogger.js';
const CODE_CORRECTION_SYSTEM_PROMPT = ` const CODE_CORRECTION_SYSTEM_PROMPT = `
You are an expert code-editing assistant. Your task is to analyze a failed edit attempt and provide a corrected version of the text snippets. You are an expert code-editing assistant. Your task is to analyze a failed edit attempt and provide a corrected version of the text snippets.
@@ -434,7 +435,7 @@ Return ONLY the corrected target snippet in the specified JSON format with the k
throw error; throw error;
} }
console.error( debugLogger.warn(
'Error during LLM call for old string snippet correction:', 'Error during LLM call for old string snippet correction:',
error, error,
); );
@@ -523,7 +524,7 @@ Return ONLY the corrected string in the specified JSON format with the key 'corr
throw error; throw error;
} }
console.error('Error during LLM call for new_string correction:', error); debugLogger.warn('Error during LLM call for new_string correction:', error);
return originalNewString; return originalNewString;
} }
} }
@@ -593,7 +594,7 @@ Return ONLY the corrected string in the specified JSON format with the key 'corr
throw error; throw error;
} }
console.error( debugLogger.warn(
'Error during LLM call for new_string escaping correction:', 'Error during LLM call for new_string escaping correction:',
error, error,
); );
@@ -660,7 +661,7 @@ Return ONLY the corrected string in the specified JSON format with the key 'corr
throw error; throw error;
} }
console.error( debugLogger.warn(
'Error during LLM call for string escaping correction:', 'Error during LLM call for string escaping correction:',
error, error,
); );
+25 -14
View File
@@ -9,12 +9,13 @@ import fs from 'node:fs/promises';
import os from 'node:os'; import os from 'node:os';
import path from 'node:path'; import path from 'node:path';
import { reportError } from './errorReporting.js'; import { reportError } from './errorReporting.js';
import { debugLogger } from './debugLogger.js';
// Use a type alias for SpyInstance as it's not directly exported // Use a type alias for SpyInstance as it's not directly exported
type SpyInstance = ReturnType<typeof vi.spyOn>; type SpyInstance = ReturnType<typeof vi.spyOn>;
describe('reportError', () => { describe('reportError', () => {
let consoleErrorSpy: SpyInstance; let debugLoggerErrorSpy: SpyInstance;
let testDir: string; let testDir: string;
const MOCK_TIMESTAMP = '2025-01-01T00-00-00-000Z'; const MOCK_TIMESTAMP = '2025-01-01T00-00-00-000Z';
@@ -22,7 +23,9 @@ describe('reportError', () => {
// Create a temporary directory for logs // Create a temporary directory for logs
testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gemini-report-test-')); testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gemini-report-test-'));
vi.resetAllMocks(); vi.resetAllMocks();
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); debugLoggerErrorSpy = vi
.spyOn(debugLogger, 'error')
.mockImplementation(() => {});
vi.spyOn(Date.prototype, 'toISOString').mockReturnValue(MOCK_TIMESTAMP); vi.spyOn(Date.prototype, 'toISOString').mockReturnValue(MOCK_TIMESTAMP);
}); });
@@ -54,9 +57,10 @@ describe('reportError', () => {
context, context,
}); });
// Verify the console log // Verify the user feedback
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
`${baseMessage} Full report available at: ${expectedReportPath}`, `${baseMessage} Full report available at: ${expectedReportPath}`,
error,
); );
}); });
@@ -75,8 +79,9 @@ describe('reportError', () => {
error: { message: 'Test plain object error' }, error: { message: 'Test plain object error' },
}); });
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
`${baseMessage} Full report available at: ${expectedReportPath}`, `${baseMessage} Full report available at: ${expectedReportPath}`,
error,
); );
}); });
@@ -95,8 +100,9 @@ describe('reportError', () => {
error: { message: 'Just a string error' }, error: { message: 'Just a string error' },
}); });
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
`${baseMessage} Full report available at: ${expectedReportPath}`, `${baseMessage} Full report available at: ${expectedReportPath}`,
error,
); );
}); });
@@ -109,15 +115,18 @@ describe('reportError', () => {
await reportError(error, baseMessage, context, type, nonExistentDir); await reportError(error, baseMessage, context, type, nonExistentDir);
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
`${baseMessage} Additionally, failed to write detailed error report:`, `${baseMessage} Additionally, failed to write detailed error report:`,
expect.any(Error), // The actual write error expect.any(Error), // The actual write error
); );
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
'Original error that triggered report generation:', 'Original error that triggered report generation:',
error, error,
); );
expect(consoleErrorSpy).toHaveBeenCalledWith('Original context:', context); expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
'Original context:',
context,
);
}); });
it('should handle stringification failure of report content (e.g. BigInt in context)', async () => { it('should handle stringification failure of report content (e.g. BigInt in context)', async () => {
@@ -146,15 +155,15 @@ describe('reportError', () => {
await reportError(error, baseMessage, context, type, testDir); await reportError(error, baseMessage, context, type, testDir);
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
`${baseMessage} Could not stringify report content (likely due to context):`, `${baseMessage} Could not stringify report content (likely due to context):`,
stringifyError, stringifyError,
); );
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
'Original error that triggered report generation:', 'Original error that triggered report generation:',
error, error,
); );
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
'Original context could not be stringified or included in report.', 'Original context could not be stringified or included in report.',
); );
@@ -165,8 +174,9 @@ describe('reportError', () => {
error: { message: error.message, stack: error.stack }, error: { message: error.message, stack: error.stack },
}); });
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
`${baseMessage} Partial report (excluding context) available at: ${expectedMinimalReportPath}`, `${baseMessage} Partial report (excluding context) available at: ${expectedMinimalReportPath}`,
error,
); );
}); });
@@ -186,8 +196,9 @@ describe('reportError', () => {
error: { message: 'Error without context', stack: 'No context stack' }, error: { message: 'Error without context', stack: 'No context stack' },
}); });
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
`${baseMessage} Full report available at: ${expectedReportPath}`, `${baseMessage} Full report available at: ${expectedReportPath}`,
error,
); );
}); });
}); });
+26 -12
View File
@@ -8,6 +8,7 @@ import fs from 'node:fs/promises';
import os from 'node:os'; import os from 'node:os';
import path from 'node:path'; import path from 'node:path';
import type { Content } from '@google/genai'; import type { Content } from '@google/genai';
import { debugLogger } from './debugLogger.js';
interface ErrorReportData { interface ErrorReportData {
error: { message: string; stack?: string } | { message: string }; error: { message: string; stack?: string } | { message: string };
@@ -16,7 +17,7 @@ interface ErrorReportData {
} }
/** /**
* Generates an error report, writes it to a temporary file, and logs information to console.error. * Generates an error report, writes it to a temporary file, and logs information to user
* @param error The error object. * @param error The error object.
* @param context The relevant context (e.g., chat history, request contents). * @param context The relevant context (e.g., chat history, request contents).
* @param type A string to identify the type of error (e.g., 'startChat', 'generateJson-api'). * @param type A string to identify the type of error (e.g., 'startChat', 'generateJson-api').
@@ -59,13 +60,16 @@ export async function reportError(
stringifiedReportContent = JSON.stringify(reportContent, null, 2); stringifiedReportContent = JSON.stringify(reportContent, null, 2);
} catch (stringifyError) { } catch (stringifyError) {
// This can happen if context contains something like BigInt // This can happen if context contains something like BigInt
console.error( debugLogger.error(
`${baseMessage} Could not stringify report content (likely due to context):`, `${baseMessage} Could not stringify report content (likely due to context):`,
stringifyError, stringifyError,
); );
console.error('Original error that triggered report generation:', error); debugLogger.error(
'Original error that triggered report generation:',
error,
);
if (context) { if (context) {
console.error( debugLogger.error(
'Original context could not be stringified or included in report.', 'Original context could not be stringified or included in report.',
); );
} }
@@ -75,11 +79,12 @@ export async function reportError(
stringifiedReportContent = JSON.stringify(minimalReportContent, null, 2); stringifiedReportContent = JSON.stringify(minimalReportContent, null, 2);
// Still try to write the minimal report // Still try to write the minimal report
await fs.writeFile(reportPath, stringifiedReportContent); await fs.writeFile(reportPath, stringifiedReportContent);
console.error( debugLogger.error(
`${baseMessage} Partial report (excluding context) available at: ${reportPath}`, `${baseMessage} Partial report (excluding context) available at: ${reportPath}`,
error,
); );
} catch (minimalWriteError) { } catch (minimalWriteError) {
console.error( debugLogger.error(
`${baseMessage} Failed to write even a minimal error report:`, `${baseMessage} Failed to write even a minimal error report:`,
minimalWriteError, minimalWriteError,
); );
@@ -89,28 +94,37 @@ export async function reportError(
try { try {
await fs.writeFile(reportPath, stringifiedReportContent); await fs.writeFile(reportPath, stringifiedReportContent);
console.error(`${baseMessage} Full report available at: ${reportPath}`); debugLogger.error(
`${baseMessage} Full report available at: ${reportPath}`,
error,
);
} catch (writeError) { } catch (writeError) {
console.error( debugLogger.error(
`${baseMessage} Additionally, failed to write detailed error report:`, `${baseMessage} Additionally, failed to write detailed error report:`,
writeError, writeError,
); );
// Log the original error as a fallback if report writing fails // Log the original error as a fallback if report writing fails
console.error('Original error that triggered report generation:', error); debugLogger.error(
'Original error that triggered report generation:',
error,
);
if (context) { if (context) {
// Context was stringifiable, but writing the file failed. // Context was stringifiable, but writing the file failed.
// We already have stringifiedReportContent, but it might be too large for console. // We already have stringifiedReportContent, but it might be too large for console.
// So, we try to log the original context object, and if that fails, its stringified version (truncated). // So, we try to log the original context object, and if that fails, its stringified version (truncated).
try { try {
console.error('Original context:', context); debugLogger.error('Original context:', context);
} catch { } catch {
try { try {
console.error( debugLogger.error(
'Original context (stringified, truncated):', 'Original context (stringified, truncated):',
JSON.stringify(context).substring(0, 1000), JSON.stringify(context).substring(0, 1000),
); );
} catch { } catch {
console.error('Original context could not be logged or stringified.'); debugLogger.error(
'Original context could not be logged or stringified.',
);
} }
} }
} }
@@ -342,7 +342,10 @@ export async function getFolderStructure(
return `${summary}\n\n${resolvedPath}${path.sep}\n${structureLines.join('\n')}`; return `${summary}\n\n${resolvedPath}${path.sep}\n${structureLines.join('\n')}`;
} catch (error: unknown) { } catch (error: unknown) {
console.error(`Error getting folder structure for ${resolvedPath}:`, error); debugLogger.warn(
`Error getting folder structure for ${resolvedPath}:`,
error,
);
return `Error processing directory "${resolvedPath}": ${getErrorMessage(error)}`; return `Error processing directory "${resolvedPath}": ${getErrorMessage(error)}`;
} }
} }
+1 -1
View File
@@ -31,7 +31,7 @@ const logger = {
debugLogger.warn('[WARN] [MemoryDiscovery]', ...args), debugLogger.warn('[WARN] [MemoryDiscovery]', ...args),
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
error: (...args: any[]) => error: (...args: any[]) =>
console.error('[ERROR] [MemoryDiscovery]', ...args), debugLogger.error('[ERROR] [MemoryDiscovery]', ...args),
}; };
export interface GeminiFileContent { export interface GeminiFileContent {
+4 -4
View File
@@ -232,7 +232,7 @@ export async function retryWithBackoff<T>(
continue; continue;
} }
} catch (fallbackError) { } catch (fallbackError) {
console.warn('Model fallback failed:', fallbackError); debugLogger.warn('Model fallback failed:', fallbackError);
} }
} }
throw classifiedError instanceof RetryableQuotaError throw classifiedError instanceof RetryableQuotaError
@@ -241,7 +241,7 @@ export async function retryWithBackoff<T>(
} }
if (classifiedError instanceof RetryableQuotaError) { if (classifiedError instanceof RetryableQuotaError) {
console.warn( debugLogger.warn(
`Attempt ${attempt} failed: ${classifiedError.message}. Retrying after ${classifiedError.retryDelayMs}ms...`, `Attempt ${attempt} failed: ${classifiedError.message}. Retrying after ${classifiedError.retryDelayMs}ms...`,
); );
await delay(classifiedError.retryDelayMs, signal); await delay(classifiedError.retryDelayMs, signal);
@@ -300,7 +300,7 @@ function logRetryAttempt(
if (errorStatus === 429) { if (errorStatus === 429) {
debugLogger.warn(message, error); debugLogger.warn(message, error);
} else if (errorStatus && errorStatus >= 500 && errorStatus < 600) { } else if (errorStatus && errorStatus >= 500 && errorStatus < 600) {
console.error(message, error); debugLogger.warn(message, error);
} else if (error instanceof Error) { } else if (error instanceof Error) {
// Fallback for errors that might not have a status but have a message // Fallback for errors that might not have a status but have a message
if (error.message.includes('429')) { if (error.message.includes('429')) {
@@ -309,7 +309,7 @@ function logRetryAttempt(
error, error,
); );
} else if (error.message.match(/5\d{2}/)) { } else if (error.message.match(/5\d{2}/)) {
console.error( debugLogger.warn(
`Attempt ${attempt} failed with 5xx error. Retrying with backoff...`, `Attempt ${attempt} failed with 5xx error. Retrying with backoff...`,
error, error,
); );
+3 -2
View File
@@ -19,6 +19,7 @@ import type {
ResolvedModelConfig, ResolvedModelConfig,
} from '../services/modelConfigService.js'; } from '../services/modelConfigService.js';
import { DEFAULT_GEMINI_MODEL } from '../config/models.js'; import { DEFAULT_GEMINI_MODEL } from '../config/models.js';
import { debugLogger } from './debugLogger.js';
// Mock GeminiClient and Config constructor // Mock GeminiClient and Config constructor
vi.mock('../core/client.js'); vi.mock('../core/client.js');
@@ -58,11 +59,11 @@ describe('summarizers', () => {
(mockGeminiClient.generateContent as Mock) = vi.fn(); (mockGeminiClient.generateContent as Mock) = vi.fn();
vi.spyOn(console, 'error').mockImplementation(() => {}); vi.spyOn(console, 'error').mockImplementation(() => {});
vi.spyOn(debugLogger, 'warn').mockImplementation(() => {});
}); });
afterEach(() => { afterEach(() => {
vi.clearAllMocks(); vi.restoreAllMocks();
(console.error as Mock).mockRestore();
}); });
describe('summarizeToolOutput', () => { describe('summarizeToolOutput', () => {