import { afterEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ resolvePreferredNodePath: vi.fn(), resolveGatewayProgramArguments: vi.fn(), resolveSystemNodeInfo: vi.fn(), renderSystemNodeWarning: vi.fn(), buildServiceEnvironment: vi.fn(), })); vi.mock("../daemon/runtime-paths.js", () => ({ resolvePreferredNodePath: mocks.resolvePreferredNodePath, resolveSystemNodeInfo: mocks.resolveSystemNodeInfo, renderSystemNodeWarning: mocks.renderSystemNodeWarning, })); vi.mock("../daemon/program-args.js", () => ({ resolveGatewayProgramArguments: mocks.resolveGatewayProgramArguments, })); vi.mock("../daemon/service-env.js", () => ({ buildServiceEnvironment: mocks.buildServiceEnvironment, })); import { buildGatewayInstallPlan, gatewayInstallErrorHint, resolveGatewayDevMode, } from "./daemon-install-helpers.js"; afterEach(() => { vi.resetAllMocks(); }); describe("resolveGatewayDevMode", () => { it("detects dev mode for src ts entrypoints", () => { expect(resolveGatewayDevMode(["node", "/Users/me/openclaw/src/cli/index.ts"])).toBe(true); expect(resolveGatewayDevMode(["node", "C:\\Users\\me\\openclaw\\src\\cli\\index.ts"])).toBe( true, ); expect(resolveGatewayDevMode(["node", "/Users/me/openclaw/dist/cli/index.js"])).toBe(false); }); }); function mockNodeGatewayPlanFixture( params: { workingDirectory?: string; version?: string; supported?: boolean; warning?: string; serviceEnvironment?: Record; } = {}, ) { const { workingDirectory = "/Users/me", version = "22.0.0", supported = true, warning, serviceEnvironment = { OPENCLAW_PORT: "3000" }, } = params; mocks.resolvePreferredNodePath.mockResolvedValue("/opt/node"); mocks.resolveGatewayProgramArguments.mockResolvedValue({ programArguments: ["node", "gateway"], workingDirectory, }); mocks.resolveSystemNodeInfo.mockResolvedValue({ path: "/opt/node", version, supported, }); mocks.renderSystemNodeWarning.mockReturnValue(warning); mocks.buildServiceEnvironment.mockReturnValue(serviceEnvironment); } describe("buildGatewayInstallPlan", () => { it("uses provided nodePath and returns plan", async () => { mockNodeGatewayPlanFixture(); const plan = await buildGatewayInstallPlan({ env: {}, port: 3000, runtime: "node", nodePath: "/custom/node", }); expect(plan.programArguments).toEqual(["node", "gateway"]); expect(plan.workingDirectory).toBe("/Users/me"); expect(plan.environment).toEqual({ OPENCLAW_PORT: "3000" }); expect(mocks.resolvePreferredNodePath).not.toHaveBeenCalled(); }); it("emits warnings when renderSystemNodeWarning returns one", async () => { const warn = vi.fn(); mockNodeGatewayPlanFixture({ workingDirectory: undefined, version: "18.0.0", supported: false, warning: "Node too old", serviceEnvironment: {}, }); await buildGatewayInstallPlan({ env: {}, port: 3000, runtime: "node", warn, }); expect(warn).toHaveBeenCalledWith("Node too old", "Gateway runtime"); expect(mocks.resolvePreferredNodePath).toHaveBeenCalled(); }); it("merges config env vars into the environment", async () => { mockNodeGatewayPlanFixture({ serviceEnvironment: { OPENCLAW_PORT: "3000", HOME: "/Users/me", }, }); const plan = await buildGatewayInstallPlan({ env: {}, port: 3000, runtime: "node", config: { env: { vars: { GOOGLE_API_KEY: "test-key", }, CUSTOM_VAR: "custom-value", }, }, }); // Config env vars should be present expect(plan.environment.GOOGLE_API_KEY).toBe("test-key"); expect(plan.environment.CUSTOM_VAR).toBe("custom-value"); // Service environment vars should take precedence expect(plan.environment.OPENCLAW_PORT).toBe("3000"); expect(plan.environment.HOME).toBe("/Users/me"); }); it("does not include empty config env values", async () => { mockNodeGatewayPlanFixture(); const plan = await buildGatewayInstallPlan({ env: {}, port: 3000, runtime: "node", config: { env: { vars: { VALID_KEY: "valid", EMPTY_KEY: "", }, }, }, }); expect(plan.environment.VALID_KEY).toBe("valid"); expect(plan.environment.EMPTY_KEY).toBeUndefined(); }); it("drops whitespace-only config env values", async () => { mockNodeGatewayPlanFixture({ serviceEnvironment: {} }); const plan = await buildGatewayInstallPlan({ env: {}, port: 3000, runtime: "node", config: { env: { vars: { VALID_KEY: "valid", }, TRIMMED_KEY: " ", }, }, }); expect(plan.environment.VALID_KEY).toBe("valid"); expect(plan.environment.TRIMMED_KEY).toBeUndefined(); }); it("keeps service env values over config env vars", async () => { mockNodeGatewayPlanFixture({ serviceEnvironment: { HOME: "/Users/service", OPENCLAW_PORT: "3000", }, }); const plan = await buildGatewayInstallPlan({ env: {}, port: 3000, runtime: "node", config: { env: { HOME: "/Users/config", vars: { OPENCLAW_PORT: "9999", }, }, }, }); expect(plan.environment.HOME).toBe("/Users/service"); expect(plan.environment.OPENCLAW_PORT).toBe("3000"); }); }); describe("gatewayInstallErrorHint", () => { it("returns platform-specific hints", () => { expect(gatewayInstallErrorHint("win32")).toContain("Run as administrator"); expect(gatewayInstallErrorHint("linux")).toMatch( /(?:openclaw|openclaw)( --profile isolated)? gateway install/, ); }); });