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:
Jacob Richman
2025-11-20 10:44:02 -08:00
committed by GitHub
parent e20d282088
commit d1e35f8660
82 changed files with 1523 additions and 868 deletions
+29 -26
View File
@@ -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);
});
});
+11 -56
View File
@@ -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', () => {
+2 -2
View File
@@ -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;
}
+110 -5
View File
@@ -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();
+83 -14
View File
@@ -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();
});
});