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