diff --git a/src/cli/gateway-cli.coverage.test.ts b/src/cli/gateway-cli.coverage.test.ts index 8ebe1352895..878934b5b51 100644 --- a/src/cli/gateway-cli.coverage.test.ts +++ b/src/cli/gateway-cli.coverage.test.ts @@ -228,16 +228,59 @@ describe("gateway-cli coverage", () => { }); it("prints stop hints on GatewayLockError when service is loaded", async () => { + await withEnvOverride( + { + LAUNCH_JOB_LABEL: undefined, + LAUNCH_JOB_NAME: undefined, + XPC_SERVICE_NAME: undefined, + OPENCLAW_LAUNCHD_LABEL: undefined, + OPENCLAW_SYSTEMD_UNIT: undefined, + INVOCATION_ID: undefined, + SYSTEMD_EXEC_PID: undefined, + JOURNAL_STREAM: undefined, + OPENCLAW_WINDOWS_TASK_NAME: undefined, + OPENCLAW_SERVICE_MARKER: undefined, + OPENCLAW_SERVICE_KIND: undefined, + }, + async () => { + resetRuntimeCapture(); + serviceIsLoaded.mockResolvedValue(true); + startGatewayServer.mockRejectedValueOnce( + new GatewayLockError("another gateway instance is already listening"), + ); + await expect( + runGatewayCommand(["gateway", "--token", "test-token", "--allow-unconfigured"]), + ).rejects.toThrow("__exit__:0"); + + expect(startGatewayServer).toHaveBeenCalled(); + expect(runtimeErrors.join("\n")).toContain("Gateway failed to start:"); + expect(runtimeErrors.join("\n")).toContain("gateway stop"); + }, + ); + }); + + it("keeps exit 1 for gateway bind failures wrapped as GatewayLockError", async () => { resetRuntimeCapture(); serviceIsLoaded.mockResolvedValue(true); startGatewayServer.mockRejectedValueOnce( - new GatewayLockError("another gateway instance is already listening"), + new GatewayLockError("failed to bind gateway socket on ws://127.0.0.1:18789: Error: boom"), ); + await expectGatewayExit(["gateway", "--token", "test-token", "--allow-unconfigured"]); - expect(startGatewayServer).toHaveBeenCalled(); - expect(runtimeErrors.join("\n")).toContain("Gateway failed to start:"); - expect(runtimeErrors.join("\n")).toContain("gateway stop"); + expect(runtimeErrors.join("\n")).toContain("failed to bind gateway socket"); + }); + + it("keeps exit 1 for gateway lock acquisition failures", async () => { + resetRuntimeCapture(); + serviceIsLoaded.mockResolvedValue(true); + startGatewayServer.mockRejectedValueOnce( + new GatewayLockError("failed to acquire gateway lock at /tmp/openclaw/gateway.lock"), + ); + + await expectGatewayExit(["gateway", "--token", "test-token", "--allow-unconfigured"]); + + expect(runtimeErrors.join("\n")).toContain("failed to acquire gateway lock"); }); it("uses env/config port when --port is omitted", async () => { diff --git a/src/cli/gateway-cli/run.ts b/src/cli/gateway-cli/run.ts index c6c6dde43b4..fd1ebcd97e5 100644 --- a/src/cli/gateway-cli/run.ts +++ b/src/cli/gateway-cli/run.ts @@ -162,6 +162,23 @@ function resolveGatewayRunOptions(opts: GatewayRunOpts, command?: Command): Gate return resolved; } +function isGatewayLockError(err: unknown): err is GatewayLockError { + return ( + err instanceof GatewayLockError || + (!!err && typeof err === "object" && (err as { name?: string }).name === "GatewayLockError") + ); +} + +function isHealthyGatewayLockError(err: unknown): boolean { + if (!isGatewayLockError(err) || typeof err.message !== "string") { + return false; + } + return ( + err.message.includes("gateway already running") || + err.message.includes("another gateway instance is already listening") + ); +} + async function runGatewayCommand(opts: GatewayRunOpts) { const isDevProfile = process.env.OPENCLAW_PROFILE?.trim().toLowerCase() === "dev"; const devMode = Boolean(opts.dev) || isDevProfile; @@ -457,10 +474,7 @@ async function runGatewayCommand(opts: GatewayRunOpts) { } } } catch (err) { - if ( - err instanceof GatewayLockError || - (err && typeof err === "object" && (err as { name?: string }).name === "GatewayLockError") - ) { + if (isGatewayLockError(err)) { const errMessage = describeUnknownError(err); defaultRuntime.error( `Gateway failed to start: ${errMessage}\nIf the gateway is supervised, stop it with: ${formatCliCommand("openclaw gateway stop")}`, @@ -476,7 +490,7 @@ async function runGatewayCommand(opts: GatewayRunOpts) { // ignore diagnostics failures } await maybeExplainGatewayServiceStop(); - defaultRuntime.exit(1); + defaultRuntime.exit(isHealthyGatewayLockError(err) ? 0 : 1); return; } defaultRuntime.error(`Gateway failed to start: ${String(err)}`);