import { afterEach, beforeAll, describe, expect, it } from 'vitest' import { execFileSync, spawn, spawnSync } from 'node:child_process' import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { dirname, join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' const here = dirname(fileURLToPath(import.meta.url)) const libRoot = resolve(here, '..', '..') // platform/lib/mcp-spawn-tee const shim = join(libRoot, 'dist', 'index.js') const tsc = resolve(libRoot, '..', '..', 'node_modules', '.bin', 'tsc') // Build the real artifact once; tests exercise the shipped shim, not a // re-implementation — that is what catches stdio-wiring regressions. beforeAll(() => { execFileSync(tsc, ['-p', 'tsconfig.json'], { cwd: libRoot, stdio: 'inherit' }) expect(existsSync(shim)).toBe(true) }) let tmp = '' afterEach(() => { if (tmp) { rmSync(tmp, { recursive: true, force: true }); tmp = '' } }) function makeStub(body: string): { stub: string; logDir: string } { tmp = mkdtempSync(join(tmpdir(), 'spawn-tee-')) const stub = join(tmp, 'stub-entry.js') writeFileSync(stub, body, 'utf8') return { stub, logDir: join(tmp, 'logs') } } function runSync(stub: string, env: Record) { return spawnSync(process.execPath, [shim, stub], { env: { ...process.env, SESSION_ID: '', PLATFORM_PORT: '', ...env }, encoding: 'utf8', }) } function sessionLog(logDir: string, server: string, sessionId: string): string { return readFileSync(join(logDir, `mcp-${server}-${sessionId}.log`), 'utf8') } describe('mcp-spawn-tee in-process shim', () => { it('runs the entry in the shim process — one node runtime, not two', () => { const { stub, logDir } = makeStub('process.stdout.write(String(process.pid)); process.exit(0)') const r = runSync(stub, { LOG_DIR: logDir, SESSION_ID: 'pid1', MCP_SPAWN_TEE_NAME: 'stub' }) // The entry's process.pid equals the spawned shim's pid: same process. expect(r.stdout).toBe(String(r.pid)) }) it('tees entry stderr to a per-session file and emits one op=exit on clean exit', () => { const { stub, logDir } = makeStub('process.stderr.write("hello-from-entry\\n"); process.exit(0)') runSync(stub, { LOG_DIR: logDir, SESSION_ID: 'abcd1234-session', MCP_SPAWN_TEE_NAME: 'stub' }) const log = sessionLog(logDir, 'stub', 'abcd1234-session') expect(log).toContain('hello-from-entry') const exits = log.split('\n').filter((l) => l.includes('op=exit')) expect(exits).toHaveLength(1) expect(exits[0]).toContain('code=0') expect(exits[0]).toMatch(/lifetimeMs=\d+/) expect(exits[0]).toContain('stderr-tail=') expect(exits[0]).toContain('hello-from-entry') }) it('emits op=spawn at start and op=boot on first entry stderr', () => { const { stub, logDir } = makeStub('process.stderr.write("[stub] boot line\\n"); process.exit(0)') runSync(stub, { LOG_DIR: logDir, SESSION_ID: 'sess-0001', MCP_SPAWN_TEE_NAME: 'stub' }) const log = sessionLog(logDir, 'stub', 'sess-0001') expect(log).toMatch(/op=spawn .*server=stub pid=\d+ entry=/) expect(log).toContain('op=boot') expect(log).toContain('head="[stub] boot line"') }) it('op=exit carries a non-zero exit code', () => { const { stub, logDir } = makeStub('process.stderr.write("dying\\n"); process.exit(3)') const r = runSync(stub, { LOG_DIR: logDir, SESSION_ID: 's3', MCP_SPAWN_TEE_NAME: 'stub' }) expect(r.status).toBe(3) const exit = sessionLog(logDir, 'stub', 's3').split('\n').find((l) => l.includes('op=exit'))! expect(exit).toContain('code=3') expect(exit).toContain('signal=—') }) it('op=exit carries the signal name when terminated by a catchable signal', async () => { const { stub, logDir } = makeStub('process.stderr.write("alive\\n"); setInterval(() => {}, 1000)') const cp = spawn(process.execPath, [shim, stub], { env: { ...process.env, SESSION_ID: 'sig1', PLATFORM_PORT: '', LOG_DIR: logDir, MCP_SPAWN_TEE_NAME: 'stub' }, }) await new Promise((res) => setTimeout(res, 500)) cp.kill('SIGTERM') const exitCode = await new Promise((res) => cp.on('exit', (code) => res(code))) const exit = sessionLog(logDir, 'stub', 'sig1').split('\n').find((l) => l.includes('op=exit'))! expect(exit).toContain('signal=SIGTERM') // The shim propagates a catchable-signal kill as 128+signum (SIGTERM=15 → 143). expect(exitCode).toBe(143) }) it('defers to an entry-owned signal handler: a graceful exit reads code=0 signal=—, not a signal death', async () => { // The entry installs its own SIGINT handler that exits cleanly — as the real // MCP entries do (closeDriver then process.exit(0)). The shim must NOT stamp // signal=SIGINT on that clean exit; it defers and the entry's exit wins. const { stub, logDir } = makeStub( 'process.on("SIGINT", () => { process.stderr.write("graceful\\n"); process.exit(0) }); ' + 'process.stderr.write("ready\\n"); setInterval(() => {}, 1000)', ) const cp = spawn(process.execPath, [shim, stub], { env: { ...process.env, SESSION_ID: 'sigint-own', PLATFORM_PORT: '', LOG_DIR: logDir, MCP_SPAWN_TEE_NAME: 'stub' }, }) await new Promise((res) => setTimeout(res, 500)) cp.kill('SIGINT') const exitCode = await new Promise((res) => cp.on('exit', (code) => res(code))) const exit = sessionLog(logDir, 'stub', 'sigint-own').split('\n').find((l) => l.includes('op=exit'))! expect(exit).toContain('code=0') expect(exit).toContain('signal=—') expect(exitCode).toBe(0) }) it('leaves stderr on disk but writes no op=exit when SIGKILLed (accepted gap)', async () => { const { stub, logDir } = makeStub('process.stderr.write("pre-kill stderr\\n"); setInterval(() => {}, 1000)') const cp = spawn(process.execPath, [shim, stub], { env: { ...process.env, SESSION_ID: 'kill1', PLATFORM_PORT: '', LOG_DIR: logDir, MCP_SPAWN_TEE_NAME: 'stub' }, }) await new Promise((res) => setTimeout(res, 500)) cp.kill('SIGKILL') await new Promise((res) => cp.on('exit', () => res())) const log = sessionLog(logDir, 'stub', 'kill1') expect(log).toContain('pre-kill stderr') expect(log).not.toContain('op=exit') }) it('passes entry stdout through untouched (MCP transport intact)', () => { const { stub, logDir } = makeStub('process.stdout.write("JSONRPC-PAYLOAD"); process.exit(0)') const r = runSync(stub, { LOG_DIR: logDir, SESSION_ID: 'out1', MCP_SPAWN_TEE_NAME: 'stub' }) expect(r.stdout).toBe('JSONRPC-PAYLOAD') }) it('writes the nosession sink when SESSION_ID is absent', () => { const { stub, logDir } = makeStub('process.stderr.write("enum-boot\\n"); process.exit(0)') runSync(stub, { LOG_DIR: logDir, MCP_SPAWN_TEE_NAME: 'stub' }) // SESSION_ID forced empty by runSync expect(readdirSync(logDir)).toContain('mcp-stub-nosession.log') expect(readFileSync(join(logDir, 'mcp-stub-nosession.log'), 'utf8')).toContain('enum-boot') }) it('writes no date-named sink for any spawn', () => { const { stub, logDir } = makeStub('process.stderr.write("x\\n"); process.exit(0)') runSync(stub, { LOG_DIR: logDir, SESSION_ID: 'sess-nodate', MCP_SPAWN_TEE_NAME: 'stub' }) expect(readdirSync(logDir).some((f) => /stderr-\d{4}-\d{2}-\d{2}\.log$/.test(f))).toBe(false) }) it('does not crash when PLATFORM_PORT is unreachable', () => { const { stub, logDir } = makeStub('process.stderr.write("x\\n"); process.exit(0)') const r = runSync(stub, { LOG_DIR: logDir, SESSION_ID: 'p1', MCP_SPAWN_TEE_NAME: 'stub', PLATFORM_PORT: '59999' }) expect(r.status).toBe(0) expect(sessionLog(logDir, 'stub', 'p1')).toContain('op=exit') }) })