mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 12:28:37 +00:00
fix(gateway): make stale token cleanup non-fatal
This commit is contained in:
150
src/gateway/client.test.ts
Normal file
150
src/gateway/client.test.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { Buffer } from "node:buffer";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { DeviceIdentity } from "../infra/device-identity.js";
|
||||
|
||||
const wsInstances = vi.hoisted((): MockWebSocket[] => []);
|
||||
const clearDeviceAuthTokenMock = vi.hoisted(() => vi.fn());
|
||||
const logDebugMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
type WsEvent = "open" | "message" | "close" | "error";
|
||||
type WsEventHandlers = {
|
||||
open: () => void;
|
||||
message: (data: string | Buffer) => void;
|
||||
close: (code: number, reason: Buffer) => void;
|
||||
error: (err: unknown) => void;
|
||||
};
|
||||
|
||||
class MockWebSocket {
|
||||
private openHandlers: WsEventHandlers["open"][] = [];
|
||||
private messageHandlers: WsEventHandlers["message"][] = [];
|
||||
private closeHandlers: WsEventHandlers["close"][] = [];
|
||||
private errorHandlers: WsEventHandlers["error"][] = [];
|
||||
|
||||
constructor(_url: string, _options?: unknown) {
|
||||
wsInstances.push(this);
|
||||
}
|
||||
|
||||
on(event: "open", handler: WsEventHandlers["open"]): void;
|
||||
on(event: "message", handler: WsEventHandlers["message"]): void;
|
||||
on(event: "close", handler: WsEventHandlers["close"]): void;
|
||||
on(event: "error", handler: WsEventHandlers["error"]): void;
|
||||
on(event: WsEvent, handler: WsEventHandlers[WsEvent]): void {
|
||||
switch (event) {
|
||||
case "open":
|
||||
this.openHandlers.push(handler as WsEventHandlers["open"]);
|
||||
return;
|
||||
case "message":
|
||||
this.messageHandlers.push(handler as WsEventHandlers["message"]);
|
||||
return;
|
||||
case "close":
|
||||
this.closeHandlers.push(handler as WsEventHandlers["close"]);
|
||||
return;
|
||||
case "error":
|
||||
this.errorHandlers.push(handler as WsEventHandlers["error"]);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
close(_code?: number, _reason?: string): void {}
|
||||
|
||||
emitClose(code: number, reason: string): void {
|
||||
for (const handler of this.closeHandlers) {
|
||||
handler(code, Buffer.from(reason));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vi.mock("ws", () => ({
|
||||
WebSocket: MockWebSocket,
|
||||
}));
|
||||
|
||||
vi.mock("../infra/device-auth-store.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../infra/device-auth-store.js")>();
|
||||
return {
|
||||
...actual,
|
||||
clearDeviceAuthToken: (...args: unknown[]) => clearDeviceAuthTokenMock(...args),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../logger.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../logger.js")>();
|
||||
return {
|
||||
...actual,
|
||||
logDebug: (...args: unknown[]) => logDebugMock(...args),
|
||||
};
|
||||
});
|
||||
|
||||
const { GatewayClient } = await import("./client.js");
|
||||
|
||||
function getLatestWs(): MockWebSocket {
|
||||
const ws = wsInstances.at(-1);
|
||||
if (!ws) {
|
||||
throw new Error("missing mock websocket instance");
|
||||
}
|
||||
return ws;
|
||||
}
|
||||
|
||||
describe("GatewayClient close handling", () => {
|
||||
beforeEach(() => {
|
||||
wsInstances.length = 0;
|
||||
clearDeviceAuthTokenMock.mockReset();
|
||||
logDebugMock.mockReset();
|
||||
});
|
||||
|
||||
it("clears stale token on device token mismatch close", () => {
|
||||
const onClose = vi.fn();
|
||||
const identity: DeviceIdentity = {
|
||||
deviceId: "dev-1",
|
||||
privateKeyPem: "private-key",
|
||||
publicKeyPem: "public-key",
|
||||
};
|
||||
const client = new GatewayClient({
|
||||
url: "ws://127.0.0.1:18789",
|
||||
deviceIdentity: identity,
|
||||
onClose,
|
||||
});
|
||||
|
||||
client.start();
|
||||
getLatestWs().emitClose(
|
||||
1008,
|
||||
"unauthorized: DEVICE token mismatch (rotate/reissue device token)",
|
||||
);
|
||||
|
||||
expect(clearDeviceAuthTokenMock).toHaveBeenCalledWith({ deviceId: "dev-1", role: "operator" });
|
||||
expect(onClose).toHaveBeenCalledWith(
|
||||
1008,
|
||||
"unauthorized: DEVICE token mismatch (rotate/reissue device token)",
|
||||
);
|
||||
client.stop();
|
||||
});
|
||||
|
||||
it("does not break close flow when token clear throws", () => {
|
||||
clearDeviceAuthTokenMock.mockImplementation(() => {
|
||||
throw new Error("disk unavailable");
|
||||
});
|
||||
const onClose = vi.fn();
|
||||
const identity: DeviceIdentity = {
|
||||
deviceId: "dev-2",
|
||||
privateKeyPem: "private-key",
|
||||
publicKeyPem: "public-key",
|
||||
};
|
||||
const client = new GatewayClient({
|
||||
url: "ws://127.0.0.1:18789",
|
||||
deviceIdentity: identity,
|
||||
onClose,
|
||||
});
|
||||
|
||||
client.start();
|
||||
expect(() => {
|
||||
getLatestWs().emitClose(1008, "unauthorized: device token mismatch");
|
||||
}).not.toThrow();
|
||||
|
||||
expect(logDebugMock).toHaveBeenCalledWith(
|
||||
expect.stringContaining("failed clearing stale device-auth token"),
|
||||
);
|
||||
expect(onClose).toHaveBeenCalledWith(1008, "unauthorized: device token mismatch");
|
||||
client.stop();
|
||||
});
|
||||
});
|
||||
@@ -157,12 +157,20 @@ export class GatewayClient {
|
||||
// If closed due to device token mismatch, clear the stored token so next attempt can get a fresh one
|
||||
if (
|
||||
code === 1008 &&
|
||||
reasonText.includes("device token mismatch") &&
|
||||
reasonText.toLowerCase().includes("device token mismatch") &&
|
||||
this.opts.deviceIdentity
|
||||
) {
|
||||
const role = this.opts.role ?? "operator";
|
||||
clearDeviceAuthToken({ deviceId: this.opts.deviceIdentity.deviceId, role });
|
||||
logDebug(`cleared stale device-auth token for device ${this.opts.deviceIdentity.deviceId}`);
|
||||
try {
|
||||
clearDeviceAuthToken({ deviceId: this.opts.deviceIdentity.deviceId, role });
|
||||
logDebug(
|
||||
`cleared stale device-auth token for device ${this.opts.deviceIdentity.deviceId}`,
|
||||
);
|
||||
} catch (err) {
|
||||
logDebug(
|
||||
`failed clearing stale device-auth token for device ${this.opts.deviceIdentity.deviceId}: ${String(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
this.flushPendingErrors(new Error(`gateway closed (${code}): ${reasonText}`));
|
||||
this.scheduleReconnect();
|
||||
|
||||
Reference in New Issue
Block a user