Files
ai-terminal-assistant/packages/core/tests/unit/mcp/config.test.ts
T
kurihada 5e32375f0e feat: 重构为 Monorepo 架构并实现 HTTP Server
架构变更:
- 采用 pnpm workspaces 实现 Monorepo 结构
- 将现有代码迁移到 packages/core
- 新增 packages/server HTTP 服务层

Server 功能:
- REST API: 会话管理、工具管理、配置管理
- WebSocket: 实时双向通信支持
- SSE: 服务端事件推送
- Hono + Bun 作为运行时

API 端点:
- GET/POST /api/sessions - 会话 CRUD
- GET/POST /api/sessions/:id/messages - 消息管理
- GET /api/sessions/:id/events - SSE 事件流
- WS /api/ws/:sessionId - WebSocket 连接
- GET/POST /api/tools - 工具管理
- GET/PUT /api/config - 配置管理
2025-12-12 10:42:20 +08:00

352 lines
9.5 KiB
TypeScript

/**
* MCP 配置加载和验证测试
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import {
loadMCPConfig,
validateMCPConfig,
normalizeMCPConfig,
getEnabledServers,
isToolEnabled,
resolveEnvVariables,
resolveEnvInObject,
} from '../../../src/mcp/config.js';
import type { MCPConfig } from '../../../src/mcp/types.js';
// Mock fs 模块
vi.mock('fs');
vi.mock('path', async () => {
const actual = await vi.importActual('path');
return {
...actual,
extname: (actual as typeof import('path')).extname,
};
});
describe('MCP Config', () => {
beforeEach(() => {
vi.resetAllMocks();
// 默认不存在配置文件
vi.mocked(fs.existsSync).mockReturnValue(false);
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('resolveEnvVariables', () => {
it('应该解析环境变量引用', () => {
process.env.TEST_VAR = 'test_value';
const result = resolveEnvVariables('{env:TEST_VAR}');
expect(result).toBe('test_value');
delete process.env.TEST_VAR;
});
it('应该处理多个环境变量', () => {
process.env.VAR1 = 'value1';
process.env.VAR2 = 'value2';
const result = resolveEnvVariables('{env:VAR1}/{env:VAR2}');
expect(result).toBe('value1/value2');
delete process.env.VAR1;
delete process.env.VAR2;
});
it('应该对未设置的变量返回空字符串', () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const result = resolveEnvVariables('{env:NONEXISTENT_VAR}');
expect(result).toBe('');
expect(warnSpy).toHaveBeenCalled();
warnSpy.mockRestore();
});
it('应该保留普通字符串不变', () => {
const result = resolveEnvVariables('plain string');
expect(result).toBe('plain string');
});
});
describe('resolveEnvInObject', () => {
it('应该递归解析对象中的环境变量', () => {
process.env.TEST_KEY = 'secret';
const obj = {
key: '{env:TEST_KEY}',
nested: {
value: '{env:TEST_KEY}',
},
array: ['{env:TEST_KEY}'],
};
const result = resolveEnvInObject(obj);
expect(result).toEqual({
key: 'secret',
nested: {
value: 'secret',
},
array: ['secret'],
});
delete process.env.TEST_KEY;
});
it('应该保留非字符串值不变', () => {
const obj = {
number: 123,
boolean: true,
null: null,
};
const result = resolveEnvInObject(obj);
expect(result).toEqual(obj);
});
});
describe('validateMCPConfig', () => {
it('应该接受空配置', () => {
const errors = validateMCPConfig({});
expect(errors).toHaveLength(0);
});
it('应该接受有效的本地服务器配置', () => {
const config: MCPConfig = {
mcp: {
filesystem: {
type: 'local',
command: ['npx', '-y', '@anthropic-ai/mcp-server-filesystem'],
},
},
};
const errors = validateMCPConfig(config);
expect(errors).toHaveLength(0);
});
it('应该接受有效的远程服务器配置', () => {
const config: MCPConfig = {
mcp: {
remote: {
type: 'remote',
url: 'https://mcp.example.com',
},
},
};
const errors = validateMCPConfig(config);
expect(errors).toHaveLength(0);
});
it('应该拒绝无效的服务器名称', () => {
const config: MCPConfig = {
mcp: {
'123invalid': {
type: 'local',
command: ['echo'],
},
},
};
const errors = validateMCPConfig(config);
expect(errors.length).toBeGreaterThan(0);
expect(errors[0]).toContain('服务器名称');
});
it('应该拒绝缺少 command 的本地服务器', () => {
const config: MCPConfig = {
mcp: {
server: {
type: 'local',
command: [],
},
},
};
const errors = validateMCPConfig(config);
expect(errors.length).toBeGreaterThan(0);
expect(errors[0]).toContain('command');
});
it('应该拒绝无效的 URL', () => {
const config: MCPConfig = {
mcp: {
remote: {
type: 'remote',
url: 'invalid-url',
},
},
};
const errors = validateMCPConfig(config);
expect(errors.length).toBeGreaterThan(0);
expect(errors[0]).toContain('url');
});
it('应该验证 tools 配置', () => {
const config: MCPConfig = {
mcp: {},
tools: {
'valid-tool': true,
'invalid-tool': 'not-boolean' as unknown as boolean,
},
};
const errors = validateMCPConfig(config);
expect(errors.length).toBeGreaterThan(0);
expect(errors[0]).toContain('布尔值');
});
});
describe('normalizeMCPConfig', () => {
it('应该添加默认值', () => {
const config: MCPConfig = {
mcp: {
server: {
type: 'local',
command: ['echo'],
},
},
};
const normalized = normalizeMCPConfig(config);
expect(normalized.mcp?.server.enabled).toBe(true);
expect(normalized.mcp?.server.timeout).toBe(30000);
});
it('应该保留显式设置的值', () => {
const config: MCPConfig = {
mcp: {
server: {
type: 'local',
command: ['echo'],
enabled: false,
timeout: 5000,
},
},
};
const normalized = normalizeMCPConfig(config);
expect(normalized.mcp?.server.enabled).toBe(false);
expect(normalized.mcp?.server.timeout).toBe(5000);
});
});
describe('getEnabledServers', () => {
it('应该返回启用的服务器', () => {
const config: MCPConfig = {
mcp: {
enabled: {
type: 'local',
command: ['echo'],
enabled: true,
},
disabled: {
type: 'local',
command: ['echo'],
enabled: false,
},
default: {
type: 'local',
command: ['echo'],
},
},
};
const servers = getEnabledServers(config);
expect(servers).toHaveLength(2);
expect(servers.map((s) => s.name)).toContain('enabled');
expect(servers.map((s) => s.name)).toContain('default');
expect(servers.map((s) => s.name)).not.toContain('disabled');
});
it('应该处理空配置', () => {
const servers = getEnabledServers({});
expect(servers).toHaveLength(0);
});
});
describe('isToolEnabled', () => {
it('没有配置时默认启用', () => {
expect(isToolEnabled('any-tool')).toBe(true);
expect(isToolEnabled('any-tool', undefined)).toBe(true);
});
it('应该精确匹配工具名', () => {
const config = {
'server-tool': false,
'server-other': true,
};
expect(isToolEnabled('server-tool', config)).toBe(false);
expect(isToolEnabled('server-other', config)).toBe(true);
});
it('应该支持通配符匹配', () => {
const config = {
'server-*': false,
};
expect(isToolEnabled('server-tool1', config)).toBe(false);
expect(isToolEnabled('server-tool2', config)).toBe(false);
expect(isToolEnabled('other-tool', config)).toBe(true);
});
it('精确匹配应优先于通配符', () => {
const config = {
'server-*': false,
'server-special': true,
};
expect(isToolEnabled('server-special', config)).toBe(true);
expect(isToolEnabled('server-other', config)).toBe(false);
});
});
describe('loadMCPConfig', () => {
it('没有配置文件时返回空配置', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
const config = loadMCPConfig('/test/dir');
expect(config).toEqual({});
});
it('应该加载 JSON 配置文件', () => {
const testConfig = {
mcp: {
server: {
type: 'local',
command: ['echo'],
},
},
};
vi.mocked(fs.existsSync).mockImplementation((filePath) => {
return String(filePath).endsWith('config.json');
});
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(testConfig));
const config = loadMCPConfig('/test/dir');
expect(config.mcp).toBeDefined();
});
it('应该合并用户级和项目级配置', () => {
const userConfig = {
mcp: {
userServer: {
type: 'local',
command: ['user-cmd'],
},
},
};
const projectConfig = {
mcp: {
projectServer: {
type: 'local',
command: ['project-cmd'],
},
},
};
let callCount = 0;
vi.mocked(fs.existsSync).mockImplementation((filePath) => {
return (
String(filePath).includes('.ai-assist') &&
String(filePath).endsWith('config.json')
);
});
vi.mocked(fs.readFileSync).mockImplementation(() => {
callCount++;
// 第一次调用返回用户配置,第二次返回项目配置
return JSON.stringify(callCount === 1 ? userConfig : projectConfig);
});
const config = loadMCPConfig('/test/dir');
expect(config.mcp?.userServer).toBeDefined();
expect(config.mcp?.projectServer).toBeDefined();
});
});
});