mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-25 20:44:46 -07:00
Protect stdout and stderr so JavaScript code can't accidentally write to stdout corrupting ink rendering (#13247)
Bypassing rules as link checker failure is spurious.
This commit is contained in:
@@ -4,76 +4,79 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { debugLogger } from './debugLogger.js';
|
||||
|
||||
describe('DebugLogger', () => {
|
||||
// Spy on all console methods before each test
|
||||
beforeEach(() => {
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
vi.spyOn(console, 'debug').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
// Restore original console methods after each test
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should call console.log with the correct arguments', () => {
|
||||
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
const message = 'This is a log message';
|
||||
const data = { key: 'value' };
|
||||
debugLogger.log(message, data);
|
||||
expect(console.log).toHaveBeenCalledWith(message, data);
|
||||
expect(console.log).toHaveBeenCalledTimes(1);
|
||||
expect(spy).toHaveBeenCalledWith(message, data);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call console.warn with the correct arguments', () => {
|
||||
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const message = 'This is a warning message';
|
||||
const data = [1, 2, 3];
|
||||
debugLogger.warn(message, data);
|
||||
expect(console.warn).toHaveBeenCalledWith(message, data);
|
||||
expect(console.warn).toHaveBeenCalledTimes(1);
|
||||
expect(spy).toHaveBeenCalledWith(message, data);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call console.error with the correct arguments', () => {
|
||||
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const message = 'This is an error message';
|
||||
const error = new Error('Something went wrong');
|
||||
debugLogger.error(message, error);
|
||||
expect(console.error).toHaveBeenCalledWith(message, error);
|
||||
expect(console.error).toHaveBeenCalledTimes(1);
|
||||
expect(spy).toHaveBeenCalledWith(message, error);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call console.debug with the correct arguments', () => {
|
||||
const spy = vi.spyOn(console, 'debug').mockImplementation(() => {});
|
||||
const message = 'This is a debug message';
|
||||
const obj = { a: { b: 'c' } };
|
||||
debugLogger.debug(message, obj);
|
||||
expect(console.debug).toHaveBeenCalledWith(message, obj);
|
||||
expect(console.debug).toHaveBeenCalledTimes(1);
|
||||
expect(spy).toHaveBeenCalledWith(message, obj);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle multiple arguments correctly for all methods', () => {
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {});
|
||||
|
||||
debugLogger.log('one', 2, true);
|
||||
expect(console.log).toHaveBeenCalledWith('one', 2, true);
|
||||
expect(logSpy).toHaveBeenCalledWith('one', 2, true);
|
||||
|
||||
debugLogger.warn('one', 2, false);
|
||||
expect(console.warn).toHaveBeenCalledWith('one', 2, false);
|
||||
expect(warnSpy).toHaveBeenCalledWith('one', 2, false);
|
||||
|
||||
debugLogger.error('one', 2, null);
|
||||
expect(console.error).toHaveBeenCalledWith('one', 2, null);
|
||||
expect(errorSpy).toHaveBeenCalledWith('one', 2, null);
|
||||
|
||||
debugLogger.debug('one', 2, undefined);
|
||||
expect(console.debug).toHaveBeenCalledWith('one', 2, undefined);
|
||||
expect(debugSpy).toHaveBeenCalledWith('one', 2, undefined);
|
||||
});
|
||||
|
||||
it('should handle calls with no arguments', () => {
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
debugLogger.log();
|
||||
expect(console.log).toHaveBeenCalledWith();
|
||||
expect(console.log).toHaveBeenCalledTimes(1);
|
||||
expect(logSpy).toHaveBeenCalledWith();
|
||||
expect(logSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
debugLogger.warn();
|
||||
expect(console.warn).toHaveBeenCalledWith();
|
||||
expect(console.warn).toHaveBeenCalledTimes(1);
|
||||
expect(warnSpy).toHaveBeenCalledWith();
|
||||
expect(warnSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
type EditorType,
|
||||
} from './editor.js';
|
||||
import { execSync, spawn, spawnSync } from 'node:child_process';
|
||||
import { debugLogger } from './debugLogger.js';
|
||||
|
||||
vi.mock('child_process', () => ({
|
||||
execSync: vi.fn(),
|
||||
@@ -342,7 +343,7 @@ describe('editor utils', () => {
|
||||
});
|
||||
(spawn as Mock).mockReturnValue({ on: mockSpawnOn });
|
||||
|
||||
await openDiff('old.txt', 'new.txt', editor, () => {});
|
||||
await openDiff('old.txt', 'new.txt', editor);
|
||||
const diffCommand = getDiffCommand('old.txt', 'new.txt', editor)!;
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
diffCommand.command,
|
||||
@@ -365,9 +366,9 @@ describe('editor utils', () => {
|
||||
});
|
||||
(spawn as Mock).mockReturnValue({ on: mockSpawnOn });
|
||||
|
||||
await expect(
|
||||
openDiff('old.txt', 'new.txt', editor, () => {}),
|
||||
).rejects.toThrow('spawn error');
|
||||
await expect(openDiff('old.txt', 'new.txt', editor)).rejects.toThrow(
|
||||
'spawn error',
|
||||
);
|
||||
});
|
||||
|
||||
it(`should reject if ${editor} exits with non-zero code`, async () => {
|
||||
@@ -378,9 +379,9 @@ describe('editor utils', () => {
|
||||
});
|
||||
(spawn as Mock).mockReturnValue({ on: mockSpawnOn });
|
||||
|
||||
await expect(
|
||||
openDiff('old.txt', 'new.txt', editor, () => {}),
|
||||
).rejects.toThrow(`${editor} exited with code 1`);
|
||||
await expect(openDiff('old.txt', 'new.txt', editor)).rejects.toThrow(
|
||||
`${editor} exited with code 1`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -388,7 +389,7 @@ describe('editor utils', () => {
|
||||
|
||||
for (const editor of terminalEditors) {
|
||||
it(`should call spawnSync for ${editor}`, async () => {
|
||||
await openDiff('old.txt', 'new.txt', editor, () => {});
|
||||
await openDiff('old.txt', 'new.txt', editor);
|
||||
const diffCommand = getDiffCommand('old.txt', 'new.txt', editor)!;
|
||||
expect(spawnSync).toHaveBeenCalledWith(
|
||||
diffCommand.command,
|
||||
@@ -402,60 +403,14 @@ describe('editor utils', () => {
|
||||
|
||||
it('should log an error if diff command is not available', async () => {
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.spyOn(debugLogger, 'error')
|
||||
.mockImplementation(() => {});
|
||||
// @ts-expect-error Testing unsupported editor
|
||||
await openDiff('old.txt', 'new.txt', 'foobar', () => {});
|
||||
await openDiff('old.txt', 'new.txt', 'foobar');
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'No diff tool available. Install a supported editor.',
|
||||
);
|
||||
});
|
||||
|
||||
describe('onEditorClose callback', () => {
|
||||
const terminalEditors: EditorType[] = ['vim', 'neovim', 'emacs'];
|
||||
for (const editor of terminalEditors) {
|
||||
it(`should call onEditorClose for ${editor} on close`, async () => {
|
||||
const onEditorClose = vi.fn();
|
||||
await openDiff('old.txt', 'new.txt', editor, onEditorClose);
|
||||
expect(onEditorClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it(`should call onEditorClose for ${editor} on error`, async () => {
|
||||
const onEditorClose = vi.fn();
|
||||
const mockError = new Error('spawn error');
|
||||
(spawnSync as Mock).mockImplementation(() => {
|
||||
throw mockError;
|
||||
});
|
||||
|
||||
await expect(
|
||||
openDiff('old.txt', 'new.txt', editor, onEditorClose),
|
||||
).rejects.toThrow('spawn error');
|
||||
expect(onEditorClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
}
|
||||
|
||||
const guiEditors: EditorType[] = [
|
||||
'vscode',
|
||||
'vscodium',
|
||||
'windsurf',
|
||||
'cursor',
|
||||
'zed',
|
||||
'antigravity',
|
||||
];
|
||||
for (const editor of guiEditors) {
|
||||
it(`should not call onEditorClose for ${editor}`, async () => {
|
||||
const onEditorClose = vi.fn();
|
||||
const mockSpawnOn = vi.fn((event, cb) => {
|
||||
if (event === 'close') {
|
||||
cb(0);
|
||||
}
|
||||
});
|
||||
(spawn as Mock).mockReturnValue({ on: mockSpawnOn });
|
||||
await openDiff('old.txt', 'new.txt', editor, onEditorClose);
|
||||
expect(onEditorClose).not.toHaveBeenCalled();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('allowEditorTypeInSandbox', () => {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import { execSync, spawn, spawnSync } from 'node:child_process';
|
||||
import { debugLogger } from './debugLogger.js';
|
||||
import { coreEvents, CoreEvent } from './events.js';
|
||||
|
||||
export type EditorType =
|
||||
| 'vscode'
|
||||
@@ -189,7 +190,6 @@ export async function openDiff(
|
||||
oldPath: string,
|
||||
newPath: string,
|
||||
editor: EditorType,
|
||||
onEditorClose: () => void,
|
||||
): Promise<void> {
|
||||
const diffCommand = getDiffCommand(oldPath, newPath, editor);
|
||||
if (!diffCommand) {
|
||||
@@ -211,7 +211,7 @@ export async function openDiff(
|
||||
throw new Error(`${editor} exited with code ${result.status}`);
|
||||
}
|
||||
} finally {
|
||||
onEditorClose();
|
||||
coreEvents.emit(CoreEvent.ExternalEditorClosed);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ describe('CoreEventEmitter', () => {
|
||||
|
||||
// Attach listener and drain
|
||||
events.on(CoreEvent.UserFeedback, listener);
|
||||
events.drainFeedbackBacklog();
|
||||
events.drainBacklogs();
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
expect(listener).toHaveBeenCalledWith(expect.objectContaining(payload));
|
||||
@@ -61,7 +61,7 @@ describe('CoreEventEmitter', () => {
|
||||
}
|
||||
|
||||
events.on(CoreEvent.UserFeedback, listener);
|
||||
events.drainFeedbackBacklog();
|
||||
events.drainBacklogs();
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(MAX_BACKLOG_SIZE);
|
||||
// Verify strictly that the FIRST call was Message 10 (0-9 dropped)
|
||||
@@ -77,11 +77,11 @@ describe('CoreEventEmitter', () => {
|
||||
events.emitFeedback('error', 'Test error');
|
||||
|
||||
events.on(CoreEvent.UserFeedback, listener);
|
||||
events.drainFeedbackBacklog();
|
||||
events.drainBacklogs();
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
|
||||
listener.mockClear();
|
||||
events.drainFeedbackBacklog();
|
||||
events.drainBacklogs();
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -138,7 +138,7 @@ describe('CoreEventEmitter', () => {
|
||||
});
|
||||
|
||||
events.on(CoreEvent.UserFeedback, listener);
|
||||
events.drainFeedbackBacklog();
|
||||
events.drainBacklogs();
|
||||
|
||||
// Expectation with atomic snapshot:
|
||||
// 1. loop starts with ['Buffered 1', 'Buffered 2']
|
||||
@@ -157,6 +157,111 @@ describe('CoreEventEmitter', () => {
|
||||
expect(listener.mock.calls[2][0]).toMatchObject({ message: 'Buffered 2' });
|
||||
});
|
||||
|
||||
describe('ConsoleLog Event', () => {
|
||||
it('should emit console log immediately when a listener is present', () => {
|
||||
const listener = vi.fn();
|
||||
events.on(CoreEvent.ConsoleLog, listener);
|
||||
|
||||
const payload = {
|
||||
type: 'info' as const,
|
||||
content: 'Test log',
|
||||
};
|
||||
|
||||
events.emitConsoleLog(payload.type, payload.content);
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
expect(listener).toHaveBeenCalledWith(expect.objectContaining(payload));
|
||||
});
|
||||
|
||||
it('should buffer console logs when no listener is present', () => {
|
||||
const listener = vi.fn();
|
||||
const payload = {
|
||||
type: 'warn' as const,
|
||||
content: 'Buffered log',
|
||||
};
|
||||
|
||||
// Emit while no listeners attached
|
||||
events.emitConsoleLog(payload.type, payload.content);
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
|
||||
// Attach listener and drain
|
||||
events.on(CoreEvent.ConsoleLog, listener);
|
||||
events.drainBacklogs();
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
expect(listener).toHaveBeenCalledWith(expect.objectContaining(payload));
|
||||
});
|
||||
|
||||
it('should respect the backlog size limit for console logs', () => {
|
||||
const listener = vi.fn();
|
||||
const MAX_BACKLOG_SIZE = 10000;
|
||||
|
||||
for (let i = 0; i < MAX_BACKLOG_SIZE + 10; i++) {
|
||||
events.emitConsoleLog('debug', `Log ${i}`);
|
||||
}
|
||||
|
||||
events.on(CoreEvent.ConsoleLog, listener);
|
||||
events.drainBacklogs();
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(MAX_BACKLOG_SIZE);
|
||||
// Verify strictly that the FIRST call was Log 10 (0-9 dropped)
|
||||
expect(listener.mock.calls[0][0]).toMatchObject({ content: 'Log 10' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Output Event', () => {
|
||||
it('should emit output immediately when a listener is present', () => {
|
||||
const listener = vi.fn();
|
||||
events.on(CoreEvent.Output, listener);
|
||||
|
||||
const payload = {
|
||||
isStderr: false,
|
||||
chunk: 'Test output',
|
||||
encoding: 'utf8' as BufferEncoding,
|
||||
};
|
||||
|
||||
events.emitOutput(payload.isStderr, payload.chunk, payload.encoding);
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
expect(listener).toHaveBeenCalledWith(expect.objectContaining(payload));
|
||||
});
|
||||
|
||||
it('should buffer output when no listener is present', () => {
|
||||
const listener = vi.fn();
|
||||
const payload = {
|
||||
isStderr: true,
|
||||
chunk: 'Buffered output',
|
||||
};
|
||||
|
||||
// Emit while no listeners attached
|
||||
events.emitOutput(payload.isStderr, payload.chunk);
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
|
||||
// Attach listener and drain
|
||||
events.on(CoreEvent.Output, listener);
|
||||
events.drainBacklogs();
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
expect(listener).toHaveBeenCalledWith(expect.objectContaining(payload));
|
||||
});
|
||||
|
||||
it('should respect the backlog size limit for output', () => {
|
||||
const listener = vi.fn();
|
||||
const MAX_BACKLOG_SIZE = 10000;
|
||||
|
||||
for (let i = 0; i < MAX_BACKLOG_SIZE + 10; i++) {
|
||||
events.emitOutput(false, `Output ${i}`);
|
||||
}
|
||||
|
||||
events.on(CoreEvent.Output, listener);
|
||||
events.drainBacklogs();
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(MAX_BACKLOG_SIZE);
|
||||
// Verify strictly that the FIRST call was Output 10 (0-9 dropped)
|
||||
expect(listener.mock.calls[0][0]).toMatchObject({ chunk: 'Output 10' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('ModelChanged Event', () => {
|
||||
it('should emit ModelChanged event with correct payload', () => {
|
||||
const listener = vi.fn();
|
||||
|
||||
@@ -54,6 +54,23 @@ export interface ModelChangedPayload {
|
||||
model: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload for the 'console-log' event.
|
||||
*/
|
||||
export interface ConsoleLogPayload {
|
||||
type: 'log' | 'warn' | 'error' | 'debug' | 'info';
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload for the 'output' event.
|
||||
*/
|
||||
export interface OutputPayload {
|
||||
isStderr: boolean;
|
||||
chunk: Uint8Array | string;
|
||||
encoding?: BufferEncoding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload for the 'memory-changed' event.
|
||||
*/
|
||||
@@ -63,24 +80,56 @@ export enum CoreEvent {
|
||||
UserFeedback = 'user-feedback',
|
||||
FallbackModeChanged = 'fallback-mode-changed',
|
||||
ModelChanged = 'model-changed',
|
||||
ConsoleLog = 'console-log',
|
||||
Output = 'output',
|
||||
MemoryChanged = 'memory-changed',
|
||||
ExternalEditorClosed = 'external-editor-closed',
|
||||
}
|
||||
|
||||
export interface CoreEvents {
|
||||
[CoreEvent.UserFeedback]: [UserFeedbackPayload];
|
||||
[CoreEvent.FallbackModeChanged]: [FallbackModeChangedPayload];
|
||||
[CoreEvent.ModelChanged]: [ModelChangedPayload];
|
||||
[CoreEvent.ConsoleLog]: [ConsoleLogPayload];
|
||||
[CoreEvent.Output]: [OutputPayload];
|
||||
[CoreEvent.MemoryChanged]: [MemoryChangedPayload];
|
||||
[CoreEvent.ExternalEditorClosed]: never[];
|
||||
}
|
||||
|
||||
type EventBacklogItem = {
|
||||
[K in keyof CoreEvents]: {
|
||||
event: K;
|
||||
args: CoreEvents[K];
|
||||
};
|
||||
}[keyof CoreEvents];
|
||||
|
||||
export class CoreEventEmitter extends EventEmitter<CoreEvents> {
|
||||
private _feedbackBacklog: UserFeedbackPayload[] = [];
|
||||
private _eventBacklog: EventBacklogItem[] = [];
|
||||
private static readonly MAX_BACKLOG_SIZE = 10000;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
private _emitOrQueue<K extends keyof CoreEvents>(
|
||||
event: K,
|
||||
...args: CoreEvents[K]
|
||||
): void {
|
||||
if (this.listenerCount(event) === 0) {
|
||||
if (this._eventBacklog.length >= CoreEventEmitter.MAX_BACKLOG_SIZE) {
|
||||
this._eventBacklog.shift();
|
||||
}
|
||||
this._eventBacklog.push({ event, args } as EventBacklogItem);
|
||||
} else {
|
||||
(
|
||||
this.emit as <K extends keyof CoreEvents>(
|
||||
event: K,
|
||||
...args: CoreEvents[K]
|
||||
) => boolean
|
||||
)(event, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends actionable feedback to the user.
|
||||
* Buffers automatically if the UI hasn't subscribed yet.
|
||||
@@ -91,15 +140,30 @@ export class CoreEventEmitter extends EventEmitter<CoreEvents> {
|
||||
error?: unknown,
|
||||
): void {
|
||||
const payload: UserFeedbackPayload = { severity, message, error };
|
||||
this._emitOrQueue(CoreEvent.UserFeedback, payload);
|
||||
}
|
||||
|
||||
if (this.listenerCount(CoreEvent.UserFeedback) === 0) {
|
||||
if (this._feedbackBacklog.length >= CoreEventEmitter.MAX_BACKLOG_SIZE) {
|
||||
this._feedbackBacklog.shift();
|
||||
}
|
||||
this._feedbackBacklog.push(payload);
|
||||
} else {
|
||||
this.emit(CoreEvent.UserFeedback, payload);
|
||||
}
|
||||
/**
|
||||
* Broadcasts a console log message.
|
||||
*/
|
||||
emitConsoleLog(
|
||||
type: 'log' | 'warn' | 'error' | 'debug' | 'info',
|
||||
content: string,
|
||||
): void {
|
||||
const payload: ConsoleLogPayload = { type, content };
|
||||
this._emitOrQueue(CoreEvent.ConsoleLog, payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts stdout/stderr output.
|
||||
*/
|
||||
emitOutput(
|
||||
isStderr: boolean,
|
||||
chunk: Uint8Array | string,
|
||||
encoding?: BufferEncoding,
|
||||
): void {
|
||||
const payload: OutputPayload = { isStderr, chunk, encoding };
|
||||
this._emitOrQueue(CoreEvent.Output, payload);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,11 +187,16 @@ export class CoreEventEmitter extends EventEmitter<CoreEvents> {
|
||||
* Flushes buffered messages. Call this immediately after primary UI listener
|
||||
* subscribes.
|
||||
*/
|
||||
drainFeedbackBacklog(): void {
|
||||
const backlog = [...this._feedbackBacklog];
|
||||
this._feedbackBacklog.length = 0; // Clear in-place
|
||||
for (const payload of backlog) {
|
||||
this.emit(CoreEvent.UserFeedback, payload);
|
||||
drainBacklogs(): void {
|
||||
const backlog = [...this._eventBacklog];
|
||||
this._eventBacklog.length = 0; // Clear in-place
|
||||
for (const item of backlog) {
|
||||
(
|
||||
this.emit as <K extends keyof CoreEvents>(
|
||||
event: K,
|
||||
...args: CoreEvents[K]
|
||||
) => boolean
|
||||
)(item.event, ...item.args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import * as os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { GEMINI_DIR } from './paths.js';
|
||||
import { debugLogger } from './debugLogger.js';
|
||||
|
||||
vi.mock('node:fs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('node:fs')>();
|
||||
@@ -92,7 +93,7 @@ describe('InstallationManager', () => {
|
||||
throw new Error('Read error');
|
||||
});
|
||||
const consoleWarnSpy = vi
|
||||
.spyOn(console, 'warn')
|
||||
.spyOn(debugLogger, 'warn')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const id = installationManager.getInstallationId();
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from './llm-edit-fixer.js';
|
||||
import { promptIdContext } from './promptIdContext.js';
|
||||
import type { BaseLlmClient } from '../core/baseLlmClient.js';
|
||||
import { debugLogger } from './debugLogger.js';
|
||||
|
||||
// Mock the BaseLlmClient
|
||||
const mockGenerateJson = vi.fn();
|
||||
@@ -92,7 +93,7 @@ describe('FixLLMEditWithInstruction', () => {
|
||||
it('should generate and use a fallback promptId when context is not available', async () => {
|
||||
mockGenerateJson.mockResolvedValue(mockApiResponse);
|
||||
const consoleWarnSpy = vi
|
||||
.spyOn(console, 'warn')
|
||||
.spyOn(debugLogger, 'warn')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
// Run the function outside of any context
|
||||
|
||||
@@ -25,6 +25,7 @@ import { Config, type GeminiCLIExtension } from '../config/config.js';
|
||||
import { Storage } from '../config/storage.js';
|
||||
import { SimpleExtensionLoader } from './extensionLoader.js';
|
||||
import { CoreEvent, coreEvents } from './events.js';
|
||||
import { debugLogger } from './debugLogger.js';
|
||||
|
||||
vi.mock('os', async (importOriginal) => {
|
||||
const actualOs = await importOriginal<typeof os>();
|
||||
@@ -439,7 +440,7 @@ My code memory
|
||||
|
||||
it('should respect the maxDirs parameter during downward scan', async () => {
|
||||
const consoleDebugSpy = vi
|
||||
.spyOn(console, 'debug')
|
||||
.spyOn(debugLogger, 'debug')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
// Create directories in parallel for better performance
|
||||
@@ -469,7 +470,7 @@ My code memory
|
||||
expect.stringContaining('Scanning [1/1]:'),
|
||||
);
|
||||
|
||||
vi.mocked(console.debug).mockRestore();
|
||||
consoleDebugSpy.mockRestore();
|
||||
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
|
||||
@@ -9,6 +9,7 @@ import * as fs from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
import { marked } from 'marked';
|
||||
import { processImports, validateImportPath } from './memoryImportProcessor.js';
|
||||
import { debugLogger } from './debugLogger.js';
|
||||
|
||||
// Helper function to create platform-agnostic test paths
|
||||
function testPath(...segments: string[]): string {
|
||||
@@ -32,11 +33,6 @@ function testPath(...segments: string[]): string {
|
||||
vi.mock('fs/promises');
|
||||
const mockedFs = vi.mocked(fs);
|
||||
|
||||
// Mock console methods to capture warnings
|
||||
const originalConsoleWarn = console.warn;
|
||||
const originalConsoleError = console.error;
|
||||
const originalConsoleDebug = console.debug;
|
||||
|
||||
// Helper functions using marked for parsing and validation
|
||||
const parseMarkdown = (content: string) => marked.lexer(content);
|
||||
|
||||
@@ -94,16 +90,13 @@ describe('memoryImportProcessor', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Mock console methods
|
||||
console.warn = vi.fn();
|
||||
console.error = vi.fn();
|
||||
console.debug = vi.fn();
|
||||
vi.spyOn(debugLogger, 'warn').mockImplementation(() => {});
|
||||
vi.spyOn(debugLogger, 'error').mockImplementation(() => {});
|
||||
vi.spyOn(debugLogger, 'debug').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore console methods
|
||||
console.warn = originalConsoleWarn;
|
||||
console.error = originalConsoleError;
|
||||
console.debug = originalConsoleDebug;
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('processImports', () => {
|
||||
@@ -173,7 +166,7 @@ describe('memoryImportProcessor', () => {
|
||||
|
||||
// Verify the imported content is present
|
||||
expect(result.content).toContain(importedContent);
|
||||
expect(console.warn).not.toHaveBeenCalled();
|
||||
expect(debugLogger.warn).not.toHaveBeenCalled();
|
||||
expect(mockedFs.readFile).toHaveBeenCalledWith(
|
||||
path.resolve(basePath, './instructions.txt'),
|
||||
'utf-8',
|
||||
@@ -215,7 +208,7 @@ describe('memoryImportProcessor', () => {
|
||||
expect(result.content).toContain(
|
||||
'<!-- Import failed: ./nonexistent.md - File not found -->',
|
||||
);
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
expect(debugLogger.error).toHaveBeenCalledWith(
|
||||
'[ERROR] [ImportProcessor]',
|
||||
'Failed to import ./nonexistent.md: File not found',
|
||||
);
|
||||
@@ -237,7 +230,7 @@ describe('memoryImportProcessor', () => {
|
||||
|
||||
const result = await processImports(content, basePath, true, importState);
|
||||
|
||||
expect(console.warn).toHaveBeenCalledWith(
|
||||
expect(debugLogger.warn).toHaveBeenCalledWith(
|
||||
'[WARN] [ImportProcessor]',
|
||||
'Maximum import depth (1) reached. Stopping import processing.',
|
||||
);
|
||||
|
||||
@@ -20,7 +20,7 @@ const logger = {
|
||||
debugLogger.warn('[WARN] [ImportProcessor]', ...args),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
error: (...args: any[]) =>
|
||||
console.error('[ERROR] [ImportProcessor]', ...args),
|
||||
debugLogger.error('[ERROR] [ImportProcessor]', ...args),
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,6 +8,7 @@ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { execSync } from 'node:child_process';
|
||||
import * as os from 'node:os';
|
||||
import { detect as chardetDetect } from 'chardet';
|
||||
import { debugLogger } from './debugLogger.js';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('child_process');
|
||||
@@ -30,7 +31,7 @@ describe('Shell Command Processor - Encoding Functions', () => {
|
||||
let mockedChardetDetect: ReturnType<typeof vi.mocked<typeof chardetDetect>>;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
consoleWarnSpy = vi.spyOn(debugLogger, 'warn').mockImplementation(() => {});
|
||||
mockedExecSync = vi.mocked(execSync);
|
||||
mockedOsPlatform = vi.mocked(os.platform);
|
||||
mockedChardetDetect = vi.mocked(chardetDetect);
|
||||
|
||||
@@ -11,6 +11,7 @@ import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { GEMINI_DIR } from './paths.js';
|
||||
import { debugLogger } from './debugLogger.js';
|
||||
|
||||
vi.mock('os', async (importOriginal) => {
|
||||
const os = await importOriginal<typeof import('os')>();
|
||||
@@ -102,7 +103,7 @@ describe('UserAccountManager', () => {
|
||||
fs.mkdirSync(path.dirname(accountsFile()), { recursive: true });
|
||||
fs.writeFileSync(accountsFile(), 'not valid json');
|
||||
const consoleLogSpy = vi
|
||||
.spyOn(console, 'log')
|
||||
.spyOn(debugLogger, 'log')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
await userAccountManager.cacheGoogleAccount('test1@google.com');
|
||||
@@ -121,7 +122,7 @@ describe('UserAccountManager', () => {
|
||||
JSON.stringify({ active: 'test1@google.com', old: 'not-an-array' }),
|
||||
);
|
||||
const consoleLogSpy = vi
|
||||
.spyOn(console, 'log')
|
||||
.spyOn(debugLogger, 'log')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
await userAccountManager.cacheGoogleAccount('test2@google.com');
|
||||
@@ -161,7 +162,7 @@ describe('UserAccountManager', () => {
|
||||
fs.mkdirSync(path.dirname(accountsFile()), { recursive: true });
|
||||
fs.writeFileSync(accountsFile(), '{ "active": "test@google.com"'); // Invalid JSON
|
||||
const consoleLogSpy = vi
|
||||
.spyOn(console, 'log')
|
||||
.spyOn(debugLogger, 'log')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const account = userAccountManager.getCachedGoogleAccount();
|
||||
@@ -210,7 +211,7 @@ describe('UserAccountManager', () => {
|
||||
fs.mkdirSync(path.dirname(accountsFile()), { recursive: true });
|
||||
fs.writeFileSync(accountsFile(), 'not valid json');
|
||||
const consoleLogSpy = vi
|
||||
.spyOn(console, 'log')
|
||||
.spyOn(debugLogger, 'log')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
await userAccountManager.clearCachedGoogleAccount();
|
||||
@@ -272,7 +273,7 @@ describe('UserAccountManager', () => {
|
||||
fs.mkdirSync(path.dirname(accountsFile()), { recursive: true });
|
||||
fs.writeFileSync(accountsFile(), 'invalid json');
|
||||
const consoleDebugSpy = vi
|
||||
.spyOn(console, 'log')
|
||||
.spyOn(debugLogger, 'log')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
expect(userAccountManager.getLifetimeGoogleAccounts()).toBe(0);
|
||||
@@ -319,7 +320,7 @@ describe('UserAccountManager', () => {
|
||||
JSON.stringify({ active: null, old: 1 }),
|
||||
);
|
||||
const consoleLogSpy = vi
|
||||
.spyOn(console, 'log')
|
||||
.spyOn(debugLogger, 'log')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
expect(userAccountManager.getLifetimeGoogleAccounts()).toBe(0);
|
||||
|
||||
@@ -9,6 +9,7 @@ import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { WorkspaceContext } from './workspaceContext.js';
|
||||
import { debugLogger } from './debugLogger.js';
|
||||
|
||||
describe('WorkspaceContext with real filesystem', () => {
|
||||
let tempDir: string;
|
||||
@@ -395,7 +396,7 @@ describe('WorkspaceContext with optional directories', () => {
|
||||
fs.mkdirSync(existingDir1, { recursive: true });
|
||||
fs.mkdirSync(existingDir2, { recursive: true });
|
||||
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
vi.spyOn(debugLogger, 'warn').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -410,8 +411,8 @@ describe('WorkspaceContext with optional directories', () => {
|
||||
]);
|
||||
const directories = workspaceContext.getDirectories();
|
||||
expect(directories).toEqual([cwd, existingDir1]);
|
||||
expect(console.warn).toHaveBeenCalledTimes(1);
|
||||
expect(console.warn).toHaveBeenCalledWith(
|
||||
expect(debugLogger.warn).toHaveBeenCalledTimes(1);
|
||||
expect(debugLogger.warn).toHaveBeenCalledWith(
|
||||
`[WARN] Skipping unreadable directory: ${nonExistentDir} (Directory does not exist: ${nonExistentDir})`,
|
||||
);
|
||||
});
|
||||
@@ -420,6 +421,6 @@ describe('WorkspaceContext with optional directories', () => {
|
||||
const workspaceContext = new WorkspaceContext(cwd, [existingDir1]);
|
||||
const directories = workspaceContext.getDirectories();
|
||||
expect(directories).toEqual([cwd, existingDir1]);
|
||||
expect(console.warn).not.toHaveBeenCalled();
|
||||
expect(debugLogger.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user