/** * 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(); }); }); });