diff --git a/scripts/e2e/mcp-channels-harness.ts b/scripts/e2e/mcp-channels-harness.ts index a1c5a049ad83..3d50f7745625 100644 --- a/scripts/e2e/mcp-channels-harness.ts +++ b/scripts/e2e/mcp-channels-harness.ts @@ -1,20 +1,21 @@ // Shared MCP-channel Docker E2E harness helpers. -// The mounted test harness imports packaged dist modules so bridge assertions run -// against the OpenClaw npm tarball installed in the functional image. +// Gateway connection setup loads the packaged protocol module lazily so Docker +// assertions run against the OpenClaw npm tarball installed in the functional image. import process from "node:process"; import { setTimeout as delay } from "node:timers/promises"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { z } from "zod"; -import { PROTOCOL_VERSION } from "../../dist/gateway/protocol/index.js"; -import { formatErrorMessage } from "../../dist/infra/errors.js"; -import { readStringValue } from "../../dist/normalization-core/string-coerce.js"; import { createGatewayWsClient, type GatewayEventFrame } from "../lib/gateway-ws-client.ts"; import { resolveGatewaySuccessPayload } from "./lib/gateway-frame-payload.mjs"; import { readMcpChannelLimits } from "./mcp-channel-limits.ts"; import { createMcpClientTempState, type McpClientTempState } from "./mcp-client-temp-state.ts"; import { connectMcpWithTimeout } from "./mcp-connect-timeout.ts"; +type GatewayProtocolModule = { + PROTOCOL_VERSION: number; +}; + export const ClaudeChannelNotificationSchema = z.object({ method: z.literal("notifications/claude/channel"), params: z.object({ @@ -54,6 +55,7 @@ const MCP_CHANNEL_LIMITS = readMcpChannelLimits(); const MCP_CONNECT_TIMEOUT_MS = MCP_CHANNEL_LIMITS.connectTimeoutMs; const GATEWAY_EVENT_RETAIN_LIMIT = MCP_CHANNEL_LIMITS.gatewayEventRetainLimit; const MCP_RAW_MESSAGE_RETAIN_LIMIT = MCP_CHANNEL_LIMITS.rawMessageRetainLimit; +let gatewayProtocolModule: Promise | undefined; export function assert(condition: unknown, message: string): asserts condition { if (!condition) { @@ -68,6 +70,52 @@ function pushBounded(items: T[], item: T, limit: number): void { } } +async function loadGatewayProtocol(): Promise { + gatewayProtocolModule ??= import("../../dist/gateway/protocol/index.js"); + return await gatewayProtocolModule; +} + +function readStringValue(value: unknown): string | undefined { + return typeof value === "string" ? value : undefined; +} + +function formatErrorMessage(error: unknown): string { + if (error instanceof Error) { + let message = error.message || error.name || "Error"; + let cause = error.cause; + const seen = new Set([error]); + const seenMessages = new Set([message]); + while (cause && !seen.has(cause)) { + seen.add(cause); + if (cause instanceof Error) { + const causeMessage = cause.message || cause.name; + if (causeMessage && !seenMessages.has(causeMessage)) { + message += ` | ${causeMessage}`; + seenMessages.add(causeMessage); + } + cause = cause.cause; + continue; + } + if (typeof cause === "string" && !seenMessages.has(cause)) { + message += ` | ${cause}`; + } + break; + } + return message; + } + if (typeof error === "string") { + return error; + } + if (typeof error === "number" || typeof error === "boolean" || typeof error === "bigint") { + return String(error); + } + try { + return JSON.stringify(error) ?? ""; + } catch { + return Object.prototype.toString.call(error); + } +} + export function extractTextFromGatewayPayload( payload: Record | undefined, ): string | undefined { @@ -133,6 +181,7 @@ async function connectGatewayOnce(params: { url: string; token: string; }): Promise { + const { PROTOCOL_VERSION } = await loadGatewayProtocol(); const requestedScopes = ["operator.read", "operator.write", "operator.pairing", "operator.admin"]; const events: Array<{ event: string; payload: Record }> = []; const gatewayClient = createGatewayWsClient({