From 3ea27c63e2fcafb5a4fe185077a3ea38a33e01f5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 18 Apr 2026 22:59:34 +0100 Subject: [PATCH] test: mock bundle MCP materialization boundary --- src/agents/pi-bundle-mcp-materialize.ts | 10 +- src/agents/pi-bundle-mcp-test-harness.ts | 2 +- .../pi-bundle-mcp-tools.materialize.test.ts | 206 ++++++++---------- 3 files changed, 97 insertions(+), 121 deletions(-) diff --git a/src/agents/pi-bundle-mcp-materialize.ts b/src/agents/pi-bundle-mcp-materialize.ts index ab381ef1ca3..4cc408a2c8d 100644 --- a/src/agents/pi-bundle-mcp-materialize.ts +++ b/src/agents/pi-bundle-mcp-materialize.ts @@ -129,9 +129,15 @@ export async function createBundleMcpToolRuntime(params: { workspaceDir: string; cfg?: OpenClawConfig; reservedToolNames?: Iterable; + createRuntime?: (params: { + sessionId: string; + workspaceDir: string; + cfg?: OpenClawConfig; + }) => SessionMcpRuntime; }): Promise { - const { createSessionMcpRuntime } = await import("./pi-bundle-mcp-runtime.js"); - const runtime = createSessionMcpRuntime({ + const createRuntime = + params.createRuntime ?? (await import("./pi-bundle-mcp-runtime.js")).createSessionMcpRuntime; + const runtime = createRuntime({ sessionId: `bundle-mcp:${crypto.randomUUID()}`, workspaceDir: params.workspaceDir, cfg: params.cfg, diff --git a/src/agents/pi-bundle-mcp-test-harness.ts b/src/agents/pi-bundle-mcp-test-harness.ts index 77abc0e1a7c..62eca448abe 100644 --- a/src/agents/pi-bundle-mcp-test-harness.ts +++ b/src/agents/pi-bundle-mcp-test-harness.ts @@ -8,7 +8,6 @@ import { writeClaudeBundle, writeExecutable, } from "./bundle-mcp-shared.test-harness.js"; -import { __testing } from "./pi-bundle-mcp-tools.js"; const require = createRequire(import.meta.url); const SDK_SERVER_MCP_PATH = require.resolve("@modelcontextprotocol/sdk/server/mcp.js"); @@ -17,6 +16,7 @@ const SDK_SERVER_SSE_PATH = require.resolve("@modelcontextprotocol/sdk/server/ss const tempDirs: string[] = []; export async function cleanupBundleMcpHarness(): Promise { + const { __testing } = await import("./pi-bundle-mcp-tools.js"); await __testing.resetSessionMcpRuntimeManager(); await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true }))); } diff --git a/src/agents/pi-bundle-mcp-tools.materialize.test.ts b/src/agents/pi-bundle-mcp-tools.materialize.test.ts index 855167daa7d..e9bad2fd500 100644 --- a/src/agents/pi-bundle-mcp-tools.materialize.test.ts +++ b/src/agents/pi-bundle-mcp-tools.materialize.test.ts @@ -1,23 +1,29 @@ -import path from "node:path"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { describe, expect, it } from "vitest"; import { createBundleMcpToolRuntime, materializeBundleMcpToolsForRun, } from "./pi-bundle-mcp-materialize.js"; -import { - cleanupBundleMcpHarness, - makeTempDir, - startSseProbeServer, - writeBundleProbeMcpServer, -} from "./pi-bundle-mcp-test-harness.js"; import type { McpCatalogTool } from "./pi-bundle-mcp-types.js"; import type { SessionMcpRuntime } from "./pi-bundle-mcp-types.js"; -afterEach(async () => { - await cleanupBundleMcpHarness(); -}); - -function makeToolRuntime(tools?: McpCatalogTool[]): SessionMcpRuntime { +function makeToolRuntime( + params: { + tools?: McpCatalogTool[]; + serverName?: string; + resultText?: string; + } = {}, +): SessionMcpRuntime { + const serverName = params.serverName ?? "bundleProbe"; + const tools = params.tools ?? [ + { + serverName, + safeServerName: serverName, + toolName: "bundle_probe", + description: "Bundle probe", + inputSchema: { type: "object", properties: {} }, + fallbackDescription: "Bundle probe", + }, + ]; return { sessionId: "session-collision", workspaceDir: "/tmp", @@ -29,25 +35,16 @@ function makeToolRuntime(tools?: McpCatalogTool[]): SessionMcpRuntime { version: 1, generatedAt: 0, servers: { - bundleProbe: { - serverName: "bundleProbe", - launchSummary: "bundleProbe", - toolCount: 1, + [serverName]: { + serverName, + launchSummary: serverName, + toolCount: tools.length, }, }, - tools: tools ?? [ - { - serverName: "bundleProbe", - safeServerName: "bundleProbe", - toolName: "bundle_probe", - description: "Bundle probe", - inputSchema: { type: "object", properties: {} }, - fallbackDescription: "Bundle probe", - }, - ], + tools, }), callTool: async () => ({ - content: [{ type: "text", text: "FROM-BUNDLE" }], + content: [{ type: "text", text: params.resultText ?? "FROM-BUNDLE" }], isError: false, }), dispose: async () => {}, @@ -81,19 +78,18 @@ describe("createBundleMcpToolRuntime", () => { expect(runtime.tools.map((tool) => tool.name)).toEqual(["bundleProbe__bundle_probe-2"]); }); - it("loads configured stdio MCP tools without a bundle", async () => { - const workspaceDir = await makeTempDir("openclaw-bundle-mcp-tools-"); - const serverScriptPath = path.join(workspaceDir, "servers", "configured-probe.mjs"); - await writeBundleProbeMcpServer(serverScriptPath); - + it("materializes configured MCP tools through the session runtime boundary", async () => { + const created: Parameters< + NonNullable[0]["createRuntime"]> + >[0][] = []; const runtime = await createBundleMcpToolRuntime({ - workspaceDir, + workspaceDir: "/workspace", cfg: { mcp: { servers: { configuredProbe: { command: "node", - args: [serverScriptPath], + args: ["configured-probe.mjs"], env: { BUNDLE_PROBE_TEXT: "FROM-CONFIG", }, @@ -101,96 +97,70 @@ describe("createBundleMcpToolRuntime", () => { }, }, }, + createRuntime: (params) => { + created.push(params); + return makeToolRuntime({ + serverName: "configuredProbe", + resultText: "FROM-CONFIG", + }); + }, }); - try { - expect(runtime.tools.map((tool) => tool.name)).toEqual(["configuredProbe__bundle_probe"]); - const result = await runtime.tools[0].execute( - "call-configured-probe", - {}, - undefined, - undefined, - ); - expect(result.content[0]).toMatchObject({ - type: "text", - text: "FROM-CONFIG", - }); - expect(result.details).toEqual({ - mcpServer: "configuredProbe", - mcpTool: "bundle_probe", - }); - } finally { - await runtime.dispose(); - } - }); + expect(created).toHaveLength(1); + expect(created[0].sessionId).toMatch(/^bundle-mcp:/); + expect(created[0].workspaceDir).toBe("/workspace"); + expect(created[0].cfg?.mcp?.servers?.configuredProbe).toMatchObject({ + command: "node", + args: ["configured-probe.mjs"], + }); - it("loads configured SSE MCP tools via url", async () => { - vi.useRealTimers(); - const sseServer = await startSseProbeServer(); - - try { - const workspaceDir = await makeTempDir("openclaw-bundle-mcp-sse-"); - const runtime = await createBundleMcpToolRuntime({ - workspaceDir, - cfg: { - mcp: { - servers: { - sseProbe: { - url: `http://127.0.0.1:${sseServer.port}/sse`, - transport: "sse", - }, - }, - }, - }, - }); - - try { - expect(runtime.tools.map((tool) => tool.name)).toEqual(["sseProbe__sse_probe"]); - const result = await runtime.tools[0].execute("call-sse-probe", {}, undefined, undefined); - expect(result.content[0]).toMatchObject({ - type: "text", - text: "FROM-SSE", - }); - expect(result.details).toEqual({ - mcpServer: "sseProbe", - mcpTool: "sse_probe", - }); - } finally { - await runtime.dispose(); - } - } finally { - await sseServer.close(); - } + expect(runtime.tools.map((tool) => tool.name)).toEqual(["configuredProbe__bundle_probe"]); + const result = await runtime.tools[0].execute( + "call-configured-probe", + {}, + undefined, + undefined, + ); + expect(result.content[0]).toMatchObject({ + type: "text", + text: "FROM-CONFIG", + }); + expect(result.details).toEqual({ + mcpServer: "configuredProbe", + mcpTool: "bundle_probe", + }); }); it("returns tools sorted alphabetically for stable prompt-cache keys", async () => { const runtime = await materializeBundleMcpToolsForRun({ - runtime: makeToolRuntime([ - { - serverName: "multi", - safeServerName: "multi", - toolName: "zeta", - description: "z", - inputSchema: { type: "object", properties: {} }, - fallbackDescription: "z", - }, - { - serverName: "multi", - safeServerName: "multi", - toolName: "alpha", - description: "a", - inputSchema: { type: "object", properties: {} }, - fallbackDescription: "a", - }, - { - serverName: "multi", - safeServerName: "multi", - toolName: "mu", - description: "m", - inputSchema: { type: "object", properties: {} }, - fallbackDescription: "m", - }, - ]), + runtime: makeToolRuntime({ + tools: [ + { + serverName: "multi", + safeServerName: "multi", + toolName: "zeta", + description: "z", + inputSchema: { type: "object", properties: {} }, + fallbackDescription: "z", + }, + { + serverName: "multi", + safeServerName: "multi", + toolName: "alpha", + description: "a", + inputSchema: { type: "object", properties: {} }, + fallbackDescription: "a", + }, + { + serverName: "multi", + safeServerName: "multi", + toolName: "mu", + description: "m", + inputSchema: { type: "object", properties: {} }, + fallbackDescription: "m", + }, + ], + }), }); expect(runtime.tools.map((tool) => tool.name)).toEqual([