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
+2 -1
View File
@@ -13,6 +13,7 @@ import {
afterEach,
type Mock,
} from 'vitest';
import { debugLogger } from '../utils/debugLogger.js';
import { AgentExecutor, type ActivityCallback } from './executor.js';
import { makeFakeConfig } from '../test-utils/config.js';
import { ToolRegistry } from '../tools/tool-registry.js';
@@ -927,7 +928,7 @@ describe('AgentExecutor', () => {
]);
const consoleWarnSpy = vi
.spyOn(console, 'warn')
.spyOn(debugLogger, 'warn')
.mockImplementation(() => {});
await executor.run({ goal: 'Sec test' }, signal);
+16 -15
View File
@@ -9,6 +9,7 @@ import { AgentRegistry, getModelConfigAlias } from './registry.js';
import { makeFakeConfig } from '../test-utils/config.js';
import type { AgentDefinition } from './types.js';
import type { Config } from '../config/config.js';
import { debugLogger } from '../utils/debugLogger.js';
// A test-only subclass to expose the protected `registerAgent` method.
class TestableAgentRegistry extends AgentRegistry {
@@ -60,14 +61,14 @@ describe('AgentRegistry', () => {
it('should log the count of loaded agents in debug mode', async () => {
const debugConfig = makeFakeConfig({ debugMode: true });
const debugRegistry = new TestableAgentRegistry(debugConfig);
const consoleLogSpy = vi
.spyOn(console, 'log')
const debugLogSpy = vi
.spyOn(debugLogger, 'log')
.mockImplementation(() => {});
await debugRegistry.initialize();
const agentCount = debugRegistry.getAllDefinitions().length;
expect(consoleLogSpy).toHaveBeenCalledWith(
expect(debugLogSpy).toHaveBeenCalledWith(
`[AgentRegistry] Initialized with ${agentCount} agents.`,
);
});
@@ -107,28 +108,28 @@ describe('AgentRegistry', () => {
it('should reject an agent definition missing a name', () => {
const invalidAgent = { ...MOCK_AGENT_V1, name: '' };
const consoleWarnSpy = vi
.spyOn(console, 'warn')
const debugWarnSpy = vi
.spyOn(debugLogger, 'warn')
.mockImplementation(() => {});
registry.testRegisterAgent(invalidAgent);
expect(registry.getDefinition('MockAgent')).toBeUndefined();
expect(consoleWarnSpy).toHaveBeenCalledWith(
expect(debugWarnSpy).toHaveBeenCalledWith(
'[AgentRegistry] Skipping invalid agent definition. Missing name or description.',
);
});
it('should reject an agent definition missing a description', () => {
const invalidAgent = { ...MOCK_AGENT_V1, description: '' };
const consoleWarnSpy = vi
.spyOn(console, 'warn')
const debugWarnSpy = vi
.spyOn(debugLogger, 'warn')
.mockImplementation(() => {});
registry.testRegisterAgent(invalidAgent as AgentDefinition);
expect(registry.getDefinition('MockAgent')).toBeUndefined();
expect(consoleWarnSpy).toHaveBeenCalledWith(
expect(debugWarnSpy).toHaveBeenCalledWith(
'[AgentRegistry] Skipping invalid agent definition. Missing name or description.',
);
});
@@ -149,27 +150,27 @@ describe('AgentRegistry', () => {
it('should log overwrites when in debug mode', () => {
const debugConfig = makeFakeConfig({ debugMode: true });
const debugRegistry = new TestableAgentRegistry(debugConfig);
const consoleLogSpy = vi
.spyOn(console, 'log')
const debugLogSpy = vi
.spyOn(debugLogger, 'log')
.mockImplementation(() => {});
debugRegistry.testRegisterAgent(MOCK_AGENT_V1);
debugRegistry.testRegisterAgent(MOCK_AGENT_V2);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect(debugLogSpy).toHaveBeenCalledWith(
`[AgentRegistry] Overriding agent 'MockAgent'`,
);
});
it('should not log overwrites when not in debug mode', () => {
const consoleLogSpy = vi
.spyOn(console, 'log')
const debugLogSpy = vi
.spyOn(debugLogger, 'log')
.mockImplementation(() => {});
registry.testRegisterAgent(MOCK_AGENT_V1);
registry.testRegisterAgent(MOCK_AGENT_V2);
expect(consoleLogSpy).not.toHaveBeenCalledWith(
expect(debugLogSpy).not.toHaveBeenCalledWith(
`[AgentRegistry] Overriding agent 'MockAgent'`,
);
});
@@ -34,6 +34,7 @@ vi.mock('node:path');
vi.mock('../utils/events.js', () => ({
coreEvents: {
emitFeedback: vi.fn(),
emitConsoleLog: vi.fn(),
},
}));
+5 -4
View File
@@ -26,6 +26,7 @@ import type { Config } from '../config/config.js';
import readline from 'node:readline';
import { FORCE_ENCRYPTED_FILE_ENV_VAR } from '../mcp/token-storage/index.js';
import { GEMINI_DIR } from '../utils/paths.js';
import { debugLogger } from '../utils/debugLogger.js';
vi.mock('os', async (importOriginal) => {
const os = await importOriginal<typeof import('os')>();
@@ -241,7 +242,7 @@ describe('oauth2', () => {
(readline.createInterface as Mock).mockReturnValue(mockReadline);
const consoleLogSpy = vi
.spyOn(console, 'log')
.spyOn(debugLogger, 'log')
.mockImplementation(() => {});
const client = await getOauthClient(
@@ -855,7 +856,7 @@ describe('oauth2', () => {
} as unknown as Response);
const consoleLogSpy = vi
.spyOn(console, 'log')
.spyOn(debugLogger, 'log')
.mockImplementation(() => {});
let requestCallback!: http.RequestListener;
@@ -935,10 +936,10 @@ describe('oauth2', () => {
(readline.createInterface as Mock).mockReturnValue(mockReadline);
const consoleLogSpy = vi
.spyOn(console, 'log')
.spyOn(debugLogger, 'log')
.mockImplementation(() => {});
const consoleErrorSpy = vi
.spyOn(console, 'error')
.spyOn(debugLogger, 'error')
.mockImplementation(() => {});
await expect(
+1
View File
@@ -145,6 +145,7 @@ vi.mock('../agents/subagent-tool-wrapper.js', () => ({
const mockCoreEvents = vi.hoisted(() => ({
emitFeedback: vi.fn(),
emitModelChanged: vi.fn(),
emitConsoleLog: vi.fn(),
}));
const mockSetGlobalProxy = vi.hoisted(() => vi.fn());
@@ -292,7 +292,6 @@ describe('CoreToolScheduler', () => {
onAllToolCallsComplete,
onToolCallsUpdate,
getPreferredEditor: () => 'vscode',
onEditorClose: vi.fn(),
});
const abortController = new AbortController();
@@ -358,7 +357,6 @@ describe('CoreToolScheduler', () => {
onAllToolCallsComplete,
onToolCallsUpdate,
getPreferredEditor: () => 'vscode',
onEditorClose: vi.fn(),
});
const abortController = new AbortController();
@@ -460,7 +458,6 @@ describe('CoreToolScheduler', () => {
onAllToolCallsComplete,
onToolCallsUpdate,
getPreferredEditor: () => 'vscode',
onEditorClose: vi.fn(),
});
const abortController = new AbortController();
@@ -557,7 +554,6 @@ describe('CoreToolScheduler', () => {
onAllToolCallsComplete,
onToolCallsUpdate,
getPreferredEditor: () => 'vscode',
onEditorClose: vi.fn(),
});
const request = {
@@ -595,7 +591,6 @@ describe('CoreToolScheduler', () => {
const scheduler = new CoreToolScheduler({
config: mockConfig,
getPreferredEditor: () => 'vscode',
onEditorClose: vi.fn(),
});
// Test that the right tool is selected, with only 1 result, for typos
@@ -650,7 +645,6 @@ describe('CoreToolScheduler with payload', () => {
onAllToolCallsComplete,
onToolCallsUpdate,
getPreferredEditor: () => 'vscode',
onEditorClose: vi.fn(),
});
const abortController = new AbortController();
@@ -952,7 +946,6 @@ describe('CoreToolScheduler edit cancellation', () => {
onAllToolCallsComplete,
onToolCallsUpdate,
getPreferredEditor: () => 'vscode',
onEditorClose: vi.fn(),
});
const abortController = new AbortController();
@@ -1038,7 +1031,6 @@ describe('CoreToolScheduler YOLO mode', () => {
onAllToolCallsComplete,
onToolCallsUpdate,
getPreferredEditor: () => 'vscode',
onEditorClose: vi.fn(),
});
const abortController = new AbortController();
@@ -1125,7 +1117,6 @@ describe('CoreToolScheduler request queueing', () => {
onAllToolCallsComplete,
onToolCallsUpdate,
getPreferredEditor: () => 'vscode',
onEditorClose: vi.fn(),
});
const abortController = new AbortController();
@@ -1239,7 +1230,6 @@ describe('CoreToolScheduler request queueing', () => {
onAllToolCallsComplete,
onToolCallsUpdate,
getPreferredEditor: () => 'vscode',
onEditorClose: vi.fn(),
});
const abortController = new AbortController();
@@ -1347,7 +1337,6 @@ describe('CoreToolScheduler request queueing', () => {
onAllToolCallsComplete,
onToolCallsUpdate,
getPreferredEditor: () => 'vscode',
onEditorClose: vi.fn(),
});
const abortController = new AbortController();
@@ -1404,7 +1393,6 @@ describe('CoreToolScheduler request queueing', () => {
onAllToolCallsComplete,
onToolCallsUpdate,
getPreferredEditor: () => 'vscode',
onEditorClose: vi.fn(),
});
const abortController = new AbortController();
@@ -1508,7 +1496,6 @@ describe('CoreToolScheduler request queueing', () => {
});
},
getPreferredEditor: () => 'vscode',
onEditorClose: vi.fn(),
});
const abortController = new AbortController();
@@ -1632,7 +1619,6 @@ describe('CoreToolScheduler Sequential Execution', () => {
onAllToolCallsComplete,
onToolCallsUpdate,
getPreferredEditor: () => 'vscode',
onEditorClose: vi.fn(),
});
const abortController = new AbortController();
@@ -1732,7 +1718,6 @@ describe('CoreToolScheduler Sequential Execution', () => {
onAllToolCallsComplete,
onToolCallsUpdate,
getPreferredEditor: () => 'vscode',
onEditorClose: vi.fn(),
});
const requests = [
@@ -1831,7 +1816,6 @@ describe('CoreToolScheduler Sequential Execution', () => {
onAllToolCallsComplete,
onToolCallsUpdate,
getPreferredEditor: () => 'vscode',
onEditorClose: vi.fn(),
});
const abortController = new AbortController();
@@ -1865,7 +1849,7 @@ describe('CoreToolScheduler Sequential Execution', () => {
const overrides =
modifyWithEditorSpy.mock.calls[
modifyWithEditorSpy.mock.calls.length - 1
][5];
][4];
expect(overrides).toEqual({
currentContent: 'originalContent',
proposedContent: 'newContent',
@@ -329,7 +329,6 @@ interface CoreToolSchedulerOptions {
onAllToolCallsComplete?: AllToolCallsCompleteHandler;
onToolCallsUpdate?: ToolCallsUpdateHandler;
getPreferredEditor: () => EditorType | undefined;
onEditorClose: () => void;
}
export class CoreToolScheduler {
@@ -346,7 +345,6 @@ export class CoreToolScheduler {
private onToolCallsUpdate?: ToolCallsUpdateHandler;
private getPreferredEditor: () => EditorType | undefined;
private config: Config;
private onEditorClose: () => void;
private isFinalizingToolCalls = false;
private isScheduling = false;
private isCancelling = false;
@@ -365,7 +363,6 @@ export class CoreToolScheduler {
this.onAllToolCallsComplete = options.onAllToolCallsComplete;
this.onToolCallsUpdate = options.onToolCallsUpdate;
this.getPreferredEditor = options.getPreferredEditor;
this.onEditorClose = options.onEditorClose;
// Subscribe to message bus for ASK_USER policy decisions
// Use a static WeakMap to ensure we only subscribe ONCE per MessageBus instance
@@ -995,7 +992,6 @@ export class CoreToolScheduler {
modifyContext as ModifyContext<typeof waitingToolCall.request.args>,
editorType,
signal,
this.onEditorClose,
contentOverrides,
);
this.setArgsInternal(callId, updatedParams);
+12 -11
View File
@@ -29,6 +29,7 @@ import type { Content } from '@google/genai';
import crypto from 'node:crypto';
import os from 'node:os';
import { GEMINI_DIR } from '../utils/paths.js';
import { debugLogger } from '../utils/debugLogger.js';
const TMP_DIR_NAME = 'tmp';
const LOG_FILE_NAME = 'logs.json';
@@ -193,7 +194,7 @@ describe('Logger', () => {
it('should handle invalid JSON in log file by backing it up and starting fresh', async () => {
await fs.writeFile(TEST_LOG_FILE_PATH, 'invalid json');
const consoleDebugSpy = vi
.spyOn(console, 'debug')
.spyOn(debugLogger, 'debug')
.mockImplementation(() => {});
const newLogger = new Logger(testSessionId, new Storage(process.cwd()));
@@ -221,7 +222,7 @@ describe('Logger', () => {
JSON.stringify({ not: 'an array' }),
);
const consoleDebugSpy = vi
.spyOn(console, 'debug')
.spyOn(debugLogger, 'debug')
.mockImplementation(() => {});
const newLogger = new Logger(testSessionId, new Storage(process.cwd()));
@@ -280,7 +281,7 @@ describe('Logger', () => {
);
uninitializedLogger.close(); // Ensure it's treated as uninitialized
const consoleDebugSpy = vi
.spyOn(console, 'debug')
.spyOn(debugLogger, 'debug')
.mockImplementation(() => {});
await uninitializedLogger.logMessage(MessageSenderType.USER, 'test');
expect(consoleDebugSpy).toHaveBeenCalledWith(
@@ -336,7 +337,7 @@ describe('Logger', () => {
it('should not throw, not increment messageId, and log error if writing to file fails', async () => {
vi.spyOn(fs, 'writeFile').mockRejectedValueOnce(new Error('Disk full'));
const consoleDebugSpy = vi
.spyOn(console, 'debug')
.spyOn(debugLogger, 'debug')
.mockImplementation(() => {});
const initialMessageId = logger['messageId'];
const initialLogCount = logger['logs'].length;
@@ -455,7 +456,7 @@ describe('Logger', () => {
);
uninitializedLogger.close();
const consoleErrorSpy = vi
.spyOn(console, 'error')
.spyOn(debugLogger, 'error')
.mockImplementation(() => {});
await expect(
@@ -554,7 +555,7 @@ describe('Logger', () => {
);
await fs.writeFile(taggedFilePath, 'invalid json');
const consoleErrorSpy = vi
.spyOn(console, 'error')
.spyOn(debugLogger, 'error')
.mockImplementation(() => {});
const loadedCheckpoint = await logger.loadCheckpoint(tag);
expect(loadedCheckpoint).toEqual({ history: [] });
@@ -571,7 +572,7 @@ describe('Logger', () => {
);
uninitializedLogger.close();
const consoleErrorSpy = vi
.spyOn(console, 'error')
.spyOn(debugLogger, 'error')
.mockImplementation(() => {});
const loadedCheckpoint = await uninitializedLogger.loadCheckpoint('tag');
expect(loadedCheckpoint).toEqual({ history: [] });
@@ -643,7 +644,7 @@ describe('Logger', () => {
}),
);
const consoleErrorSpy = vi
.spyOn(console, 'error')
.spyOn(debugLogger, 'error')
.mockImplementation(() => {});
await expect(logger.deleteCheckpoint(tag)).rejects.toThrow(
@@ -662,7 +663,7 @@ describe('Logger', () => {
);
uninitializedLogger.close();
const consoleErrorSpy = vi
.spyOn(console, 'error')
.spyOn(debugLogger, 'error')
.mockImplementation(() => {});
const result = await uninitializedLogger.deleteCheckpoint(tag);
@@ -715,7 +716,7 @@ describe('Logger', () => {
}),
);
const consoleErrorSpy = vi
.spyOn(console, 'error')
.spyOn(debugLogger, 'error')
.mockImplementation(() => {});
await expect(logger.checkpointExists(tag)).rejects.toThrow(
@@ -758,7 +759,7 @@ describe('Logger', () => {
await logger.logMessage(MessageSenderType.USER, 'A message');
logger.close();
const consoleDebugSpy = vi
.spyOn(console, 'debug')
.spyOn(debugLogger, 'debug')
.mockImplementation(() => {});
await logger.logMessage(MessageSenderType.USER, 'Another message');
expect(consoleDebugSpy).toHaveBeenCalledWith(
@@ -22,9 +22,12 @@ export async function executeToolCall(
const scheduler = new CoreToolScheduler({
config,
getPreferredEditor: () => undefined,
onEditorClose: () => {},
onAllToolCallsComplete: async (completedToolCalls) => {
resolve(completedToolCalls[0]);
if (completedToolCalls.length > 0) {
resolve(completedToolCalls[0]);
} else {
reject(new Error('No completed tool calls returned.'));
}
},
});
+4 -1
View File
@@ -13,6 +13,7 @@ import path from 'node:path';
import type { Config } from '../config/config.js';
import { CodebaseInvestigatorAgent } from '../agents/codebase-investigator.js';
import { GEMINI_DIR } from '../utils/paths.js';
import { debugLogger } from '../utils/debugLogger.js';
// Mock tool names if they are dynamically generated or complex
vi.mock('../tools/ls', () => ({ LSTool: { Name: 'list_directory' } }));
@@ -346,7 +347,9 @@ describe('resolvePathFromEnv helper function', () => {
vi.spyOn(os, 'homedir').mockImplementation(() => {
throw new Error('Cannot resolve home directory');
});
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const consoleSpy = vi
.spyOn(debugLogger, 'warn')
.mockImplementation(() => {});
const result = resolvePathFromEnv('~/documents/file.txt');
expect(result).toEqual({
@@ -30,6 +30,7 @@ vi.mock('./oauth-token-storage.js', () => {
vi.mock('../utils/events.js', () => ({
coreEvents: {
emitFeedback: vi.fn(),
emitConsoleLog: vi.fn(),
},
}));
@@ -20,6 +20,7 @@ import {
import { promptIdContext } from '../../utils/promptIdContext.js';
import type { Content } from '@google/genai';
import type { ResolvedModelConfig } from '../../services/modelConfigService.js';
import { debugLogger } from '../../utils/debugLogger.js';
vi.mock('../../core/baseLlmClient.js');
vi.mock('../../utils/promptIdContext.js');
@@ -132,7 +133,7 @@ describe('ClassifierStrategy', () => {
it('should return null if the classifier API call fails', async () => {
const consoleWarnSpy = vi
.spyOn(console, 'warn')
.spyOn(debugLogger, 'warn')
.mockImplementation(() => {});
const testError = new Error('API Failure');
vi.mocked(mockBaseLlmClient.generateJson).mockRejectedValue(testError);
@@ -150,7 +151,7 @@ describe('ClassifierStrategy', () => {
it('should return null if the classifier returns a malformed JSON object', async () => {
const consoleWarnSpy = vi
.spyOn(console, 'warn')
.spyOn(debugLogger, 'warn')
.mockImplementation(() => {});
const malformedApiResponse = {
reasoning: 'This is a simple task.',
@@ -252,7 +253,7 @@ describe('ClassifierStrategy', () => {
it('should use a fallback promptId if not found in context', async () => {
const consoleWarnSpy = vi
.spyOn(console, 'warn')
.spyOn(debugLogger, 'warn')
.mockImplementation(() => {});
vi.mocked(promptIdContext.getStore).mockReturnValue(undefined);
const mockApiResponse = {
@@ -17,6 +17,7 @@ import {
import { ActivityType } from './activity-types.js';
import type { ActivityEvent } from './activity-monitor.js';
import type { Config } from '../config/config.js';
import { debugLogger } from '../utils/debugLogger.js';
// Mock the dependencies
vi.mock('./metrics.js', () => ({
@@ -191,7 +192,9 @@ describe('ActivityMonitor', () => {
};
// Spy on console.debug to check error handling
const debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {});
const debugSpy = vi
.spyOn(debugLogger, 'debug')
.mockImplementation(() => {});
activityMonitor.addListener(faultyListener);
activityMonitor.addListener(goodListener);
@@ -40,6 +40,7 @@ vi.mock('../mcp/oauth-utils.js');
vi.mock('../utils/events.js', () => ({
coreEvents: {
emitFeedback: vi.fn(),
emitConsoleLog: vi.fn(),
},
}));
@@ -18,6 +18,7 @@ import fs from 'node:fs';
import fsp from 'node:fs/promises';
import os from 'node:os';
import * as path from 'node:path';
import { debugLogger } from '../utils/debugLogger.js';
// Mock dependencies
const mockOpenDiff = vi.hoisted(() => vi.fn());
@@ -104,7 +105,6 @@ describe('modifyWithEditor', () => {
mockModifyContext,
'vscode' as EditorType,
abortSignal,
vi.fn(),
);
expect(mockModifyContext.getCurrentContent).toHaveBeenCalledWith(
@@ -171,7 +171,6 @@ describe('modifyWithEditor', () => {
mockModifyContext,
'vscode' as EditorType,
abortSignal,
vi.fn(),
);
});
});
@@ -187,7 +186,6 @@ describe('modifyWithEditor', () => {
mockModifyContext,
'vscode' as EditorType,
abortSignal,
vi.fn(),
);
expect(mockCreatePatch).toHaveBeenCalledWith(
@@ -216,7 +214,6 @@ describe('modifyWithEditor', () => {
mockModifyContext,
'vscode' as EditorType,
abortSignal,
vi.fn(),
);
expect(mockCreatePatch).toHaveBeenCalledWith(
@@ -246,7 +243,6 @@ describe('modifyWithEditor', () => {
mockModifyContext,
'vscode' as EditorType,
abortSignal,
vi.fn(),
{
currentContent: overrideCurrent,
proposedContent: overrideProposed,
@@ -274,7 +270,6 @@ describe('modifyWithEditor', () => {
mockModifyContext,
'vscode' as EditorType,
abortSignal,
vi.fn(),
{
currentContent: null,
proposedContent: 'override proposed content',
@@ -305,7 +300,6 @@ describe('modifyWithEditor', () => {
mockModifyContext,
'vscode' as EditorType,
abortSignal,
vi.fn(),
),
).rejects.toThrow('Editor failed to open');
@@ -321,7 +315,7 @@ describe('modifyWithEditor', () => {
it('should handle temp file cleanup errors gracefully', async () => {
const consoleErrorSpy = vi
.spyOn(console, 'error')
.spyOn(debugLogger, 'error')
.mockImplementation(() => {});
vi.spyOn(fs, 'unlinkSync').mockImplementation(() => {
throw new Error('Failed to delete file');
@@ -335,7 +329,6 @@ describe('modifyWithEditor', () => {
mockModifyContext,
'vscode' as EditorType,
abortSignal,
vi.fn(),
);
expect(consoleErrorSpy).toHaveBeenCalledTimes(3);
@@ -362,7 +355,6 @@ describe('modifyWithEditor', () => {
mockModifyContext,
'vscode' as EditorType,
abortSignal,
vi.fn(),
);
expect(mockOpenDiff).toHaveBeenCalledOnce();
@@ -384,7 +376,6 @@ describe('modifyWithEditor', () => {
mockModifyContext,
'vscode' as EditorType,
abortSignal,
vi.fn(),
);
expect(mockOpenDiff).toHaveBeenCalledOnce();
+1 -2
View File
@@ -176,7 +176,6 @@ export async function modifyWithEditor<ToolParams>(
modifyContext: ModifyContext<ToolParams>,
editorType: EditorType,
_abortSignal: AbortSignal,
onEditorClose: () => void,
overrides?: ModifyContentOverrides,
): Promise<ModifyResult<ToolParams>> {
const hasCurrentOverride =
@@ -199,7 +198,7 @@ export async function modifyWithEditor<ToolParams>(
);
try {
await openDiff(oldPath, newPath, editorType, onEditorClose);
await openDiff(oldPath, newPath, editorType);
const result = getUpdatedParams(
oldPath,
newPath,
+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();
});
});