Files
adolf/test/scripts/gateway-network-client.test.ts
alvis bedb527145
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Vendor OpenClaw source as Adolf fork baseline
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11),
free to diverge. Tree copied sans upstream .git; upstream remote added for
future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19.
Preserves docs/ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
2026-07-05 09:36:54 +00:00

249 lines
7.9 KiB
TypeScript

// Gateway Network Client tests cover gateway network client script behavior.
import { EventEmitter } from "node:events";
import { describe, expect, it, vi } 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";
describe("gateway network WebSocket open guard", () => {
function healthResponse() {
return {
ok: true,
payload: {
agents: [],
channelOrder: [],
channels: {},
defaultAgentId: "codex",
durationMs: 3,
ok: true,
sessions: { count: 0, path: "/state/sessions", recent: [] },
ts: Date.now(),
},
};
}
it("rejects loose client timeout env values instead of parsing prefixes", () => {
expect(() =>
readGatewayNetworkClientConnectTimeoutMs({
OPENCLAW_GATEWAY_NETWORK_CLIENT_CONNECT_TIMEOUT_MS: "100ms",
}),
).toThrow("invalid OPENCLAW_GATEWAY_NETWORK_CLIENT_CONNECT_TIMEOUT_MS: 100ms");
expect(() =>
readGatewayNetworkClientConnectTimeoutMs({
OPENCLAW_GATEWAY_NETWORK_CONNECT_READY_TIMEOUT_MS: "1e3",
}),
).toThrow("invalid OPENCLAW_GATEWAY_NETWORK_CONNECT_READY_TIMEOUT_MS: 1e3");
expect(() =>
readGatewayNetworkClientConnectTimeoutMs({
OPENCLAW_GATEWAY_NETWORK_CLIENT_CONNECT_TIMEOUT_MS: "0",
}),
).toThrow("invalid OPENCLAW_GATEWAY_NETWORK_CLIENT_CONNECT_TIMEOUT_MS: 0");
});
it("prefers the explicit client timeout over the connect-ready fallback", () => {
expect(
readGatewayNetworkClientConnectTimeoutMs({
OPENCLAW_GATEWAY_NETWORK_CLIENT_CONNECT_TIMEOUT_MS: "5000",
OPENCLAW_GATEWAY_NETWORK_CONNECT_READY_TIMEOUT_MS: "1000",
}),
).toBe(5000);
expect(
readGatewayNetworkClientConnectTimeoutMs({
OPENCLAW_GATEWAY_NETWORK_CONNECT_READY_TIMEOUT_MS: "3000",
}),
).toBe(3000);
});
it("resolves matching frames and ignores unrelated frames", async () => {
const ws = new EventEmitter();
const frame = onceFrame(ws, (message) => message?.id === "target", 1000);
ws.emit("message", JSON.stringify({ id: "noise" }));
ws.emit("message", JSON.stringify({ id: "target", ok: true }));
await expect(frame).resolves.toEqual({ id: "target", ok: true });
});
it("times out when no matching frame arrives", async () => {
const ws = new EventEmitter();
const frame = onceFrame(ws, () => false, 10);
ws.emit("message", JSON.stringify({ id: "noise" }));
await expect(frame).rejects.toThrow("timeout");
});
it("rejects frame waits immediately when the socket closes", async () => {
const ws = new EventEmitter();
const startedAt = Date.now();
const frame = onceFrame(ws, () => false, 1000);
ws.emit("close", 1006, Buffer.from("bye"));
await expect(frame).rejects.toThrow("closed before frame: 1006 bye");
expect(Date.now() - startedAt).toBeLessThan(250);
});
it("rejects frame waits immediately on socket errors", async () => {
const ws = new EventEmitter();
const frame = onceFrame(ws, () => false, 1000);
ws.emit("error", new Error("socket exploded"));
await expect(frame).rejects.toThrow("socket exploded");
});
it("rejects invalid JSON frames instead of crashing the process", async () => {
const ws = new EventEmitter();
const frame = onceFrame(ws, () => false, 1000);
ws.emit("message", "{nope");
await expect(frame).rejects.toThrow();
});
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);
},
};
return {
get closeCount() {
return closeCount;
},
sentMethods,
stdout,
deps: {
delay: async () => {},
onceFrame: async (
_ws: unknown,
predicate: (frame: unknown) => boolean,
_timeoutMs?: number,
) => {
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 }, healthResponse()]);
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("bounds socket and frame waits by the client deadline", async () => {
const harness = createNetworkClientHarness([{ ok: true }, healthResponse()]);
const openSocket = vi.fn(harness.deps.openSocket);
const onceFrame = vi.fn(harness.deps.onceFrame);
await runGatewayNetworkClient(
{ token: "secret-token", url: "ws://127.0.0.1:12345", timeoutMs: 250 },
{
...harness.deps,
onceFrame,
openSocket,
},
);
expect(openSocket.mock.calls[0]?.[1]).toBeGreaterThan(0);
expect(openSocket.mock.calls[0]?.[1]).toBeLessThanOrEqual(250);
expect(onceFrame.mock.calls.map((call) => call[2])).toHaveLength(2);
for (const frameTimeoutMs of onceFrame.mock.calls.map((call) => call[2])) {
expect(frameTimeoutMs).toBeGreaterThan(0);
expect(frameTimeoutMs).toBeLessThanOrEqual(250);
}
});
it("does not sleep past the remaining client deadline between retries", async () => {
const delays: number[] = [];
let now = 1_000;
const dateSpy = vi.spyOn(Date, "now").mockImplementation(() => now);
try {
await expect(
runGatewayNetworkClient(
{ token: "secret-token", url: "ws://127.0.0.1:12345", timeoutMs: 250 },
{
delay: async (ms: number) => {
delays.push(ms);
now += ms;
},
openSocket: async () => {
now += 200;
throw new Error("ECONNREFUSED");
},
protocolVersion: 1,
stdout: () => {},
},
),
).rejects.toThrow("ECONNREFUSED");
} finally {
dateSpy.mockRestore();
}
expect(delays).toEqual([50]);
});
it("fails a connected socket whose health success lacks summary evidence", async () => {
const harness = createNetworkClientHarness([{ ok: true }, { ok: true }]);
await expect(
runGatewayNetworkClient(
{ token: "secret-token", url: "ws://127.0.0.1:12345", timeoutMs: 1000 },
harness.deps,
),
).rejects.toThrow("health failed: missing health summary payload");
expect(harness.sentMethods).toEqual(["connect", "health"]);
expect(harness.stdout).toEqual([]);
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);
});
});