From f48a89cb1cac8b49ac38dacd9aaa7974efcc2fa9 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 28 May 2026 14:52:47 +0200 Subject: [PATCH] fix(test): detect signaled cross-os gateway exits --- scripts/openclaw-cross-os-release-checks.ts | 12 +++++++---- .../openclaw-cross-os-release-checks.test.ts | 20 ++++++++++++++++++- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/scripts/openclaw-cross-os-release-checks.ts b/scripts/openclaw-cross-os-release-checks.ts index 87eb4ffa556..893cb934928 100644 --- a/scripts/openclaw-cross-os-release-checks.ts +++ b/scripts/openclaw-cross-os-release-checks.ts @@ -3359,7 +3359,11 @@ async function runDashboardSmoke(params) { throw new Error(`Dashboard HTML did not become ready at ${dashboardUrl}.`); } -async function stopGateway(gateway) { +export function hasChildExited(child) { + return child.exitCode !== null || (child.signalCode ?? null) !== null; +} + +export async function stopGateway(gateway) { try { if (!gateway?.child?.pid) { return; @@ -3377,12 +3381,12 @@ async function stopGateway(gateway) { } return; } - if (gateway.child.exitCode !== null) { + if (hasChildExited(gateway.child)) { return; } gateway.child.kill("SIGTERM"); const exitedAfterTerm = await waitForChildExit(gateway.child, 2_000); - if (!exitedAfterTerm && gateway.child.exitCode === null) { + if (!exitedAfterTerm && !hasChildExited(gateway.child)) { gateway.child.kill("SIGKILL"); await waitForChildExit(gateway.child, 5_000); } @@ -3392,7 +3396,7 @@ async function stopGateway(gateway) { } async function waitForChildExit(child, timeoutMs) { - if (child.exitCode !== null) { + if (hasChildExited(child)) { return true; } return new Promise((resolvePromise) => { diff --git a/test/scripts/openclaw-cross-os-release-checks.test.ts b/test/scripts/openclaw-cross-os-release-checks.test.ts index d6adb24607a..a16addfb4d0 100644 --- a/test/scripts/openclaw-cross-os-release-checks.test.ts +++ b/test/scripts/openclaw-cross-os-release-checks.test.ts @@ -11,7 +11,7 @@ import { createConnection as createNetConnection, createServer as createNetServe import { tmpdir } from "node:os"; import { join, win32 } from "node:path"; import { setTimeout as delay } from "node:timers/promises"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { LOCAL_BUILD_METADATA_DIST_PATHS } from "../../scripts/lib/local-build-metadata-paths.mjs"; import { agentOutputHasExpectedOkMarker, @@ -40,6 +40,7 @@ import { CROSS_OS_DASHBOARD_SMOKE_TIMEOUT_MS, CROSS_OS_AGENT_TURN_TIMEOUT_SECONDS, CROSS_OS_COMMAND_HEARTBEAT_SECONDS, + hasChildExited, isImmutableReleaseRef, isRecoverableWindowsPackagedUpgradeSwapCleanupFailure, isRecoverableWindowsPackagedUpgradeTimeoutError, @@ -78,6 +79,7 @@ import { shouldSkipOptionalCrossOsAgentTurnError, shouldUseManagedGatewayForInstallerRuntime, shouldUseManagedGatewayService, + stopGateway, verifyDevUpdateStatus, verifyPackagedUpgradeUpdateResult, writePackageDistInventoryForCandidate, @@ -927,6 +929,22 @@ describe("scripts/openclaw-cross-os-release-checks", () => { } }); + it("treats signaled managed gateway children as exited", async () => { + const child = { + exitCode: null, + kill: vi.fn(), + pid: 1234, + signalCode: "SIGTERM", + }; + const closeLog = vi.fn(); + + expect(hasChildExited(child)).toBe(true); + await stopGateway({ child, closeLog }); + + expect(child.kill).not.toHaveBeenCalled(); + expect(closeLog).toHaveBeenCalledTimes(1); + }); + it("derives the installed prefix from resolved CLI paths", () => { expect( resolveInstalledPrefixDirFromCliPath(