From 731dfcc5f9d8f4e46d554d095ef34be98d154e22 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 17 Jun 2026 14:33:12 +0200 Subject: [PATCH] fix(sdk): settle transport connect on close --- packages/sdk/src/transport.test.ts | 53 ++++++++++++++++++++++++++++++ packages/sdk/src/transport.ts | 7 ++++ 2 files changed, 60 insertions(+) create mode 100644 packages/sdk/src/transport.test.ts diff --git a/packages/sdk/src/transport.test.ts b/packages/sdk/src/transport.test.ts new file mode 100644 index 00000000000..312bc36cfb8 --- /dev/null +++ b/packages/sdk/src/transport.test.ts @@ -0,0 +1,53 @@ +// OpenClaw SDK tests cover transport behavior. +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { GatewayClientTransport } from "./transport.js"; + +type MockGatewayClientInstance = { + opts: { + onConnectError?: (error: Error) => void; + onHelloOk?: (hello: unknown) => void; + }; + request: ReturnType; + start: ReturnType; + stopAndWait: ReturnType; +}; + +const gatewayClientMocks = vi.hoisted(() => ({ + instances: [] as MockGatewayClientInstance[], +})); + +vi.mock("@openclaw/gateway-client", () => ({ + GatewayClient: class { + readonly opts: MockGatewayClientInstance["opts"]; + readonly request = vi.fn(); + readonly start = vi.fn(); + readonly stopAndWait = vi.fn(async () => {}); + + constructor(opts: MockGatewayClientInstance["opts"]) { + this.opts = opts; + gatewayClientMocks.instances.push(this); + } + }, +})); + +describe("GatewayClientTransport", () => { + beforeEach(() => { + gatewayClientMocks.instances.length = 0; + }); + + it("rejects a pending connect when the transport closes before hello-ok", async () => { + const transport = new GatewayClientTransport(); + + const connect = transport.connect(); + const connectExpectation = expect(connect).rejects.toThrow( + "gateway transport closed before connect completed", + ); + const client = gatewayClientMocks.instances[0]; + expect(client?.start).toHaveBeenCalledTimes(1); + + await transport.close(); + + await connectExpectation; + expect(client?.stopAndWait).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/sdk/src/transport.ts b/packages/sdk/src/transport.ts index 858989410f8..075a0c0b908 100644 --- a/packages/sdk/src/transport.ts +++ b/packages/sdk/src/transport.ts @@ -78,6 +78,7 @@ export class GatewayClientTransport implements ConnectableOpenClawTransport { private readonly options: GatewayClientTransportOptions; private client: GatewayClientLike | null = null; private connectPromise: Promise | null = null; + private rejectPendingConnect: ((error: Error) => void) | null = null; private closePromise: Promise | null = null; constructor(options: GatewayClientTransportOptions = {}) { @@ -89,6 +90,7 @@ export class GatewayClientTransport implements ConnectableOpenClawTransport { return this.connectPromise; } this.connectPromise = new Promise((resolve, reject) => { + this.rejectPendingConnect = reject; const client = new GatewayClient({ ...this.options, onEvent: (event: unknown) => { @@ -98,6 +100,7 @@ export class GatewayClientTransport implements ConnectableOpenClawTransport { }, onHelloOk: (_hello: unknown) => { this.options.onHelloOk?.(_hello); + this.rejectPendingConnect = null; resolve(); }, onConnectError: (error: Error) => { @@ -109,6 +112,7 @@ export class GatewayClientTransport implements ConnectableOpenClawTransport { this.connectPromise = null; } void client.stopAndWait().catch(() => {}); + this.rejectPendingConnect = null; reject(error); }, onReconnectPaused: this.options.onReconnectPaused, @@ -145,6 +149,9 @@ export class GatewayClientTransport implements ConnectableOpenClawTransport { this.eventsHub.close(); const client = this.client; this.client = null; + const rejectPendingConnect = this.rejectPendingConnect; + this.rejectPendingConnect = null; + rejectPendingConnect?.(new Error("gateway transport closed before connect completed")); this.connectPromise = null; this.closePromise = client?.stopAndWait() ?? Promise.resolve(); await this.closePromise;