import { describe, expect, it } from "vitest"; import { encode } from "./framing.js"; import type { SpawnedProcess, SpawnProcess } from "./transport.js"; import { createStdioTransport } from "./transport.js"; /** * In-memory pipe pair: simulates a child process. `emitStdout` pushes framed * bytes the server "wrote" to stdout (which we read); `writtenToStdin` captures * what we wrote to the child's stdin (our outgoing framed messages). */ function makePipe(): { process: SpawnedProcess; emitStdout: (data: Uint8Array) => void; emitEnd: () => void; writtenToStdin: () => Uint8Array[]; killed: () => boolean; } { const dataListeners: Array<(data: Uint8Array) => void> = []; const endListeners: Array<() => void> = []; const stdinWrites: Uint8Array[] = []; let killed = false; const process: SpawnedProcess = { stdin: { write: (bytes: Uint8Array) => { stdinWrites.push(bytes); }, }, stdout: { on: (event: string, cb: (data: Uint8Array) => void) => { if (event === "data") dataListeners.push(cb); else if (event === "end") endListeners.push(cb as unknown as () => void); }, }, pid: 12345, kill: () => { killed = true; }, }; return { process, emitStdout: (data: Uint8Array) => { for (const cb of dataListeners) cb(data); }, emitEnd: () => { for (const cb of endListeners) cb(); }, writtenToStdin: () => stdinWrites, killed: () => killed, }; } describe("createStdioTransport", () => { it("creates connection with correct pid", () => { const pair = makePipe(); const spawn: SpawnProcess = () => pair.process; const { connection } = createStdioTransport({ spawn, command: ["test-server"] }, "/tmp"); expect(connection.pid).toBe(12345); connection.close(); }); it("connection sends framed messages via stdin", () => { const pair = makePipe(); const spawn: SpawnProcess = () => pair.process; const { connection } = createStdioTransport({ spawn, command: ["test"] }, "/tmp"); connection.notify("test/method", { key: "value" }); const writes = pair.writtenToStdin(); expect(writes.length).toBe(1); const text = new TextDecoder().decode(writes[0]); expect(text).toContain("Content-Length:"); expect(text).toContain('"method":"test/method"'); connection.close(); }); it("close kills the child process", () => { const pair = makePipe(); const spawn: SpawnProcess = () => pair.process; const { connection } = createStdioTransport({ spawn, command: ["test"] }, "/tmp"); connection.close(); expect(pair.killed()).toBe(true); }); it("pipes stdout through framing: a notification triggers onNotification", async () => { const pair = makePipe(); const spawn: SpawnProcess = () => pair.process; const { connection } = createStdioTransport({ spawn, command: ["test"] }, "/tmp"); let received: unknown = null; connection.onNotification("notifications/tools/list_changed", (params) => { received = params; }); // Simulate the server writing a framed notification to stdout. const notification = JSON.stringify({ jsonrpc: "2.0", method: "notifications/tools/list_changed", params: { reason: "tools added" }, }); pair.emitStdout(encode(notification)); // onNotification is invoked synchronously inside the data handler. expect(received).toEqual({ reason: "tools added" }); connection.close(); }); it("pipes stdout through framing: a response resolves a request", async () => { const pair = makePipe(); const spawn: SpawnProcess = () => pair.process; const { connection } = createStdioTransport({ spawn, command: ["test"] }, "/tmp"); const resultPromise = connection.send("tools/list"); // The request was framed and written to stdin; respond via stdout. const response = JSON.stringify({ jsonrpc: "2.0", id: 1, result: { tools: [{ name: "t", description: "d", inputSchema: { type: "object" } }] }, }); pair.emitStdout(encode(response)); const result = await resultPromise; expect(result).toEqual({ tools: [{ name: "t", description: "d", inputSchema: { type: "object" } }], }); connection.close(); }); it("handles a frame split across two stdout chunks", async () => { const pair = makePipe(); const spawn: SpawnProcess = () => pair.process; const { connection } = createStdioTransport({ spawn, command: ["test"] }, "/tmp"); const resultPromise = connection.send("ping"); const response = encode(JSON.stringify({ jsonrpc: "2.0", id: 1, result: { ok: true } })); const mid = Math.floor(response.length / 2); pair.emitStdout(response.slice(0, mid)); pair.emitStdout(response.slice(mid)); await expect(resultPromise).resolves.toEqual({ ok: true }); connection.close(); }); });