Sync upstream Gemini-CLI v0.8.2 (#838)

This commit is contained in:
tanzhenxin
2025-10-23 09:27:04 +08:00
committed by GitHub
parent 096fabb5d6
commit eb95c131be
644 changed files with 70389 additions and 23709 deletions

View File

@@ -13,6 +13,16 @@ vi.mock('fs/promises', () => ({
writeFile: vi.fn(),
}));
vi.mock('os', () => {
const homedir = vi.fn(() => '/home/user');
return {
default: {
homedir,
},
homedir,
};
});
vi.mock('../../config/settings.js', async () => {
const actual = await vi.importActual('../../config/settings.js');
return {
@@ -26,15 +36,20 @@ const mockedLoadSettings = loadSettings as vi.Mock;
describe('mcp add command', () => {
let parser: yargs.Argv;
let mockSetValue: vi.Mock;
let mockConsoleError: vi.Mock;
beforeEach(() => {
vi.resetAllMocks();
const yargsInstance = yargs([]).command(addCommand);
parser = yargsInstance;
mockSetValue = vi.fn();
mockConsoleError = vi.fn();
vi.spyOn(console, 'error').mockImplementation(mockConsoleError);
mockedLoadSettings.mockReturnValue({
forScope: () => ({ settings: {} }),
setValue: mockSetValue,
workspace: { path: '/path/to/project' },
user: { path: '/home/user' },
});
});
@@ -119,4 +134,218 @@ describe('mcp add command', () => {
},
);
});
describe('when handling scope and directory', () => {
const serverName = 'test-server';
const command = 'echo';
const setupMocks = (cwd: string, workspacePath: string) => {
vi.spyOn(process, 'cwd').mockReturnValue(cwd);
mockedLoadSettings.mockReturnValue({
forScope: () => ({ settings: {} }),
setValue: mockSetValue,
workspace: { path: workspacePath },
user: { path: '/home/user' },
});
};
describe('when in a project directory', () => {
beforeEach(() => {
setupMocks('/path/to/project', '/path/to/project');
});
it('should use project scope by default', async () => {
await parser.parseAsync(`add ${serverName} ${command}`);
expect(mockSetValue).toHaveBeenCalledWith(
SettingScope.Workspace,
'mcpServers',
expect.any(Object),
);
});
it('should use project scope when --scope=project is used', async () => {
await parser.parseAsync(`add --scope project ${serverName} ${command}`);
expect(mockSetValue).toHaveBeenCalledWith(
SettingScope.Workspace,
'mcpServers',
expect.any(Object),
);
});
it('should use user scope when --scope=user is used', async () => {
await parser.parseAsync(`add --scope user ${serverName} ${command}`);
expect(mockSetValue).toHaveBeenCalledWith(
SettingScope.User,
'mcpServers',
expect.any(Object),
);
});
});
describe('when in a subdirectory of a project', () => {
beforeEach(() => {
setupMocks('/path/to/project/subdir', '/path/to/project');
});
it('should use project scope by default', async () => {
await parser.parseAsync(`add ${serverName} ${command}`);
expect(mockSetValue).toHaveBeenCalledWith(
SettingScope.Workspace,
'mcpServers',
expect.any(Object),
);
});
});
describe('when in the home directory', () => {
beforeEach(() => {
setupMocks('/home/user', '/home/user');
});
it('should show an error by default', async () => {
const mockProcessExit = vi
.spyOn(process, 'exit')
.mockImplementation((() => {
throw new Error('process.exit called');
}) as (code?: number) => never);
await expect(
parser.parseAsync(`add ${serverName} ${command}`),
).rejects.toThrow('process.exit called');
expect(mockConsoleError).toHaveBeenCalledWith(
'Error: Please use --scope user to edit settings in the home directory.',
);
expect(mockProcessExit).toHaveBeenCalledWith(1);
expect(mockSetValue).not.toHaveBeenCalled();
});
it('should show an error when --scope=project is used explicitly', async () => {
const mockProcessExit = vi
.spyOn(process, 'exit')
.mockImplementation((() => {
throw new Error('process.exit called');
}) as (code?: number) => never);
await expect(
parser.parseAsync(`add --scope project ${serverName} ${command}`),
).rejects.toThrow('process.exit called');
expect(mockConsoleError).toHaveBeenCalledWith(
'Error: Please use --scope user to edit settings in the home directory.',
);
expect(mockProcessExit).toHaveBeenCalledWith(1);
expect(mockSetValue).not.toHaveBeenCalled();
});
it('should use user scope when --scope=user is used', async () => {
await parser.parseAsync(`add --scope user ${serverName} ${command}`);
expect(mockSetValue).toHaveBeenCalledWith(
SettingScope.User,
'mcpServers',
expect.any(Object),
);
expect(mockConsoleError).not.toHaveBeenCalled();
});
});
describe('when in a subdirectory of home (not a project)', () => {
beforeEach(() => {
setupMocks('/home/user/some/dir', '/home/user/some/dir');
});
it('should use project scope by default', async () => {
await parser.parseAsync(`add ${serverName} ${command}`);
expect(mockSetValue).toHaveBeenCalledWith(
SettingScope.Workspace,
'mcpServers',
expect.any(Object),
);
});
it('should write to the WORKSPACE scope, not the USER scope', async () => {
await parser.parseAsync(`add my-new-server echo`);
// We expect setValue to be called once.
expect(mockSetValue).toHaveBeenCalledTimes(1);
// We get the scope that setValue was called with.
const calledScope = mockSetValue.mock.calls[0][0];
// We assert that the scope was Workspace, not User.
expect(calledScope).toBe(SettingScope.Workspace);
});
});
describe('when outside of home (not a project)', () => {
beforeEach(() => {
setupMocks('/tmp/foo', '/tmp/foo');
});
it('should use project scope by default', async () => {
await parser.parseAsync(`add ${serverName} ${command}`);
expect(mockSetValue).toHaveBeenCalledWith(
SettingScope.Workspace,
'mcpServers',
expect.any(Object),
);
});
});
});
describe('when updating an existing server', () => {
const serverName = 'existing-server';
const initialCommand = 'echo old';
const updatedCommand = 'echo';
const updatedArgs = ['new'];
beforeEach(() => {
mockedLoadSettings.mockReturnValue({
forScope: () => ({
settings: {
mcpServers: {
[serverName]: {
command: initialCommand,
},
},
},
}),
setValue: mockSetValue,
workspace: { path: '/path/to/project' },
user: { path: '/home/user' },
});
});
it('should update the existing server in the project scope', async () => {
await parser.parseAsync(
`add ${serverName} ${updatedCommand} ${updatedArgs.join(' ')}`,
);
expect(mockSetValue).toHaveBeenCalledWith(
SettingScope.Workspace,
'mcpServers',
expect.objectContaining({
[serverName]: expect.objectContaining({
command: updatedCommand,
args: updatedArgs,
}),
}),
);
});
it('should update the existing server in the user scope', async () => {
await parser.parseAsync(
`add --scope user ${serverName} ${updatedCommand} ${updatedArgs.join(' ')}`,
);
expect(mockSetValue).toHaveBeenCalledWith(
SettingScope.User,
'mcpServers',
expect.objectContaining({
[serverName]: expect.objectContaining({
command: updatedCommand,
args: updatedArgs,
}),
}),
);
});
});
});

View File

@@ -36,9 +36,19 @@ async function addMcpServer(
includeTools,
excludeTools,
} = options;
const settings = loadSettings(process.cwd());
const inHome = settings.workspace.path === settings.user.path;
if (scope === 'project' && inHome) {
console.error(
'Error: Please use --scope user to edit settings in the home directory.',
);
process.exit(1);
}
const settingsScope =
scope === 'user' ? SettingScope.User : SettingScope.Workspace;
const settings = loadSettings(process.cwd());
let newServer: Partial<MCPServerConfig> = {};

View File

@@ -7,7 +7,7 @@
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { listMcpServers } from './list.js';
import { loadSettings } from '../../config/settings.js';
import { loadExtensions } from '../../config/extension.js';
import { ExtensionStorage, loadExtensions } from '../../config/extension.js';
import { createTransport } from '@qwen-code/qwen-code-core';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
@@ -16,6 +16,9 @@ vi.mock('../../config/settings.js', () => ({
}));
vi.mock('../../config/extension.js', () => ({
loadExtensions: vi.fn(),
ExtensionStorage: {
getUserExtensionsDir: vi.fn(),
},
}));
vi.mock('@qwen-code/qwen-code-core', () => ({
createTransport: vi.fn(),
@@ -29,11 +32,12 @@ vi.mock('@qwen-code/qwen-code-core', () => ({
getWorkspaceSettingsPath: () => '/tmp/qwen/workspace-settings.json',
getProjectTempDir: () => '/test/home/.qwen/tmp/mocked_hash',
})),
GEMINI_CONFIG_DIR: '.qwen',
QWEN_CONFIG_DIR: '.qwen',
getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)),
}));
vi.mock('@modelcontextprotocol/sdk/client/index.js');
const mockedExtensionStorage = ExtensionStorage as vi.Mock;
const mockedLoadSettings = loadSettings as vi.Mock;
const mockedLoadExtensions = loadExtensions as vi.Mock;
const mockedCreateTransport = createTransport as vi.Mock;
@@ -69,6 +73,9 @@ describe('mcp list command', () => {
MockedClient.mockImplementation(() => mockClient);
mockedCreateTransport.mockResolvedValue(mockTransport);
mockedLoadExtensions.mockReturnValue([]);
mockedExtensionStorage.getUserExtensionsDir.mockReturnValue(
'/mocked/extensions/dir',
);
});
afterEach(() => {

View File

@@ -10,7 +10,8 @@ import { loadSettings } from '../../config/settings.js';
import type { MCPServerConfig } from '@qwen-code/qwen-code-core';
import { MCPServerStatus, createTransport } from '@qwen-code/qwen-code-core';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { loadExtensions } from '../../config/extension.js';
import { ExtensionStorage, loadExtensions } from '../../config/extension.js';
import { ExtensionEnablementManager } from '../../config/extensions/extensionEnablement.js';
const COLOR_GREEN = '\u001b[32m';
const COLOR_YELLOW = '\u001b[33m';
@@ -20,8 +21,10 @@ const RESET_COLOR = '\u001b[0m';
async function getMcpServersFromConfig(): Promise<
Record<string, MCPServerConfig>
> {
const settings = loadSettings(process.cwd());
const extensions = loadExtensions(process.cwd());
const settings = loadSettings();
const extensions = loadExtensions(
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
);
const mcpServers = { ...(settings.merged.mcpServers || {}) };
for (const extension of extensions) {
Object.entries(extension.config.mcpServers || {}).forEach(

View File

@@ -17,7 +17,7 @@ async function removeMcpServer(
const { scope } = options;
const settingsScope =
scope === 'user' ? SettingScope.User : SettingScope.Workspace;
const settings = loadSettings(process.cwd());
const settings = loadSettings();
const existingSettings = settings.forScope(settingsScope).settings;
const mcpServers = existingSettings.mcpServers || {};