mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-30 11:44:15 +00:00
fix: enhance connectGateway to handle stale clients on reconnect
This commit is contained in:
committed by
Peter Steinberger
parent
51c4a0a9e9
commit
9228ef0856
146
ui/src/ui/app-gateway.node.test.ts
Normal file
146
ui/src/ui/app-gateway.node.test.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { connectGateway } from "./app-gateway.ts";
|
||||
|
||||
type GatewayClientMock = {
|
||||
start: ReturnType<typeof vi.fn>;
|
||||
stop: ReturnType<typeof vi.fn>;
|
||||
emitClose: (code: number, reason?: string) => void;
|
||||
emitGap: (expected: number, received: number) => void;
|
||||
emitEvent: (evt: { event: string; payload?: unknown; seq?: number }) => void;
|
||||
};
|
||||
|
||||
const gatewayClientInstances: GatewayClientMock[] = [];
|
||||
|
||||
vi.mock("./gateway.ts", () => {
|
||||
class GatewayBrowserClient {
|
||||
readonly start = vi.fn();
|
||||
readonly stop = vi.fn();
|
||||
|
||||
constructor(
|
||||
private opts: {
|
||||
onClose?: (info: { code: number; reason: string }) => void;
|
||||
onGap?: (info: { expected: number; received: number }) => void;
|
||||
onEvent?: (evt: { event: string; payload?: unknown; seq?: number }) => void;
|
||||
},
|
||||
) {
|
||||
gatewayClientInstances.push({
|
||||
start: this.start,
|
||||
stop: this.stop,
|
||||
emitClose: (code, reason) => {
|
||||
this.opts.onClose?.({ code, reason: reason ?? "" });
|
||||
},
|
||||
emitGap: (expected, received) => {
|
||||
this.opts.onGap?.({ expected, received });
|
||||
},
|
||||
emitEvent: (evt) => {
|
||||
this.opts.onEvent?.(evt);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { GatewayBrowserClient };
|
||||
});
|
||||
|
||||
function createHost() {
|
||||
return {
|
||||
settings: {
|
||||
gatewayUrl: "ws://127.0.0.1:18789",
|
||||
token: "",
|
||||
sessionKey: "main",
|
||||
lastActiveSessionKey: "main",
|
||||
theme: "system",
|
||||
chatFocusMode: false,
|
||||
chatShowThinking: true,
|
||||
splitRatio: 0.6,
|
||||
navCollapsed: false,
|
||||
navGroupsCollapsed: {},
|
||||
},
|
||||
password: "",
|
||||
client: null,
|
||||
connected: false,
|
||||
hello: null,
|
||||
lastError: null,
|
||||
eventLogBuffer: [],
|
||||
eventLog: [],
|
||||
tab: "overview",
|
||||
presenceEntries: [],
|
||||
presenceError: null,
|
||||
presenceStatus: null,
|
||||
agentsLoading: false,
|
||||
agentsList: null,
|
||||
agentsError: null,
|
||||
debugHealth: null,
|
||||
assistantName: "OpenClaw",
|
||||
assistantAvatar: null,
|
||||
assistantAgentId: null,
|
||||
sessionKey: "main",
|
||||
chatRunId: null,
|
||||
refreshSessionsAfterChat: new Set<string>(),
|
||||
execApprovalQueue: [],
|
||||
execApprovalError: null,
|
||||
} as unknown as Parameters<typeof connectGateway>[0];
|
||||
}
|
||||
|
||||
describe("connectGateway", () => {
|
||||
beforeEach(() => {
|
||||
gatewayClientInstances.length = 0;
|
||||
});
|
||||
|
||||
it("ignores stale client onGap callbacks after reconnect", () => {
|
||||
const host = createHost();
|
||||
|
||||
connectGateway(host);
|
||||
const firstClient = gatewayClientInstances[0];
|
||||
expect(firstClient).toBeDefined();
|
||||
|
||||
connectGateway(host);
|
||||
const secondClient = gatewayClientInstances[1];
|
||||
expect(secondClient).toBeDefined();
|
||||
|
||||
firstClient.emitGap(10, 13);
|
||||
expect(host.lastError).toBeNull();
|
||||
|
||||
secondClient.emitGap(20, 24);
|
||||
expect(host.lastError).toBe(
|
||||
"event gap detected (expected seq 20, got 24); refresh recommended",
|
||||
);
|
||||
});
|
||||
|
||||
it("ignores stale client onEvent callbacks after reconnect", () => {
|
||||
const host = createHost();
|
||||
|
||||
connectGateway(host);
|
||||
const firstClient = gatewayClientInstances[0];
|
||||
expect(firstClient).toBeDefined();
|
||||
|
||||
connectGateway(host);
|
||||
const secondClient = gatewayClientInstances[1];
|
||||
expect(secondClient).toBeDefined();
|
||||
|
||||
firstClient.emitEvent({ event: "presence", payload: { presence: [{ host: "stale" }] } });
|
||||
expect(host.eventLogBuffer).toHaveLength(0);
|
||||
|
||||
secondClient.emitEvent({ event: "presence", payload: { presence: [{ host: "active" }] } });
|
||||
expect(host.eventLogBuffer).toHaveLength(1);
|
||||
expect(host.eventLogBuffer[0]?.event).toBe("presence");
|
||||
});
|
||||
|
||||
it("ignores stale client onClose callbacks after reconnect", () => {
|
||||
const host = createHost();
|
||||
|
||||
connectGateway(host);
|
||||
const firstClient = gatewayClientInstances[0];
|
||||
expect(firstClient).toBeDefined();
|
||||
|
||||
connectGateway(host);
|
||||
const secondClient = gatewayClientInstances[1];
|
||||
expect(secondClient).toBeDefined();
|
||||
|
||||
firstClient.emitClose(1005);
|
||||
expect(host.lastError).toBeNull();
|
||||
|
||||
secondClient.emitClose(1005);
|
||||
expect(host.lastError).toBe("disconnected (1005): no reason");
|
||||
});
|
||||
});
|
||||
@@ -122,14 +122,17 @@ export function connectGateway(host: GatewayHost) {
|
||||
host.execApprovalQueue = [];
|
||||
host.execApprovalError = null;
|
||||
|
||||
host.client?.stop();
|
||||
host.client = new GatewayBrowserClient({
|
||||
const previousClient = host.client;
|
||||
const client = new GatewayBrowserClient({
|
||||
url: host.settings.gatewayUrl,
|
||||
token: host.settings.token.trim() ? host.settings.token : undefined,
|
||||
password: host.password.trim() ? host.password : undefined,
|
||||
clientName: "openclaw-control-ui",
|
||||
mode: "webchat",
|
||||
onHello: (hello) => {
|
||||
if (host.client !== client) {
|
||||
return;
|
||||
}
|
||||
host.connected = true;
|
||||
host.lastError = null;
|
||||
host.hello = hello;
|
||||
@@ -147,18 +150,31 @@ export function connectGateway(host: GatewayHost) {
|
||||
void refreshActiveTab(host as unknown as Parameters<typeof refreshActiveTab>[0]);
|
||||
},
|
||||
onClose: ({ code, reason }) => {
|
||||
if (host.client !== client) {
|
||||
return;
|
||||
}
|
||||
host.connected = false;
|
||||
// Code 1012 = Service Restart (expected during config saves, don't show as error)
|
||||
if (code !== 1012) {
|
||||
host.lastError = `disconnected (${code}): ${reason || "no reason"}`;
|
||||
}
|
||||
},
|
||||
onEvent: (evt) => handleGatewayEvent(host, evt),
|
||||
onEvent: (evt) => {
|
||||
if (host.client !== client) {
|
||||
return;
|
||||
}
|
||||
handleGatewayEvent(host, evt);
|
||||
},
|
||||
onGap: ({ expected, received }) => {
|
||||
if (host.client !== client) {
|
||||
return;
|
||||
}
|
||||
host.lastError = `event gap detected (expected seq ${expected}, got ${received}); refresh recommended`;
|
||||
},
|
||||
});
|
||||
host.client.start();
|
||||
host.client = client;
|
||||
previousClient?.stop();
|
||||
client.start();
|
||||
}
|
||||
|
||||
export function handleGatewayEvent(host: GatewayHost, evt: GatewayEventFrame) {
|
||||
|
||||
Reference in New Issue
Block a user