mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-22 11:04:42 -07:00
Migrate console to coreEvents.emitFeedback or debugLogger (#15219)
This commit is contained in:
@@ -29,6 +29,7 @@ import {
|
||||
type Config,
|
||||
type ResumedSessionData,
|
||||
debugLogger,
|
||||
coreEvents,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { act } from 'react';
|
||||
import { type InitializationResult } from './core/initializer.js';
|
||||
@@ -819,9 +820,7 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
.mockImplementation((code) => {
|
||||
throw new MockProcessExitError(code);
|
||||
});
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
const emitFeedbackSpy = vi.spyOn(coreEvents, 'emitFeedback');
|
||||
|
||||
vi.mocked(loadSettings).mockReturnValue({
|
||||
merged: { advanced: {}, security: { auth: {} }, ui: { theme: 'test' } },
|
||||
@@ -875,12 +874,13 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
if (!(e instanceof MockProcessExitError)) throw e;
|
||||
}
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect(emitFeedbackSpy).toHaveBeenCalledWith(
|
||||
'error',
|
||||
expect.stringContaining('Error resuming session: Session not found'),
|
||||
);
|
||||
expect(processExitSpy).toHaveBeenCalledWith(42);
|
||||
processExitSpy.mockRestore();
|
||||
consoleErrorSpy.mockRestore();
|
||||
emitFeedbackSpy.mockRestore();
|
||||
});
|
||||
|
||||
it.skip('should log error when cleanupExpiredSessions fails', async () => {
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
type ResumedSessionData,
|
||||
type OutputPayload,
|
||||
type ConsoleLogPayload,
|
||||
type UserFeedbackPayload,
|
||||
sessionId,
|
||||
logUserPrompt,
|
||||
AuthType,
|
||||
@@ -597,7 +598,8 @@ export async function main() {
|
||||
// Use the existing session ID to continue recording to the same session
|
||||
config.setSessionId(resumedSessionData.conversation.sessionId);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
`Error resuming session: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
);
|
||||
await runExitCleanup();
|
||||
@@ -719,13 +721,25 @@ export function initializeOutputListenersAndFlush() {
|
||||
}
|
||||
});
|
||||
|
||||
coreEvents.on(CoreEvent.ConsoleLog, (payload: ConsoleLogPayload) => {
|
||||
if (payload.type === 'error' || payload.type === 'warn') {
|
||||
writeToStderr(payload.content);
|
||||
} else {
|
||||
writeToStdout(payload.content);
|
||||
}
|
||||
});
|
||||
if (coreEvents.listenerCount(CoreEvent.ConsoleLog) === 0) {
|
||||
coreEvents.on(CoreEvent.ConsoleLog, (payload: ConsoleLogPayload) => {
|
||||
if (payload.type === 'error' || payload.type === 'warn') {
|
||||
writeToStderr(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();
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ const mockCoreEvents = vi.hoisted(() => ({
|
||||
off: vi.fn(),
|
||||
drainBacklogs: vi.fn(),
|
||||
emit: vi.fn(),
|
||||
emitFeedback: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
@@ -785,11 +786,6 @@ describe('runNonInteractive', () => {
|
||||
throw testError;
|
||||
});
|
||||
|
||||
// Mock console.error to capture JSON error output
|
||||
const consoleErrorJsonSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
let thrownError: Error | null = null;
|
||||
try {
|
||||
await runNonInteractive({
|
||||
@@ -807,7 +803,8 @@ describe('runNonInteractive', () => {
|
||||
// Should throw because of mocked process.exit
|
||||
expect(thrownError?.message).toBe('process.exit(1) called');
|
||||
|
||||
expect(consoleErrorJsonSpy).toHaveBeenCalledWith(
|
||||
expect(mockCoreEvents.emitFeedback).toHaveBeenCalledWith(
|
||||
'error',
|
||||
JSON.stringify(
|
||||
{
|
||||
session_id: 'test-session-id',
|
||||
@@ -831,11 +828,6 @@ describe('runNonInteractive', () => {
|
||||
throw fatalError;
|
||||
});
|
||||
|
||||
// Mock console.error to capture JSON error output
|
||||
const consoleErrorJsonSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
let thrownError: Error | null = null;
|
||||
try {
|
||||
await runNonInteractive({
|
||||
@@ -853,7 +845,8 @@ describe('runNonInteractive', () => {
|
||||
// Should throw because of mocked process.exit with custom exit code
|
||||
expect(thrownError?.message).toBe('process.exit(42) called');
|
||||
|
||||
expect(consoleErrorJsonSpy).toHaveBeenCalledWith(
|
||||
expect(mockCoreEvents.emitFeedback).toHaveBeenCalledWith(
|
||||
'error',
|
||||
JSON.stringify(
|
||||
{
|
||||
session_id: 'test-session-id',
|
||||
|
||||
@@ -10,7 +10,7 @@ import toml from '@iarna/toml';
|
||||
import { glob } from 'glob';
|
||||
import { z } from 'zod';
|
||||
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 {
|
||||
CommandContext,
|
||||
@@ -126,7 +126,8 @@ export class FileCommandLoader implements ICommandLoader {
|
||||
!signal.aborted &&
|
||||
(error as { code?: string })?.code !== 'ENOENT'
|
||||
) {
|
||||
console.error(
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
`[FileCommandLoader] Error loading commands from ${dirInfo.path}:`,
|
||||
error,
|
||||
);
|
||||
@@ -189,7 +190,8 @@ export class FileCommandLoader implements ICommandLoader {
|
||||
try {
|
||||
fileContent = await fs.readFile(filePath, 'utf-8');
|
||||
} catch (error: unknown) {
|
||||
console.error(
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
`[FileCommandLoader] Failed to read file ${filePath}:`,
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
@@ -200,7 +202,8 @@ export class FileCommandLoader implements ICommandLoader {
|
||||
try {
|
||||
parsed = toml.parse(fileContent);
|
||||
} catch (error: unknown) {
|
||||
console.error(
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
`[FileCommandLoader] Failed to parse TOML file ${filePath}:`,
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
@@ -210,7 +213,8 @@ export class FileCommandLoader implements ICommandLoader {
|
||||
const validationResult = TomlCommandDefSchema.safeParse(parsed);
|
||||
|
||||
if (!validationResult.success) {
|
||||
console.error(
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
`[FileCommandLoader] Skipping invalid command file: ${filePath}. Validation errors:`,
|
||||
validationResult.error.flatten(),
|
||||
);
|
||||
@@ -278,7 +282,8 @@ export class FileCommandLoader implements ICommandLoader {
|
||||
_args: string,
|
||||
): Promise<SlashCommandActionReturn> => {
|
||||
if (!context.invocation) {
|
||||
console.error(
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
`[FileCommandLoader] Critical error: Command '${baseCommandName}' was executed without invocation context.`,
|
||||
);
|
||||
return {
|
||||
|
||||
@@ -842,16 +842,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
const handleClearScreen = useCallback(() => {
|
||||
historyManager.clearItems();
|
||||
clearConsoleMessagesState();
|
||||
if (!isAlternateBuffer) {
|
||||
console.clear();
|
||||
}
|
||||
refreshStatic();
|
||||
}, [
|
||||
historyManager,
|
||||
clearConsoleMessagesState,
|
||||
refreshStatic,
|
||||
isAlternateBuffer,
|
||||
]);
|
||||
}, [historyManager, clearConsoleMessagesState, refreshStatic]);
|
||||
|
||||
const { handleInput: vimHandleInput } = useVim(buffer, handleFinalSubmit);
|
||||
|
||||
|
||||
@@ -4,14 +4,30 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
|
||||
import { render } from '../test-utils/render.js';
|
||||
import { act } from 'react';
|
||||
import { IdeIntegrationNudge } from './IdeIntegrationNudge.js';
|
||||
import { KeypressProvider } from './contexts/KeypressContext.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
|
||||
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', () => {
|
||||
const defaultProps = {
|
||||
ide: {
|
||||
@@ -21,24 +37,20 @@ describe('IdeIntegrationNudge', () => {
|
||||
onComplete: vi.fn(),
|
||||
};
|
||||
|
||||
const originalError = console.error;
|
||||
|
||||
afterEach(() => {
|
||||
console.error = originalError;
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
console.error = (...args) => {
|
||||
vi.mocked(debugLogger.warn).mockImplementation((...args) => {
|
||||
if (
|
||||
typeof args[0] === 'string' &&
|
||||
/was not wrapped in act/.test(args[0])
|
||||
) {
|
||||
return;
|
||||
}
|
||||
originalError.call(console, ...args);
|
||||
};
|
||||
});
|
||||
vi.stubEnv('GEMINI_CLI_IDE_SERVER_PORT', '');
|
||||
vi.stubEnv('GEMINI_CLI_IDE_WORKSPACE_PATH', '');
|
||||
});
|
||||
|
||||
@@ -5,12 +5,27 @@
|
||||
*/
|
||||
|
||||
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 { AuthInProgress } from './AuthInProgress.js';
|
||||
import { useKeypress, type Key } from '../hooks/useKeypress.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
|
||||
// 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', () => ({
|
||||
useKeypress: vi.fn(),
|
||||
}));
|
||||
@@ -22,24 +37,20 @@ vi.mock('../components/CliSpinner.js', () => ({
|
||||
describe('AuthInProgress', () => {
|
||||
const onTimeout = vi.fn();
|
||||
|
||||
const originalError = console.error;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
console.error = (...args) => {
|
||||
vi.mocked(debugLogger.error).mockImplementation((...args) => {
|
||||
if (
|
||||
typeof args[0] === 'string' &&
|
||||
args[0].includes('was not wrapped in act')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
originalError.call(console, ...args);
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
console.error = originalError;
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { LoadedSettings } from '../../config/settings.js';
|
||||
import { KeypressProvider } from '../contexts/KeypressContext.js';
|
||||
import { act } from 'react';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
@@ -101,7 +102,7 @@ describe('EditorSettingsDialog', () => {
|
||||
await waitFor(() => {
|
||||
const frame = lastFrame() || '';
|
||||
if (!frame.includes('> Apply To')) {
|
||||
console.log(
|
||||
debugLogger.debug(
|
||||
'Waiting for scope focus. Current frame:',
|
||||
JSON.stringify(frame),
|
||||
);
|
||||
@@ -166,7 +167,7 @@ describe('EditorSettingsDialog', () => {
|
||||
|
||||
const frame = lastFrame() || '';
|
||||
if (!frame.includes('(Also modified')) {
|
||||
console.log(
|
||||
debugLogger.debug(
|
||||
'Modified message test failure. Frame:',
|
||||
JSON.stringify(frame),
|
||||
);
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
EDITOR_DISPLAY_NAMES,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { coreEvents } from '@google/gemini-cli-core';
|
||||
|
||||
interface EditorDialogProps {
|
||||
onSelect: (
|
||||
@@ -68,7 +69,10 @@ export function EditorSettingsDialog({
|
||||
)
|
||||
: 0;
|
||||
if (editorIndex === -1) {
|
||||
console.error(`Editor is not supported: ${currentPreference}`);
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
`Editor is not supported: ${currentPreference}`,
|
||||
);
|
||||
editorIndex = 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import * as processUtils from '../../utils/processUtils.js';
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { IdeTrustChangeDialog } from './IdeTrustChangeDialog.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
|
||||
describe('IdeTrustChangeDialog', () => {
|
||||
beforeEach(() => {
|
||||
@@ -39,8 +40,8 @@ describe('IdeTrustChangeDialog', () => {
|
||||
});
|
||||
|
||||
it('renders a generic message and logs an error for NONE reason', () => {
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
const debugLoggerWarnSpy = vi
|
||||
.spyOn(debugLogger, 'warn')
|
||||
.mockImplementation(() => {});
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<IdeTrustChangeDialog reason="NONE" />,
|
||||
@@ -48,7 +49,7 @@ describe('IdeTrustChangeDialog', () => {
|
||||
|
||||
const frameText = lastFrame();
|
||||
expect(frameText).toContain('Workspace trust has changed.');
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect(debugLoggerWarnSpy).toHaveBeenCalledWith(
|
||||
'IdeTrustChangeDialog rendered with unexpected reason "NONE"',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import { theme } from '../semantic-colors.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { relaunchApp } from '../../utils/processUtils.js';
|
||||
import { type RestartReason } from '../hooks/useIdeTrustListener.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
|
||||
interface IdeTrustChangeDialogProps {
|
||||
reason: RestartReason;
|
||||
@@ -28,7 +29,7 @@ export const IdeTrustChangeDialog = ({ reason }: IdeTrustChangeDialogProps) => {
|
||||
let message = 'Workspace trust has changed.';
|
||||
if (reason === 'NONE') {
|
||||
// This should not happen, but provides a fallback and a debug log.
|
||||
console.error(
|
||||
debugLogger.warn(
|
||||
'IdeTrustChangeDialog rendered with unexpected reason "NONE"',
|
||||
);
|
||||
} else if (reason === 'CONNECTION_CHANGE') {
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
calculateTransformedLine,
|
||||
} from './shared/text-buffer.js';
|
||||
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 type { CommandContext, SlashCommand } from '../commands/types.js';
|
||||
import { CommandKind } from '../commands/types.js';
|
||||
@@ -577,8 +577,8 @@ describe('InputPrompt', () => {
|
||||
});
|
||||
|
||||
it('should handle errors during clipboard operations', async () => {
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
const debugLoggerErrorSpy = vi
|
||||
.spyOn(debugLogger, 'error')
|
||||
.mockImplementation(() => {});
|
||||
vi.mocked(clipboardUtils.clipboardHasImage).mockRejectedValue(
|
||||
new Error('Clipboard error'),
|
||||
@@ -592,14 +592,14 @@ describe('InputPrompt', () => {
|
||||
stdin.write('\x16'); // Ctrl+V
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
|
||||
'Error handling clipboard image:',
|
||||
expect.any(Error),
|
||||
);
|
||||
});
|
||||
expect(mockBuffer.setText).not.toHaveBeenCalled();
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
debugLoggerErrorSpy.mockRestore();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
||||
import type { CommandContext, SlashCommand } from '../commands/types.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import { ApprovalMode } from '@google/gemini-cli-core';
|
||||
import { ApprovalMode, debugLogger } from '@google/gemini-cli-core';
|
||||
import {
|
||||
parseInputForHighlighting,
|
||||
parseSegmentsFromTokens,
|
||||
@@ -354,7 +354,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
const offset = buffer.getOffset();
|
||||
buffer.replaceRangeByOffset(offset, offset, textToInsert);
|
||||
} catch (error) {
|
||||
console.error('Error handling clipboard image:', error);
|
||||
debugLogger.error('Error handling clipboard image:', error);
|
||||
}
|
||||
}, [buffer, config]);
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { theme } from '../semantic-colors.js';
|
||||
import { StreamingState } from '../types.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 os from 'node:os';
|
||||
@@ -66,7 +66,7 @@ export const Notifications = () => {
|
||||
});
|
||||
await fs.writeFile(screenReaderNudgeFilePath, 'true');
|
||||
} 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,
|
||||
TOGGLE_TYPES,
|
||||
} 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 type { Config } from '@google/gemini-cli-core';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
@@ -254,7 +254,11 @@ export function SettingsDialog({
|
||||
if (key === 'general.vimMode' && newValue !== vimEnabled) {
|
||||
// Call toggleVimEnabled to sync the VimModeContext local state
|
||||
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 { toCodePoints } from '../../utils/textUtils.js';
|
||||
import { useOverflowActions } from '../../contexts/OverflowContext.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
|
||||
let enableDebugLog = false;
|
||||
|
||||
@@ -28,7 +29,7 @@ function debugReportError(message: string, element: React.ReactNode) {
|
||||
if (!enableDebugLog) return;
|
||||
|
||||
if (!React.isValidElement(element)) {
|
||||
console.error(
|
||||
debugLogger.warn(
|
||||
message,
|
||||
`Invalid element: '${String(element)}' typeof=${typeof element}`,
|
||||
);
|
||||
@@ -44,10 +45,13 @@ function debugReportError(message: string, element: React.ReactNode) {
|
||||
const lineNumber = elementWithSource._source?.lineNumber;
|
||||
sourceMessage = fileName ? `${fileName}:${lineNumber}` : '<Unknown file>';
|
||||
} 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 {
|
||||
children?: React.ReactNode;
|
||||
|
||||
@@ -10,7 +10,12 @@ import os from 'node:os';
|
||||
import pathMod from 'node:path';
|
||||
import * as path from 'node:path';
|
||||
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 {
|
||||
toCodePoints,
|
||||
cpLen,
|
||||
@@ -1411,7 +1416,7 @@ function textBufferReducerLogic(
|
||||
break;
|
||||
default: {
|
||||
const exhaustiveCheck: never = dir;
|
||||
console.error(
|
||||
debugLogger.error(
|
||||
`Unknown visual movement direction: ${exhaustiveCheck}`,
|
||||
);
|
||||
return state;
|
||||
@@ -1751,7 +1756,7 @@ function textBufferReducerLogic(
|
||||
|
||||
default: {
|
||||
const exhaustiveCheck: never = action;
|
||||
console.error(`Unknown action encountered: ${exhaustiveCheck}`);
|
||||
debugLogger.error(`Unknown action encountered: ${exhaustiveCheck}`);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -2173,7 +2178,11 @@ export function useTextBuffer({
|
||||
newText = newText.replace(/\r\n?/g, '\n');
|
||||
dispatch({ type: 'set_text', payload: newText, pushToUndo: false });
|
||||
} catch (err) {
|
||||
console.error('[useTextBuffer] external editor error', err);
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
'[useTextBuffer] external editor error',
|
||||
err,
|
||||
);
|
||||
} finally {
|
||||
coreEvents.emit(CoreEvent.ExternalEditorClosed);
|
||||
if (wasRaw) setRawMode?.(true);
|
||||
|
||||
@@ -502,7 +502,7 @@ export async function handleAtCommand({
|
||||
const errorMessages = resourceReadDisplays
|
||||
.filter((d) => d.status === ToolCallStatus.Error)
|
||||
.map((d) => d.resultDisplay);
|
||||
console.error(errorMessages);
|
||||
debugLogger.error(errorMessages);
|
||||
const errorMsg = `Exiting due to an error processing the @ command: ${firstError.resultDisplay}`;
|
||||
return { processedQuery: null, error: errorMsg };
|
||||
}
|
||||
|
||||
@@ -244,7 +244,6 @@ describe('useSlashCommandProcessor', () => {
|
||||
});
|
||||
|
||||
expect(mockClearItems).toHaveBeenCalled();
|
||||
expect(console.clear).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call console.clear if alternate buffer is not active', async () => {
|
||||
@@ -262,7 +261,6 @@ describe('useSlashCommandProcessor', () => {
|
||||
});
|
||||
|
||||
expect(mockClearItems).toHaveBeenCalled();
|
||||
expect(console.clear).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -50,7 +50,6 @@ import {
|
||||
type ExtensionUpdateStatus,
|
||||
} from '../state/extensions.js';
|
||||
import { appEvents } from '../../utils/events.js';
|
||||
import { useAlternateBuffer } from './useAlternateBuffer.js';
|
||||
import {
|
||||
LogoutConfirmationDialog,
|
||||
LogoutChoice,
|
||||
@@ -96,7 +95,6 @@ export const useSlashCommandProcessor = (
|
||||
const [commands, setCommands] = useState<readonly SlashCommand[] | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const alternateBuffer = useAlternateBuffer();
|
||||
const [reloadTrigger, setReloadTrigger] = useState(0);
|
||||
|
||||
const reloadCommands = useCallback(() => {
|
||||
@@ -212,9 +210,6 @@ export const useSlashCommandProcessor = (
|
||||
addItem,
|
||||
clear: () => {
|
||||
clearItems();
|
||||
if (!alternateBuffer) {
|
||||
console.clear();
|
||||
}
|
||||
refreshStatic();
|
||||
setBannerVisible(false);
|
||||
},
|
||||
@@ -238,7 +233,6 @@ export const useSlashCommandProcessor = (
|
||||
},
|
||||
}),
|
||||
[
|
||||
alternateBuffer,
|
||||
config,
|
||||
settings,
|
||||
gitService,
|
||||
|
||||
@@ -8,7 +8,10 @@ import { useEffect } from 'react';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import { loadTrustedFolders } from '../../config/trustedFolders.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 type { UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||
import { MessageType, type HistoryItem } from '../types.js';
|
||||
@@ -133,7 +136,7 @@ export function useIncludeDirsTrust(
|
||||
}
|
||||
|
||||
if (undefinedTrustDirs.length > 0) {
|
||||
console.log(
|
||||
debugLogger.log(
|
||||
'Creating custom dialog with undecidedDirs:',
|
||||
undefinedTrustDirs,
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { useReducer, useRef, useEffect, useCallback } from 'react';
|
||||
import { useKeypress, type Key } from './useKeypress.js';
|
||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
|
||||
export interface SelectionListItem<T> {
|
||||
key: string;
|
||||
@@ -198,7 +199,7 @@ function selectionListReducer(
|
||||
|
||||
default: {
|
||||
const exhaustiveCheck: never = action;
|
||||
console.error(`Unknown selection list action: ${exhaustiveCheck}`);
|
||||
debugLogger.warn(`Unknown selection list action: ${exhaustiveCheck}`);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
ConversationRecord,
|
||||
MessageRecord,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { coreEvents } from '@google/gemini-cli-core';
|
||||
|
||||
// Mock modules
|
||||
vi.mock('fs/promises');
|
||||
@@ -52,6 +53,7 @@ describe('useSessionBrowser', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.spyOn(coreEvents, 'emitFeedback').mockImplementation(() => {});
|
||||
mockedPath.join.mockImplementation((...args) => args.join('/'));
|
||||
vi.mocked(mockConfig.storage.getProjectTempDir).mockReturnValue(
|
||||
MOCKED_PROJECT_TEMP_DIR,
|
||||
@@ -100,9 +102,6 @@ describe('useSessionBrowser', () => {
|
||||
fileName: MOCKED_FILENAME,
|
||||
} as SessionInfo;
|
||||
mockedFs.readFile.mockRejectedValue(new Error('File not found'));
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useSessionBrowser(mockConfig, mockOnLoadHistory),
|
||||
@@ -112,9 +111,12 @@ describe('useSessionBrowser', () => {
|
||||
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);
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should handle JSON parse error', async () => {
|
||||
@@ -124,9 +126,6 @@ describe('useSessionBrowser', () => {
|
||||
fileName: MOCKED_FILENAME,
|
||||
} as SessionInfo;
|
||||
mockedFs.readFile.mockResolvedValue('invalid json');
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useSessionBrowser(mockConfig, mockOnLoadHistory),
|
||||
@@ -136,9 +135,12 @@ describe('useSessionBrowser', () => {
|
||||
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);
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import type {
|
||||
ResumedSessionData,
|
||||
} from '@google/gemini-cli-core';
|
||||
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 { MessageType, ToolCallStatus } from '../types.js';
|
||||
|
||||
@@ -79,7 +79,7 @@ export const useSessionBrowser = (
|
||||
resumedSessionData,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error resuming session:', error);
|
||||
coreEvents.emitFeedback('error', 'Error resuming session:', error);
|
||||
setIsSessionBrowserOpen(false);
|
||||
}
|
||||
},
|
||||
@@ -103,7 +103,7 @@ export const useSessionBrowser = (
|
||||
chatRecordingService.deleteSession(session.file);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting session:', error);
|
||||
coreEvents.emitFeedback('error', 'Error deleting session:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import * as fs from 'node:fs/promises';
|
||||
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;
|
||||
|
||||
@@ -52,7 +52,7 @@ async function readHistoryFile(filePath: string): Promise<string[]> {
|
||||
return result;
|
||||
} catch (err) {
|
||||
if (isNodeError(err) && err.code === 'ENOENT') return [];
|
||||
console.error('Error reading history:', err);
|
||||
debugLogger.error('Error reading history:', err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ async function writeHistoryFile(
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||
await fs.writeFile(filePath, history.join('\n'));
|
||||
} 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
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import util from 'node:util';
|
||||
import type { ConsoleMessageItem } from '../types.js';
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import React from 'react';
|
||||
import { Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import stringWidth from 'string-width';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
|
||||
// Constants for Markdown parsing
|
||||
const BOLD_MARKER_LENGTH = 2; // For "**"
|
||||
@@ -144,7 +145,7 @@ const RenderInlineInternal: React.FC<RenderInlineProps> = ({
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing inline markdown part:', fullMatch, e);
|
||||
debugLogger.warn('Error parsing inline markdown part:', fullMatch, e);
|
||||
renderedNode = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,22 @@
|
||||
* 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 { OutputFormat, FatalInputError } from '@google/gemini-cli-core';
|
||||
import {
|
||||
OutputFormat,
|
||||
FatalInputError,
|
||||
debugLogger,
|
||||
coreEvents,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
getErrorMessage,
|
||||
handleError,
|
||||
@@ -14,6 +27,12 @@ import {
|
||||
handleCancellationError,
|
||||
handleMaxTurnsExceededError,
|
||||
} from './errors.js';
|
||||
import { runSyncCleanup } from './cleanup.js';
|
||||
|
||||
// Mock the cleanup module
|
||||
vi.mock('./cleanup.js', () => ({
|
||||
runSyncCleanup: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock the core modules
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
@@ -63,6 +82,9 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
JsonStreamEventType: {
|
||||
RESULT: 'result',
|
||||
},
|
||||
coreEvents: {
|
||||
emitFeedback: vi.fn(),
|
||||
},
|
||||
FatalToolExecutionError: class extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
@@ -85,7 +107,10 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
describe('errors', () => {
|
||||
let mockConfig: Config;
|
||||
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';
|
||||
|
||||
@@ -93,8 +118,19 @@ describe('errors', () => {
|
||||
// Reset mocks
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Mock console.error
|
||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
// Mock debugLogger
|
||||
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
|
||||
processExitSpy = vi.spyOn(process, 'exit').mockImplementation((code) => {
|
||||
@@ -110,7 +146,8 @@ describe('errors', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleErrorSpy.mockRestore();
|
||||
debugLoggerErrorSpy.mockRestore();
|
||||
debugLoggerWarnSpy.mockRestore();
|
||||
processExitSpy.mockRestore();
|
||||
});
|
||||
|
||||
@@ -141,14 +178,14 @@ describe('errors', () => {
|
||||
).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');
|
||||
|
||||
expect(() => {
|
||||
handleError(testError, mockConfig);
|
||||
}).toThrow(testError);
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('API Error: Test error');
|
||||
expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle non-Error objects', () => {
|
||||
@@ -157,8 +194,6 @@ describe('errors', () => {
|
||||
expect(() => {
|
||||
handleError(testError, mockConfig);
|
||||
}).toThrow(testError);
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('API Error: String error');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -169,14 +204,16 @@ describe('errors', () => {
|
||||
).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');
|
||||
|
||||
expect(() => {
|
||||
handleError(testError, mockConfig);
|
||||
}).toThrow('process.exit called with code: 1');
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledTimes(1);
|
||||
expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledWith(
|
||||
'error',
|
||||
JSON.stringify(
|
||||
{
|
||||
session_id: TEST_SESSION_ID,
|
||||
@@ -190,16 +227,20 @@ describe('errors', () => {
|
||||
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');
|
||||
|
||||
expect(() => {
|
||||
handleError(testError, mockConfig, 42);
|
||||
}).toThrow('process.exit called with code: 42');
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledTimes(1);
|
||||
expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledWith(
|
||||
'error',
|
||||
JSON.stringify(
|
||||
{
|
||||
session_id: TEST_SESSION_ID,
|
||||
@@ -213,16 +254,19 @@ describe('errors', () => {
|
||||
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');
|
||||
|
||||
expect(() => {
|
||||
handleError(fatalError, mockConfig);
|
||||
}).toThrow('process.exit called with code: 42');
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledTimes(1);
|
||||
expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledWith(
|
||||
'error',
|
||||
JSON.stringify(
|
||||
{
|
||||
session_id: TEST_SESSION_ID,
|
||||
@@ -236,6 +280,7 @@ describe('errors', () => {
|
||||
2,
|
||||
),
|
||||
);
|
||||
expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle error with code property', () => {
|
||||
@@ -259,7 +304,8 @@ describe('errors', () => {
|
||||
handleError(errorWithStatus, mockConfig);
|
||||
}).toThrow('process.exit called with code: 1'); // string codes become 1
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledWith(
|
||||
'error',
|
||||
JSON.stringify(
|
||||
{
|
||||
session_id: TEST_SESSION_ID,
|
||||
@@ -283,12 +329,14 @@ describe('errors', () => {
|
||||
).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');
|
||||
|
||||
expect(() => {
|
||||
handleError(testError, mockConfig);
|
||||
}).toThrow('process.exit called with code: 1');
|
||||
|
||||
expect(runSyncCleanupSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should extract exitCode from FatalError instances', () => {
|
||||
@@ -312,10 +360,10 @@ describe('errors', () => {
|
||||
).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);
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect(debugLoggerWarnSpy).toHaveBeenCalledWith(
|
||||
'Error executing tool test-tool: Tool failed',
|
||||
);
|
||||
});
|
||||
@@ -329,10 +377,24 @@ describe('errors', () => {
|
||||
'Custom display message',
|
||||
);
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect(debugLoggerWarnSpy).toHaveBeenCalledWith(
|
||||
'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', () => {
|
||||
@@ -351,29 +413,32 @@ describe('errors', () => {
|
||||
'invalid_tool_params',
|
||||
);
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect(debugLoggerWarnSpy).toHaveBeenCalledWith(
|
||||
'Error executing tool test-tool: Tool failed',
|
||||
);
|
||||
// Should not exit for non-fatal errors
|
||||
expect(processExitSpy).not.toHaveBeenCalled();
|
||||
expect(coreEventsEmitFeedbackSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not exit for file not found errors', () => {
|
||||
handleToolError(toolName, toolError, mockConfig, 'file_not_found');
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect(debugLoggerWarnSpy).toHaveBeenCalledWith(
|
||||
'Error executing tool test-tool: Tool failed',
|
||||
);
|
||||
expect(processExitSpy).not.toHaveBeenCalled();
|
||||
expect(coreEventsEmitFeedbackSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not exit for permission denied errors', () => {
|
||||
handleToolError(toolName, toolError, mockConfig, 'permission_denied');
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect(debugLoggerWarnSpy).toHaveBeenCalledWith(
|
||||
'Error executing tool test-tool: Tool failed',
|
||||
);
|
||||
expect(processExitSpy).not.toHaveBeenCalled();
|
||||
expect(coreEventsEmitFeedbackSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not exit for path not in workspace errors', () => {
|
||||
@@ -384,10 +449,11 @@ describe('errors', () => {
|
||||
'path_not_in_workspace',
|
||||
);
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect(debugLoggerWarnSpy).toHaveBeenCalledWith(
|
||||
'Error executing tool test-tool: Tool failed',
|
||||
);
|
||||
expect(processExitSpy).not.toHaveBeenCalled();
|
||||
expect(coreEventsEmitFeedbackSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should prefer resultDisplay over error message', () => {
|
||||
@@ -399,7 +465,7 @@ describe('errors', () => {
|
||||
'Display message',
|
||||
);
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect(debugLoggerWarnSpy).toHaveBeenCalledWith(
|
||||
'Error executing tool test-tool: Display message',
|
||||
);
|
||||
expect(processExitSpy).not.toHaveBeenCalled();
|
||||
@@ -407,12 +473,14 @@ describe('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(() => {
|
||||
handleToolError(toolName, toolError, mockConfig, 'no_space_left');
|
||||
}).toThrow('process.exit called with code: 54');
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledTimes(1);
|
||||
expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledWith(
|
||||
'error',
|
||||
JSON.stringify(
|
||||
{
|
||||
session_id: TEST_SESSION_ID,
|
||||
@@ -426,6 +494,8 @@ describe('errors', () => {
|
||||
2,
|
||||
),
|
||||
);
|
||||
expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
|
||||
expect(runSyncCleanupSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -437,15 +507,17 @@ describe('errors', () => {
|
||||
).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(() => {
|
||||
handleToolError(toolName, toolError, mockConfig, 'no_space_left');
|
||||
}).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', () => {
|
||||
handleToolError(toolName, toolError, mockConfig, 'invalid_tool_params');
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect(debugLoggerWarnSpy).toHaveBeenCalledWith(
|
||||
'Error executing tool test-tool: Tool failed',
|
||||
);
|
||||
expect(processExitSpy).not.toHaveBeenCalled();
|
||||
@@ -461,12 +533,18 @@ describe('errors', () => {
|
||||
).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(() => {
|
||||
handleCancellationError(mockConfig);
|
||||
}).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);
|
||||
});
|
||||
|
||||
it('should format cancellation as JSON and exit with 130', () => {
|
||||
it('should format cancellation as JSON, emit feedback once, and exit with 130', () => {
|
||||
expect(() => {
|
||||
handleCancellationError(mockConfig);
|
||||
}).toThrow('process.exit called with code: 130');
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledTimes(1);
|
||||
expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledWith(
|
||||
'error',
|
||||
JSON.stringify(
|
||||
{
|
||||
session_id: TEST_SESSION_ID,
|
||||
@@ -496,6 +576,7 @@ describe('errors', () => {
|
||||
2,
|
||||
),
|
||||
);
|
||||
expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -510,6 +591,7 @@ describe('errors', () => {
|
||||
expect(() => {
|
||||
handleCancellationError(mockConfig);
|
||||
}).toThrow('process.exit called with code: 130');
|
||||
expect(coreEventsEmitFeedbackSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -522,14 +604,18 @@ describe('errors', () => {
|
||||
).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(() => {
|
||||
handleMaxTurnsExceededError(mockConfig);
|
||||
}).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.',
|
||||
);
|
||||
expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
|
||||
expect(runSyncCleanupSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -540,12 +626,14 @@ describe('errors', () => {
|
||||
).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(() => {
|
||||
handleMaxTurnsExceededError(mockConfig);
|
||||
}).toThrow('process.exit called with code: 53');
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledTimes(1);
|
||||
expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledWith(
|
||||
'error',
|
||||
JSON.stringify(
|
||||
{
|
||||
session_id: TEST_SESSION_ID,
|
||||
@@ -560,6 +648,7 @@ describe('errors', () => {
|
||||
2,
|
||||
),
|
||||
);
|
||||
expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -574,6 +663,7 @@ describe('errors', () => {
|
||||
expect(() => {
|
||||
handleMaxTurnsExceededError(mockConfig);
|
||||
}).toThrow('process.exit called with code: 53');
|
||||
expect(coreEventsEmitFeedbackSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
FatalCancellationError,
|
||||
FatalToolExecutionError,
|
||||
isFatalToolError,
|
||||
debugLogger,
|
||||
coreEvents,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { runSyncCleanup } from './cleanup.js';
|
||||
|
||||
@@ -103,11 +105,10 @@ export function handleError(
|
||||
config.getSessionId(),
|
||||
);
|
||||
|
||||
console.error(formattedError);
|
||||
coreEvents.emitFeedback('error', formattedError);
|
||||
runSyncCleanup();
|
||||
process.exit(getNumericExitCode(errorCode));
|
||||
} else {
|
||||
console.error(errorMessage);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -155,16 +156,16 @@ export function handleToolError(
|
||||
errorType ?? toolExecutionError.exitCode,
|
||||
config.getSessionId(),
|
||||
);
|
||||
console.error(formattedError);
|
||||
coreEvents.emitFeedback('error', formattedError);
|
||||
} else {
|
||||
console.error(errorMessage);
|
||||
coreEvents.emitFeedback('error', errorMessage);
|
||||
}
|
||||
runSyncCleanup();
|
||||
process.exit(toolExecutionError.exitCode);
|
||||
}
|
||||
|
||||
// Non-fatal: log and continue
|
||||
console.error(errorMessage);
|
||||
debugLogger.warn(errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -196,11 +197,11 @@ export function handleCancellationError(config: Config): never {
|
||||
config.getSessionId(),
|
||||
);
|
||||
|
||||
console.error(formattedError);
|
||||
coreEvents.emitFeedback('error', formattedError);
|
||||
runSyncCleanup();
|
||||
process.exit(cancellationError.exitCode);
|
||||
} else {
|
||||
console.error(cancellationError.message);
|
||||
coreEvents.emitFeedback('error', cancellationError.message);
|
||||
runSyncCleanup();
|
||||
process.exit(cancellationError.exitCode);
|
||||
}
|
||||
@@ -237,11 +238,11 @@ export function handleMaxTurnsExceededError(config: Config): never {
|
||||
config.getSessionId(),
|
||||
);
|
||||
|
||||
console.error(formattedError);
|
||||
coreEvents.emitFeedback('error', formattedError);
|
||||
runSyncCleanup();
|
||||
process.exit(maxTurnsError.exitCode);
|
||||
} else {
|
||||
console.error(maxTurnsError.message);
|
||||
coreEvents.emitFeedback('error', maxTurnsError.message);
|
||||
runSyncCleanup();
|
||||
process.exit(maxTurnsError.exitCode);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,19 @@ import { RELAUNCH_EXIT_CODE } from './processUtils.js';
|
||||
import type { ChildProcess } 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) => {
|
||||
const actual = await importOriginal<typeof import('node:child_process')>();
|
||||
return {
|
||||
@@ -33,23 +46,21 @@ import { relaunchAppInChildProcess, relaunchOnExitCode } from './relaunch.js';
|
||||
|
||||
describe('relaunchOnExitCode', () => {
|
||||
let processExitSpy: MockInstance;
|
||||
let consoleErrorSpy: MockInstance;
|
||||
let stdinResumeSpy: MockInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
|
||||
throw new Error('PROCESS_EXIT_CALLED');
|
||||
});
|
||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
stdinResumeSpy = vi
|
||||
.spyOn(process.stdin, 'resume')
|
||||
.mockImplementation(() => process.stdin);
|
||||
vi.clearAllMocks();
|
||||
mocks.writeToStderr.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
processExitSpy.mockRestore();
|
||||
consoleErrorSpy.mockRestore();
|
||||
stdinResumeSpy.mockRestore();
|
||||
});
|
||||
|
||||
@@ -90,9 +101,10 @@ describe('relaunchOnExitCode', () => {
|
||||
);
|
||||
|
||||
expect(runner).toHaveBeenCalledTimes(1);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Fatal error: Failed to relaunch the CLI process.',
|
||||
error,
|
||||
expect(mocks.writeToStderr).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'Fatal error: Failed to relaunch the CLI process.',
|
||||
),
|
||||
);
|
||||
expect(stdinResumeSpy).toHaveBeenCalled();
|
||||
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||
@@ -101,7 +113,6 @@ describe('relaunchOnExitCode', () => {
|
||||
|
||||
describe('relaunchAppInChildProcess', () => {
|
||||
let processExitSpy: MockInstance;
|
||||
let consoleErrorSpy: MockInstance;
|
||||
let stdinPauseSpy: MockInstance;
|
||||
let stdinResumeSpy: MockInstance;
|
||||
|
||||
@@ -113,6 +124,7 @@ describe('relaunchAppInChildProcess', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.writeToStderr.mockClear();
|
||||
|
||||
process.env = { ...originalEnv };
|
||||
delete process.env['GEMINI_CLI_NO_RELAUNCH'];
|
||||
@@ -124,7 +136,6 @@ describe('relaunchAppInChildProcess', () => {
|
||||
processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
|
||||
throw new Error('PROCESS_EXIT_CALLED');
|
||||
});
|
||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
stdinPauseSpy = vi
|
||||
.spyOn(process.stdin, 'pause')
|
||||
.mockImplementation(() => process.stdin);
|
||||
@@ -140,7 +151,6 @@ describe('relaunchAppInChildProcess', () => {
|
||||
process.execPath = originalExecPath;
|
||||
|
||||
processExitSpy.mockRestore();
|
||||
consoleErrorSpy.mockRestore();
|
||||
stdinPauseSpy.mockRestore();
|
||||
stdinResumeSpy.mockRestore();
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import { spawn } from 'node:child_process';
|
||||
import { RELAUNCH_EXIT_CODE } from './processUtils.js';
|
||||
import { writeToStderr } from '@google/gemini-cli-core';
|
||||
|
||||
export async function relaunchOnExitCode(runner: () => Promise<number>) {
|
||||
while (true) {
|
||||
@@ -17,7 +18,11 @@ export async function relaunchOnExitCode(runner: () => Promise<number>) {
|
||||
}
|
||||
} catch (error) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,7 +251,9 @@ export async function start_sandbox(
|
||||
}
|
||||
|
||||
// stop if image is missing
|
||||
if (!(await ensureSandboxImageIsPresent(config.command, image))) {
|
||||
if (
|
||||
!(await ensureSandboxImageIsPresent(config.command, image, cliConfig))
|
||||
) {
|
||||
const remedy =
|
||||
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.'
|
||||
@@ -718,8 +720,12 @@ async function imageExists(sandbox: string, image: string): Promise<boolean> {
|
||||
});
|
||||
}
|
||||
|
||||
async function pullImage(sandbox: string, image: string): Promise<boolean> {
|
||||
console.info(`Attempting to pull image ${image} using ${sandbox}...`);
|
||||
async function pullImage(
|
||||
sandbox: string,
|
||||
image: string,
|
||||
cliConfig?: Config,
|
||||
): Promise<boolean> {
|
||||
debugLogger.debug(`Attempting to pull image ${image} using ${sandbox}...`);
|
||||
return new Promise((resolve) => {
|
||||
const args = ['pull', image];
|
||||
const pullProcess = spawn(sandbox, args, { stdio: 'pipe' });
|
||||
@@ -727,11 +733,14 @@ async function pullImage(sandbox: string, image: string): Promise<boolean> {
|
||||
let stderrData = '';
|
||||
|
||||
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) => {
|
||||
stderrData += data.toString();
|
||||
// eslint-disable-next-line no-console
|
||||
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) => {
|
||||
if (code === 0) {
|
||||
console.info(`Successfully pulled image ${image}.`);
|
||||
debugLogger.log(`Successfully pulled image ${image}.`);
|
||||
cleanup();
|
||||
resolve(true);
|
||||
} else {
|
||||
@@ -788,6 +797,7 @@ async function pullImage(sandbox: string, image: string): Promise<boolean> {
|
||||
async function ensureSandboxImageIsPresent(
|
||||
sandbox: string,
|
||||
image: string,
|
||||
cliConfig?: Config,
|
||||
): Promise<boolean> {
|
||||
debugLogger.log(`Checking for sandbox image: ${image}`);
|
||||
if (await imageExists(sandbox, image)) {
|
||||
@@ -801,7 +811,7 @@ async function ensureSandboxImageIsPresent(
|
||||
return false;
|
||||
}
|
||||
|
||||
if (await pullImage(sandbox, image)) {
|
||||
if (await pullImage(sandbox, image, cliConfig)) {
|
||||
// After attempting to pull, check again to be certain
|
||||
if (await imageExists(sandbox, image)) {
|
||||
debugLogger.log(`Sandbox image ${image} is now available after pulling.`);
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { cleanupExpiredSessions } from './sessionCleanup.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
|
||||
function createTestConfig(): Config {
|
||||
@@ -112,7 +116,7 @@ describe('Session Cleanup Integration', () => {
|
||||
});
|
||||
|
||||
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 settings: Settings = {
|
||||
|
||||
@@ -100,6 +100,8 @@ function createTestSessions(): SessionInfo[] {
|
||||
describe('Session Cleanup', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.spyOn(debugLogger, 'error').mockImplementation(() => {});
|
||||
vi.spyOn(debugLogger, 'warn').mockImplementation(() => {});
|
||||
// By default, return all test sessions as valid
|
||||
const sessions = createTestSessions();
|
||||
mockGetAllSessionFiles.mockResolvedValue(
|
||||
@@ -154,20 +156,16 @@ describe('Session Cleanup', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
const result = await cleanupExpiredSessions(config, settings);
|
||||
|
||||
expect(result.disabled).toBe(true);
|
||||
expect(result.scanned).toBe(0);
|
||||
expect(result.deleted).toBe(0);
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
expect(debugLogger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'Session cleanup disabled: Error: Invalid retention period format',
|
||||
),
|
||||
);
|
||||
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
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
|
||||
mockGetAllSessionFiles.mockRejectedValue(
|
||||
new Error('Directory access failed'),
|
||||
@@ -349,11 +345,9 @@ describe('Session Cleanup', () => {
|
||||
|
||||
expect(result.disabled).toBe(false);
|
||||
expect(result.failed).toBe(1);
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
expect(debugLogger.warn).toHaveBeenCalledWith(
|
||||
'Session cleanup failed: Directory access failed',
|
||||
);
|
||||
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
expect(result.disabled).toBe(true);
|
||||
expect(result.scanned).toBe(0);
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
expect(debugLogger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
input === '0d'
|
||||
? 'Invalid retention period: 0d. Value must be greater than 0'
|
||||
: `Invalid retention period format: ${input}`,
|
||||
),
|
||||
);
|
||||
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
// 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);
|
||||
|
||||
expect(result.disabled).toBe(true);
|
||||
expect(result.scanned).toBe(0);
|
||||
// Empty string means no valid retention method specified
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
expect(debugLogger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Either maxAge or maxCount must be specified'),
|
||||
);
|
||||
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
// Test edge cases
|
||||
@@ -1082,17 +1068,13 @@ describe('Session Cleanup', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
const result = await cleanupExpiredSessions(config, settings);
|
||||
|
||||
expect(result.disabled).toBe(true);
|
||||
expect(result.scanned).toBe(0);
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
expect(debugLogger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Either maxAge or maxCount must be specified'),
|
||||
);
|
||||
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
expect(result.disabled).toBe(true);
|
||||
expect(result.scanned).toBe(0);
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
expect(debugLogger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('maxCount must be at least 1'),
|
||||
);
|
||||
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe('maxAge format validation', () => {
|
||||
@@ -1135,21 +1113,14 @@ describe('Session Cleanup', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const errorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const result = await cleanupExpiredSessions(config, settings);
|
||||
|
||||
expect(result.disabled).toBe(true);
|
||||
expect(result.scanned).toBe(0);
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
expect(debugLogger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Invalid retention period format: 30'),
|
||||
);
|
||||
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should reject invalid maxAge format - invalid unit', async () => {
|
||||
const config = createMockConfig({
|
||||
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);
|
||||
|
||||
expect(result.disabled).toBe(true);
|
||||
expect(result.scanned).toBe(0);
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
expect(debugLogger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Invalid retention period format: 30x'),
|
||||
);
|
||||
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should reject invalid maxAge format - no number', async () => {
|
||||
const config = createMockConfig({
|
||||
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);
|
||||
|
||||
expect(result.disabled).toBe(true);
|
||||
expect(result.scanned).toBe(0);
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
expect(debugLogger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Invalid retention period format: d'),
|
||||
);
|
||||
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should reject invalid maxAge format - decimal number', async () => {
|
||||
const config = createMockConfig({
|
||||
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);
|
||||
|
||||
expect(result.disabled).toBe(true);
|
||||
expect(result.scanned).toBe(0);
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
expect(debugLogger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Invalid retention period format: 1.5d'),
|
||||
);
|
||||
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should reject invalid maxAge format - negative number', async () => {
|
||||
const config = createMockConfig({
|
||||
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);
|
||||
|
||||
expect(result.disabled).toBe(true);
|
||||
expect(result.scanned).toBe(0);
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
expect(debugLogger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Invalid retention period format: -5d'),
|
||||
);
|
||||
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should accept valid maxAge format - hours', async () => {
|
||||
const config = createMockConfig();
|
||||
const settings: Settings = {
|
||||
@@ -1362,23 +1305,16 @@ describe('Session Cleanup', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const errorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const result = await cleanupExpiredSessions(config, settings);
|
||||
|
||||
expect(result.disabled).toBe(true);
|
||||
expect(result.scanned).toBe(0);
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
expect(debugLogger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'maxAge cannot be less than minRetention (1d)',
|
||||
),
|
||||
);
|
||||
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should reject maxAge less than custom minRetention', async () => {
|
||||
const config = createMockConfig({
|
||||
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);
|
||||
|
||||
expect(result.disabled).toBe(true);
|
||||
expect(result.scanned).toBe(0);
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
expect(debugLogger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'maxAge cannot be less than minRetention (3d)',
|
||||
),
|
||||
);
|
||||
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should accept maxAge equal to minRetention', async () => {
|
||||
const config = createMockConfig();
|
||||
const settings: Settings = {
|
||||
@@ -1537,21 +1466,14 @@ describe('Session Cleanup', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const errorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const result = await cleanupExpiredSessions(config, settings);
|
||||
|
||||
expect(result.disabled).toBe(true);
|
||||
expect(result.scanned).toBe(0);
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
expect(debugLogger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('maxCount must be at least 1'),
|
||||
);
|
||||
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should accept valid maxCount in normal range', async () => {
|
||||
const config = createMockConfig();
|
||||
const settings: Settings = {
|
||||
@@ -1611,22 +1533,15 @@ describe('Session Cleanup', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const errorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const result = await cleanupExpiredSessions(config, settings);
|
||||
|
||||
expect(result.disabled).toBe(true);
|
||||
expect(result.scanned).toBe(0);
|
||||
// Should fail on first validation error (maxAge format)
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
expect(debugLogger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Invalid retention period format'),
|
||||
);
|
||||
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should reject if maxAge is invalid even when maxCount is valid', async () => {
|
||||
const config = createMockConfig({
|
||||
getDebugMode: vi.fn().mockReturnValue(true),
|
||||
@@ -1642,20 +1557,14 @@ describe('Session Cleanup', () => {
|
||||
};
|
||||
|
||||
// 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);
|
||||
|
||||
// Should reject due to invalid maxAge format
|
||||
expect(result.disabled).toBe(true);
|
||||
expect(result.scanned).toBe(0);
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
expect(debugLogger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Invalid retention period format'),
|
||||
);
|
||||
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ export async function cleanupExpiredSessions(
|
||||
);
|
||||
if (validationErrorMessage) {
|
||||
// Log validation errors to console for visibility
|
||||
console.error(`Session cleanup disabled: ${validationErrorMessage}`);
|
||||
debugLogger.warn(`Session cleanup disabled: ${validationErrorMessage}`);
|
||||
return { ...result, disabled: true };
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ export async function cleanupExpiredSessions(
|
||||
: sessionToDelete.sessionInfo.id;
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Unknown error';
|
||||
console.error(
|
||||
debugLogger.warn(
|
||||
`Failed to delete session ${sessionId}: ${errorMessage}`,
|
||||
);
|
||||
result.failed++;
|
||||
@@ -133,7 +133,7 @@ export async function cleanupExpiredSessions(
|
||||
// Global error handler - don't let cleanup failures break startup
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Unknown error';
|
||||
console.error(`Session cleanup failed: ${errorMessage}`);
|
||||
debugLogger.warn(`Session cleanup failed: ${errorMessage}`);
|
||||
result.failed++;
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ function validateRetentionConfig(
|
||||
} catch (error) {
|
||||
// If minRetention format is invalid, fall back to default
|
||||
if (config.getDebugMode()) {
|
||||
console.error(`Failed to parse minRetention: ${error}`);
|
||||
debugLogger.warn(`Failed to parse minRetention: ${error}`);
|
||||
}
|
||||
minRetentionMs = parseRetentionPeriod(DEFAULT_MIN_RETENTION);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,11 @@ import { ChatRecordingService } from '@google/gemini-cli-core';
|
||||
import { listSessions, deleteSession } from './sessions.js';
|
||||
import { SessionSelector, type SessionInfo } from './sessionUtils.js';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
writeToStdout: vi.fn(),
|
||||
writeToStderr: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock the SessionSelector and ChatRecordingService
|
||||
vi.mock('./sessionUtils.js', () => ({
|
||||
SessionSelector: vi.fn(),
|
||||
@@ -22,13 +27,14 @@ vi.mock('@google/gemini-cli-core', async () => {
|
||||
...actual,
|
||||
ChatRecordingService: vi.fn(),
|
||||
generateSummary: vi.fn().mockResolvedValue(undefined),
|
||||
writeToStdout: mocks.writeToStdout,
|
||||
writeToStderr: mocks.writeToStderr,
|
||||
};
|
||||
});
|
||||
|
||||
describe('listSessions', () => {
|
||||
let mockConfig: Config;
|
||||
let mockListSessions: ReturnType<typeof vi.fn>;
|
||||
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create mock config
|
||||
@@ -49,14 +55,12 @@ describe('listSessions', () => {
|
||||
listSessions: mockListSessions,
|
||||
}) as unknown as InstanceType<typeof SessionSelector>,
|
||||
);
|
||||
|
||||
// Spy on console.log
|
||||
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
consoleLogSpy.mockRestore();
|
||||
mocks.writeToStdout.mockClear();
|
||||
mocks.writeToStderr.mockClear();
|
||||
});
|
||||
|
||||
it('should display message when no previous sessions were found', async () => {
|
||||
@@ -68,7 +72,7 @@ describe('listSessions', () => {
|
||||
|
||||
// Assert
|
||||
expect(mockListSessions).toHaveBeenCalledOnce();
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect(mocks.writeToStdout).toHaveBeenCalledWith(
|
||||
'No previous sessions found for this project.',
|
||||
);
|
||||
});
|
||||
@@ -127,32 +131,32 @@ describe('listSessions', () => {
|
||||
expect(mockListSessions).toHaveBeenCalledOnce();
|
||||
|
||||
// Check that the header was displayed
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect(mocks.writeToStdout).toHaveBeenCalledWith(
|
||||
'\nAvailable sessions for this project (3):\n',
|
||||
);
|
||||
|
||||
// Check that each session was logged
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect(mocks.writeToStdout).toHaveBeenCalledWith(
|
||||
expect.stringContaining('1. First user message'),
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect(mocks.writeToStdout).toHaveBeenCalledWith(
|
||||
expect.stringContaining('[session-1]'),
|
||||
);
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect(mocks.writeToStdout).toHaveBeenCalledWith(
|
||||
expect.stringContaining('2. Second user message'),
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect(mocks.writeToStdout).toHaveBeenCalledWith(
|
||||
expect.stringContaining('[session-2]'),
|
||||
);
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect(mocks.writeToStdout).toHaveBeenCalledWith(
|
||||
expect.stringContaining('3. Current session'),
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect(mocks.writeToStdout).toHaveBeenCalledWith(
|
||||
expect.stringContaining(', current)'),
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect(mocks.writeToStdout).toHaveBeenCalledWith(
|
||||
expect.stringContaining('[current-session-id]'),
|
||||
);
|
||||
});
|
||||
@@ -209,7 +213,7 @@ describe('listSessions', () => {
|
||||
|
||||
// Assert
|
||||
// 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] =>
|
||||
typeof call[0] === 'string' &&
|
||||
call[0].includes('[session-') &&
|
||||
@@ -246,13 +250,13 @@ describe('listSessions', () => {
|
||||
await listSessions(mockConfig);
|
||||
|
||||
// Assert
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect(mocks.writeToStdout).toHaveBeenCalledWith(
|
||||
expect.stringContaining('1. Test message'),
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect(mocks.writeToStdout).toHaveBeenCalledWith(
|
||||
expect.stringContaining('some time ago'),
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect(mocks.writeToStdout).toHaveBeenCalledWith(
|
||||
expect.stringContaining('[abc123def456]'),
|
||||
);
|
||||
});
|
||||
@@ -281,13 +285,13 @@ describe('listSessions', () => {
|
||||
await listSessions(mockConfig);
|
||||
|
||||
// Assert
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect(mocks.writeToStdout).toHaveBeenCalledWith(
|
||||
'\nAvailable sessions for this project (1):\n',
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect(mocks.writeToStdout).toHaveBeenCalledWith(
|
||||
expect.stringContaining('1. Only session'),
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect(mocks.writeToStdout).toHaveBeenCalledWith(
|
||||
expect.stringContaining(', current)'),
|
||||
);
|
||||
});
|
||||
@@ -318,10 +322,10 @@ describe('listSessions', () => {
|
||||
await listSessions(mockConfig);
|
||||
|
||||
// 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(consoleLogSpy).not.toHaveBeenCalledWith(
|
||||
expect(mocks.writeToStdout).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('How do I add dark mode to my React application'),
|
||||
);
|
||||
});
|
||||
@@ -331,8 +335,6 @@ describe('deleteSession', () => {
|
||||
let mockConfig: Config;
|
||||
let mockListSessions: ReturnType<typeof vi.fn>;
|
||||
let mockDeleteSession: ReturnType<typeof vi.fn>;
|
||||
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
|
||||
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create mock config
|
||||
@@ -362,16 +364,10 @@ describe('deleteSession', () => {
|
||||
deleteSession: mockDeleteSession,
|
||||
}) as unknown as InstanceType<typeof ChatRecordingService>,
|
||||
);
|
||||
|
||||
// Spy on console methods
|
||||
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
consoleLogSpy.mockRestore();
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should display error when no sessions are found', async () => {
|
||||
@@ -383,7 +379,7 @@ describe('deleteSession', () => {
|
||||
|
||||
// Assert
|
||||
expect(mockListSessions).toHaveBeenCalledOnce();
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect(mocks.writeToStderr).toHaveBeenCalledWith(
|
||||
'No sessions found for this project.',
|
||||
);
|
||||
expect(mockDeleteSession).not.toHaveBeenCalled();
|
||||
@@ -416,10 +412,10 @@ describe('deleteSession', () => {
|
||||
// Assert
|
||||
expect(mockListSessions).toHaveBeenCalledOnce();
|
||||
expect(mockDeleteSession).toHaveBeenCalledWith('session-file-123');
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect(mocks.writeToStdout).toHaveBeenCalledWith(
|
||||
'Deleted session 1: Test session (some time ago)',
|
||||
);
|
||||
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||
expect(mocks.writeToStderr).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should delete session by index', async () => {
|
||||
@@ -463,7 +459,7 @@ describe('deleteSession', () => {
|
||||
// Assert
|
||||
expect(mockListSessions).toHaveBeenCalledOnce();
|
||||
expect(mockDeleteSession).toHaveBeenCalledWith('session-file-2');
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect(mocks.writeToStdout).toHaveBeenCalledWith(
|
||||
'Deleted session 2: Second session (some time ago)',
|
||||
);
|
||||
});
|
||||
@@ -492,7 +488,7 @@ describe('deleteSession', () => {
|
||||
await deleteSession(mockConfig, 'invalid-id');
|
||||
|
||||
// Assert
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect(mocks.writeToStderr).toHaveBeenCalledWith(
|
||||
'Invalid session identifier "invalid-id". Use --list-sessions to see available sessions.',
|
||||
);
|
||||
expect(mockDeleteSession).not.toHaveBeenCalled();
|
||||
@@ -522,7 +518,7 @@ describe('deleteSession', () => {
|
||||
await deleteSession(mockConfig, '999');
|
||||
|
||||
// Assert
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect(mocks.writeToStderr).toHaveBeenCalledWith(
|
||||
'Invalid session identifier "999". Use --list-sessions to see available sessions.',
|
||||
);
|
||||
expect(mockDeleteSession).not.toHaveBeenCalled();
|
||||
@@ -552,7 +548,7 @@ describe('deleteSession', () => {
|
||||
await deleteSession(mockConfig, '0');
|
||||
|
||||
// Assert
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect(mocks.writeToStderr).toHaveBeenCalledWith(
|
||||
'Invalid session identifier "0". Use --list-sessions to see available sessions.',
|
||||
);
|
||||
expect(mockDeleteSession).not.toHaveBeenCalled();
|
||||
@@ -582,7 +578,7 @@ describe('deleteSession', () => {
|
||||
await deleteSession(mockConfig, '1');
|
||||
|
||||
// Assert
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect(mocks.writeToStderr).toHaveBeenCalledWith(
|
||||
'Cannot delete the current active session.',
|
||||
);
|
||||
expect(mockDeleteSession).not.toHaveBeenCalled();
|
||||
@@ -612,7 +608,7 @@ describe('deleteSession', () => {
|
||||
await deleteSession(mockConfig, 'current-session-id');
|
||||
|
||||
// Assert
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect(mocks.writeToStderr).toHaveBeenCalledWith(
|
||||
'Cannot delete the current active session.',
|
||||
);
|
||||
expect(mockDeleteSession).not.toHaveBeenCalled();
|
||||
@@ -646,7 +642,7 @@ describe('deleteSession', () => {
|
||||
|
||||
// Assert
|
||||
expect(mockDeleteSession).toHaveBeenCalledWith('session-file-1');
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect(mocks.writeToStderr).toHaveBeenCalledWith(
|
||||
'Failed to delete session: File deletion failed',
|
||||
);
|
||||
});
|
||||
@@ -679,7 +675,7 @@ describe('deleteSession', () => {
|
||||
await deleteSession(mockConfig, '1');
|
||||
|
||||
// Assert
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect(mocks.writeToStderr).toHaveBeenCalledWith(
|
||||
'Failed to delete session: Unknown error',
|
||||
);
|
||||
});
|
||||
@@ -737,7 +733,7 @@ describe('deleteSession', () => {
|
||||
|
||||
// Assert
|
||||
expect(mockDeleteSession).toHaveBeenCalledWith('session-file-1');
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect(mocks.writeToStdout).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Oldest session'),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
import {
|
||||
ChatRecordingService,
|
||||
generateSummary,
|
||||
writeToStderr,
|
||||
writeToStdout,
|
||||
type Config,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
@@ -23,11 +25,13 @@ export async function listSessions(config: Config): Promise<void> {
|
||||
const sessions = await sessionSelector.listSessions();
|
||||
|
||||
if (sessions.length === 0) {
|
||||
console.log('No previous sessions found for this project.');
|
||||
writeToStdout('No previous sessions found for this project.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`\nAvailable sessions for this project (${sessions.length}):\n`);
|
||||
writeToStdout(
|
||||
`\nAvailable sessions for this project (${sessions.length}):\n`,
|
||||
);
|
||||
|
||||
sessions
|
||||
.sort(
|
||||
@@ -41,8 +45,8 @@ export async function listSessions(config: Config): Promise<void> {
|
||||
session.displayName.length > 100
|
||||
? session.displayName.slice(0, 97) + '...'
|
||||
: session.displayName;
|
||||
console.log(
|
||||
` ${index + 1}. ${title} (${time}${current}) [${session.id}]`,
|
||||
writeToStdout(
|
||||
` ${index + 1}. ${title} (${time}${current}) [${session.id}]\n`,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -55,7 +59,7 @@ export async function deleteSession(
|
||||
const sessions = await sessionSelector.listSessions();
|
||||
|
||||
if (sessions.length === 0) {
|
||||
console.error('No sessions found for this project.');
|
||||
writeToStderr('No sessions found for this project.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -76,7 +80,7 @@ export async function deleteSession(
|
||||
// Parse session index
|
||||
const index = parseInt(sessionIndex, 10);
|
||||
if (isNaN(index) || index < 1 || index > sessions.length) {
|
||||
console.error(
|
||||
writeToStderr(
|
||||
`Invalid session identifier "${sessionIndex}". Use --list-sessions to see available sessions.`,
|
||||
);
|
||||
return;
|
||||
@@ -86,7 +90,7 @@ export async function deleteSession(
|
||||
|
||||
// Prevent deleting the current session
|
||||
if (sessionToDelete.isCurrentSession) {
|
||||
console.error('Cannot delete the current active session.');
|
||||
writeToStderr('Cannot delete the current active session.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -96,11 +100,11 @@ export async function deleteSession(
|
||||
chatRecordingService.deleteSession(sessionToDelete.file);
|
||||
|
||||
const time = formatRelativeTime(sessionToDelete.lastUpdated);
|
||||
console.log(
|
||||
writeToStdout(
|
||||
`Deleted session ${sessionToDelete.index}: ${sessionToDelete.firstUserMessage} (${time})`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
writeToStderr(
|
||||
`Failed to delete session: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
makeFakeConfig,
|
||||
debugLogger,
|
||||
ExitCodes,
|
||||
coreEvents,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import * as auth from './config/auth.js';
|
||||
@@ -36,8 +37,8 @@ describe('validateNonInterActiveAuth', () => {
|
||||
let originalEnvGeminiApiKey: string | undefined;
|
||||
let originalEnvVertexAi: string | undefined;
|
||||
let originalEnvGcp: string | undefined;
|
||||
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
|
||||
let debugLoggerErrorSpy: ReturnType<typeof vi.spyOn>;
|
||||
let coreEventsEmitFeedbackSpy: MockInstance;
|
||||
let processExitSpy: MockInstance;
|
||||
let refreshAuthMock: Mock;
|
||||
let mockSettings: LoadedSettings;
|
||||
@@ -49,10 +50,12 @@ describe('validateNonInterActiveAuth', () => {
|
||||
delete process.env['GEMINI_API_KEY'];
|
||||
delete process.env['GOOGLE_GENAI_USE_VERTEXAI'];
|
||||
delete process.env['GOOGLE_GENAI_USE_GCA'];
|
||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
debugLoggerErrorSpy = vi
|
||||
.spyOn(debugLogger, 'error')
|
||||
.mockImplementation(() => {});
|
||||
coreEventsEmitFeedbackSpy = vi
|
||||
.spyOn(coreEvents, 'emitFeedback')
|
||||
.mockImplementation(() => {});
|
||||
processExitSpy = vi
|
||||
.spyOn(process, 'exit')
|
||||
.mockImplementation((code?: string | number | null | undefined) => {
|
||||
@@ -302,6 +305,7 @@ describe('validateNonInterActiveAuth', () => {
|
||||
|
||||
expect(validateAuthMethodSpy).not.toHaveBeenCalled();
|
||||
expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
|
||||
expect(coreEventsEmitFeedbackSpy).not.toHaveBeenCalled();
|
||||
expect(processExitSpy).not.toHaveBeenCalled();
|
||||
// We still expect refreshAuth to be called with the (invalid) type
|
||||
expect(refreshAuthMock).toHaveBeenCalledWith('invalid-auth-type');
|
||||
@@ -404,7 +408,8 @@ describe('validateNonInterActiveAuth', () => {
|
||||
expect(thrown?.message).toBe(
|
||||
`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);
|
||||
expect(payload.error.type).toBe('Error');
|
||||
expect(payload.error.code).toBe(ExitCodes.FATAL_AUTHENTICATION_ERROR);
|
||||
@@ -439,7 +444,8 @@ describe('validateNonInterActiveAuth', () => {
|
||||
`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);
|
||||
expect(payload.error.type).toBe('Error');
|
||||
expect(payload.error.code).toBe(ExitCodes.FATAL_AUTHENTICATION_ERROR);
|
||||
@@ -477,7 +483,8 @@ describe('validateNonInterActiveAuth', () => {
|
||||
`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);
|
||||
expect(payload.error.type).toBe('Error');
|
||||
expect(payload.error.code).toBe(ExitCodes.FATAL_AUTHENTICATION_ERROR);
|
||||
|
||||
Reference in New Issue
Block a user