test: speed up channel mcp tests

This commit is contained in:
Peter Steinberger
2026-04-28 11:49:11 +01:00
parent 8260b64f7a
commit ba722fd126
4 changed files with 50 additions and 29 deletions

View File

@@ -1,10 +1,7 @@
import { randomUUID } from "node:crypto";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { resolveGatewayClientBootstrap } from "../gateway/client-bootstrap.js";
import { GatewayClient, GatewayClientRequestError } from "../gateway/client.js";
import { APPROVALS_SCOPE, READ_SCOPE, WRITE_SCOPE } from "../gateway/method-scopes.js";
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../gateway/protocol/client-info.js";
import type { GatewayClient } from "../gateway/client.js";
import type { EventFrame } from "../gateway/protocol/index.js";
import { extractFirstTextBlock } from "../shared/chat-message-content.js";
import {
@@ -88,6 +85,17 @@ export class OpenClawChannelBridge {
return;
}
this.started = true;
const [
{ resolveGatewayClientBootstrap },
{ GatewayClient: GatewayClientCtor },
{ APPROVALS_SCOPE, READ_SCOPE, WRITE_SCOPE },
{ GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES },
] = await Promise.all([
import("../gateway/client-bootstrap.js"),
import("../gateway/client.js"),
import("../gateway/method-scopes.js"),
import("../gateway/protocol/client-info.js"),
]);
const bootstrap = await resolveGatewayClientBootstrap({
config: this.cfg,
gatewayUrl: this.params.gatewayUrl,
@@ -102,7 +110,7 @@ export class OpenClawChannelBridge {
return;
}
this.gateway = new GatewayClient({
this.gateway = new GatewayClientCtor({
url: bootstrap.url,
token: bootstrap.auth.token,
password: bootstrap.auth.password,
@@ -525,7 +533,11 @@ export class OpenClawChannelBridge {
}
export function shouldRetryInitialMcpGatewayConnect(error: Error): boolean {
if (error instanceof GatewayClientRequestError) {
if (
error.name === "GatewayClientRequestError" &&
"retryable" in error &&
typeof error.retryable === "boolean"
) {
return error.retryable;
}
const message = error.message.toLowerCase();

View File

@@ -79,6 +79,16 @@ vi.mock("./channel-tools.js", () => ({
registerChannelMcpTools: vi.fn(),
}));
async function waitForTransport(): Promise<{ onclose?: (() => void) | undefined }> {
for (let attempt = 0; attempt < 20; attempt += 1) {
if (transportState.lastTransport) {
return transportState.lastTransport;
}
await new Promise((resolve) => setTimeout(resolve, 0));
}
throw new Error("MCP stdio transport was not created");
}
describe("serveOpenClawChannelMcp shutdown", () => {
const unhandledRejections: unknown[] = [];
const onUnhandledRejection = (reason: unknown) => {
@@ -102,9 +112,9 @@ describe("serveOpenClawChannelMcp shutdown", () => {
const { serveOpenClawChannelMcp } = await import("./channel-server.js");
const servePromise = serveOpenClawChannelMcp({ verbose: false });
await Promise.resolve();
const transport = await waitForTransport();
transportState.lastTransport?.onclose?.();
transport.onclose?.();
await servePromise;
await new Promise((resolve) => setTimeout(resolve, 0));

View File

@@ -2,7 +2,6 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { describe, expect, test, vi } from "vitest";
import { z } from "zod";
import { GatewayClientRequestError } from "../gateway/client.js";
import { shouldRetryInitialMcpGatewayConnect } from "./channel-bridge.js";
import { createOpenClawChannelMcpServer, OpenClawChannelBridge } from "./channel-server.js";
import { extractAttachmentsFromMessage } from "./channel-shared.js";
@@ -26,6 +25,7 @@ const ClaudePermissionNotificationSchema = z.object({
async function connectMcpWithoutGateway(params?: { claudeChannelMode?: "auto" | "on" | "off" }) {
const serverHarness = await createOpenClawChannelMcpServer({
claudeChannelMode: params?.claudeChannelMode ?? "auto",
config: {} as never,
verbose: false,
});
const client = new Client({ name: "mcp-test-client", version: "1.0.0" });
@@ -74,29 +74,20 @@ async function flushMcpNotifications() {
await Promise.resolve();
}
function gatewayRequestError(retryable: boolean): Error {
return Object.assign(new Error(retryable ? "gateway busy" : "auth failed"), {
name: "GatewayClientRequestError",
retryable,
});
}
describe("openclaw channel mcp server", () => {
test("keeps initial MCP gateway connection alive through transient connect errors", () => {
expect(
shouldRetryInitialMcpGatewayConnect(new Error("gateway request timeout for connect")),
).toBe(true);
expect(
shouldRetryInitialMcpGatewayConnect(
new GatewayClientRequestError({
code: "BUSY",
message: "gateway busy",
retryable: true,
}),
),
).toBe(true);
expect(
shouldRetryInitialMcpGatewayConnect(
new GatewayClientRequestError({
code: "UNAUTHORIZED",
message: "auth failed",
retryable: false,
}),
),
).toBe(false);
expect(shouldRetryInitialMcpGatewayConnect(gatewayRequestError(true))).toBe(true);
expect(shouldRetryInitialMcpGatewayConnect(gatewayRequestError(false))).toBe(false);
});
describe("gateway-backed flows", () => {

View File

@@ -1,6 +1,6 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { getRuntimeConfig, type OpenClawConfig } from "../config/config.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { VERSION } from "../version.js";
import { OpenClawChannelBridge } from "./channel-bridge.js";
import { ClaudePermissionRequestSchema, type ClaudeChannelMode } from "./channel-shared.js";
@@ -17,13 +17,21 @@ export type OpenClawMcpServeOptions = {
verbose?: boolean;
};
async function resolveMcpConfig(config: OpenClawConfig | undefined): Promise<OpenClawConfig> {
if (config) {
return config;
}
const { getRuntimeConfig } = await import("../config/config.js");
return getRuntimeConfig();
}
export async function createOpenClawChannelMcpServer(opts: OpenClawMcpServeOptions = {}): Promise<{
server: McpServer;
bridge: OpenClawChannelBridge;
start: () => Promise<void>;
close: () => Promise<void>;
}> {
const cfg = opts.config ?? getRuntimeConfig();
const cfg = await resolveMcpConfig(opts.config);
const claudeChannelMode = opts.claudeChannelMode ?? "auto";
const capabilities = getChannelMcpCapabilities(claudeChannelMode);
const server = new McpServer(