// @vitest-environment node /* eslint-disable security/detect-non-literal-fs-filename, sonarjs/publicly-writable-directories */ import { chmod, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import http from 'node:http'; import net from 'node:net'; import os from 'node:os'; import path from 'node:path'; import { AgentTeamsMcpHttpServer } from '@main/services/team/AgentTeamsMcpHttpServer'; import { OpenCodeBridgeCommandClient } from '@main/services/team/opencode/bridge/OpenCodeBridgeCommandClient'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; const FAKE_MCP_HTTP_SERVER_SOURCE = String.raw` const fs = require('node:fs'); const http = require('node:http'); function readArg(name) { const index = process.argv.indexOf(name); return index >= 0 ? process.argv[index + 1] : null; } const host = readArg('--host') || '127.0.0.1'; const endpoint = readArg('--endpoint') || '/mcp'; const port = Number(readArg('--port')); const controlFile = process.env.AGENT_TEAMS_MCP_TEST_CONTROL_FILE; const healthIdentity = process.env.AGENT_TEAMS_MCP_HTTP_IDENTITY_SERVICE === 'agent-teams-mcp-http' && process.env.AGENT_TEAMS_MCP_HTTP_CLAUDE_DIR_HASH && process.env.AGENT_TEAMS_MCP_HTTP_LAUNCH_SPEC_HASH && process.env.AGENT_TEAMS_MCP_HTTP_OWNER_INSTANCE_ID ? { schemaVersion: 1, service: 'agent-teams-mcp-http', transport: 'httpStream', host, port, endpoint, claudeDirHash: process.env.AGENT_TEAMS_MCP_HTTP_CLAUDE_DIR_HASH, launchSpecHash: process.env.AGENT_TEAMS_MCP_HTTP_LAUNCH_SPEC_HASH, ownerInstanceId: process.env.AGENT_TEAMS_MCP_HTTP_OWNER_INSTANCE_ID, } : null; function readControl() { if (!controlFile) { return 'healthy'; } try { return fs.readFileSync(controlFile, 'utf8').trim() || 'healthy'; } catch { return 'healthy'; } } function isUnhealthy() { const control = readControl(); if (control === 'unhealthy-once:' + port) { try { fs.writeFileSync(controlFile, 'healthy'); } catch {} return true; } if (control === 'unhealthy-pid:' + process.pid) { return true; } return control === 'unhealthy-all' || control === 'unhealthy-port:' + port; } const server = http.createServer((request, response) => { if (request.url === '/health') { if (isUnhealthy()) { response.writeHead(503, { 'content-type': 'text/plain' }); response.end('unhealthy'); return; } response.writeHead(200, { 'content-type': 'text/plain' }); response.end(healthIdentity ? JSON.stringify(healthIdentity) : 'ok'); return; } if (request.url === endpoint) { response.writeHead(200, { 'content-type': 'application/json' }); response.end('{"jsonrpc":"2.0","result":{}}'); return; } response.writeHead(404, { 'content-type': 'text/plain' }); response.end('not found'); }); server.listen(port, host); function shutdown() { server.close(() => process.exit(0)); setTimeout(() => process.exit(0), 500).unref(); } process.on('SIGTERM', shutdown); process.on('SIGINT', shutdown); `; const FAKE_OPENCODE_BRIDGE_BINARY_SOURCE = String.raw`#!/usr/bin/env node const fs = require('node:fs'); const http = require('node:http'); function readArg(name) { const index = process.argv.indexOf(name); return index >= 0 ? process.argv[index + 1] : null; } function readHealthStatus(url) { return new Promise((resolve) => { if (!url) { resolve(null); return; } const target = new URL(url); target.pathname = '/health'; target.search = ''; target.hash = ''; const request = http.get( { host: target.hostname, port: Number(target.port), path: target.pathname, timeout: 750, }, (response) => { response.resume(); resolve(response.statusCode || null); } ); request.once('timeout', () => { request.destroy(); resolve(null); }); request.once('error', () => resolve(null)); }); } async function main() { const inputPath = readArg('--input'); if (!inputPath) { console.error('missing --input'); process.exit(64); } const envelope = JSON.parse(fs.readFileSync(inputPath, 'utf8')); const mcpUrl = process.env.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL || null; const healthStatus = await readHealthStatus(mcpUrl); if (healthStatus !== 200) { console.error( JSON.stringify({ kind: 'mcp_unreachable', mcpUrl, healthStatus, }) ); process.exit(7); } process.stdout.write( JSON.stringify({ ok: true, schemaVersion: envelope.schemaVersion, requestId: envelope.requestId, command: envelope.command, completedAt: new Date().toISOString(), durationMs: 1, runtime: { providerId: 'opencode', binaryPath: process.argv[1], binaryFingerprint: 'fake-runtime', version: 'fake-opencode-bridge-e2e', capabilitySnapshotId: 'fake-capabilities', }, diagnostics: [], data: { runId: envelope.body && envelope.body.runId ? envelope.body.runId : null, observedMcpUrl: mcpUrl, healthStatus, }, }) + '\n' ); } main().catch((error) => { console.error(error && error.stack ? error.stack : String(error)); process.exit(1); }); `; const describePosix = process.platform === 'win32' ? describe.skip : describe; async function allocateLoopbackPort(excluded: Set = new Set()): Promise { while (true) { const port = await new Promise((resolve, reject) => { const server = net.createServer(); server.once('error', reject); server.listen(0, '127.0.0.1', () => { const address = server.address(); if (!address || typeof address === 'string') { server.close(() => reject(new Error('Failed to allocate test port'))); return; } server.close(() => resolve(address.port)); }); }); if (!excluded.has(port)) { excluded.add(port); return port; } } } async function readHealthStatus(url: string): Promise { const target = new URL(url); target.pathname = '/health'; target.search = ''; target.hash = ''; return new Promise((resolve) => { const request = http.get( { host: target.hostname, port: Number(target.port), path: target.pathname, timeout: 500, }, (response) => { response.resume(); resolve(response.statusCode ?? null); } ); request.once('timeout', () => { request.destroy(); resolve(null); }); request.once('error', () => resolve(null)); }); } async function waitForHealthDown(url: string): Promise { const startedAt = Date.now(); while (Date.now() - startedAt < 5_000) { if ((await readHealthStatus(url)) !== 200) { return; } await new Promise((resolve) => setTimeout(resolve, 50)); } throw new Error(`Expected ${url} health endpoint to go down`); } async function writeFakeMcpHttpServer(tempDir: string): Promise { const scriptDir = path.join(tempDir, 'fake-mcp'); const scriptPath = path.join(scriptDir, 'server.cjs'); await mkdir(scriptDir, { recursive: true }); await writeFile(scriptPath, FAKE_MCP_HTTP_SERVER_SOURCE, 'utf8'); return scriptPath; } async function writeFakeOpenCodeBridgeBinary(tempDir: string): Promise { const scriptDir = path.join(tempDir, 'fake-runtime'); const scriptPath = path.join(scriptDir, 'claude-multimodel-fake'); await mkdir(scriptDir, { recursive: true }); await writeFile(scriptPath, FAKE_OPENCODE_BRIDGE_BINARY_SOURCE, 'utf8'); await chmod(scriptPath, 0o755); return scriptPath; } describePosix('AgentTeamsMcpHttpServer integration', () => { let tempDir: string | null = null; let originalControlFileEnv: string | undefined; let originalMcpHttpPortEnv: string | undefined; const servers: AgentTeamsMcpHttpServer[] = []; beforeEach(async () => { tempDir = await mkdtemp(path.join(os.tmpdir(), 'agent-teams-mcp-http-integration-')); originalControlFileEnv = process.env.AGENT_TEAMS_MCP_TEST_CONTROL_FILE; originalMcpHttpPortEnv = process.env.CLAUDE_TEAM_OPENCODE_MCP_HTTP_PORT; }); afterEach(async () => { await Promise.all(servers.splice(0).map((server) => server.stop())); vi.mocked(console.warn).mockClear(); if (originalControlFileEnv === undefined) { delete process.env.AGENT_TEAMS_MCP_TEST_CONTROL_FILE; } else { process.env.AGENT_TEAMS_MCP_TEST_CONTROL_FILE = originalControlFileEnv; } if (originalMcpHttpPortEnv === undefined) { delete process.env.CLAUDE_TEAM_OPENCODE_MCP_HTTP_PORT; } else { process.env.CLAUDE_TEAM_OPENCODE_MCP_HTTP_PORT = originalMcpHttpPortEnv; } if (tempDir) { await rm(tempDir, { recursive: true, force: true }); tempDir = null; } }); function createControlledServer(input: { scriptPath: string; controlFile: string; allocatePort?: () => Promise; statePath?: string; }): AgentTeamsMcpHttpServer { const server = new AgentTeamsMcpHttpServer({ statePath: input.statePath ?? path.join(tempDir!, `mcp-http-state-${servers.length}.json`), disableOrphanCleanup: true, resolveLaunchSpec: () => Promise.resolve({ command: process.execPath, args: [input.scriptPath], }), allocatePort: input.allocatePort, }); servers.push(server); process.env.AGENT_TEAMS_MCP_TEST_CONTROL_FILE = input.controlFile; return server; } it('starts the actual Agent Teams MCP HTTP server and proves its health endpoint', async () => { const server = new AgentTeamsMcpHttpServer({ statePath: path.join(tempDir!, 'actual-mcp-http-state.json'), disableOrphanCleanup: true, }); servers.push(server); const handle = await server.ensureStarted(); expect(handle.url).toMatch(/^http:\/\/127\.0\.0\.1:\d+\/mcp$/); expect(handle.pid).toEqual(expect.any(Number)); expect(await readHealthStatus(handle.url)).toBe(200); }); it('reuses a healthy cached bridge URL after a real loopback health recheck', async () => { const scriptPath = await writeFakeMcpHttpServer(tempDir!); const controlFile = path.join(tempDir!, 'health-control.txt'); await writeFile(controlFile, 'healthy', 'utf8'); const server = createControlledServer({ scriptPath, controlFile }); const first = await server.ensureStarted(); const warnCountAfterFirstStart = vi.mocked(console.warn).mock.calls.length; const second = await server.ensureStarted(); expect(second).toEqual(first); expect(await readHealthStatus(first.url)).toBe(200); expect(vi.mocked(console.warn).mock.calls.slice(warnCountAfterFirstStart)).toEqual([]); }); it('adopts a healthy MCP HTTP child from persistent state across server managers', async () => { const scriptPath = await writeFakeMcpHttpServer(tempDir!); const controlFile = path.join(tempDir!, 'health-control.txt'); const statePath = path.join(tempDir!, 'shared-mcp-http-state.json'); await writeFile(controlFile, 'healthy', 'utf8'); const firstServer = createControlledServer({ scriptPath, controlFile, statePath }); const first = await firstServer.ensureStarted(); const secondServer = createControlledServer({ scriptPath, controlFile, statePath }); const second = await secondServer.ensureStarted(); expect(second.port).toBe(first.port); expect(second.pid).toBe(first.pid); expect(second.diagnostics).toContain(`opencode_app_mcp_adopted_state_server:${first.port}`); expect(await readHealthStatus(second.url)).toBe(200); }); it('falls back from an unknown healthy stable-port server and leaves it alive', async () => { const scriptPath = await writeFakeMcpHttpServer(tempDir!); const controlFile = path.join(tempDir!, 'health-control.txt'); await writeFile(controlFile, 'healthy', 'utf8'); let unknownServer: http.Server | null = null; let stablePort = 0; let fallbackPort = 0; for (let candidate = 43_100; candidate < 65_534; candidate += 1) { const stableCandidate = http.createServer((request, response) => { if (request.url === '/health') { response.writeHead(200, { 'content-type': 'text/plain' }); response.end('unknown-ok'); return; } response.writeHead(404); response.end('not found'); }); const fallbackProbe = net.createServer(); try { await new Promise((resolve, reject) => { stableCandidate.once('error', reject); stableCandidate.listen(candidate, '127.0.0.1', () => resolve()); }); await new Promise((resolve, reject) => { fallbackProbe.once('error', reject); fallbackProbe.listen(candidate + 1, '127.0.0.1', () => resolve()); }); await new Promise((resolve) => fallbackProbe.close(() => resolve())); unknownServer = stableCandidate; stablePort = candidate; fallbackPort = candidate + 1; break; } catch { await new Promise((resolve) => fallbackProbe.close(() => resolve())).catch( () => undefined ); await new Promise((resolve) => stableCandidate.close(() => resolve())).catch( () => undefined ); } } if (!unknownServer) { throw new Error('Failed to reserve contiguous ports for unknown stable-port test'); } process.env.CLAUDE_TEAM_OPENCODE_MCP_HTTP_PORT = String(stablePort); const server = createControlledServer({ scriptPath, controlFile }); try { const handle = await server.ensureStarted(); expect(handle.port).toBe(fallbackPort); expect(handle.diagnostics).toContain(`opencode_app_mcp_port_occupied_unknown:${stablePort}`); expect(handle.diagnostics).toContain( `opencode_app_mcp_preferred_port_unavailable:${stablePort}` ); expect(await readHealthStatus(`http://127.0.0.1:${stablePort}/mcp`)).toBe(200); expect(await readHealthStatus(handle.url)).toBe(200); } finally { await new Promise((resolve) => unknownServer?.close(() => resolve())); vi.mocked(console.warn).mockClear(); } }); it('restarts a stale but still-running MCP HTTP child when cached URL health turns unhealthy', async () => { const scriptPath = await writeFakeMcpHttpServer(tempDir!); const controlFile = path.join(tempDir!, 'health-control.txt'); const usedPorts = new Set(); await writeFile(controlFile, 'healthy', 'utf8'); const server = createControlledServer({ scriptPath, controlFile, allocatePort: () => allocateLoopbackPort(usedPorts), }); const first = await server.ensureStarted(); expect(first.pid).toEqual(expect.any(Number)); await writeFile(controlFile, `unhealthy-pid:${first.pid}`, 'utf8'); const second = await server.ensureStarted(); expect(second.port).toBe(first.port); expect(second.url).toBe(first.url); expect(second.pid).not.toBe(first.pid); expect(await readHealthStatus(second.url)).toBe(200); expect(vi.mocked(console.warn).mock.calls[0]?.join(' ')).toContain('failed health reuse check'); vi.mocked(console.warn).mockClear(); }); it('recovers when the cached MCP HTTP child dies and the old URL refuses connections', async () => { const scriptPath = await writeFakeMcpHttpServer(tempDir!); const controlFile = path.join(tempDir!, 'health-control.txt'); const usedPorts = new Set(); await writeFile(controlFile, 'healthy', 'utf8'); const server = createControlledServer({ scriptPath, controlFile, allocatePort: () => allocateLoopbackPort(usedPorts), }); const first = await server.ensureStarted(); expect(first.pid).toEqual(expect.any(Number)); process.kill(first.pid!, 'SIGTERM'); await waitForHealthDown(first.url); const second = await server.ensureStarted(); expect(second.port).not.toBe(first.port); expect(second.pid).not.toBe(first.pid); expect(await readHealthStatus(second.url)).toBe(200); }); it('passes a refreshed healthy MCP URL into a real bridge child process after cached health goes stale', async () => { const scriptPath = await writeFakeMcpHttpServer(tempDir!); const bridgeBinaryPath = await writeFakeOpenCodeBridgeBinary(tempDir!); const controlFile = path.join(tempDir!, 'health-control.txt'); const usedPorts = new Set(); await writeFile(controlFile, 'healthy', 'utf8'); const server = createControlledServer({ scriptPath, controlFile, allocatePort: () => allocateLoopbackPort(usedPorts), }); const bridgeEnv: NodeJS.ProcessEnv = { PATH: process.env.PATH, }; let requestIdCounter = 0; const client = new OpenCodeBridgeCommandClient({ binaryPath: bridgeBinaryPath, tempDirectory: path.join(tempDir!, 'bridge-input'), env: bridgeEnv, envProvider: async () => { const mcpHttpServer = await server.ensureStarted(); bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL = mcpHttpServer.url; return { ...bridgeEnv, CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL: mcpHttpServer.url, }; }, requestIdFactory: () => { requestIdCounter += 1; return `req-refresh-${requestIdCounter}`; }, }); const firstResult = await client.execute<{ runId: string }, { observedMcpUrl: string }>( 'opencode.launchTeam', { runId: 'run-1' }, { cwd: tempDir!, timeoutMs: 5_000, } ); expect(firstResult.ok).toBe(true); if (!firstResult.ok) { throw new Error(firstResult.error.message); } const firstHandle = server.getCurrentHandle(); expect(firstHandle?.pid).toEqual(expect.any(Number)); await writeFile( controlFile, `unhealthy-pid:${firstHandle?.pid}`, 'utf8' ); const secondResult = await client.execute<{ runId: string }, { observedMcpUrl: string }>( 'opencode.launchTeam', { runId: 'run-2' }, { cwd: tempDir!, timeoutMs: 5_000, } ); expect(secondResult.ok).toBe(true); if (!secondResult.ok) { throw new Error(secondResult.error.message); } expect(secondResult.data.observedMcpUrl).toBe(firstResult.data.observedMcpUrl); expect(await readHealthStatus(secondResult.data.observedMcpUrl)).toBe(200); }); it('fails closed when a bridge child receives an unreachable MCP URL without env refresh', async () => { const scriptPath = await writeFakeMcpHttpServer(tempDir!); const bridgeBinaryPath = await writeFakeOpenCodeBridgeBinary(tempDir!); const controlFile = path.join(tempDir!, 'health-control.txt'); await writeFile(controlFile, 'healthy', 'utf8'); const server = createControlledServer({ scriptPath, controlFile }); const first = await server.ensureStarted(); await server.stop(); await waitForHealthDown(first.url); const client = new OpenCodeBridgeCommandClient({ binaryPath: bridgeBinaryPath, tempDirectory: path.join(tempDir!, 'bridge-input-stale'), env: { PATH: process.env.PATH, CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL: first.url, }, requestIdFactory: () => 'req-stale-mcp', }); const result = await client.execute<{ runId: string }, { observedMcpUrl: string }>( 'opencode.launchTeam', { runId: 'run-stale' }, { cwd: tempDir!, timeoutMs: 5_000, } ); expect(result.ok).toBe(false); if (result.ok) { throw new Error('Expected stale MCP URL to fail'); } expect(result.error.kind).toBe('provider_error'); expect(result.error.details?.stderr).toContain('mcp_unreachable'); expect(result.error.details?.stderr).toContain(first.url); }); });