- Fix TS error: use kanbanTaskState?.reviewer instead of task.reviewer (reviewer exists on KanbanTaskState, not TeamTask) - Fix crossTeam tests: update assertions to match XML tag protocol format (CROSS_TEAM_PREFIX_TAG → CROSS_TEAM_TAG_NAME with XML attributes) - Fix controller test: use 'bob' instead of 'alice' as commenter (alice is the lead = same as task owner, so no notification fires) - Fix TeamMcpConfigBuilder test: use path.join() for cross-platform paths (forward slashes fail on Windows where path.join uses backslashes) - Fix atomicWrite EPERM on Windows: add retry logic for rename operations (Windows CI antivirus can briefly hold files, causing EPERM on rename)
213 lines
6.4 KiB
TypeScript
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',
|
|
path.join(process.cwd(), 'mcp-server'),
|
|
'exec',
|
|
'tsx',
|
|
path.join(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',
|
|
path.join(process.cwd(), 'mcp-server'),
|
|
'exec',
|
|
'tsx',
|
|
path.join(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']);
|
|
});
|
|
});
|