mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
test(e2e): assert mcp reconnect temp state
This commit is contained in:
@@ -66,6 +66,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Release/CI/E2E: reject oversized OpenAI image-auth mock request bodies before Docker proof runs can accumulate unbounded payloads.
|
||||
- Release/CI/E2E: require the Kitchen Sink RPC walk to prove every expected plugin tool is cataloged and effective before invoking tool fixtures.
|
||||
- Release/CI/E2E: stop tracked Docker build commands when centralized build wrappers receive shutdown signals.
|
||||
- Release/CI/E2E: cover MCP channel pairing reconnects by asserting the same temporary client state is reused across reconnects.
|
||||
- Release/CI/E2E: fail secret-provider proof runs when temporary state cleanup still fails after retries instead of hiding the cleanup error.
|
||||
- Release/CI/E2E: fail package-candidate ref proofs when temporary source worktree cleanup fails instead of leaving stale worktrees behind.
|
||||
- Release/CI/E2E: remove package tarball extract directories when tar extraction fails before validation can continue.
|
||||
|
||||
@@ -11,7 +11,10 @@ import {
|
||||
maybeApprovePendingBridgePairing,
|
||||
waitFor,
|
||||
} from "./mcp-channels-harness.ts";
|
||||
import { createMcpClientTempState } from "./mcp-client-temp-state.ts";
|
||||
import {
|
||||
connectMcpClientWithPairingReconnect,
|
||||
createMcpClientTempState,
|
||||
} from "./mcp-client-temp-state.ts";
|
||||
|
||||
function summarizeSessionRows(rows: Array<Record<string, unknown>> | undefined) {
|
||||
return (rows ?? []).map((entry) => ({
|
||||
@@ -107,23 +110,17 @@ async function main() {
|
||||
"expected seeded gateway deliveryContext target",
|
||||
);
|
||||
|
||||
mcpHandle = await connectMcpClient({
|
||||
gatewayUrl,
|
||||
gatewayToken,
|
||||
mcpHandle = await connectMcpClientWithPairingReconnect({
|
||||
tempState: mcpTempState,
|
||||
connect: (tempState) =>
|
||||
connectMcpClient({
|
||||
gatewayUrl,
|
||||
gatewayToken,
|
||||
tempState,
|
||||
}),
|
||||
maybeApprovePairing: () => maybeApprovePendingBridgePairing(gateway),
|
||||
});
|
||||
let mcp = mcpHandle.client;
|
||||
|
||||
if (await maybeApprovePendingBridgePairing(gateway)) {
|
||||
await Promise.allSettled([mcp.close(), mcpHandle.transport.close()]);
|
||||
mcpHandle.cleanup();
|
||||
mcpHandle = await connectMcpClient({
|
||||
gatewayUrl,
|
||||
gatewayToken,
|
||||
tempState: mcpTempState,
|
||||
});
|
||||
mcp = mcpHandle.client;
|
||||
}
|
||||
const mcp = mcpHandle.client;
|
||||
const callTool = <T>(params: Parameters<typeof mcp.callTool>[0]) =>
|
||||
mcp.callTool(params, undefined, { timeout: 240_000 }) as Promise<T>;
|
||||
|
||||
|
||||
@@ -9,6 +9,12 @@ export type McpClientTempState = {
|
||||
tokenFile: string;
|
||||
};
|
||||
|
||||
export type ReconnectableMcpClientHandle = {
|
||||
cleanup: () => void;
|
||||
client: { close: () => Promise<unknown> };
|
||||
transport: { close: () => Promise<unknown> };
|
||||
};
|
||||
|
||||
export function createMcpClientTempState(params: {
|
||||
gatewayToken: string;
|
||||
tempRoot?: string;
|
||||
@@ -27,3 +33,28 @@ export function createMcpClientTempState(params: {
|
||||
tokenFile,
|
||||
};
|
||||
}
|
||||
|
||||
export async function connectMcpClientWithPairingReconnect<
|
||||
T extends ReconnectableMcpClientHandle,
|
||||
>(params: {
|
||||
connect: (tempState: McpClientTempState) => Promise<T>;
|
||||
maybeApprovePairing: () => Promise<boolean>;
|
||||
tempState: McpClientTempState;
|
||||
}): Promise<T> {
|
||||
let handle = await params.connect(params.tempState);
|
||||
let shouldReconnect: boolean;
|
||||
try {
|
||||
shouldReconnect = await params.maybeApprovePairing();
|
||||
} catch (error) {
|
||||
await Promise.allSettled([handle.client.close(), handle.transport.close()]);
|
||||
handle.cleanup();
|
||||
throw error;
|
||||
}
|
||||
if (!shouldReconnect) {
|
||||
return handle;
|
||||
}
|
||||
await Promise.allSettled([handle.client.close(), handle.transport.close()]);
|
||||
handle.cleanup();
|
||||
handle = await params.connect(params.tempState);
|
||||
return handle;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync, statSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createMcpClientTempState } from "../../scripts/e2e/mcp-client-temp-state.js";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
connectMcpClientWithPairingReconnect,
|
||||
createMcpClientTempState,
|
||||
type McpClientTempState,
|
||||
} from "../../scripts/e2e/mcp-client-temp-state.js";
|
||||
|
||||
describe("mcp-channels harness", () => {
|
||||
it("creates unique client temp state and removes token files on cleanup", () => {
|
||||
@@ -27,11 +31,69 @@ describe("mcp-channels harness", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("reuses one MCP temp state across the pairing reconnect path", () => {
|
||||
const source = readFileSync("scripts/e2e/mcp-channels-docker-client.ts", "utf8");
|
||||
it("reuses one MCP temp state across the pairing reconnect path", async () => {
|
||||
const tempState = createMcpClientTempState({ gatewayToken: "pairing-token" });
|
||||
const firstHandle = {
|
||||
cleanup: vi.fn(),
|
||||
client: { close: vi.fn(async () => undefined) },
|
||||
transport: { close: vi.fn(async () => undefined) },
|
||||
};
|
||||
const secondHandle = {
|
||||
cleanup: vi.fn(),
|
||||
client: { close: vi.fn(async () => undefined) },
|
||||
transport: { close: vi.fn(async () => undefined) },
|
||||
};
|
||||
const connectCalls: McpClientTempState[] = [];
|
||||
const connect = vi.fn(async (state: McpClientTempState) => {
|
||||
connectCalls.push(state);
|
||||
return connectCalls.length === 1 ? firstHandle : secondHandle;
|
||||
});
|
||||
|
||||
expect(source).toContain("const mcpTempState = createMcpClientTempState({ gatewayToken });");
|
||||
expect(source.match(/tempState: mcpTempState/gu)).toHaveLength(2);
|
||||
expect(source).toContain("mcpTempState.cleanup();");
|
||||
try {
|
||||
await expect(
|
||||
connectMcpClientWithPairingReconnect({
|
||||
connect,
|
||||
maybeApprovePairing: async () => true,
|
||||
tempState,
|
||||
}),
|
||||
).resolves.toBe(secondHandle);
|
||||
|
||||
expect(connect).toHaveBeenCalledTimes(2);
|
||||
expect(connectCalls).toEqual([tempState, tempState]);
|
||||
expect(firstHandle.client.close).toHaveBeenCalledOnce();
|
||||
expect(firstHandle.transport.close).toHaveBeenCalledOnce();
|
||||
expect(firstHandle.cleanup).toHaveBeenCalledOnce();
|
||||
expect(secondHandle.cleanup).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
tempState.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it("cleans up the first MCP client when pairing approval fails", async () => {
|
||||
const tempState = createMcpClientTempState({ gatewayToken: "pairing-token" });
|
||||
const handle = {
|
||||
cleanup: vi.fn(),
|
||||
client: { close: vi.fn(async () => undefined) },
|
||||
transport: { close: vi.fn(async () => undefined) },
|
||||
};
|
||||
const failure = new Error("pairing approval failed");
|
||||
|
||||
try {
|
||||
await expect(
|
||||
connectMcpClientWithPairingReconnect({
|
||||
connect: async () => handle,
|
||||
maybeApprovePairing: async () => {
|
||||
throw failure;
|
||||
},
|
||||
tempState,
|
||||
}),
|
||||
).rejects.toBe(failure);
|
||||
|
||||
expect(handle.client.close).toHaveBeenCalledOnce();
|
||||
expect(handle.transport.close).toHaveBeenCalledOnce();
|
||||
expect(handle.cleanup).toHaveBeenCalledOnce();
|
||||
} finally {
|
||||
tempState.cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user