mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-16 12:25:44 +00:00
111 lines
3.7 KiB
TypeScript
111 lines
3.7 KiB
TypeScript
import type { EventFrame } from "@openclaw/gateway-protocol";
|
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
import { GatewayProtocolClient, type GatewayProtocolSocketHandlers } from "./protocol-client.js";
|
|
|
|
type SyntheticConnection = {
|
|
handlers: GatewayProtocolSocketHandlers;
|
|
close: (code?: number, reason?: string) => void;
|
|
};
|
|
|
|
function createSequenceClient(): {
|
|
client: GatewayProtocolClient<Record<string, never>>;
|
|
connections: SyntheticConnection[];
|
|
onEvent: ReturnType<typeof vi.fn<(event: EventFrame) => void>>;
|
|
onGap: ReturnType<typeof vi.fn<(info: { expected: number; received: number }) => void>>;
|
|
} {
|
|
const connections: SyntheticConnection[] = [];
|
|
const onEvent = vi.fn<(event: EventFrame) => void>();
|
|
const onGap = vi.fn<(info: { expected: number; received: number }) => void>();
|
|
const client = new GatewayProtocolClient<Record<string, never>>({
|
|
createSocket: (handlers) => {
|
|
let open = true;
|
|
const close = (code = 1000, reason = "") => {
|
|
open = false;
|
|
handlers.close(code, reason);
|
|
};
|
|
connections.push({ handlers, close });
|
|
return {
|
|
isOpen: () => open,
|
|
send: () => {},
|
|
close,
|
|
};
|
|
},
|
|
createRequestId: () => "request-1",
|
|
buildConnectPlan: () => ({}),
|
|
buildConnectParams: (plan) => plan,
|
|
resolveClose: () => ({ retry: true, notify: true }),
|
|
onEvent,
|
|
onGap,
|
|
handshake: { mode: "require-challenge", timeoutMs: 100 },
|
|
reconnect: { initialMs: 10, multiplier: 2, maxMs: 100 },
|
|
});
|
|
return { client, connections, onEvent, onGap };
|
|
}
|
|
|
|
function sendEvent(connection: SyntheticConnection, seq: number): void {
|
|
connection.handlers.message(
|
|
JSON.stringify({ type: "event", event: "board.changed", payload: {}, seq }),
|
|
);
|
|
}
|
|
|
|
describe("GatewayProtocolClient event sequences", () => {
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
test("establishes a fresh baseline after an automatic reconnect", async () => {
|
|
vi.useFakeTimers();
|
|
const { client, connections, onEvent, onGap } = createSequenceClient();
|
|
client.start();
|
|
const firstConnection = connections[0];
|
|
if (!firstConnection) {
|
|
throw new Error("synthetic protocol connection missing");
|
|
}
|
|
sendEvent(firstConnection, 1);
|
|
|
|
firstConnection.close(1012, "service restart");
|
|
await vi.advanceTimersByTimeAsync(10);
|
|
const replacementConnection = connections[1];
|
|
if (!replacementConnection) {
|
|
throw new Error("synthetic replacement connection missing");
|
|
}
|
|
sendEvent(replacementConnection, 3);
|
|
|
|
expect(onGap).not.toHaveBeenCalled();
|
|
expect(onEvent).toHaveBeenCalledTimes(2);
|
|
|
|
sendEvent(replacementConnection, 5);
|
|
|
|
expect(onGap).toHaveBeenCalledExactlyOnceWith({ expected: 4, received: 5 });
|
|
expect(onEvent).toHaveBeenCalledTimes(3);
|
|
client.stop();
|
|
});
|
|
|
|
test("resets on restart without admitting retired socket frames", () => {
|
|
const { client, connections, onEvent, onGap } = createSequenceClient();
|
|
client.start();
|
|
const firstConnection = connections[0];
|
|
if (!firstConnection) {
|
|
throw new Error("synthetic protocol connection missing");
|
|
}
|
|
sendEvent(firstConnection, 1);
|
|
|
|
client.stop();
|
|
client.start();
|
|
const replacementConnection = connections[1];
|
|
if (!replacementConnection) {
|
|
throw new Error("synthetic replacement connection missing");
|
|
}
|
|
sendEvent(firstConnection, 10);
|
|
sendEvent(replacementConnection, 3);
|
|
|
|
expect(onGap).not.toHaveBeenCalled();
|
|
expect(onEvent).toHaveBeenCalledTimes(2);
|
|
|
|
sendEvent(replacementConnection, 5);
|
|
|
|
expect(onGap).toHaveBeenCalledExactlyOnceWith({ expected: 4, received: 5 });
|
|
expect(onEvent).toHaveBeenCalledTimes(3);
|
|
client.stop();
|
|
});
|
|
});
|