Files
kurihada eb80b2c9e6 fix(core): 修复测试用例以匹配最新实现
- todo-manager.test: 修复空日期字符串导致的 Invalid time value 错误
- config-loader.test: 更新测试以匹配简化后的配置加载逻辑
- mcp/config.test: 修复配置路径匹配问题
- task.test/task-extended.test: 添加缺失的 agentEventEmitter mock
- presets/index.test: 更新预设 Agent 数量和 maxSteps 测试
- agent.test: 添加缺失的 mock 函数并修正模式切换测试
- 删除过时的 session/manager.test 和 storage.test (使用已废弃的 API)
2025-12-16 22:33:46 +08:00

355 lines
9.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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) => {
const pathStr = String(filePath);
// 用户级配置(mcp.json)或项目级配置(.ai-assist/config.json
return pathStr.endsWith('mcp.json') || pathStr.endsWith('config.json');
});
vi.mocked(fs.readFileSync).mockImplementation((filePath) => {
const pathStr = String(filePath);
callCount++;
// 根据文件路径返回不同配置
if (pathStr.endsWith('mcp.json')) {
return JSON.stringify(userConfig);
}
return JSON.stringify(projectConfig);
});
const config = loadMCPConfig('/test/dir');
expect(config.mcp?.userServer).toBeDefined();
expect(config.mcp?.projectServer).toBeDefined();
});
});
});