Control UI: clear queued connect timeout on stop (#57338)

Merged via squash.

Prepared head SHA: a359fe8367
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
Gustavo Madeira Santana 2026-03-29 20:54:21 -04:00 committed by GitHub
parent fb81e3fc7f
commit b952e404fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 44 additions and 4 deletions

View File

@ -227,6 +227,7 @@ Docs: https://docs.openclaw.ai
- Gateway/SQLite transient handling: keep unhandled `SQLITE_CANTOPEN`, `SQLITE_BUSY`, `SQLITE_LOCKED`, and `SQLITE_IOERR` failures non-fatal in the global rejection handler so macOS LaunchAgent restarts do not enter a crash-throttle loop. (#57018)
- Control UI/gateway: reconnect the browser client when gateway event sequence gaps are detected, so stale non-chat state recovers automatically instead of only telling the user to refresh. (#23912) thanks @Olshansk.
- ClawDock/docs: move the helper scripts to `scripts/clawdock`, publish ClawDock as a first-class docs page on the docs site, and document reinstalling local helper copies from the new raw GitHub path. (#23912) thanks @Olshansk.
- Control UI/gateway: clear queued browser connect timeouts on client stop so aborted or replaced gateway clients do not send delayed connect requests after shutdown. (#57338) thanks @gumadeiras.
## 2026.3.24

View File

@ -92,6 +92,16 @@ type ConnectFrame = {
};
};
function stubWindowGlobals(storage?: ReturnType<typeof createStorageMock>) {
vi.stubGlobal("window", {
location: { href: "http://127.0.0.1:18789/" },
localStorage: storage,
setTimeout: (handler: (...args: unknown[]) => void, timeout?: number, ...args: unknown[]) =>
globalThis.setTimeout(() => handler(...args), timeout),
clearTimeout: (timeoutId: number | undefined) => globalThis.clearTimeout(timeoutId),
});
}
function getLatestWebSocket(): MockWebSocket {
const ws = wsInstances.at(-1);
if (!ws) {
@ -182,10 +192,7 @@ describe("GatewayBrowserClient", () => {
});
vi.stubGlobal("localStorage", storage);
Object.defineProperty(window, "localStorage", {
configurable: true,
value: storage,
});
stubWindowGlobals(storage);
localStorage.clear();
vi.stubGlobal("WebSocket", MockWebSocket);
@ -377,6 +384,26 @@ describe("GatewayBrowserClient", () => {
vi.useRealTimers();
});
it("cancels a queued connect send when stopped before the timeout fires", async () => {
vi.useFakeTimers();
const client = new GatewayBrowserClient({
url: "ws://127.0.0.1:18789",
token: "shared-auth-token",
});
client.start();
const ws = getLatestWebSocket();
ws.emitOpen();
client.stop();
await vi.advanceTimersByTimeAsync(750);
expect(ws.sent).toHaveLength(0);
vi.useRealTimers();
});
it("does not auto-reconnect on AUTH_TOKEN_MISSING", async () => {
vi.useFakeTimers();
localStorage.clear();
@ -408,6 +435,14 @@ describe("GatewayBrowserClient", () => {
});
describe("shouldRetryWithDeviceToken", () => {
beforeEach(() => {
stubWindowGlobals();
});
afterEach(() => {
vi.unstubAllGlobals();
});
it("allows a bounded retry for trusted loopback endpoints", () => {
expect(
shouldRetryWithDeviceToken({

View File

@ -294,6 +294,10 @@ export class GatewayBrowserClient {
stop() {
this.closed = true;
if (this.connectTimer !== null) {
window.clearTimeout(this.connectTimer);
this.connectTimer = null;
}
this.ws?.close();
this.ws = null;
this.pendingConnectError = undefined;