test: mock bundle MCP materialization boundary

This commit is contained in:
Peter Steinberger
2026-04-18 22:59:34 +01:00
parent d9b05e601e
commit 3ea27c63e2
3 changed files with 97 additions and 121 deletions

View File

@@ -129,9 +129,15 @@ export async function createBundleMcpToolRuntime(params: {
workspaceDir: string;
cfg?: OpenClawConfig;
reservedToolNames?: Iterable<string>;
createRuntime?: (params: {
sessionId: string;
workspaceDir: string;
cfg?: OpenClawConfig;
}) => SessionMcpRuntime;
}): Promise<BundleMcpToolRuntime> {
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,

View File

@@ -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<void> {
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 })));
}

View File

@@ -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<Parameters<typeof createBundleMcpToolRuntime>[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([