Files
claw3d/tests/unit/gatewayClient.reconnect-recovery.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

135 lines
3.7 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import { GatewayClient } from "@/lib/gateway/GatewayClient";
type MockClientOptions = {
token?: unknown;
onHello?: (hello: unknown) => void;
onClose?: (info: { code: number; reason: string }) => void;
};
type MockInstance = {
opts: MockClientOptions;
stopped: boolean;
};
let instances: MockInstance[] = [];
vi.mock("@/lib/gateway/openclaw/GatewayBrowserClient", () => {
class GatewayBrowserClient {
connected = false;
private index: number;
constructor(opts: MockClientOptions) {
this.index = instances.length;
instances.push({ opts, stopped: false });
}
start() {
this.connected = true;
}
stop() {
this.connected = false;
instances[this.index]!.stopped = true;
}
request() {
return Promise.resolve({});
}
}
return { GatewayBrowserClient };
});
afterEach(() => {
instances = [];
});
describe("GatewayClient reconnect recovery", () => {
it("allows a fresh connect after unexpected close", async () => {
const client = new GatewayClient();
const statuses: string[] = [];
client.onStatus((status) => statuses.push(status));
const firstConnect = client.connect({
gatewayUrl: "ws://example.invalid",
token: "old-token",
});
const first = instances[0];
if (!first) throw new Error("Expected first GatewayBrowserClient instance");
const onHelloFirst = first.opts.onHello;
const onCloseFirst = first.opts.onClose;
if (!onHelloFirst || !onCloseFirst) {
throw new Error("Expected first instance callbacks");
}
onHelloFirst({});
await expect(firstConnect).resolves.toBeUndefined();
onCloseFirst({ code: 1012, reason: "upstream closed" });
expect(first.stopped).toBe(true);
expect(statuses.at(-1)).toBe("disconnected");
const secondConnect = client.connect({
gatewayUrl: "ws://example.invalid",
token: "new-token",
});
const second = instances[1];
if (!second) throw new Error("Expected second GatewayBrowserClient instance");
expect(second.opts.token).toBe("new-token");
const onHelloSecond = second.opts.onHello;
if (!onHelloSecond) {
throw new Error("Expected second instance onHello callback");
}
onHelloSecond({});
await expect(secondConnect).resolves.toBeUndefined();
expect(statuses.at(-1)).toBe("connected");
});
it("ignores stale onClose callbacks from old instances", async () => {
const client = new GatewayClient();
const statuses: string[] = [];
client.onStatus((status) => statuses.push(status));
const firstConnect = client.connect({ gatewayUrl: "ws://example.invalid" });
const first = instances[0];
if (!first) throw new Error("Expected first GatewayBrowserClient instance");
const onHelloFirst = first.opts.onHello;
const onCloseFirst = first.opts.onClose;
if (!onHelloFirst || !onCloseFirst) {
throw new Error("Expected first instance callbacks");
}
onHelloFirst({});
await firstConnect;
onCloseFirst({ code: 1012, reason: "upstream closed" });
const secondConnect = client.connect({ gatewayUrl: "ws://example.invalid" });
const second = instances[1];
if (!second) throw new Error("Expected second GatewayBrowserClient instance");
const onHelloSecond = second.opts.onHello;
if (!onHelloSecond) {
throw new Error("Expected second instance onHello callback");
}
onHelloSecond({});
await secondConnect;
const statusCountBeforeStaleClose = statuses.length;
onCloseFirst({ code: 1012, reason: "late stale close" });
expect(statuses.length).toBe(statusCountBeforeStaleClose);
expect(statuses.at(-1)).toBe("connected");
});
});