mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
test(e2e): exercise gateway network health client
This commit is contained in:
@@ -1,36 +1,28 @@
|
||||
// WebSocket client helpers for gateway network E2E scenarios.
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { WebSocket } from "ws";
|
||||
import { PROTOCOL_VERSION } from "../../../../dist/gateway/protocol/index.js";
|
||||
import { waitForWebSocketOpen } from "../websocket-open.mjs";
|
||||
import { readGatewayNetworkClientConnectTimeoutMs } from "./limits.mjs";
|
||||
import { onceFrame } from "./ws-frames.mjs";
|
||||
|
||||
const url = process.env.GW_URL;
|
||||
const token = process.env.GW_TOKEN;
|
||||
if (!url || !token) {
|
||||
throw new Error("missing GW_URL/GW_TOKEN");
|
||||
}
|
||||
|
||||
const deadline = Date.now() + readGatewayNetworkClientConnectTimeoutMs();
|
||||
|
||||
function delay(ms) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
async function openSocket(timeoutMs = 10_000) {
|
||||
async function openSocket(url, timeoutMs = 10_000) {
|
||||
const ws = new WebSocket(url);
|
||||
await waitForWebSocketOpen(ws, timeoutMs, "ws open timeout");
|
||||
return ws;
|
||||
}
|
||||
|
||||
function responseError(method, response) {
|
||||
export function responseError(method, response) {
|
||||
const message = response.error?.message ?? "unknown";
|
||||
return new Error(`${method} failed: ${message}`);
|
||||
}
|
||||
|
||||
function isRetryableStartupError(message) {
|
||||
export function isRetryableStartupError(message) {
|
||||
return (
|
||||
message.includes("gateway starting") ||
|
||||
message.includes("closed before frame") ||
|
||||
@@ -42,59 +34,90 @@ function isRetryableStartupError(message) {
|
||||
);
|
||||
}
|
||||
|
||||
let lastError;
|
||||
while (Date.now() < deadline) {
|
||||
let ws;
|
||||
try {
|
||||
ws = await openSocket();
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "req",
|
||||
id: "c1",
|
||||
method: "connect",
|
||||
params: {
|
||||
minProtocol: PROTOCOL_VERSION,
|
||||
maxProtocol: PROTOCOL_VERSION,
|
||||
client: {
|
||||
id: "test",
|
||||
displayName: "docker-net-e2e",
|
||||
version: "dev",
|
||||
platform: process.platform,
|
||||
mode: "test",
|
||||
},
|
||||
caps: [],
|
||||
auth: { token },
|
||||
},
|
||||
}),
|
||||
);
|
||||
async function readProtocolVersion() {
|
||||
const protocol = await import("../../../../dist/gateway/protocol/index.js");
|
||||
return protocol.PROTOCOL_VERSION;
|
||||
}
|
||||
|
||||
const connectRes = await onceFrame(ws, (frame) => frame?.type === "res" && frame?.id === "c1");
|
||||
if (!connectRes.ok) {
|
||||
lastError = responseError("connect", connectRes);
|
||||
export async function runGatewayNetworkClient(
|
||||
{ token, url, timeoutMs = readGatewayNetworkClientConnectTimeoutMs() },
|
||||
deps = {},
|
||||
) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
const delayImpl = deps.delay ?? delay;
|
||||
const onceFrameImpl = deps.onceFrame ?? onceFrame;
|
||||
const openSocketImpl = deps.openSocket ?? ((targetUrl) => openSocket(targetUrl));
|
||||
const protocolVersion = deps.protocolVersion ?? (await readProtocolVersion());
|
||||
const stdout = deps.stdout ?? console.log;
|
||||
|
||||
let lastError;
|
||||
while (Date.now() < deadline) {
|
||||
let ws;
|
||||
try {
|
||||
ws = await openSocketImpl(url);
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "req",
|
||||
id: "c1",
|
||||
method: "connect",
|
||||
params: {
|
||||
minProtocol: protocolVersion,
|
||||
maxProtocol: protocolVersion,
|
||||
client: {
|
||||
id: "test",
|
||||
displayName: "docker-net-e2e",
|
||||
version: "dev",
|
||||
platform: process.platform,
|
||||
mode: "test",
|
||||
},
|
||||
caps: [],
|
||||
auth: { token },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const connectRes = await onceFrameImpl(
|
||||
ws,
|
||||
(frame) => frame?.type === "res" && frame?.id === "c1",
|
||||
);
|
||||
if (!connectRes.ok) {
|
||||
lastError = responseError("connect", connectRes);
|
||||
if (!isRetryableStartupError(lastError.message)) {
|
||||
throw lastError;
|
||||
}
|
||||
} else {
|
||||
ws.send(JSON.stringify({ type: "req", id: "h1", method: "health" }));
|
||||
const healthRes = await onceFrameImpl(
|
||||
ws,
|
||||
(frame) => frame?.type === "res" && frame?.id === "h1",
|
||||
);
|
||||
if (healthRes.ok) {
|
||||
stdout("ok");
|
||||
return;
|
||||
}
|
||||
|
||||
throw responseError("health", healthRes);
|
||||
}
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? error : new Error(String(error));
|
||||
if (!isRetryableStartupError(lastError.message)) {
|
||||
throw lastError;
|
||||
}
|
||||
} else {
|
||||
ws.send(JSON.stringify({ type: "req", id: "h1", method: "health" }));
|
||||
const healthRes = await onceFrame(ws, (frame) => frame?.type === "res" && frame?.id === "h1");
|
||||
if (healthRes.ok) {
|
||||
ws.close();
|
||||
console.log("ok");
|
||||
process.exit(0);
|
||||
}
|
||||
} finally {
|
||||
ws?.close();
|
||||
}
|
||||
|
||||
throw responseError("health", healthRes);
|
||||
}
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? error : new Error(String(error));
|
||||
if (!isRetryableStartupError(lastError.message)) {
|
||||
throw lastError;
|
||||
}
|
||||
} finally {
|
||||
ws?.close();
|
||||
await delayImpl(500);
|
||||
}
|
||||
|
||||
await delay(500);
|
||||
throw lastError ?? new Error("connect failed: timeout");
|
||||
}
|
||||
|
||||
throw lastError ?? new Error("connect failed: timeout");
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
const url = process.env.GW_URL;
|
||||
const token = process.env.GW_TOKEN;
|
||||
if (!url || !token) {
|
||||
throw new Error("missing GW_URL/GW_TOKEN");
|
||||
}
|
||||
await runGatewayNetworkClient({ token, url });
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Gateway Network Client tests cover gateway network client script behavior.
|
||||
import { EventEmitter } from "node:events";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { runGatewayNetworkClient } from "../../scripts/e2e/lib/gateway-network/client.mjs";
|
||||
import { readGatewayNetworkClientConnectTimeoutMs } from "../../scripts/e2e/lib/gateway-network/limits.mjs";
|
||||
import { onceFrame } from "../../scripts/e2e/lib/gateway-network/ws-frames.mjs";
|
||||
|
||||
@@ -86,15 +86,75 @@ describe("gateway network WebSocket open guard", () => {
|
||||
await expect(frame).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("proves health after the authenticated connect handshake", () => {
|
||||
const client = readFileSync("scripts/e2e/lib/gateway-network/client.mjs", "utf8");
|
||||
const connectIndex = client.indexOf('method: "connect"');
|
||||
const healthIndex = client.indexOf('method: "health"');
|
||||
function createNetworkClientHarness(
|
||||
responses: Array<{ error?: { message?: string }; ok: boolean }>,
|
||||
) {
|
||||
const frames = [...responses];
|
||||
const sentMethods: string[] = [];
|
||||
const stdout: string[] = [];
|
||||
let closeCount = 0;
|
||||
const socket = {
|
||||
close: () => {
|
||||
closeCount += 1;
|
||||
},
|
||||
send: (payload: string) => {
|
||||
sentMethods.push(JSON.parse(payload).method);
|
||||
},
|
||||
};
|
||||
|
||||
expect(connectIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(healthIndex).toBeGreaterThan(connectIndex);
|
||||
expect(client).toContain('responseError("health", healthRes)');
|
||||
expect(client).toContain('message.includes("closed before open")');
|
||||
expect(client).toContain('message.includes("closed before frame")');
|
||||
return {
|
||||
get closeCount() {
|
||||
return closeCount;
|
||||
},
|
||||
sentMethods,
|
||||
stdout,
|
||||
deps: {
|
||||
delay: async () => {},
|
||||
onceFrame: async (_ws: unknown, predicate: (frame: unknown) => boolean) => {
|
||||
const frame = {
|
||||
type: "res",
|
||||
id: sentMethods.at(-1) === "connect" ? "c1" : "h1",
|
||||
...frames.shift(),
|
||||
};
|
||||
expect(predicate(frame)).toBe(true);
|
||||
return frame;
|
||||
},
|
||||
openSocket: async () => socket,
|
||||
protocolVersion: 1,
|
||||
stdout: (message: string) => {
|
||||
stdout.push(message);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
it("proves health after the authenticated connect handshake", async () => {
|
||||
const harness = createNetworkClientHarness([{ ok: true }, { ok: true }]);
|
||||
|
||||
await runGatewayNetworkClient(
|
||||
{ token: "secret-token", url: "ws://127.0.0.1:12345", timeoutMs: 1000 },
|
||||
harness.deps,
|
||||
);
|
||||
|
||||
expect(harness.sentMethods).toEqual(["connect", "health"]);
|
||||
expect(harness.stdout).toEqual(["ok"]);
|
||||
expect(harness.closeCount).toBe(1);
|
||||
});
|
||||
|
||||
it("fails a connected socket whose health probe fails", async () => {
|
||||
const harness = createNetworkClientHarness([
|
||||
{ ok: true },
|
||||
{ ok: false, error: { message: "health unavailable" } },
|
||||
]);
|
||||
|
||||
await expect(
|
||||
runGatewayNetworkClient(
|
||||
{ token: "secret-token", url: "ws://127.0.0.1:12345", timeoutMs: 1000 },
|
||||
harness.deps,
|
||||
),
|
||||
).rejects.toThrow("health failed: health unavailable");
|
||||
|
||||
expect(harness.sentMethods).toEqual(["connect", "health"]);
|
||||
expect(harness.closeCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user