mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
test: speed up channel mcp tests
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user