fix(sdk): settle transport connect on close

This commit is contained in:
Vincent Koc
2026-06-17 14:33:12 +02:00
parent 2e27a37791
commit 731dfcc5f9
2 changed files with 60 additions and 0 deletions

View File

@@ -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<typeof vi.fn>;
start: ReturnType<typeof vi.fn>;
stopAndWait: ReturnType<typeof vi.fn>;
};
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);
});
});

View File

@@ -78,6 +78,7 @@ export class GatewayClientTransport implements ConnectableOpenClawTransport {
private readonly options: GatewayClientTransportOptions;
private client: GatewayClientLike | null = null;
private connectPromise: Promise<void> | null = null;
private rejectPendingConnect: ((error: Error) => void) | null = null;
private closePromise: Promise<void> | null = null;
constructor(options: GatewayClientTransportOptions = {}) {
@@ -89,6 +90,7 @@ export class GatewayClientTransport implements ConnectableOpenClawTransport {
return this.connectPromise;
}
this.connectPromise = new Promise<void>((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;