Skip to content

Commit a202dd0

Browse files
authored
test: link mcp gateway transport coverage (#97609)
1 parent 2452572 commit a202dd0

13 files changed

Lines changed: 216 additions & 349 deletions
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
title: MCP Gateway connect startup retry
2+
3+
scenario:
4+
id: mcp-gateway-connect-startup-retry
5+
surface: runtime
6+
coverage:
7+
primary:
8+
- gateway.connect-request
9+
- gateway.protocol-version-negotiation
10+
- gateway.startup-retry
11+
objective: Verify the Gateway client connect frame, protocol range, and retry behavior used by MCP channel bridge clients.
12+
successCriteria:
13+
- GatewayClient sends a `connect` request after `connect.challenge`.
14+
- The connect request advertises the current supported protocol range.
15+
- Retryable startup-unavailable connect responses do not fire terminal callbacks and schedule a reconnect.
16+
docsRefs:
17+
- docs/gateway/protocol.md
18+
- docs/cli/mcp.md
19+
- docs/concepts/qa-e2e-automation.md
20+
codeRefs:
21+
- src/gateway/client.test.ts
22+
- test/e2e/qa-lab/runtime/mcp-channels.fixture.ts
23+
- test/e2e/qa-lab/runtime/mcp-client-temp-state.fixture.ts
24+
- test/e2e/qa-lab/runtime/mcp-gateway-transport.e2e.test.ts
25+
execution:
26+
kind: vitest
27+
path: src/gateway/client.test.ts
28+
summary: Vitest coverage for Gateway connect request, protocol negotiation, and startup retry behavior used by MCP bridge clients.

scripts/e2e/cron-mcp-cleanup-docker-client.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ import path from "node:path";
77
import { setTimeout as delay } from "node:timers/promises";
88
import { pathToFileURL } from "node:url";
99
import { promisify } from "node:util";
10+
import type { GatewayRpcClient } from "../../test/e2e/qa-lab/runtime/mcp-channels.fixture.ts";
1011
import { readPositiveIntEnv } from "./lib/env-limits.mjs";
11-
import type { GatewayRpcClient } from "./mcp-channels-harness.ts";
1212

1313
const execFileAsync = promisify(execFile);
1414
const PROBE_PID_WAIT_MS = readCronMcpCleanupProbePidWaitMs();
15-
type McpChannelsHarness = typeof import("./mcp-channels-harness.ts");
15+
type McpChannelsHarness = typeof import("../../test/e2e/qa-lab/runtime/mcp-channels.fixture.ts");
1616
let mcpChannelsHarness: McpChannelsHarness | undefined;
1717

1818
type CronJob = { id?: string };
@@ -21,7 +21,7 @@ type AgentRunResult = { runId?: string; status?: string };
2121
type CronFinishedPayload = { status?: unknown };
2222

2323
async function loadMcpChannelsHarness(): Promise<McpChannelsHarness> {
24-
mcpChannelsHarness ??= await import("./mcp-channels-harness.ts");
24+
mcpChannelsHarness ??= await import("../../test/e2e/qa-lab/runtime/mcp-channels.fixture.ts");
2525
return mcpChannelsHarness;
2626
}
2727

scripts/e2e/mcp-channels-docker.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ docker_e2e_run_with_harness \
6666
tsx scripts/e2e/mcp-channels-seed.ts >/tmp/mcp-channels-seed.log
6767
gateway_pid=\"\$(openclaw_e2e_start_gateway \"\$entry\" $PORT /tmp/mcp-channels-gateway.log)\"
6868
openclaw_e2e_wait_gateway_ready \"\$gateway_pid\" /tmp/mcp-channels-gateway.log 480 $PORT
69-
tsx scripts/e2e/mcp-channels-docker-client.ts
69+
tsx test/e2e/qa-lab/runtime/mcp-channels-docker-client.ts
7070
" >"$CLIENT_LOG" 2>&1
7171
status=${PIPESTATUS[0]}
7272
set -e

scripts/e2e/mcp-connect-timeout.ts

Lines changed: 0 additions & 55 deletions
This file was deleted.

scripts/e2e/mcp-websocket-open.ts

Lines changed: 0 additions & 90 deletions
This file was deleted.

scripts/test-projects.test-support.mjs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -895,9 +895,20 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([
895895
],
896896
],
897897
[
898-
"scripts/e2e/mcp-channels-docker-client.ts",
898+
"test/e2e/qa-lab/runtime/mcp-channels-docker-client.ts",
899899
["test/scripts/docker-e2e-plan.test.ts", "test/scripts/plugin-prerelease-test-plan.test.ts"],
900900
],
901+
[
902+
"test/e2e/qa-lab/runtime/mcp-channels.fixture.ts",
903+
[
904+
"test/e2e/qa-lab/runtime/mcp-gateway-transport.e2e.test.ts",
905+
"test/scripts/cron-mcp-cleanup-docker-client.test.ts",
906+
],
907+
],
908+
[
909+
"test/e2e/qa-lab/runtime/mcp-client-temp-state.fixture.ts",
910+
["test/e2e/qa-lab/runtime/mcp-gateway-transport.e2e.test.ts"],
911+
],
901912
["scripts/e2e/mcp-channels-seed.ts", ["test/scripts/docker-e2e-seeds.test.ts"]],
902913
["scripts/e2e/docker-openai-seed.ts", ["test/scripts/docker-e2e-seeds.test.ts"]],
903914
[
@@ -925,7 +936,6 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([
925936
"scripts/e2e/lib/mcp-code-mode-probe-server.ts",
926937
["test/scripts/docker-e2e-seeds.test.ts", "test/scripts/mcp-code-mode-gateway-client.test.ts"],
927938
],
928-
["scripts/e2e/mcp-client-temp-state.ts", ["test/scripts/mcp-channels-harness.test.ts"]],
929939
[
930940
"scripts/e2e/cron-cli-docker.sh",
931941
["test/scripts/docker-build-helper.test.ts", "test/scripts/docker-e2e-observability.test.ts"],

scripts/e2e/mcp-channels-docker-client.ts renamed to test/e2e/qa-lab/runtime/mcp-channels-docker-client.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Mcp Channels Docker Client script supports OpenClaw repository automation.
1+
// MCP channels Docker client drives the QA-owned channel bridge smoke.
22
import { randomUUID } from "node:crypto";
33
import {
44
assert,
@@ -11,11 +11,11 @@ import {
1111
type GatewayRpcClient,
1212
maybeApprovePendingBridgePairing,
1313
waitFor,
14-
} from "./mcp-channels-harness.ts";
14+
} from "./mcp-channels.fixture.ts";
1515
import {
1616
connectMcpClientWithPairingReconnect,
1717
createMcpClientTempState,
18-
} from "./mcp-client-temp-state.ts";
18+
} from "./mcp-client-temp-state.fixture.ts";
1919

2020
function summarizeSessionRows(rows: Array<Record<string, unknown>> | undefined) {
2121
return (rows ?? []).map((entry) => ({

scripts/e2e/mcp-channels-harness.ts renamed to test/e2e/qa-lab/runtime/mcp-channels.fixture.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
1-
// Shared MCP-channel Docker E2E harness helpers.
1+
// Shared MCP-channel QA/Docker E2E fixture helpers.
22
// The mounted test harness imports packaged dist modules so bridge assertions run
33
// against the OpenClaw npm tarball installed in the functional image.
44
import process from "node:process";
55
import { setTimeout as delay } from "node:timers/promises";
66
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
77
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
88
import { z } from "zod";
9-
import { PROTOCOL_VERSION } from "../../dist/gateway/protocol/index.js";
10-
import { formatErrorMessage } from "../../dist/infra/errors.js";
11-
import { readStringValue } from "../../dist/normalization-core/string-coerce.js";
12-
import { createGatewayWsClient, type GatewayEventFrame } from "../lib/gateway-ws-client.ts";
13-
import { resolveGatewaySuccessPayload } from "./lib/gateway-frame-payload.mjs";
14-
import { readMcpChannelLimits } from "./mcp-channel-limits.ts";
15-
import { createMcpClientTempState, type McpClientTempState } from "./mcp-client-temp-state.ts";
16-
import { connectMcpWithTimeout } from "./mcp-connect-timeout.ts";
9+
import { PROTOCOL_VERSION } from "../../../../dist/gateway/protocol/index.js";
10+
import { formatErrorMessage } from "../../../../dist/infra/errors.js";
11+
import { readStringValue } from "../../../../dist/normalization-core/string-coerce.js";
12+
import { resolveGatewaySuccessPayload } from "../../../../scripts/e2e/lib/gateway-frame-payload.mjs";
13+
import { readMcpChannelLimits } from "../../../../scripts/e2e/mcp-channel-limits.ts";
14+
import {
15+
createGatewayWsClient,
16+
type GatewayEventFrame,
17+
} from "../../../../scripts/lib/gateway-ws-client.ts";
18+
import {
19+
connectMcpWithTimeout,
20+
createMcpClientTempState,
21+
type McpClientTempState,
22+
} from "./mcp-client-temp-state.fixture.ts";
1723

1824
export const ClaudeChannelNotificationSchema = z.object({
1925
method: z.literal("notifications/claude/channel"),

scripts/e2e/mcp-client-temp-state.ts renamed to test/e2e/qa-lab/runtime/mcp-client-temp-state.fixture.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Mcp Client Temp State script supports OpenClaw repository automation.
1+
// MCP client temp-state helpers used by QA-owned MCP E2E fixtures.
22
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
33
import { tmpdir } from "node:os";
44
import path from "node:path";
@@ -16,6 +16,12 @@ export type ReconnectableMcpClientHandle = {
1616
transport: { close: () => Promise<unknown> };
1717
};
1818

19+
type McpConnectTransport = {
20+
close?(): Promise<void> | void;
21+
};
22+
23+
const MCP_TIMEOUT_CLOSE_GRACE_MS = 5_000;
24+
1925
export function createMcpClientTempState(params: {
2026
gatewayToken: string;
2127
tempRoot?: string;
@@ -35,6 +41,55 @@ export function createMcpClientTempState(params: {
3541
};
3642
}
3743

44+
export async function connectMcpWithTimeout<TTransport extends McpConnectTransport>(
45+
client: { connect(transport: TTransport): Promise<void> },
46+
transport: TTransport,
47+
timeoutMs: number,
48+
): Promise<void> {
49+
let timedOut = false;
50+
let timeout: NodeJS.Timeout | undefined;
51+
const timeoutPromise = new Promise<never>((_, reject) => {
52+
timeout = setTimeout(() => {
53+
timedOut = true;
54+
reject(new Error(`MCP stdio connect timed out after ${timeoutMs}ms`));
55+
}, timeoutMs);
56+
timeout.unref?.();
57+
});
58+
59+
try {
60+
await Promise.race([client.connect(transport), timeoutPromise]);
61+
} catch (error) {
62+
if (timedOut) {
63+
await closeTimedOutTransport(transport);
64+
}
65+
throw error;
66+
} finally {
67+
if (timeout) {
68+
clearTimeout(timeout);
69+
}
70+
}
71+
}
72+
73+
async function closeTimedOutTransport(transport: McpConnectTransport): Promise<void> {
74+
if (!transport.close) {
75+
return;
76+
}
77+
let timer: NodeJS.Timeout | undefined;
78+
try {
79+
await Promise.race([
80+
Promise.resolve(transport.close()).catch(() => undefined),
81+
new Promise<void>((resolve) => {
82+
timer = setTimeout(resolve, MCP_TIMEOUT_CLOSE_GRACE_MS);
83+
timer.unref?.();
84+
}),
85+
]);
86+
} finally {
87+
if (timer) {
88+
clearTimeout(timer);
89+
}
90+
}
91+
}
92+
3893
export async function connectMcpClientWithPairingReconnect<
3994
T extends ReconnectableMcpClientHandle,
4095
>(params: {

0 commit comments

Comments
 (0)