agent-ecosystem/test/main/services/team/TeamMcpConfigBuilder.test.ts
iliya 1ccc1432fc feat: enhance task change handling and improve plugin catalog integration
- Added a 'source' field to PluginCatalogItem to distinguish between official and third-party plugins.
- Refactored ChangeExtractorService to improve caching mechanisms and normalize file paths for better consistency.
- Updated TaskBoundaryParser to support task IDs with underscores, enhancing task identification.
- Enhanced TeamMcpConfigBuilder to merge user-defined MCP configurations with generated ones, improving configuration management.
- Improved UI components to display source information for plugins and MCP servers, enhancing user experience and clarity.
2026-03-11 18:16:40 +02:00

213 lines
6.4 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
let mockHomeDir = '';
vi.mock('@main/utils/pathDecoder', async (importOriginal) => {
const actual = await importOriginal<typeof import('@main/utils/pathDecoder')>();
return {
...actual,
getHomeDir: () => mockHomeDir || actual.getHomeDir(),
};
});
import { TeamMcpConfigBuilder } from '@main/services/team/TeamMcpConfigBuilder';
describe('TeamMcpConfigBuilder', () => {
const createdPaths: string[] = [];
const createdDirs: string[] = [];
afterEach(() => {
for (const filePath of createdPaths.splice(0)) {
try {
fs.rmSync(filePath, { force: true });
} catch {
// ignore cleanup issues in temp dir
}
}
for (const dirPath of createdDirs.splice(0)) {
try {
fs.rmSync(dirPath, { recursive: true, force: true });
} catch {
// ignore cleanup issues in temp dir
}
}
mockHomeDir = '';
});
it('prefers the source MCP entry when workspace source is available', async () => {
const builder = new TeamMcpConfigBuilder();
const configPath = await builder.writeConfigFile();
createdPaths.push(configPath);
const raw = fs.readFileSync(configPath, 'utf8');
const parsed = JSON.parse(raw) as {
mcpServers?: Record<string, { command?: string; args?: string[] }>;
};
const server = parsed.mcpServers?.['agent-teams'];
expect(server?.command).toBe('pnpm');
expect(server?.args).toEqual([
'--dir',
`${process.cwd()}/mcp-server`,
'exec',
'tsx',
`${process.cwd()}/mcp-server/src/index.ts`,
]);
});
it('merges top-level user MCP with generated agent-teams config', async () => {
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'team-mcp-home-'));
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'team-mcp-project-'));
createdDirs.push(homeDir, projectDir);
mockHomeDir = homeDir;
fs.writeFileSync(
path.join(homeDir, '.claude.json'),
JSON.stringify(
{
mcpServers: {
globalOnly: { type: 'http', url: 'https://global.example.com/mcp' },
duplicateServer: { type: 'http', url: 'https://global.example.com/duplicate' },
},
},
null,
2
)
);
fs.writeFileSync(
path.join(projectDir, '.mcp.json'),
JSON.stringify(
{
mcpServers: {
projectOnly: { command: 'node', args: ['project-server.js'] },
duplicateServer: { command: 'node', args: ['project-override.js'] },
},
},
null,
2
)
);
const builder = new TeamMcpConfigBuilder();
const configPath = await builder.writeConfigFile(projectDir);
createdPaths.push(configPath);
const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8')) as {
mcpServers: Record<string, { command?: string; args?: string[]; type?: string; url?: string }>;
};
expect(Object.keys(parsed.mcpServers).sort()).toEqual([
'agent-teams',
'duplicateServer',
'globalOnly',
]);
expect(parsed.mcpServers.globalOnly).toMatchObject({
type: 'http',
url: 'https://global.example.com/mcp',
});
expect(parsed.mcpServers.duplicateServer).toMatchObject({
type: 'http',
url: 'https://global.example.com/duplicate',
});
});
it('does not inline project MCP config to preserve native Claude precedence', async () => {
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'team-mcp-home-'));
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'team-mcp-project-'));
createdDirs.push(homeDir, projectDir);
mockHomeDir = homeDir;
fs.writeFileSync(path.join(homeDir, '.claude.json'), JSON.stringify({ mcpServers: {} }, null, 2));
fs.writeFileSync(
path.join(projectDir, '.mcp.json'),
JSON.stringify(
{
mcpServers: {
projectOnly: { command: 'node', args: ['project-server.js'] },
},
},
null,
2
)
);
const builder = new TeamMcpConfigBuilder();
const configPath = await builder.writeConfigFile(projectDir);
createdPaths.push(configPath);
const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8')) as {
mcpServers: Record<string, { command?: string; args?: string[] }>;
};
expect(parsed.mcpServers.projectOnly).toBeUndefined();
expect(Object.keys(parsed.mcpServers)).toEqual(['agent-teams']);
});
it('generated agent-teams server overrides same-named user MCP entry', async () => {
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'team-mcp-home-'));
createdDirs.push(homeDir);
mockHomeDir = homeDir;
fs.writeFileSync(
path.join(homeDir, '.claude.json'),
JSON.stringify(
{
mcpServers: {
'agent-teams': { command: 'node', args: ['user-server.js'] },
},
},
null,
2
)
);
const builder = new TeamMcpConfigBuilder();
const configPath = await builder.writeConfigFile();
createdPaths.push(configPath);
const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8')) as {
mcpServers: Record<string, { command?: string; args?: string[] }>;
};
expect(parsed.mcpServers['agent-teams']).toMatchObject({
command: 'pnpm',
args: [
'--dir',
`${process.cwd()}/mcp-server`,
'exec',
'tsx',
`${process.cwd()}/mcp-server/src/index.ts`,
],
});
});
it('ignores malformed user MCP file', async () => {
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'team-mcp-home-'));
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'team-mcp-project-'));
createdDirs.push(homeDir, projectDir);
mockHomeDir = homeDir;
fs.writeFileSync(path.join(homeDir, '.claude.json'), '{ invalid json');
const builder = new TeamMcpConfigBuilder();
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
let configPath = '';
try {
configPath = await builder.writeConfigFile(projectDir);
} finally {
warnSpy.mockRestore();
}
createdPaths.push(configPath);
const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8')) as {
mcpServers: Record<string, { command?: string; args?: string[] }>;
};
expect(Object.keys(parsed.mcpServers)).toEqual(['agent-teams']);
});
});