Merge branch 'dev' into spike/team-snapshot-split-plan
This commit is contained in:
commit
bff5f43da9
2 changed files with 534 additions and 32 deletions
|
|
@ -2150,6 +2150,72 @@ function buildCombinedLogs(stdoutBuffer: string, stderrBuffer: string): string {
|
||||||
return [`[stdout]`, stdoutTrimmed, '', `[stderr]`, stderrTrimmed].join('\n');
|
return [`[stdout]`, stdoutTrimmed, '', `[stderr]`, stderrTrimmed].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AgentTeamsMcpConfigEntry {
|
||||||
|
command?: unknown;
|
||||||
|
args?: unknown;
|
||||||
|
env?: unknown;
|
||||||
|
cwd?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AgentTeamsMcpConfigFile {
|
||||||
|
mcpServers?: Record<string, AgentTeamsMcpConfigEntry>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AgentTeamsMcpLaunchSpec {
|
||||||
|
command: string;
|
||||||
|
args: string[];
|
||||||
|
cwd?: string;
|
||||||
|
env: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface McpJsonRpcErrorPayload {
|
||||||
|
code?: number;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface McpJsonRpcResponse<TResult> {
|
||||||
|
id?: number;
|
||||||
|
result?: TResult;
|
||||||
|
error?: McpJsonRpcErrorPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface McpToolsListResult {
|
||||||
|
tools?: Array<{
|
||||||
|
name?: string;
|
||||||
|
_meta?: Record<string, unknown>;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface McpToolCallResult {
|
||||||
|
content?: Array<{
|
||||||
|
type?: string;
|
||||||
|
text?: string;
|
||||||
|
}>;
|
||||||
|
isError?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AgentTeamsMcpValidationFixture {
|
||||||
|
claudeDir: string;
|
||||||
|
teamName: string;
|
||||||
|
memberName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStringArray(value: unknown): value is string[] {
|
||||||
|
return Array.isArray(value) && value.every((entry) => typeof entry === 'string');
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRecordStringValues(value: unknown): Record<string, string> {
|
||||||
|
if (!value || typeof value !== 'object') {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(value).flatMap(([key, entry]) =>
|
||||||
|
typeof entry === 'string' ? [[key, entry]] : []
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function extractLogsTail(stdoutBuffer: string, stderrBuffer: string): string | undefined {
|
function extractLogsTail(stdoutBuffer: string, stderrBuffer: string): string | undefined {
|
||||||
const trimmed = buildCombinedLogs(stdoutBuffer, stderrBuffer).trim();
|
const trimmed = buildCombinedLogs(stdoutBuffer, stderrBuffer).trim();
|
||||||
if (trimmed.length === 0) {
|
if (trimmed.length === 0) {
|
||||||
|
|
@ -12553,50 +12619,322 @@ export class TeamProvisioningService {
|
||||||
private buildAgentTeamsMcpValidationError(output: string): string {
|
private buildAgentTeamsMcpValidationError(output: string): string {
|
||||||
const detail = this.normalizeApiRetryErrorMessage(output) || output.trim();
|
const detail = this.normalizeApiRetryErrorMessage(output) || output.trim();
|
||||||
if (!detail) {
|
if (!detail) {
|
||||||
return (
|
return 'agent-teams MCP preflight failed before team launch.';
|
||||||
'agent-teams MCP loaded config but did not expose member_briefing. ' +
|
}
|
||||||
'The leader would start without required team MCP tools.'
|
return `agent-teams MCP preflight failed before team launch. Details: ${detail}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async readAgentTeamsMcpLaunchSpec(
|
||||||
|
mcpConfigPath: string
|
||||||
|
): Promise<AgentTeamsMcpLaunchSpec> {
|
||||||
|
let parsed: AgentTeamsMcpConfigFile;
|
||||||
|
try {
|
||||||
|
const raw = await fs.promises.readFile(mcpConfigPath, 'utf8');
|
||||||
|
parsed = JSON.parse(raw) as AgentTeamsMcpConfigFile;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
this.buildAgentTeamsMcpValidationError(
|
||||||
|
`Failed to read generated MCP config ${mcpConfigPath}: ${
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
}`
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
|
||||||
'agent-teams MCP loaded config but did not expose member_briefing. ' + `Details: ${detail}`
|
const server = parsed.mcpServers?.['agent-teams'];
|
||||||
|
if (!server) {
|
||||||
|
throw new Error(
|
||||||
|
this.buildAgentTeamsMcpValidationError(
|
||||||
|
`Generated MCP config ${mcpConfigPath} does not contain an "agent-teams" server entry.`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof server.command !== 'string' || server.command.trim().length === 0) {
|
||||||
|
throw new Error(
|
||||||
|
this.buildAgentTeamsMcpValidationError(
|
||||||
|
'Generated agent-teams MCP config is missing a valid launch command.'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.args !== undefined && !isStringArray(server.args)) {
|
||||||
|
throw new Error(
|
||||||
|
this.buildAgentTeamsMcpValidationError(
|
||||||
|
'Generated agent-teams MCP config has invalid args; expected a string array.'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.cwd !== undefined && typeof server.cwd !== 'string') {
|
||||||
|
throw new Error(
|
||||||
|
this.buildAgentTeamsMcpValidationError(
|
||||||
|
'Generated agent-teams MCP config has invalid cwd; expected a string path.'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
command: server.command,
|
||||||
|
args: server.args ?? [],
|
||||||
|
cwd: typeof server.cwd === 'string' ? server.cwd : undefined,
|
||||||
|
env: normalizeRecordStringValues(server.env),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createAgentTeamsMcpValidationFixture(
|
||||||
|
projectPath: string
|
||||||
|
): Promise<AgentTeamsMcpValidationFixture> {
|
||||||
|
const claudeDir = await fs.promises.mkdtemp(
|
||||||
|
path.join(os.tmpdir(), 'agent-teams-mcp-validate-')
|
||||||
);
|
);
|
||||||
|
const teamName = 'mcp-validation-team';
|
||||||
|
const memberName = 'mcp-validation-member';
|
||||||
|
const teamDir = path.join(claudeDir, 'teams', teamName);
|
||||||
|
|
||||||
|
await fs.promises.mkdir(teamDir, { recursive: true });
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(teamDir, 'config.json'),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
name: teamName,
|
||||||
|
projectPath,
|
||||||
|
members: [
|
||||||
|
{ name: 'team-lead', agentType: 'team-lead', role: 'lead' },
|
||||||
|
{ name: memberName, agentType: 'teammate', role: 'developer' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
claudeDir,
|
||||||
|
teamName,
|
||||||
|
memberName,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async validateAgentTeamsMcpRuntime(
|
private async validateAgentTeamsMcpRuntime(
|
||||||
claudePath: string,
|
_claudePath: string,
|
||||||
cwd: string,
|
cwd: string,
|
||||||
env: NodeJS.ProcessEnv,
|
env: NodeJS.ProcessEnv,
|
||||||
mcpConfigPath: string
|
mcpConfigPath: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const result = await this.spawnProbe(
|
const launchSpec = await this.readAgentTeamsMcpLaunchSpec(mcpConfigPath);
|
||||||
claudePath,
|
const fixture = await this.createAgentTeamsMcpValidationFixture(cwd);
|
||||||
[
|
let child: ReturnType<typeof spawn> | null = null;
|
||||||
'--setting-sources',
|
let stdoutBuffer = '';
|
||||||
'user,project,local',
|
let stderrBuffer = '';
|
||||||
'--mcp-config',
|
let nextRequestId = 1;
|
||||||
mcpConfigPath,
|
const pending = new Map<
|
||||||
'--',
|
number,
|
||||||
'mcp',
|
{
|
||||||
'get',
|
resolve: (value: unknown) => void;
|
||||||
'agent-teams',
|
reject: (error: Error) => void;
|
||||||
],
|
timeoutHandle: ReturnType<typeof setTimeout>;
|
||||||
cwd,
|
}
|
||||||
env,
|
>();
|
||||||
VERIFY_TIMEOUT_MS
|
|
||||||
);
|
|
||||||
|
|
||||||
const combinedOutput = buildCombinedLogs(result.stdout, result.stderr).trim();
|
const rejectAll = (error: Error): void => {
|
||||||
if (result.exitCode !== 0) {
|
for (const [id, entry] of pending) {
|
||||||
throw new Error(this.buildAgentTeamsMcpValidationError(combinedOutput));
|
clearTimeout(entry.timeoutHandle);
|
||||||
}
|
entry.reject(error);
|
||||||
|
pending.delete(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const normalizedOutput = combinedOutput.toLowerCase();
|
try {
|
||||||
if (
|
child = spawnCli(launchSpec.command, launchSpec.args, {
|
||||||
!normalizedOutput.includes('status: ✓ connected') &&
|
cwd: launchSpec.cwd ?? cwd,
|
||||||
!normalizedOutput.includes('status: connected')
|
env: { ...env, ...launchSpec.env },
|
||||||
) {
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
throw new Error(this.buildAgentTeamsMcpValidationError(combinedOutput));
|
windowsHide: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const parseStdoutLine = (line: string): void => {
|
||||||
|
let message: McpJsonRpcResponse<unknown>;
|
||||||
|
try {
|
||||||
|
message = JSON.parse(line) as McpJsonRpcResponse<unknown>;
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(
|
||||||
|
`agent-teams MCP preflight emitted non-JSON stdout line: ${
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof message.id !== 'number') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = pending.get(message.id);
|
||||||
|
if (!entry) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeout(entry.timeoutHandle);
|
||||||
|
pending.delete(message.id);
|
||||||
|
|
||||||
|
if (message.error) {
|
||||||
|
entry.reject(new Error(message.error.message ?? 'Unknown MCP JSON-RPC error'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.resolve(message.result);
|
||||||
|
};
|
||||||
|
|
||||||
|
child.stdout?.setEncoding('utf8');
|
||||||
|
child.stdout?.on('data', (chunk: string | Buffer) => {
|
||||||
|
stdoutBuffer += chunk.toString();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const newlineIndex = stdoutBuffer.indexOf('\n');
|
||||||
|
if (newlineIndex === -1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const line = stdoutBuffer.slice(0, newlineIndex).trim();
|
||||||
|
stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1);
|
||||||
|
if (!line) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
parseStdoutLine(line);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr?.setEncoding('utf8');
|
||||||
|
child.stderr?.on('data', (chunk: string | Buffer) => {
|
||||||
|
stderrBuffer += chunk.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.once('error', (error) => {
|
||||||
|
rejectAll(error instanceof Error ? error : new Error(String(error)));
|
||||||
|
});
|
||||||
|
|
||||||
|
child.once('close', (code, signal) => {
|
||||||
|
if (pending.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rejectAll(
|
||||||
|
new Error(
|
||||||
|
`agent-teams MCP process exited unexpectedly during preflight (code=${
|
||||||
|
code ?? 'null'
|
||||||
|
} signal=${signal ?? 'null'})`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const request = <TResult>(
|
||||||
|
method: string,
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
timeoutMs: number = VERIFY_TIMEOUT_MS
|
||||||
|
): Promise<TResult> =>
|
||||||
|
new Promise<TResult>((resolve, reject) => {
|
||||||
|
if (!child?.stdin) {
|
||||||
|
reject(new Error('agent-teams MCP stdin is not available'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = nextRequestId++;
|
||||||
|
const timeoutHandle = setTimeout(() => {
|
||||||
|
pending.delete(id);
|
||||||
|
reject(new Error(`agent-teams MCP request timed out: ${method}`));
|
||||||
|
}, timeoutMs);
|
||||||
|
|
||||||
|
pending.set(id, {
|
||||||
|
resolve: resolve as (value: unknown) => void,
|
||||||
|
reject,
|
||||||
|
timeoutHandle,
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stdin.write(
|
||||||
|
`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`,
|
||||||
|
(error) => {
|
||||||
|
if (!error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearTimeout(timeoutHandle);
|
||||||
|
pending.delete(id);
|
||||||
|
reject(error instanceof Error ? error : new Error(String(error)));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const notify = async (method: string, params?: Record<string, unknown>): Promise<void> => {
|
||||||
|
if (!child?.stdin) {
|
||||||
|
throw new Error('agent-teams MCP stdin is not available');
|
||||||
|
}
|
||||||
|
const stdin = child.stdin;
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
stdin.write(
|
||||||
|
`${JSON.stringify({ jsonrpc: '2.0', method, ...(params ? { params } : {}) })}\n`,
|
||||||
|
(error) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error instanceof Error ? error : new Error(String(error)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
await request('initialize', {
|
||||||
|
protocolVersion: '2024-11-05',
|
||||||
|
capabilities: {},
|
||||||
|
clientInfo: { name: 'claude-agent-teams-ui', version: '1.0.0' },
|
||||||
|
});
|
||||||
|
await notify('notifications/initialized');
|
||||||
|
|
||||||
|
const toolsList = await request<McpToolsListResult>('tools/list', {});
|
||||||
|
const memberBriefingTool = (toolsList.tools ?? []).find(
|
||||||
|
(tool) => tool.name === 'member_briefing'
|
||||||
|
);
|
||||||
|
if (!memberBriefingTool) {
|
||||||
|
throw new Error('agent-teams MCP started but tools/list did not include member_briefing');
|
||||||
|
}
|
||||||
|
|
||||||
|
const memberBriefing = await request<McpToolCallResult>('tools/call', {
|
||||||
|
name: 'member_briefing',
|
||||||
|
arguments: {
|
||||||
|
claudeDir: fixture.claudeDir,
|
||||||
|
teamName: fixture.teamName,
|
||||||
|
memberName: fixture.memberName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (memberBriefing.isError) {
|
||||||
|
throw new Error(
|
||||||
|
memberBriefing.content?.[0]?.text ??
|
||||||
|
'agent-teams MCP returned an unspecified error for member_briefing'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const briefingText = memberBriefing.content?.find((item) => item.type === 'text')?.text ?? '';
|
||||||
|
if (briefingText.trim().length === 0) {
|
||||||
|
throw new Error('agent-teams MCP returned empty content for member_briefing');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const detail = buildCombinedLogs('', stderrBuffer).trim();
|
||||||
|
const errorText =
|
||||||
|
error instanceof Error && detail.length > 0
|
||||||
|
? `${error.message}\n${detail}`
|
||||||
|
: detail || String(error);
|
||||||
|
throw new Error(this.buildAgentTeamsMcpValidationError(errorText));
|
||||||
|
} finally {
|
||||||
|
rejectAll(new Error('agent-teams MCP preflight session closed'));
|
||||||
|
if (child?.stdin && !child.stdin.destroyed) {
|
||||||
|
child.stdin.end();
|
||||||
|
}
|
||||||
|
if (child) {
|
||||||
|
killProcessTree(child);
|
||||||
|
}
|
||||||
|
await fs.promises.rm(fixture.claudeDir, { recursive: true, force: true }).catch(() => {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,118 @@ import { TeamProvisioningService } from '@main/services/team/TeamProvisioningSer
|
||||||
import { ClaudeBinaryResolver } from '@main/services/team/ClaudeBinaryResolver';
|
import { ClaudeBinaryResolver } from '@main/services/team/ClaudeBinaryResolver';
|
||||||
import { resolveInteractiveShellEnv } from '@main/utils/shellEnv';
|
import { resolveInteractiveShellEnv } from '@main/utils/shellEnv';
|
||||||
|
|
||||||
|
function getRealAgentTeamsMcpLaunchSpec(): { command: string; args: string[] } {
|
||||||
|
const workspaceRoot = process.cwd();
|
||||||
|
const distEntry = path.join(workspaceRoot, 'mcp-server', 'dist', 'index.js');
|
||||||
|
if (fs.existsSync(distEntry)) {
|
||||||
|
return {
|
||||||
|
command: process.execPath,
|
||||||
|
args: [distEntry],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
command: path.join(
|
||||||
|
workspaceRoot,
|
||||||
|
'node_modules',
|
||||||
|
'.bin',
|
||||||
|
process.platform === 'win32' ? 'tsx.cmd' : 'tsx'
|
||||||
|
),
|
||||||
|
args: [path.join(workspaceRoot, 'mcp-server', 'src', 'index.ts')],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeMcpConfig(
|
||||||
|
targetDir: string,
|
||||||
|
serverConfig: Record<string, { command: string; args: string[] }>
|
||||||
|
): string {
|
||||||
|
const configPath = path.join(targetDir, `agent-teams-mcp-${Date.now()}.json`);
|
||||||
|
fs.writeFileSync(
|
||||||
|
configPath,
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
mcpServers: serverConfig,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
return configPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeMockMcpServer(
|
||||||
|
targetDir: string,
|
||||||
|
variant: 'missing-member-briefing' | 'member-briefing-error'
|
||||||
|
): string {
|
||||||
|
const scriptPath = path.join(targetDir, `mock-mcp-${variant}.js`);
|
||||||
|
const tools =
|
||||||
|
variant === 'missing-member-briefing'
|
||||||
|
? [{ name: 'task_create' }]
|
||||||
|
: [{ name: 'member_briefing' }];
|
||||||
|
const toolCallResult =
|
||||||
|
variant === 'member-briefing-error'
|
||||||
|
? {
|
||||||
|
content: [{ type: 'text', text: 'mock member_briefing failure' }],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
content: [{ type: 'text', text: 'ok' }],
|
||||||
|
isError: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
scriptPath,
|
||||||
|
`'use strict';
|
||||||
|
let buffer = '';
|
||||||
|
function send(message) {
|
||||||
|
process.stdout.write(JSON.stringify(message) + '\\n');
|
||||||
|
}
|
||||||
|
process.stdin.setEncoding('utf8');
|
||||||
|
process.stdin.on('data', (chunk) => {
|
||||||
|
buffer += chunk;
|
||||||
|
while (true) {
|
||||||
|
const newlineIndex = buffer.indexOf('\\n');
|
||||||
|
if (newlineIndex === -1) break;
|
||||||
|
const line = buffer.slice(0, newlineIndex).trim();
|
||||||
|
buffer = buffer.slice(newlineIndex + 1);
|
||||||
|
if (!line) continue;
|
||||||
|
const message = JSON.parse(line);
|
||||||
|
if (message.method === 'initialize') {
|
||||||
|
send({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: message.id,
|
||||||
|
result: {
|
||||||
|
serverInfo: { name: 'mock-agent-teams-mcp', version: '1.0.0' },
|
||||||
|
capabilities: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (message.method === 'tools/list') {
|
||||||
|
send({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: message.id,
|
||||||
|
result: { tools: ${JSON.stringify(tools)} },
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (message.method === 'tools/call') {
|
||||||
|
send({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: message.id,
|
||||||
|
result: ${JSON.stringify(toolCallResult)},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
return scriptPath;
|
||||||
|
}
|
||||||
|
|
||||||
describe('TeamProvisioningService prepare/auth behavior', () => {
|
describe('TeamProvisioningService prepare/auth behavior', () => {
|
||||||
let tempRoot = '';
|
let tempRoot = '';
|
||||||
|
|
||||||
|
|
@ -626,4 +738,56 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('validates the generated agent-teams MCP server directly over stdio', async () => {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
const configPath = writeMcpConfig(tempRoot, {
|
||||||
|
'agent-teams': getRealAgentTeamsMcpLaunchSpec(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
(svc as any).validateAgentTeamsMcpRuntime('/fake/claude', tempRoot, process.env, configPath)
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails validation when the generated MCP config has no agent-teams entry', async () => {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
const configPath = writeMcpConfig(tempRoot, {
|
||||||
|
unrelated: getRealAgentTeamsMcpLaunchSpec(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
(svc as any).validateAgentTeamsMcpRuntime('/fake/claude', tempRoot, process.env, configPath)
|
||||||
|
).rejects.toThrow('does not contain an "agent-teams" server entry');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails validation when tools/list does not include member_briefing', async () => {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
const mockServerPath = writeMockMcpServer(tempRoot, 'missing-member-briefing');
|
||||||
|
const configPath = writeMcpConfig(tempRoot, {
|
||||||
|
'agent-teams': {
|
||||||
|
command: process.execPath,
|
||||||
|
args: [mockServerPath],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
(svc as any).validateAgentTeamsMcpRuntime('/fake/claude', tempRoot, process.env, configPath)
|
||||||
|
).rejects.toThrow('tools/list did not include member_briefing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails validation when member_briefing itself returns an MCP error', async () => {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
const mockServerPath = writeMockMcpServer(tempRoot, 'member-briefing-error');
|
||||||
|
const configPath = writeMcpConfig(tempRoot, {
|
||||||
|
'agent-teams': {
|
||||||
|
command: process.execPath,
|
||||||
|
args: [mockServerPath],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
(svc as any).validateAgentTeamsMcpRuntime('/fake/claude', tempRoot, process.env, configPath)
|
||||||
|
).rejects.toThrow('mock member_briefing failure');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue