diff --git a/src/gateway/call.test.ts b/src/gateway/call.test.ts index cd284e00309..da4c895e2e6 100644 --- a/src/gateway/call.test.ts +++ b/src/gateway/call.test.ts @@ -875,6 +875,61 @@ describe("callGateway error details", () => { expect(callResolved).toBe(true); }); + it("clears the wrapper timeout before awaiting gateway teardown", async () => { + setLocalLoopbackGatewayConfig(); + + vi.useFakeTimers(); + let releaseStop!: () => void; + let stopStarted = false; + + __testing.setDepsForTests({ + createGatewayClient: (opts) => + ({ + async request( + method: string, + params: unknown, + requestOpts?: { expectFinal?: boolean; timeoutMs?: number | null }, + ) { + lastRequestOptions = { method, params, opts: requestOpts }; + return { ok: true }; + }, + start() { + opts.onHelloOk?.({ + features: { + methods: helloMethods ?? [], + events: [], + }, + } as unknown as Parameters>[0]); + }, + stop() {}, + async stopAndWait() { + stopStarted = true; + await new Promise((resolve) => { + releaseStop = resolve; + }); + }, + }) as never, + loadConfig: loadConfig as unknown as () => OpenClawConfig, + loadOrCreateDeviceIdentity: () => deviceIdentityState.value, + resolveGatewayPort: resolveGatewayPort as unknown as ( + cfg?: OpenClawConfig, + env?: NodeJS.ProcessEnv, + ) => number, + }); + + const promise = callGateway<{ ok: true }>({ method: "health", timeoutMs: 5 }); + + await vi.waitFor(() => { + expect(stopStarted).toBe(true); + }); + + await vi.advanceTimersByTimeAsync(5); + + releaseStop(); + + await expect(promise).resolves.toEqual({ ok: true }); + }); + it("fails fast when remote mode is missing remote url", async () => { loadConfig.mockReturnValue({ gateway: { mode: "remote", bind: "loopback", remote: {} }, diff --git a/src/gateway/call.ts b/src/gateway/call.ts index b2b8f549de0..8a39007c31a 100644 --- a/src/gateway/call.ts +++ b/src/gateway/call.ts @@ -490,11 +490,13 @@ async function executeGatewayRequestWithScopes(params: { } settled = true; clearTimeout(timer); - if (err) { - reject(err); - } else { - resolve(value as T); - } + void stopGatewayClient(client).finally(() => { + if (err) { + reject(err); + } else { + resolve(value as T); + } + }); }; const client = gatewayCallDeps.createGatewayClient({ @@ -528,11 +530,9 @@ async function executeGatewayRequestWithScopes(params: { timeoutMs: opts.timeoutMs, }); ignoreClose = true; - await stopGatewayClient(client); stop(undefined, result); } catch (err) { ignoreClose = true; - await stopGatewayClient(client); stop(err as Error); } }, @@ -541,17 +541,13 @@ async function executeGatewayRequestWithScopes(params: { return; } ignoreClose = true; - void stopGatewayClient(client).finally(() => { - stop(new Error(formatGatewayCloseError(code, reason, params.connectionDetails))); - }); + stop(new Error(formatGatewayCloseError(code, reason, params.connectionDetails))); }, }); const timer = setTimeout(() => { ignoreClose = true; - void stopGatewayClient(client).finally(() => { - stop(new Error(formatGatewayTimeoutError(timeoutMs, params.connectionDetails))); - }); + stop(new Error(formatGatewayTimeoutError(timeoutMs, params.connectionDetails))); }, safeTimerTimeoutMs); client.start();