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
@@ -13,13 +13,10 @@ import {
afterEach,
type Mock,
} from 'vitest';
import { format } from 'node:util';
import { type CommandModule, type Argv } from 'yargs';
import { handleDisable, disableCommand } from './disable.js';
import { ExtensionManager } from '../../config/extension-manager.js';
import {
loadSettings,
SettingScope,
@@ -28,71 +25,53 @@ import {
import { getErrorMessage } from '../../utils/errors.js';
// Mock dependencies
vi.mock('../../config/extension-manager.js');
vi.mock('../../config/settings.js');
vi.mock('../../utils/errors.js');
const emitConsoleLog = vi.hoisted(() => vi.fn());
const debugLogger = vi.hoisted(() => ({
log: vi.fn((message, ...args) => {
emitConsoleLog('log', format(message, ...args));
}),
error: vi.fn((message, ...args) => {
emitConsoleLog('error', format(message, ...args));
}),
}));
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
debugLogger: {
log: vi.fn(),
error: vi.fn(),
coreEvents: {
emitConsoleLog,
},
debugLogger,
};
});
vi.mock('../../config/extension-manager.js');
vi.mock('../../config/settings.js');
vi.mock('../../utils/errors.js');
vi.mock('../../config/extensions/consent.js', () => ({
requestConsentNonInteractive: vi.fn(),
}));
vi.mock('../../config/extensions/extensionSettings.js', () => ({
promptForSetting: vi.fn(),
}));
describe('extensions disable command', () => {
const mockLoadSettings = vi.mocked(loadSettings);
const mockGetErrorMessage = vi.mocked(getErrorMessage);
const mockExtensionManager = vi.mocked(ExtensionManager);
interface MockDebugLogger {
log: Mock;
error: Mock;
}
let mockDebugLogger: MockDebugLogger;
beforeEach(async () => {
vi.clearAllMocks();
// We need to re-import the mocked module to get the fresh mock
mockDebugLogger = (await import('@google/gemini-cli-core'))
.debugLogger as unknown as MockDebugLogger;
mockLoadSettings.mockReturnValue({
merged: {},
} as unknown as LoadedSettings);
mockExtensionManager.prototype.loadExtensions = vi
.fn()
.mockResolvedValue(undefined);
mockExtensionManager.prototype.disableExtension = vi
.fn()
.mockResolvedValue(undefined);
});
@@ -127,54 +106,40 @@ describe('extensions disable command', () => {
'should disable an extension in the $expectedScope scope when scope is $scope',
async ({ name, scope, expectedScope, expectedLog }) => {
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
await handleDisable({ name, scope });
expect(mockExtensionManager).toHaveBeenCalledWith(
expect.objectContaining({
workspaceDir: '/test/dir',
}),
);
expect(
mockExtensionManager.prototype.loadExtensions,
).toHaveBeenCalled();
expect(
mockExtensionManager.prototype.disableExtension,
).toHaveBeenCalledWith(name, expectedScope);
expect(mockDebugLogger.log).toHaveBeenCalledWith(expectedLog);
expect(emitConsoleLog).toHaveBeenCalledWith('log', expectedLog);
mockCwd.mockRestore();
},
);
it('should log an error message and exit with code 1 when extension disabling fails', async () => {
const mockProcessExit = vi
.spyOn(process, 'exit')
.mockImplementation((() => {}) as (
code?: string | number | null | undefined,
) => never);
const error = new Error('Disable failed');
(
mockExtensionManager.prototype.disableExtension as Mock
).mockRejectedValue(error);
mockGetErrorMessage.mockReturnValue('Disable failed message');
await handleDisable({ name: 'my-extension' });
expect(mockDebugLogger.error).toHaveBeenCalledWith(
expect(emitConsoleLog).toHaveBeenCalledWith(
'error',
'Disable failed message',
);
expect(mockProcessExit).toHaveBeenCalledWith(1);
mockProcessExit.mockRestore();
});
});
@@ -184,16 +149,13 @@ describe('extensions disable command', () => {
it('should have correct command and describe', () => {
expect(command.command).toBe('disable [--scope] <name>');
expect(command.describe).toBe('Disables an extension.');
});
describe('builder', () => {
interface MockYargs {
positional: Mock;
option: Mock;
check: Mock;
}
@@ -202,9 +164,7 @@ describe('extensions disable command', () => {
beforeEach(() => {
yargsMock = {
positional: vi.fn().mockReturnThis(),
option: vi.fn().mockReturnThis(),
check: vi.fn().mockReturnThis(),
};
});
@@ -213,21 +173,15 @@ describe('extensions disable command', () => {
(command.builder as (yargs: Argv) => Argv)(
yargsMock as unknown as Argv,
);
expect(yargsMock.positional).toHaveBeenCalledWith('name', {
describe: 'The name of the extension to disable.',
type: 'string',
});
expect(yargsMock.option).toHaveBeenCalledWith('scope', {
describe: 'The scope to disable the extension in.',
type: 'string',
default: SettingScope.User,
});
expect(yargsMock.check).toHaveBeenCalled();
});
@@ -235,17 +189,12 @@ describe('extensions disable command', () => {
(command.builder as (yargs: Argv) => Argv)(
yargsMock as unknown as Argv,
);
const checkCallback = yargsMock.check.mock.calls[0][0];
const expectedError = `Invalid scope: invalid. Please use one of ${Object.values(
SettingScope,
)
.map((s) => s.toLowerCase())
.join(', ')}.`;
expect(() => checkCallback({ scope: 'invalid' })).toThrow(
expectedError,
);
@@ -257,9 +206,7 @@ describe('extensions disable command', () => {
(command.builder as (yargs: Argv) => Argv)(
yargsMock as unknown as Argv,
);
const checkCallback = yargsMock.check.mock.calls[0][0];
expect(checkCallback({ scope })).toBe(true);
},
);
@@ -267,7 +214,6 @@ describe('extensions disable command', () => {
it('handler should trigger extension disabling', async () => {
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
interface TestArgv {
name: string;
scope: string;
@@ -279,24 +225,20 @@ describe('extensions disable command', () => {
_: [],
$0: '',
};
await (command.handler as unknown as (args: TestArgv) => void)(argv);
expect(mockExtensionManager).toHaveBeenCalledWith(
expect.objectContaining({
workspaceDir: '/test/dir',
}),
);
expect(mockExtensionManager.prototype.loadExtensions).toHaveBeenCalled();
expect(
mockExtensionManager.prototype.disableExtension,
).toHaveBeenCalledWith('test-ext', SettingScope.Workspace);
expect(mockDebugLogger.log).toHaveBeenCalledWith(
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
'Extension "test-ext" successfully disabled for scope "workspace".',
);
mockCwd.mockRestore();
});
});
@@ -13,6 +13,7 @@ import {
afterEach,
type Mock,
} from 'vitest';
import { format } from 'node:util';
import { type CommandModule, type Argv } from 'yargs';
import { handleEnable, enableCommand } from './enable.js';
import { ExtensionManager } from '../../config/extension-manager.js';
@@ -24,17 +25,25 @@ import {
import { FatalConfigError } from '@google/gemini-cli-core';
// Mock dependencies
vi.mock('../../config/extension-manager.js');
vi.mock('../../config/settings.js');
const emitConsoleLog = vi.hoisted(() => vi.fn());
const debugLogger = vi.hoisted(() => ({
log: vi.fn((message, ...args) => {
emitConsoleLog('log', format(message, ...args));
}),
error: vi.fn((message, ...args) => {
emitConsoleLog('error', format(message, ...args));
}),
}));
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
debugLogger: {
log: vi.fn(),
error: vi.fn(),
coreEvents: {
emitConsoleLog,
},
debugLogger,
getErrorMessage: vi.fn((error: { message: string }) => error.message),
FatalConfigError: class extends Error {
constructor(message: string) {
@@ -44,22 +53,18 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
},
};
});
vi.mock('../../config/extension-manager.js');
vi.mock('../../config/settings.js');
vi.mock('../../config/extensions/consent.js');
vi.mock('../../config/extensions/extensionSettings.js');
describe('extensions enable command', () => {
const mockLoadSettings = vi.mocked(loadSettings);
const mockExtensionManager = vi.mocked(ExtensionManager);
interface MockDebugLogger {
log: Mock;
error: Mock;
}
let mockDebugLogger: MockDebugLogger;
beforeEach(async () => {
vi.clearAllMocks();
mockDebugLogger = (await import('@google/gemini-cli-core'))
.debugLogger as unknown as MockDebugLogger;
mockLoadSettings.mockReturnValue({
merged: {},
} as unknown as LoadedSettings);
@@ -106,7 +111,7 @@ describe('extensions enable command', () => {
expect(
mockExtensionManager.prototype.enableExtension,
).toHaveBeenCalledWith(name, expectedScope);
expect(mockDebugLogger.log).toHaveBeenCalledWith(expectedLog);
expect(emitConsoleLog).toHaveBeenCalledWith('log', expectedLog);
mockCwd.mockRestore();
},
);
@@ -13,6 +13,7 @@ import {
afterEach,
type Mock,
} from 'vitest';
import { format } from 'node:util';
import { type CommandModule, type Argv } from 'yargs';
import { handleLink, linkCommand } from './link.js';
import { ExtensionManager } from '../../config/extension-manager.js';
@@ -20,20 +21,31 @@ import { loadSettings, type LoadedSettings } from '../../config/settings.js';
import { getErrorMessage } from '../../utils/errors.js';
// Mock dependencies
vi.mock('../../config/extension-manager.js');
vi.mock('../../config/settings.js');
vi.mock('../../utils/errors.js');
const emitConsoleLog = vi.hoisted(() => vi.fn());
const debugLogger = vi.hoisted(() => ({
log: vi.fn((message, ...args) => {
emitConsoleLog('log', format(message, ...args));
}),
error: vi.fn((message, ...args) => {
emitConsoleLog('error', format(message, ...args));
}),
}));
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
debugLogger: {
log: vi.fn(),
error: vi.fn(),
coreEvents: {
emitConsoleLog,
},
debugLogger,
};
});
vi.mock('../../config/extension-manager.js');
vi.mock('../../config/settings.js');
vi.mock('../../utils/errors.js');
vi.mock('../../config/extensions/consent.js', () => ({
requestConsentNonInteractive: vi.fn(),
}));
@@ -45,16 +57,9 @@ describe('extensions link command', () => {
const mockLoadSettings = vi.mocked(loadSettings);
const mockGetErrorMessage = vi.mocked(getErrorMessage);
const mockExtensionManager = vi.mocked(ExtensionManager);
interface MockDebugLogger {
log: Mock;
error: Mock;
}
let mockDebugLogger: MockDebugLogger;
beforeEach(async () => {
vi.clearAllMocks();
mockDebugLogger = (await import('@google/gemini-cli-core'))
.debugLogger as unknown as MockDebugLogger;
mockLoadSettings.mockReturnValue({
merged: {},
} as unknown as LoadedSettings);
@@ -87,7 +92,8 @@ describe('extensions link command', () => {
source: '/local/path/to/extension',
type: 'link',
});
expect(mockDebugLogger.log).toHaveBeenCalledWith(
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
'Extension "my-linked-extension" linked successfully and enabled.',
);
mockCwd.mockRestore();
@@ -107,7 +113,10 @@ describe('extensions link command', () => {
await handleLink({ path: '/local/path/to/extension' });
expect(mockDebugLogger.error).toHaveBeenCalledWith('Link failed message');
expect(emitConsoleLog).toHaveBeenCalledWith(
'error',
'Link failed message',
);
expect(mockProcessExit).toHaveBeenCalledWith(1);
mockProcessExit.mockRestore();
});
@@ -4,15 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {
vi,
describe,
it,
expect,
beforeEach,
afterEach,
type Mock,
} from 'vitest';
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { format } from 'node:util';
import { type CommandModule } from 'yargs';
import { handleList, listCommand } from './list.js';
import { ExtensionManager } from '../../config/extension-manager.js';
@@ -20,20 +13,31 @@ import { loadSettings, type LoadedSettings } from '../../config/settings.js';
import { getErrorMessage } from '../../utils/errors.js';
// Mock dependencies
vi.mock('../../config/extension-manager.js');
vi.mock('../../config/settings.js');
vi.mock('../../utils/errors.js');
const emitConsoleLog = vi.hoisted(() => vi.fn());
const debugLogger = vi.hoisted(() => ({
log: vi.fn((message, ...args) => {
emitConsoleLog('log', format(message, ...args));
}),
error: vi.fn((message, ...args) => {
emitConsoleLog('error', format(message, ...args));
}),
}));
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
debugLogger: {
log: vi.fn(),
error: vi.fn(),
coreEvents: {
emitConsoleLog,
},
debugLogger,
};
});
vi.mock('../../config/extension-manager.js');
vi.mock('../../config/settings.js');
vi.mock('../../utils/errors.js');
vi.mock('../../config/extensions/consent.js', () => ({
requestConsentNonInteractive: vi.fn(),
}));
@@ -45,16 +49,9 @@ describe('extensions list command', () => {
const mockLoadSettings = vi.mocked(loadSettings);
const mockGetErrorMessage = vi.mocked(getErrorMessage);
const mockExtensionManager = vi.mocked(ExtensionManager);
interface MockDebugLogger {
log: Mock;
error: Mock;
}
let mockDebugLogger: MockDebugLogger;
beforeEach(async () => {
vi.clearAllMocks();
mockDebugLogger = (await import('@google/gemini-cli-core'))
.debugLogger as unknown as MockDebugLogger;
mockLoadSettings.mockReturnValue({
merged: {},
} as unknown as LoadedSettings);
@@ -72,7 +69,8 @@ describe('extensions list command', () => {
.mockResolvedValue([]);
await handleList();
expect(mockDebugLogger.log).toHaveBeenCalledWith(
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
'No extensions installed.',
);
mockCwd.mockRestore();
@@ -92,7 +90,8 @@ describe('extensions list command', () => {
);
await handleList();
expect(mockDebugLogger.log).toHaveBeenCalledWith(
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
'ext1@1.0.0\n\next2@2.0.0',
);
mockCwd.mockRestore();
@@ -112,7 +111,10 @@ describe('extensions list command', () => {
await handleList();
expect(mockDebugLogger.error).toHaveBeenCalledWith('List failed message');
expect(emitConsoleLog).toHaveBeenCalledWith(
'error',
'List failed message',
);
expect(mockProcessExit).toHaveBeenCalledWith(1);
mockProcessExit.mockRestore();
});
@@ -13,6 +13,7 @@ import {
afterEach,
type Mock,
} from 'vitest';
import { format } from 'node:util';
import { type CommandModule, type Argv } from 'yargs';
import { handleUninstall, uninstallCommand } from './uninstall.js';
import { ExtensionManager } from '../../config/extension-manager.js';
@@ -43,19 +44,31 @@ vi.mock('../../config/extension-manager.js', async (importOriginal) => {
};
});
vi.mock('../../config/settings.js');
vi.mock('../../utils/errors.js');
// Mock dependencies
const emitConsoleLog = vi.hoisted(() => vi.fn());
const debugLogger = vi.hoisted(() => ({
log: vi.fn((message, ...args) => {
emitConsoleLog('log', format(message, ...args));
}),
error: vi.fn((message, ...args) => {
emitConsoleLog('error', format(message, ...args));
}),
}));
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
debugLogger: {
log: vi.fn(),
error: vi.fn(),
coreEvents: {
emitConsoleLog,
},
debugLogger,
};
});
vi.mock('../../config/settings.js');
vi.mock('../../utils/errors.js');
vi.mock('../../config/extensions/consent.js', () => ({
requestConsentNonInteractive: vi.fn(),
}));
@@ -67,15 +80,8 @@ describe('extensions uninstall command', () => {
const mockLoadSettings = vi.mocked(loadSettings);
const mockGetErrorMessage = vi.mocked(getErrorMessage);
const mockExtensionManager = vi.mocked(ExtensionManager);
interface MockDebugLogger {
log: Mock;
error: Mock;
}
let mockDebugLogger: MockDebugLogger;
beforeEach(async () => {
mockDebugLogger = (await import('@google/gemini-cli-core'))
.debugLogger as unknown as MockDebugLogger;
mockLoadSettings.mockReturnValue({
merged: {},
} as unknown as LoadedSettings);
@@ -104,7 +110,8 @@ describe('extensions uninstall command', () => {
'my-extension',
false,
);
expect(mockDebugLogger.log).toHaveBeenCalledWith(
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
'Extension "my-extension" successfully uninstalled.',
);
mockCwd.mockRestore();
@@ -120,13 +127,16 @@ describe('extensions uninstall command', () => {
expect(mockUninstallExtension).toHaveBeenCalledWith('ext1', false);
expect(mockUninstallExtension).toHaveBeenCalledWith('ext2', false);
expect(mockUninstallExtension).toHaveBeenCalledWith('ext3', false);
expect(mockDebugLogger.log).toHaveBeenCalledWith(
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
'Extension "ext1" successfully uninstalled.',
);
expect(mockDebugLogger.log).toHaveBeenCalledWith(
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
'Extension "ext2" successfully uninstalled.',
);
expect(mockDebugLogger.log).toHaveBeenCalledWith(
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
'Extension "ext3" successfully uninstalled.',
);
mockCwd.mockRestore();
@@ -152,13 +162,16 @@ describe('extensions uninstall command', () => {
await handleUninstall({ names: ['ext1', 'ext2', 'ext3'] });
expect(mockUninstallExtension).toHaveBeenCalledTimes(3);
expect(mockDebugLogger.log).toHaveBeenCalledWith(
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
'Extension "ext1" successfully uninstalled.',
);
expect(mockDebugLogger.error).toHaveBeenCalledWith(
expect(emitConsoleLog).toHaveBeenCalledWith(
'error',
'Failed to uninstall "ext2": Extension not found',
);
expect(mockDebugLogger.log).toHaveBeenCalledWith(
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
'Extension "ext3" successfully uninstalled.',
);
expect(mockProcessExit).toHaveBeenCalledWith(1);
@@ -180,10 +193,12 @@ describe('extensions uninstall command', () => {
await handleUninstall({ names: ['ext1', 'ext2'] });
expect(mockDebugLogger.error).toHaveBeenCalledWith(
expect(emitConsoleLog).toHaveBeenCalledWith(
'error',
'Failed to uninstall "ext1": Extension not found',
);
expect(mockDebugLogger.error).toHaveBeenCalledWith(
expect(emitConsoleLog).toHaveBeenCalledWith(
'error',
'Failed to uninstall "ext2": Extension not found',
);
expect(mockProcessExit).toHaveBeenCalledWith(1);
@@ -204,7 +219,8 @@ describe('extensions uninstall command', () => {
await handleUninstall({ names: ['my-extension'] });
expect(mockDebugLogger.error).toHaveBeenCalledWith(
expect(emitConsoleLog).toHaveBeenCalledWith(
'error',
'Initialization failed message',
);
expect(mockProcessExit).toHaveBeenCalledWith(1);
@@ -13,6 +13,7 @@ import {
afterEach,
type Mock,
} from 'vitest';
import { format } from 'node:util';
import { type CommandModule, type Argv } from 'yargs';
import { handleUpdate, updateCommand } from './update.js';
import { ExtensionManager } from '../../config/extension-manager.js';
@@ -22,22 +23,33 @@ import * as github from '../../config/extensions/github.js';
import { ExtensionUpdateState } from '../../ui/state/extensions.js';
// Mock dependencies
vi.mock('../../config/extension-manager.js');
vi.mock('../../config/settings.js');
vi.mock('../../utils/errors.js');
vi.mock('../../config/extensions/update.js');
vi.mock('../../config/extensions/github.js');
const emitConsoleLog = vi.hoisted(() => vi.fn());
const debugLogger = vi.hoisted(() => ({
log: vi.fn((message, ...args) => {
emitConsoleLog('log', format(message, ...args));
}),
error: vi.fn((message, ...args) => {
emitConsoleLog('error', format(message, ...args));
}),
}));
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
debugLogger: {
log: vi.fn(),
error: vi.fn(),
coreEvents: {
emitConsoleLog,
},
debugLogger,
};
});
vi.mock('../../config/extension-manager.js');
vi.mock('../../config/settings.js');
vi.mock('../../utils/errors.js');
vi.mock('../../config/extensions/update.js');
vi.mock('../../config/extensions/github.js');
vi.mock('../../config/extensions/consent.js', () => ({
requestConsentNonInteractive: vi.fn(),
}));
@@ -57,16 +69,8 @@ describe('extensions update command', () => {
update.updateAllUpdatableExtensions,
);
interface MockDebugLogger {
log: Mock;
error: Mock;
}
let mockDebugLogger: MockDebugLogger;
beforeEach(async () => {
vi.clearAllMocks();
mockDebugLogger = (await import('@google/gemini-cli-core'))
.debugLogger as unknown as MockDebugLogger;
mockLoadSettings.mockReturnValue({
merged: { experimental: { extensionReloading: true } },
} as unknown as LoadedSettings);
@@ -106,7 +110,7 @@ describe('extensions update command', () => {
await handleUpdate({ name: 'my-extension' });
expect(mockDebugLogger.log).toHaveBeenCalledWith(expectedLog);
expect(emitConsoleLog).toHaveBeenCalledWith('log', expectedLog);
if (shouldCallUpdateExtension) {
expect(mockUpdateExtension).toHaveBeenCalled();
} else {
@@ -141,7 +145,7 @@ describe('extensions update command', () => {
await handleUpdate({ all: true });
expect(mockDebugLogger.log).toHaveBeenCalledWith(expectedLog);
expect(emitConsoleLog).toHaveBeenCalledWith('log', expectedLog);
mockCwd.mockRestore();
},
);
+9 -4
View File
@@ -4,10 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, type Mock } from 'vitest';
import { describe, it, expect, vi, type Mock, type MockInstance } from 'vitest';
import yargs, { type Argv } from 'yargs';
import { addCommand } from './add.js';
import { loadSettings, SettingScope } from '../../config/settings.js';
import { debugLogger } from '@google/gemini-cli-core';
vi.mock('fs/promises', () => ({
readFile: vi.fn(),
@@ -38,6 +39,7 @@ describe('mcp add command', () => {
let parser: Argv;
let mockSetValue: Mock;
let mockConsoleError: Mock;
let debugLoggerErrorSpy: MockInstance;
beforeEach(() => {
vi.resetAllMocks();
@@ -45,6 +47,9 @@ describe('mcp add command', () => {
parser = yargsInstance;
mockSetValue = vi.fn();
mockConsoleError = vi.fn();
debugLoggerErrorSpy = vi
.spyOn(debugLogger, 'error')
.mockImplementation(() => {});
vi.spyOn(console, 'error').mockImplementation(mockConsoleError);
mockedLoadSettings.mockReturnValue({
forScope: () => ({ settings: {} }),
@@ -232,7 +237,7 @@ describe('mcp add command', () => {
parser.parseAsync(`add ${serverName} ${command}`),
).rejects.toThrow('process.exit called');
expect(mockConsoleError).toHaveBeenCalledWith(
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
'Error: Please use --scope user to edit settings in the home directory.',
);
expect(mockProcessExit).toHaveBeenCalledWith(1);
@@ -250,7 +255,7 @@ describe('mcp add command', () => {
parser.parseAsync(`add --scope project ${serverName} ${command}`),
).rejects.toThrow('process.exit called');
expect(mockConsoleError).toHaveBeenCalledWith(
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
'Error: Please use --scope user to edit settings in the home directory.',
);
expect(mockProcessExit).toHaveBeenCalledWith(1);
@@ -264,7 +269,7 @@ describe('mcp add command', () => {
'mcpServers',
expect.any(Object),
);
expect(mockConsoleError).not.toHaveBeenCalled();
expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
});
});
+1 -6
View File
@@ -40,12 +40,6 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
GEMINI_DIR: '.gemini',
getErrorMessage: (e: unknown) =>
e instanceof Error ? e.message : String(e),
debugLogger: {
log: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
};
});
vi.mock('@modelcontextprotocol/sdk/client/index.js');
@@ -78,6 +72,7 @@ describe('mcp list command', () => {
beforeEach(() => {
vi.resetAllMocks();
vi.spyOn(debugLogger, 'log').mockImplementation(() => {});
mockTransport = { close: vi.fn() };
mockClient = {
+23 -12
View File
@@ -19,7 +19,7 @@ import { removeCommand } from './remove.js';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { GEMINI_DIR } from '@google/gemini-cli-core';
import { GEMINI_DIR, debugLogger } from '@google/gemini-cli-core';
vi.mock('fs/promises', () => ({
readFile: vi.fn(),
@@ -69,13 +69,16 @@ describe('mcp remove command', () => {
});
it('should show a message if server not found', async () => {
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const debugLogSpy = vi
.spyOn(debugLogger, 'log')
.mockImplementation(() => {});
await parser.parseAsync('remove non-existent-server');
expect(mockSetValue).not.toHaveBeenCalled();
expect(consoleSpy).toHaveBeenCalledWith(
expect(debugLogSpy).toHaveBeenCalledWith(
'Server "non-existent-server" not found in project settings.',
);
debugLogSpy.mockRestore();
});
});
@@ -123,18 +126,20 @@ describe('mcp remove command', () => {
}`;
fs.writeFileSync(settingsPath, originalContent, 'utf-8');
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const debugLogSpy = vi
.spyOn(debugLogger, 'log')
.mockImplementation(() => {});
await parser.parseAsync('remove server-to-remove');
const updatedContent = fs.readFileSync(settingsPath, 'utf-8');
expect(updatedContent).toContain('"server-to-keep"');
expect(updatedContent).not.toContain('"server-to-remove"');
expect(consoleSpy).toHaveBeenCalledWith(
expect(debugLogSpy).toHaveBeenCalledWith(
'Server "server-to-remove" removed from project settings.',
);
consoleSpy.mockRestore();
debugLogSpy.mockRestore();
});
it('should preserve comments when removing a server', async () => {
@@ -154,7 +159,9 @@ describe('mcp remove command', () => {
}`;
fs.writeFileSync(settingsPath, originalContent, 'utf-8');
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const debugLogSpy = vi
.spyOn(debugLogger, 'log')
.mockImplementation(() => {});
await parser.parseAsync('remove oldServer');
const updatedContent = fs.readFileSync(settingsPath, 'utf-8');
@@ -163,7 +170,7 @@ describe('mcp remove command', () => {
expect(updatedContent).not.toContain('"oldServer"');
expect(updatedContent).toContain('// Server to remove');
consoleSpy.mockRestore();
debugLogSpy.mockRestore();
});
it('should handle removing the only server', async () => {
@@ -177,7 +184,9 @@ describe('mcp remove command', () => {
}`;
fs.writeFileSync(settingsPath, originalContent, 'utf-8');
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const debugLogSpy = vi
.spyOn(debugLogger, 'log')
.mockImplementation(() => {});
await parser.parseAsync('remove only-server');
const updatedContent = fs.readFileSync(settingsPath, 'utf-8');
@@ -185,7 +194,7 @@ describe('mcp remove command', () => {
expect(updatedContent).not.toContain('"only-server"');
expect(updatedContent).toMatch(/"mcpServers"\s*:\s*\{\s*\}/);
consoleSpy.mockRestore();
debugLogSpy.mockRestore();
});
it('should preserve other settings when removing a server', async () => {
@@ -211,7 +220,9 @@ describe('mcp remove command', () => {
}`;
fs.writeFileSync(settingsPath, originalContent, 'utf-8');
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const debugLogSpy = vi
.spyOn(debugLogger, 'log')
.mockImplementation(() => {});
await parser.parseAsync('remove server1');
const updatedContent = fs.readFileSync(settingsPath, 'utf-8');
@@ -222,7 +233,7 @@ describe('mcp remove command', () => {
expect(updatedContent).toContain('"theme": "dark"');
expect(updatedContent).not.toContain('"server1"');
consoleSpy.mockRestore();
debugLogSpy.mockRestore();
});
});
});