From 63965dc70ba013583df42d566c17990eac5dc0fe Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 13 Apr 2026 03:23:51 -0700 Subject: [PATCH] test: stabilize gateway wake gating regression --- .../server-startup-post-attach.test.ts | 48 ++++++--- src/gateway/server-startup-post-attach.ts | 101 +++++++++++------- 2 files changed, 94 insertions(+), 55 deletions(-) diff --git a/src/gateway/server-startup-post-attach.test.ts b/src/gateway/server-startup-post-attach.test.ts index 8bd968955e8..6a018366073 100644 --- a/src/gateway/server-startup-post-attach.test.ts +++ b/src/gateway/server-startup-post-attach.test.ts @@ -110,6 +110,7 @@ const { STARTUP_UNAVAILABLE_GATEWAY_METHODS } = await import("./server-startup-unavailable-methods.js"); type PostAttachParams = Parameters[0]; +type PostAttachRuntimeDeps = NonNullable[1]>; describe("startGatewayPostAttachRuntime", () => { beforeEach(() => { @@ -144,35 +145,54 @@ describe("startGatewayPostAttachRuntime", () => { }); it("keeps startup-gated methods unavailable while sidecars are still resuming", async () => { - let resumeChannels!: () => void; - const channelsReady = new Promise((resolve) => { - resumeChannels = resolve; + let resumeSidecars!: () => void; + const sidecarsReady = new Promise<{ pluginServices: null }>((resolve) => { + resumeSidecars = () => resolve({ pluginServices: null }); }); - const startChannels = vi.fn(async () => { - await channelsReady; + const startGatewaySidecars = vi.fn(async () => { + return await sidecarsReady; }); const unavailableGatewayMethods = new Set(STARTUP_UNAVAILABLE_GATEWAY_METHODS); - const startup = startGatewayPostAttachRuntime({ - ...createPostAttachParams({ startChannels }), - unavailableGatewayMethods, - }); + const startup = startGatewayPostAttachRuntime( + { + ...createPostAttachParams(), + unavailableGatewayMethods, + }, + createPostAttachRuntimeDeps({ startGatewaySidecars }), + ); - await vi.waitFor(() => { - expect(startChannels).toHaveBeenCalledTimes(1); - }); + await vi.waitFor( + () => { + expect(startGatewaySidecars).toHaveBeenCalledTimes(1); + }, + { timeout: 10_000 }, + ); expect([...unavailableGatewayMethods]).toEqual([...STARTUP_UNAVAILABLE_GATEWAY_METHODS]); expect(hoisted.startPluginServices).not.toHaveBeenCalled(); - resumeChannels(); + resumeSidecars(); await startup; expect([...unavailableGatewayMethods]).toEqual([]); - expect(hoisted.startPluginServices).toHaveBeenCalledTimes(1); + expect(startGatewaySidecars).toHaveBeenCalledTimes(1); }); }); +function createPostAttachRuntimeDeps( + overrides: Partial = {}, +): PostAttachRuntimeDeps { + return { + getGlobalHookRunner: vi.fn(() => null), + logGatewayStartup: hoisted.logGatewayStartup, + scheduleGatewayUpdateCheck: hoisted.scheduleGatewayUpdateCheck, + startGatewaySidecars: vi.fn(async () => ({ pluginServices: null })), + startGatewayTailscaleExposure: hoisted.startGatewayTailscaleExposure, + ...overrides, + }; +} + function createPostAttachParams(overrides: Partial = {}): PostAttachParams { return { minimalTestGateway: false, diff --git a/src/gateway/server-startup-post-attach.ts b/src/gateway/server-startup-post-attach.ts index 57205937596..8a54893d220 100644 --- a/src/gateway/server-startup-post-attach.ts +++ b/src/gateway/server-startup-post-attach.ts @@ -238,43 +238,62 @@ export async function startGatewaySidecars(params: { return { pluginServices }; } -export async function startGatewayPostAttachRuntime(params: { - minimalTestGateway: boolean; - cfgAtStart: OpenClawConfig; - bindHost: string; - bindHosts: string[]; - port: number; - tlsEnabled: boolean; - log: { - info: (msg: string) => void; - warn: (msg: string) => void; - }; - isNixMode: boolean; - startupStartedAt?: number; - broadcast: (event: string, payload: unknown, opts?: { dropIfSlow?: boolean }) => void; - tailscaleMode: GatewayTailscaleMode; - resetOnExit: boolean; - controlUiBasePath: string; - logTailscale: { - info: (msg: string) => void; - warn: (msg: string) => void; - error: (msg: string) => void; - debug?: (msg: string) => void; - }; - gatewayPluginConfigAtStart: OpenClawConfig; - pluginRegistry: ReturnType; - defaultWorkspaceDir: string; - deps: CliDeps; - startChannels: () => Promise; - logHooks: { - info: (msg: string) => void; - warn: (msg: string) => void; - error: (msg: string) => void; - }; - logChannels: { info: (msg: string) => void; error: (msg: string) => void }; - unavailableGatewayMethods: Set; -}) { - logGatewayStartup({ +type GatewayPostAttachRuntimeDeps = { + getGlobalHookRunner: typeof getGlobalHookRunner; + logGatewayStartup: typeof logGatewayStartup; + scheduleGatewayUpdateCheck: typeof scheduleGatewayUpdateCheck; + startGatewaySidecars: typeof startGatewaySidecars; + startGatewayTailscaleExposure: typeof startGatewayTailscaleExposure; +}; + +const defaultGatewayPostAttachRuntimeDeps: GatewayPostAttachRuntimeDeps = { + getGlobalHookRunner, + logGatewayStartup, + scheduleGatewayUpdateCheck, + startGatewaySidecars, + startGatewayTailscaleExposure, +}; + +export async function startGatewayPostAttachRuntime( + params: { + minimalTestGateway: boolean; + cfgAtStart: OpenClawConfig; + bindHost: string; + bindHosts: string[]; + port: number; + tlsEnabled: boolean; + log: { + info: (msg: string) => void; + warn: (msg: string) => void; + }; + isNixMode: boolean; + startupStartedAt?: number; + broadcast: (event: string, payload: unknown, opts?: { dropIfSlow?: boolean }) => void; + tailscaleMode: GatewayTailscaleMode; + resetOnExit: boolean; + controlUiBasePath: string; + logTailscale: { + info: (msg: string) => void; + warn: (msg: string) => void; + error: (msg: string) => void; + debug?: (msg: string) => void; + }; + gatewayPluginConfigAtStart: OpenClawConfig; + pluginRegistry: ReturnType; + defaultWorkspaceDir: string; + deps: CliDeps; + startChannels: () => Promise; + logHooks: { + info: (msg: string) => void; + warn: (msg: string) => void; + error: (msg: string) => void; + }; + logChannels: { info: (msg: string) => void; error: (msg: string) => void }; + unavailableGatewayMethods: Set; + }, + runtimeDeps: GatewayPostAttachRuntimeDeps = defaultGatewayPostAttachRuntimeDeps, +) { + runtimeDeps.logGatewayStartup({ cfg: params.cfgAtStart, bindHost: params.bindHost, bindHosts: params.bindHosts, @@ -290,7 +309,7 @@ export async function startGatewayPostAttachRuntime(params: { const stopGatewayUpdateCheck = params.minimalTestGateway ? () => {} - : scheduleGatewayUpdateCheck({ + : runtimeDeps.scheduleGatewayUpdateCheck({ cfg: params.cfgAtStart, log: params.log, isNixMode: params.isNixMode, @@ -302,7 +321,7 @@ export async function startGatewayPostAttachRuntime(params: { const tailscaleCleanup = params.minimalTestGateway ? null - : await startGatewayTailscaleExposure({ + : await runtimeDeps.startGatewayTailscaleExposure({ tailscaleMode: params.tailscaleMode, resetOnExit: params.resetOnExit, port: params.port, @@ -313,7 +332,7 @@ export async function startGatewayPostAttachRuntime(params: { let pluginServices: PluginServicesHandle | null = null; if (!params.minimalTestGateway) { params.log.info("starting channels and sidecars..."); - ({ pluginServices } = await startGatewaySidecars({ + ({ pluginServices } = await runtimeDeps.startGatewaySidecars({ cfg: params.gatewayPluginConfigAtStart, pluginRegistry: params.pluginRegistry, defaultWorkspaceDir: params.defaultWorkspaceDir, @@ -329,7 +348,7 @@ export async function startGatewayPostAttachRuntime(params: { } if (!params.minimalTestGateway) { - const hookRunner = getGlobalHookRunner(); + const hookRunner = runtimeDeps.getGlobalHookRunner(); if (hookRunner?.hasHooks("gateway_start")) { void hookRunner.runGatewayStart({ port: params.port }, { port: params.port }).catch((err) => { params.log.warn(`gateway_start hook failed: ${String(err)}`);