mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 09:17:53 +00:00
Sync upstream Gemini-CLI v0.8.2 (#838)
This commit is contained in:
@@ -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,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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> = {};
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 || {};
|
||||
|
||||
Reference in New Issue
Block a user