Files
claw3d/tests/unit/gatewayBrowserClient.test.ts
T
Luke The Dev 4fa4f13558 First Release of Claw3D (#11)
Co-authored-by: iamlukethedev <iamlukethedev@users.noreply.github.com>
2026-03-19 23:14:04 -05:00

137 lines
3.9 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { GatewayBrowserClient } from "@/lib/gateway/openclaw/GatewayBrowserClient";
const UUID_V4_RE =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
class MockWebSocket {
static OPEN = 1;
static CLOSED = 3;
static instances: MockWebSocket[] = [];
static sent: string[] = [];
static closes: Array<{ code: number; reason: string }> = [];
readyState = MockWebSocket.OPEN;
onopen: (() => void) | null = null;
onmessage: ((event: MessageEvent) => void) | null = null;
onclose: ((event: CloseEvent) => void) | null = null;
onerror: (() => void) | null = null;
constructor(public url: string) {
MockWebSocket.instances.push(this);
}
send(data: string) {
MockWebSocket.sent.push(String(data));
}
close(code?: number, reason?: string) {
MockWebSocket.closes.push({ code: code ?? 1000, reason: reason ?? "" });
this.readyState = MockWebSocket.CLOSED;
this.onclose?.({ code: code ?? 1000, reason: reason ?? "" } as CloseEvent);
}
}
describe("GatewayBrowserClient", () => {
const originalWebSocket = globalThis.WebSocket;
const originalSubtle = globalThis.crypto?.subtle;
beforeEach(() => {
MockWebSocket.instances = [];
MockWebSocket.sent = [];
MockWebSocket.closes = [];
globalThis.WebSocket = MockWebSocket as unknown as typeof WebSocket;
if (globalThis.crypto) {
Object.defineProperty(globalThis.crypto, "subtle", {
value: undefined,
configurable: true,
});
}
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
globalThis.WebSocket = originalWebSocket;
if (globalThis.crypto) {
Object.defineProperty(globalThis.crypto, "subtle", {
value: originalSubtle,
configurable: true,
});
}
});
it("sends connect when connect.challenge arrives", async () => {
const client = new GatewayBrowserClient({ url: "ws://example.com" });
client.start();
const ws = MockWebSocket.instances[0];
if (!ws) {
throw new Error("WebSocket not created");
}
ws.onopen?.();
expect(MockWebSocket.sent).toHaveLength(0);
ws.onmessage?.({
data: JSON.stringify({
type: "event",
event: "connect.challenge",
payload: { nonce: "abc" },
}),
} as MessageEvent);
await vi.runAllTicks();
expect(MockWebSocket.sent).toHaveLength(1);
const frame = JSON.parse(MockWebSocket.sent[0] ?? "{}");
expect(frame.type).toBe("req");
expect(frame.method).toBe("connect");
expect(typeof frame.id).toBe("string");
expect(frame.id).toMatch(UUID_V4_RE);
expect(frame.params?.client?.id).toBe("openclaw-control-ui");
});
it("truncates connect-failed close reason to websocket limit", async () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const client = new GatewayBrowserClient({ url: "ws://example.com", token: "secret" });
client.start();
const ws = MockWebSocket.instances[0];
if (!ws) {
throw new Error("WebSocket not created");
}
ws.onopen?.();
vi.runAllTimers();
const connectFrame = JSON.parse(MockWebSocket.sent[0] ?? "{}");
const connectId = String(connectFrame.id ?? "");
expect(connectId).toMatch(UUID_V4_RE);
ws.onmessage?.({
data: JSON.stringify({
type: "res",
id: connectId,
ok: false,
error: {
code: "INVALID_REQUEST",
message: `invalid config ${"x".repeat(260)}`,
},
}),
} as MessageEvent);
await vi.runAllTicks();
await vi.runAllTimersAsync();
await Promise.resolve();
const lastClose = MockWebSocket.closes.at(-1);
expect(lastClose?.code).toBe(4008);
expect(lastClose?.reason.startsWith("connect failed: INVALID_REQUEST")).toBe(true);
expect(new TextEncoder().encode(lastClose?.reason ?? "").byteLength).toBeLessThanOrEqual(123);
warnSpy.mockRestore();
});
});