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
+128 -38
View File
@@ -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();
});
});
});
+10 -9
View File
@@ -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);
}
+19 -9
View File
@@ -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 -1
View File
@@ -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);
}
}
+16 -6
View File
@@ -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 = {
+18 -109
View File
@@ -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();
});
});
+4 -4
View File
@@ -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);
}
+39 -43
View File
@@ -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'),
);
});
+13 -9
View File
@@ -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'}`,
);
}