Skip to content

Commit 731dfcc

Browse files
committed
fix(sdk): settle transport connect on close
1 parent 2e27a37 commit 731dfcc

2 files changed

Lines changed: 60 additions & 0 deletions

File tree

packages/sdk/src/transport.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// OpenClaw SDK tests cover transport behavior.
2+
import { beforeEach, describe, expect, it, vi } from "vitest";
3+
import { GatewayClientTransport } from "./transport.js";
4+
5+
type MockGatewayClientInstance = {
6+
opts: {
7+
onConnectError?: (error: Error) => void;
8+
onHelloOk?: (hello: unknown) => void;
9+
};
10+
request: ReturnType<typeof vi.fn>;
11+
start: ReturnType<typeof vi.fn>;
12+
stopAndWait: ReturnType<typeof vi.fn>;
13+
};
14+
15+
const gatewayClientMocks = vi.hoisted(() => ({
16+
instances: [] as MockGatewayClientInstance[],
17+
}));
18+
19+
vi.mock("@openclaw/gateway-client", () => ({
20+
GatewayClient: class {
21+
readonly opts: MockGatewayClientInstance["opts"];
22+
readonly request = vi.fn();
23+
readonly start = vi.fn();
24+
readonly stopAndWait = vi.fn(async () => {});
25+
26+
constructor(opts: MockGatewayClientInstance["opts"]) {
27+
this.opts = opts;
28+
gatewayClientMocks.instances.push(this);
29+
}
30+
},
31+
}));
32+
33+
describe("GatewayClientTransport", () => {
34+
beforeEach(() => {
35+
gatewayClientMocks.instances.length = 0;
36+
});
37+
38+
it("rejects a pending connect when the transport closes before hello-ok", async () => {
39+
const transport = new GatewayClientTransport();
40+
41+
const connect = transport.connect();
42+
const connectExpectation = expect(connect).rejects.toThrow(
43+
"gateway transport closed before connect completed",
44+
);
45+
const client = gatewayClientMocks.instances[0];
46+
expect(client?.start).toHaveBeenCalledTimes(1);
47+
48+
await transport.close();
49+
50+
await connectExpectation;
51+
expect(client?.stopAndWait).toHaveBeenCalledTimes(1);
52+
});
53+
});

packages/sdk/src/transport.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export class GatewayClientTransport implements ConnectableOpenClawTransport {
7878
private readonly options: GatewayClientTransportOptions;
7979
private client: GatewayClientLike | null = null;
8080
private connectPromise: Promise<void> | null = null;
81+
private rejectPendingConnect: ((error: Error) => void) | null = null;
8182
private closePromise: Promise<void> | null = null;
8283

8384
constructor(options: GatewayClientTransportOptions = {}) {
@@ -89,6 +90,7 @@ export class GatewayClientTransport implements ConnectableOpenClawTransport {
8990
return this.connectPromise;
9091
}
9192
this.connectPromise = new Promise<void>((resolve, reject) => {
93+
this.rejectPendingConnect = reject;
9294
const client = new GatewayClient({
9395
...this.options,
9496
onEvent: (event: unknown) => {
@@ -98,6 +100,7 @@ export class GatewayClientTransport implements ConnectableOpenClawTransport {
98100
},
99101
onHelloOk: (_hello: unknown) => {
100102
this.options.onHelloOk?.(_hello);
103+
this.rejectPendingConnect = null;
101104
resolve();
102105
},
103106
onConnectError: (error: Error) => {
@@ -109,6 +112,7 @@ export class GatewayClientTransport implements ConnectableOpenClawTransport {
109112
this.connectPromise = null;
110113
}
111114
void client.stopAndWait().catch(() => {});
115+
this.rejectPendingConnect = null;
112116
reject(error);
113117
},
114118
onReconnectPaused: this.options.onReconnectPaused,
@@ -145,6 +149,9 @@ export class GatewayClientTransport implements ConnectableOpenClawTransport {
145149
this.eventsHub.close();
146150
const client = this.client;
147151
this.client = null;
152+
const rejectPendingConnect = this.rejectPendingConnect;
153+
this.rejectPendingConnect = null;
154+
rejectPendingConnect?.(new Error("gateway transport closed before connect completed"));
148155
this.connectPromise = null;
149156
this.closePromise = client?.stopAndWait() ?? Promise.resolve();
150157
await this.closePromise;

0 commit comments

Comments
 (0)