Files
openclaw/test/e2e/qa-lab/runtime/gateway-node-mcp.test-support.test.ts
Peter Steinberger 54ebb307cd fix(mcp): retire terminal transports and process trees (#126101)
Close terminal SSE and stateful notification streams through the owning lifecycle, and reap stdio/QA process groups before exact authority is discarded.\n\nRefs #126098, #126099, #126100.
2026-08-18 18:10:45 -07:00

85 lines
3.2 KiB
TypeScript

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { useAutoCleanupTempDirTracker } from "../../../helpers/temp-dir.js";
import {
createChildEnv,
parseNodeMcpTextRecord,
processIsAlive,
startHttpFixture,
stopChild,
} from "./gateway-node-mcp.test-support.js";
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
describe("gateway node MCP fixture ownership", () => {
it.each([
["direct", (payload: object) => payload],
["node.invoke", (payload: object) => ({ ok: true, payload })],
])("parses %s MCP text records", (_label, wrap) => {
const fact = { label: "node-stdio", marker: "ready", pid: 42 };
expect(
parseNodeMcpTextRecord(wrap({ content: [{ type: "text", text: JSON.stringify(fact) }] })),
).toEqual(fact);
});
it("kills a spawned fixture when readiness validation fails", async () => {
const root = tempDirs.make("mcp-fixture-startup-failure-");
const fixturePath = path.join(root, "invalid-fixture.mjs");
const pidPath = path.join(root, "fixture.pid");
await fs.writeFile(
fixturePath,
`import fs from "node:fs"; fs.writeFileSync(${JSON.stringify(pidPath)}, String(process.pid)); console.log(JSON.stringify({type:"wrong"})); setInterval(() => {}, 1000);`,
"utf8",
);
await expect(
startHttpFixture({
fixturePath,
labelPrefix: "node",
env: createChildEnv({ home: root, tempDir: os.tmpdir() }),
}),
).rejects.toThrow("invalid readiness");
const pid = Number(await fs.readFile(pidPath, "utf8"));
try {
await vi.waitFor(() => expect(processIsAlive(pid)).toBe(false), { timeout: 1_000 });
} finally {
if (processIsAlive(pid)) {
process.kill(pid, "SIGKILL");
}
}
expect(processIsAlive(pid)).toBe(false);
});
it("kills task-owned fixture descendants when stopping the captured root", async () => {
const root = tempDirs.make("mcp-fixture-descendant-cleanup-");
const fixturePath = path.join(root, "fixture.mjs");
const descendantPidPath = path.join(root, "descendant.pid");
await fs.writeFile(
fixturePath,
`import {spawn} from "node:child_process"; import fs from "node:fs"; const child=spawn(process.execPath,["-e","setInterval(()=>{},1000)"],{stdio:"ignore"}); fs.writeFileSync(${JSON.stringify(descendantPidPath)},String(child.pid)); console.log(JSON.stringify({type:"openclaw-mcp-parity-ready",urls:{streamableHttp:"http://127.0.0.1/mcp",sse:"http://127.0.0.1/sse"}})); setInterval(()=>{},1000);`,
"utf8",
);
const fixture = await startHttpFixture({
fixturePath,
labelPrefix: "node",
env: createChildEnv({ home: root, tempDir: os.tmpdir() }),
});
const descendantPid = Number(await fs.readFile(descendantPidPath, "utf8"));
try {
expect(processIsAlive(descendantPid)).toBe(true);
await stopChild(fixture);
await vi.waitFor(() => expect(processIsAlive(descendantPid)).toBe(false), { timeout: 1_000 });
} finally {
await stopChild(fixture);
if (processIsAlive(descendantPid)) {
process.kill(descendantPid, "SIGKILL");
}
}
});
});