From c0c10f42e22a98d6242b759f97e4bd3e27951c0b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 18 Feb 2026 23:08:40 +0000 Subject: [PATCH] refactor(commands): share daemon runtime warning helper --- src/commands/daemon-install-helpers.ts | 28 ++++---- .../daemon-install-runtime-warning.test.ts | 71 +++++++++++++++++++ .../daemon-install-runtime-warning.ts | 20 ++++++ src/commands/node-daemon-install-helpers.ts | 28 ++++---- 4 files changed, 117 insertions(+), 30 deletions(-) create mode 100644 src/commands/daemon-install-runtime-warning.test.ts create mode 100644 src/commands/daemon-install-runtime-warning.ts diff --git a/src/commands/daemon-install-helpers.ts b/src/commands/daemon-install-helpers.ts index d12181237..f027d2fdc 100644 --- a/src/commands/daemon-install-helpers.ts +++ b/src/commands/daemon-install-helpers.ts @@ -3,16 +3,14 @@ import { collectConfigEnvVars } from "../config/env-vars.js"; import type { OpenClawConfig } from "../config/types.js"; import { resolveGatewayLaunchAgentLabel } from "../daemon/constants.js"; import { resolveGatewayProgramArguments } from "../daemon/program-args.js"; -import { - renderSystemNodeWarning, - resolvePreferredNodePath, - resolveSystemNodeInfo, -} from "../daemon/runtime-paths.js"; +import { resolvePreferredNodePath } from "../daemon/runtime-paths.js"; import { buildServiceEnvironment } from "../daemon/service-env.js"; +import { + emitNodeRuntimeWarning, + type DaemonInstallWarnFn, +} from "./daemon-install-runtime-warning.js"; import type { GatewayDaemonRuntime } from "./daemon-runtime.js"; -type WarnFn = (message: string, title?: string) => void; - export type GatewayInstallPlan = { programArguments: string[]; workingDirectory?: string; @@ -32,7 +30,7 @@ export async function buildGatewayInstallPlan(params: { token?: string; devMode?: boolean; nodePath?: string; - warn?: WarnFn; + warn?: DaemonInstallWarnFn; /** Full config to extract env vars from (env vars + inline env keys). */ config?: OpenClawConfig; }): Promise { @@ -49,13 +47,13 @@ export async function buildGatewayInstallPlan(params: { runtime: params.runtime, nodePath, }); - if (params.runtime === "node") { - const systemNode = await resolveSystemNodeInfo({ env: params.env }); - const warning = renderSystemNodeWarning(systemNode, programArguments[0]); - if (warning) { - params.warn?.(warning, "Gateway runtime"); - } - } + await emitNodeRuntimeWarning({ + env: params.env, + runtime: params.runtime, + nodeProgram: programArguments[0], + warn: params.warn, + title: "Gateway runtime", + }); const serviceEnvironment = buildServiceEnvironment({ env: params.env, port: params.port, diff --git a/src/commands/daemon-install-runtime-warning.test.ts b/src/commands/daemon-install-runtime-warning.test.ts new file mode 100644 index 000000000..ee27ff4aa --- /dev/null +++ b/src/commands/daemon-install-runtime-warning.test.ts @@ -0,0 +1,71 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + resolveSystemNodeInfo: vi.fn(), + renderSystemNodeWarning: vi.fn(), +})); + +vi.mock("../daemon/runtime-paths.js", () => ({ + resolveSystemNodeInfo: mocks.resolveSystemNodeInfo, + renderSystemNodeWarning: mocks.renderSystemNodeWarning, +})); + +import { emitNodeRuntimeWarning } from "./daemon-install-runtime-warning.js"; + +afterEach(() => { + vi.resetAllMocks(); +}); + +describe("emitNodeRuntimeWarning", () => { + it("skips lookup when runtime is not node", async () => { + const warn = vi.fn(); + await emitNodeRuntimeWarning({ + env: {}, + runtime: "bun", + warn, + title: "Gateway runtime", + }); + expect(mocks.resolveSystemNodeInfo).not.toHaveBeenCalled(); + expect(mocks.renderSystemNodeWarning).not.toHaveBeenCalled(); + expect(warn).not.toHaveBeenCalled(); + }); + + it("emits warning when system node check returns one", async () => { + const warn = vi.fn(); + mocks.resolveSystemNodeInfo.mockResolvedValue({ path: "/usr/bin/node", version: "18.0.0" }); + mocks.renderSystemNodeWarning.mockReturnValue("Node too old"); + + await emitNodeRuntimeWarning({ + env: { PATH: "/usr/bin" }, + runtime: "node", + nodeProgram: "/opt/node", + warn, + title: "Node daemon runtime", + }); + + expect(mocks.resolveSystemNodeInfo).toHaveBeenCalledWith({ + env: { PATH: "/usr/bin" }, + }); + expect(mocks.renderSystemNodeWarning).toHaveBeenCalledWith( + { path: "/usr/bin/node", version: "18.0.0" }, + "/opt/node", + ); + expect(warn).toHaveBeenCalledWith("Node too old", "Node daemon runtime"); + }); + + it("does not emit when warning helper returns null", async () => { + const warn = vi.fn(); + mocks.resolveSystemNodeInfo.mockResolvedValue(null); + mocks.renderSystemNodeWarning.mockReturnValue(null); + + await emitNodeRuntimeWarning({ + env: {}, + runtime: "node", + nodeProgram: "node", + warn, + title: "Gateway runtime", + }); + + expect(warn).not.toHaveBeenCalled(); + }); +}); diff --git a/src/commands/daemon-install-runtime-warning.ts b/src/commands/daemon-install-runtime-warning.ts new file mode 100644 index 000000000..55e1de8ec --- /dev/null +++ b/src/commands/daemon-install-runtime-warning.ts @@ -0,0 +1,20 @@ +import { renderSystemNodeWarning, resolveSystemNodeInfo } from "../daemon/runtime-paths.js"; + +export type DaemonInstallWarnFn = (message: string, title?: string) => void; + +export async function emitNodeRuntimeWarning(params: { + env: Record; + runtime: string; + nodeProgram?: string; + warn?: DaemonInstallWarnFn; + title: string; +}): Promise { + if (params.runtime !== "node") { + return; + } + const systemNode = await resolveSystemNodeInfo({ env: params.env }); + const warning = renderSystemNodeWarning(systemNode, params.nodeProgram); + if (warning) { + params.warn?.(warning, params.title); + } +} diff --git a/src/commands/node-daemon-install-helpers.ts b/src/commands/node-daemon-install-helpers.ts index ef8007f8e..c2bab673e 100644 --- a/src/commands/node-daemon-install-helpers.ts +++ b/src/commands/node-daemon-install-helpers.ts @@ -1,16 +1,14 @@ import { formatNodeServiceDescription } from "../daemon/constants.js"; import { resolveNodeProgramArguments } from "../daemon/program-args.js"; -import { - renderSystemNodeWarning, - resolvePreferredNodePath, - resolveSystemNodeInfo, -} from "../daemon/runtime-paths.js"; +import { resolvePreferredNodePath } from "../daemon/runtime-paths.js"; import { buildNodeServiceEnvironment } from "../daemon/service-env.js"; import { resolveGatewayDevMode } from "./daemon-install-helpers.js"; +import { + emitNodeRuntimeWarning, + type DaemonInstallWarnFn, +} from "./daemon-install-runtime-warning.js"; import type { NodeDaemonRuntime } from "./node-daemon-runtime.js"; -type WarnFn = (message: string, title?: string) => void; - export type NodeInstallPlan = { programArguments: string[]; workingDirectory?: string; @@ -29,7 +27,7 @@ export async function buildNodeInstallPlan(params: { runtime: NodeDaemonRuntime; devMode?: boolean; nodePath?: string; - warn?: WarnFn; + warn?: DaemonInstallWarnFn; }): Promise { const devMode = params.devMode ?? resolveGatewayDevMode(); const nodePath = @@ -50,13 +48,13 @@ export async function buildNodeInstallPlan(params: { nodePath, }); - if (params.runtime === "node") { - const systemNode = await resolveSystemNodeInfo({ env: params.env }); - const warning = renderSystemNodeWarning(systemNode, programArguments[0]); - if (warning) { - params.warn?.(warning, "Node daemon runtime"); - } - } + await emitNodeRuntimeWarning({ + env: params.env, + runtime: params.runtime, + nodeProgram: programArguments[0], + warn: params.warn, + title: "Node daemon runtime", + }); const environment = buildNodeServiceEnvironment({ env: params.env }); const description = formatNodeServiceDescription({