Skip to content

Commit ba722fd

Browse files
committed
test: speed up channel mcp tests
1 parent 8260b64 commit ba722fd

4 files changed

Lines changed: 50 additions & 29 deletions

File tree

src/mcp/channel-bridge.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import { randomUUID } from "node:crypto";
22
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
33
import type { OpenClawConfig } from "../config/types.openclaw.js";
4-
import { resolveGatewayClientBootstrap } from "../gateway/client-bootstrap.js";
5-
import { GatewayClient, GatewayClientRequestError } from "../gateway/client.js";
6-
import { APPROVALS_SCOPE, READ_SCOPE, WRITE_SCOPE } from "../gateway/method-scopes.js";
7-
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../gateway/protocol/client-info.js";
4+
import type { GatewayClient } from "../gateway/client.js";
85
import type { EventFrame } from "../gateway/protocol/index.js";
96
import { extractFirstTextBlock } from "../shared/chat-message-content.js";
107
import {
@@ -88,6 +85,17 @@ export class OpenClawChannelBridge {
8885
return;
8986
}
9087
this.started = true;
88+
const [
89+
{ resolveGatewayClientBootstrap },
90+
{ GatewayClient: GatewayClientCtor },
91+
{ APPROVALS_SCOPE, READ_SCOPE, WRITE_SCOPE },
92+
{ GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES },
93+
] = await Promise.all([
94+
import("../gateway/client-bootstrap.js"),
95+
import("../gateway/client.js"),
96+
import("../gateway/method-scopes.js"),
97+
import("../gateway/protocol/client-info.js"),
98+
]);
9199
const bootstrap = await resolveGatewayClientBootstrap({
92100
config: this.cfg,
93101
gatewayUrl: this.params.gatewayUrl,
@@ -102,7 +110,7 @@ export class OpenClawChannelBridge {
102110
return;
103111
}
104112

105-
this.gateway = new GatewayClient({
113+
this.gateway = new GatewayClientCtor({
106114
url: bootstrap.url,
107115
token: bootstrap.auth.token,
108116
password: bootstrap.auth.password,
@@ -525,7 +533,11 @@ export class OpenClawChannelBridge {
525533
}
526534

527535
export function shouldRetryInitialMcpGatewayConnect(error: Error): boolean {
528-
if (error instanceof GatewayClientRequestError) {
536+
if (
537+
error.name === "GatewayClientRequestError" &&
538+
"retryable" in error &&
539+
typeof error.retryable === "boolean"
540+
) {
529541
return error.retryable;
530542
}
531543
const message = error.message.toLowerCase();

src/mcp/channel-server.shutdown-unhandled-rejection.test.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,16 @@ vi.mock("./channel-tools.js", () => ({
7979
registerChannelMcpTools: vi.fn(),
8080
}));
8181

82+
async function waitForTransport(): Promise<{ onclose?: (() => void) | undefined }> {
83+
for (let attempt = 0; attempt < 20; attempt += 1) {
84+
if (transportState.lastTransport) {
85+
return transportState.lastTransport;
86+
}
87+
await new Promise((resolve) => setTimeout(resolve, 0));
88+
}
89+
throw new Error("MCP stdio transport was not created");
90+
}
91+
8292
describe("serveOpenClawChannelMcp shutdown", () => {
8393
const unhandledRejections: unknown[] = [];
8494
const onUnhandledRejection = (reason: unknown) => {
@@ -102,9 +112,9 @@ describe("serveOpenClawChannelMcp shutdown", () => {
102112
const { serveOpenClawChannelMcp } = await import("./channel-server.js");
103113

104114
const servePromise = serveOpenClawChannelMcp({ verbose: false });
105-
await Promise.resolve();
115+
const transport = await waitForTransport();
106116

107-
transportState.lastTransport?.onclose?.();
117+
transport.onclose?.();
108118
await servePromise;
109119
await new Promise((resolve) => setTimeout(resolve, 0));
110120

src/mcp/channel-server.test.ts

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
22
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
33
import { describe, expect, test, vi } from "vitest";
44
import { z } from "zod";
5-
import { GatewayClientRequestError } from "../gateway/client.js";
65
import { shouldRetryInitialMcpGatewayConnect } from "./channel-bridge.js";
76
import { createOpenClawChannelMcpServer, OpenClawChannelBridge } from "./channel-server.js";
87
import { extractAttachmentsFromMessage } from "./channel-shared.js";
@@ -26,6 +25,7 @@ const ClaudePermissionNotificationSchema = z.object({
2625
async function connectMcpWithoutGateway(params?: { claudeChannelMode?: "auto" | "on" | "off" }) {
2726
const serverHarness = await createOpenClawChannelMcpServer({
2827
claudeChannelMode: params?.claudeChannelMode ?? "auto",
28+
config: {} as never,
2929
verbose: false,
3030
});
3131
const client = new Client({ name: "mcp-test-client", version: "1.0.0" });
@@ -74,29 +74,20 @@ async function flushMcpNotifications() {
7474
await Promise.resolve();
7575
}
7676

77+
function gatewayRequestError(retryable: boolean): Error {
78+
return Object.assign(new Error(retryable ? "gateway busy" : "auth failed"), {
79+
name: "GatewayClientRequestError",
80+
retryable,
81+
});
82+
}
83+
7784
describe("openclaw channel mcp server", () => {
7885
test("keeps initial MCP gateway connection alive through transient connect errors", () => {
7986
expect(
8087
shouldRetryInitialMcpGatewayConnect(new Error("gateway request timeout for connect")),
8188
).toBe(true);
82-
expect(
83-
shouldRetryInitialMcpGatewayConnect(
84-
new GatewayClientRequestError({
85-
code: "BUSY",
86-
message: "gateway busy",
87-
retryable: true,
88-
}),
89-
),
90-
).toBe(true);
91-
expect(
92-
shouldRetryInitialMcpGatewayConnect(
93-
new GatewayClientRequestError({
94-
code: "UNAUTHORIZED",
95-
message: "auth failed",
96-
retryable: false,
97-
}),
98-
),
99-
).toBe(false);
89+
expect(shouldRetryInitialMcpGatewayConnect(gatewayRequestError(true))).toBe(true);
90+
expect(shouldRetryInitialMcpGatewayConnect(gatewayRequestError(false))).toBe(false);
10091
});
10192

10293
describe("gateway-backed flows", () => {

src/mcp/channel-server.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3-
import { getRuntimeConfig, type OpenClawConfig } from "../config/config.js";
3+
import type { OpenClawConfig } from "../config/types.openclaw.js";
44
import { VERSION } from "../version.js";
55
import { OpenClawChannelBridge } from "./channel-bridge.js";
66
import { ClaudePermissionRequestSchema, type ClaudeChannelMode } from "./channel-shared.js";
@@ -17,13 +17,21 @@ export type OpenClawMcpServeOptions = {
1717
verbose?: boolean;
1818
};
1919

20+
async function resolveMcpConfig(config: OpenClawConfig | undefined): Promise<OpenClawConfig> {
21+
if (config) {
22+
return config;
23+
}
24+
const { getRuntimeConfig } = await import("../config/config.js");
25+
return getRuntimeConfig();
26+
}
27+
2028
export async function createOpenClawChannelMcpServer(opts: OpenClawMcpServeOptions = {}): Promise<{
2129
server: McpServer;
2230
bridge: OpenClawChannelBridge;
2331
start: () => Promise<void>;
2432
close: () => Promise<void>;
2533
}> {
26-
const cfg = opts.config ?? getRuntimeConfig();
34+
const cfg = await resolveMcpConfig(opts.config);
2735
const claudeChannelMode = opts.claudeChannelMode ?? "auto";
2836
const capabilities = getChannelMcpCapabilities(claudeChannelMode);
2937
const server = new McpServer(

0 commit comments

Comments
 (0)