From da341bfbe105d2d1bde6f05644eede7caaacde31 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 19 Feb 2026 10:16:21 +0000 Subject: [PATCH] test(daemon): dedupe service path cases and bootstrap failures --- src/daemon/launchd.test.ts | 121 +++++++++++++++++++----------------- src/daemon/schtasks.test.ts | 73 +++++++++------------- src/daemon/systemd.test.ts | 81 +++++++++++------------- 3 files changed, 132 insertions(+), 143 deletions(-) diff --git a/src/daemon/launchd.test.ts b/src/daemon/launchd.test.ts index ff824f4121..b68774cb19 100644 --- a/src/daemon/launchd.test.ts +++ b/src/daemon/launchd.test.ts @@ -15,6 +15,7 @@ const state = vi.hoisted(() => ({ dirs: new Set(), files: new Map(), })); +const defaultProgramArguments = ["node", "-e", "process.exit(0)"]; function normalizeLaunchctlArgs(file: string, args: string[]): string[] { if (file === "launchctl") { @@ -130,15 +131,19 @@ describe("launchd bootstrap repair", () => { }); describe("launchd install", () => { - it("enables service before bootstrap (clears persisted disabled state)", async () => { - const env: Record = { + function createDefaultLaunchdEnv(): Record { + return { HOME: "/Users/test", OPENCLAW_PROFILE: "default", }; + } + + it("enables service before bootstrap (clears persisted disabled state)", async () => { + const env = createDefaultLaunchdEnv(); await installLaunchAgent({ env, stdout: new PassThrough(), - programArguments: ["node", "-e", "process.exit(0)"], + programArguments: defaultProgramArguments, }); const domain = typeof process.getuid === "function" ? `gui/${process.getuid()}` : "gui/501"; @@ -158,15 +163,12 @@ describe("launchd install", () => { }); it("writes TMPDIR to LaunchAgent environment when provided", async () => { - const env: Record = { - HOME: "/Users/test", - OPENCLAW_PROFILE: "default", - }; + const env = createDefaultLaunchdEnv(); const tmpDir = "/var/folders/xy/abc123/T/"; await installLaunchAgent({ env, stdout: new PassThrough(), - programArguments: ["node", "-e", "process.exit(0)"], + programArguments: defaultProgramArguments, environment: { TMPDIR: tmpDir }, }); @@ -179,16 +181,13 @@ describe("launchd install", () => { it("shows actionable guidance when launchctl gui domain does not support bootstrap", async () => { state.bootstrapError = "Bootstrap failed: 125: Domain does not support specified action"; - const env: Record = { - HOME: "/Users/test", - OPENCLAW_PROFILE: "default", - }; + const env = createDefaultLaunchdEnv(); let message = ""; try { await installLaunchAgent({ env, stdout: new PassThrough(), - programArguments: ["node", "-e", "process.exit(0)"], + programArguments: defaultProgramArguments, }); } catch (error) { message = String(error); @@ -197,52 +196,60 @@ describe("launchd install", () => { expect(message).toContain("wrong user (including sudo)"); expect(message).toContain("https://docs.openclaw.ai/gateway"); }); + + it("surfaces generic bootstrap failures without GUI-specific guidance", async () => { + state.bootstrapError = "Operation not permitted"; + const env = createDefaultLaunchdEnv(); + + await expect( + installLaunchAgent({ + env, + stdout: new PassThrough(), + programArguments: defaultProgramArguments, + }), + ).rejects.toThrow("launchctl bootstrap failed: Operation not permitted"); + }); }); describe("resolveLaunchAgentPlistPath", () => { - it("uses default label when OPENCLAW_PROFILE is unset", () => { - const env = { HOME: "/Users/test" }; - expect(resolveLaunchAgentPlistPath(env)).toBe( - "/Users/test/Library/LaunchAgents/ai.openclaw.gateway.plist", - ); - }); - - it("uses profile-specific label when OPENCLAW_PROFILE is set to a custom value", () => { - const env = { HOME: "/Users/test", OPENCLAW_PROFILE: "jbphoenix" }; - expect(resolveLaunchAgentPlistPath(env)).toBe( - "/Users/test/Library/LaunchAgents/ai.openclaw.jbphoenix.plist", - ); - }); - - it("prefers OPENCLAW_LAUNCHD_LABEL over OPENCLAW_PROFILE", () => { - const env = { - HOME: "/Users/test", - OPENCLAW_PROFILE: "jbphoenix", - OPENCLAW_LAUNCHD_LABEL: "com.custom.label", - }; - expect(resolveLaunchAgentPlistPath(env)).toBe( - "/Users/test/Library/LaunchAgents/com.custom.label.plist", - ); - }); - - it("trims whitespace from OPENCLAW_LAUNCHD_LABEL", () => { - const env = { - HOME: "/Users/test", - OPENCLAW_LAUNCHD_LABEL: " com.custom.label ", - }; - expect(resolveLaunchAgentPlistPath(env)).toBe( - "/Users/test/Library/LaunchAgents/com.custom.label.plist", - ); - }); - - it("ignores empty OPENCLAW_LAUNCHD_LABEL and falls back to profile", () => { - const env = { - HOME: "/Users/test", - OPENCLAW_PROFILE: "myprofile", - OPENCLAW_LAUNCHD_LABEL: " ", - }; - expect(resolveLaunchAgentPlistPath(env)).toBe( - "/Users/test/Library/LaunchAgents/ai.openclaw.myprofile.plist", - ); + it.each([ + { + name: "uses default label when OPENCLAW_PROFILE is unset", + env: { HOME: "/Users/test" }, + expected: "/Users/test/Library/LaunchAgents/ai.openclaw.gateway.plist", + }, + { + name: "uses profile-specific label when OPENCLAW_PROFILE is set to a custom value", + env: { HOME: "/Users/test", OPENCLAW_PROFILE: "jbphoenix" }, + expected: "/Users/test/Library/LaunchAgents/ai.openclaw.jbphoenix.plist", + }, + { + name: "prefers OPENCLAW_LAUNCHD_LABEL over OPENCLAW_PROFILE", + env: { + HOME: "/Users/test", + OPENCLAW_PROFILE: "jbphoenix", + OPENCLAW_LAUNCHD_LABEL: "com.custom.label", + }, + expected: "/Users/test/Library/LaunchAgents/com.custom.label.plist", + }, + { + name: "trims whitespace from OPENCLAW_LAUNCHD_LABEL", + env: { + HOME: "/Users/test", + OPENCLAW_LAUNCHD_LABEL: " com.custom.label ", + }, + expected: "/Users/test/Library/LaunchAgents/com.custom.label.plist", + }, + { + name: "ignores empty OPENCLAW_LAUNCHD_LABEL and falls back to profile", + env: { + HOME: "/Users/test", + OPENCLAW_PROFILE: "myprofile", + OPENCLAW_LAUNCHD_LABEL: " ", + }, + expected: "/Users/test/Library/LaunchAgents/ai.openclaw.myprofile.plist", + }, + ])("$name", ({ env, expected }) => { + expect(resolveLaunchAgentPlistPath(env)).toBe(expected); }); }); diff --git a/src/daemon/schtasks.test.ts b/src/daemon/schtasks.test.ts index 125b1f3a0a..e4911ab03f 100644 --- a/src/daemon/schtasks.test.ts +++ b/src/daemon/schtasks.test.ts @@ -5,29 +5,15 @@ import { describe, expect, it } from "vitest"; import { parseSchtasksQuery, readScheduledTaskCommand, resolveTaskScriptPath } from "./schtasks.js"; describe("schtasks runtime parsing", () => { - it("parses status and last run info", () => { + it.each(["Ready", "Running"])("parses %s status", (status) => { const output = [ "TaskName: \\OpenClaw Gateway", - "Status: Ready", + `Status: ${status}`, "Last Run Time: 1/8/2026 1:23:45 AM", "Last Run Result: 0x0", ].join("\r\n"); expect(parseSchtasksQuery(output)).toEqual({ - status: "Ready", - lastRunTime: "1/8/2026 1:23:45 AM", - lastRunResult: "0x0", - }); - }); - - it("parses running status", () => { - const output = [ - "TaskName: \\OpenClaw Gateway", - "Status: Running", - "Last Run Time: 1/8/2026 1:23:45 AM", - "Last Run Result: 0x0", - ].join("\r\n"); - expect(parseSchtasksQuery(output)).toEqual({ - status: "Running", + status, lastRunTime: "1/8/2026 1:23:45 AM", lastRunResult: "0x0", }); @@ -35,32 +21,33 @@ describe("schtasks runtime parsing", () => { }); describe("resolveTaskScriptPath", () => { - it("uses default path when OPENCLAW_PROFILE is unset", () => { - const env = { USERPROFILE: "C:\\Users\\test" }; - expect(resolveTaskScriptPath(env)).toBe( - path.join("C:\\Users\\test", ".openclaw", "gateway.cmd"), - ); - }); - - it("uses profile-specific path when OPENCLAW_PROFILE is set to a custom value", () => { - const env = { USERPROFILE: "C:\\Users\\test", OPENCLAW_PROFILE: "jbphoenix" }; - expect(resolveTaskScriptPath(env)).toBe( - path.join("C:\\Users\\test", ".openclaw-jbphoenix", "gateway.cmd"), - ); - }); - - it("prefers OPENCLAW_STATE_DIR over profile-derived defaults", () => { - const env = { - USERPROFILE: "C:\\Users\\test", - OPENCLAW_PROFILE: "rescue", - OPENCLAW_STATE_DIR: "C:\\State\\openclaw", - }; - expect(resolveTaskScriptPath(env)).toBe(path.join("C:\\State\\openclaw", "gateway.cmd")); - }); - - it("falls back to HOME when USERPROFILE is not set", () => { - const env = { HOME: "/home/test", OPENCLAW_PROFILE: "default" }; - expect(resolveTaskScriptPath(env)).toBe(path.join("/home/test", ".openclaw", "gateway.cmd")); + it.each([ + { + name: "uses default path when OPENCLAW_PROFILE is unset", + env: { USERPROFILE: "C:\\Users\\test" }, + expected: path.join("C:\\Users\\test", ".openclaw", "gateway.cmd"), + }, + { + name: "uses profile-specific path when OPENCLAW_PROFILE is set to a custom value", + env: { USERPROFILE: "C:\\Users\\test", OPENCLAW_PROFILE: "jbphoenix" }, + expected: path.join("C:\\Users\\test", ".openclaw-jbphoenix", "gateway.cmd"), + }, + { + name: "prefers OPENCLAW_STATE_DIR over profile-derived defaults", + env: { + USERPROFILE: "C:\\Users\\test", + OPENCLAW_PROFILE: "rescue", + OPENCLAW_STATE_DIR: "C:\\State\\openclaw", + }, + expected: path.join("C:\\State\\openclaw", "gateway.cmd"), + }, + { + name: "falls back to HOME when USERPROFILE is not set", + env: { HOME: "/home/test", OPENCLAW_PROFILE: "default" }, + expected: path.join("/home/test", ".openclaw", "gateway.cmd"), + }, + ])("$name", ({ env, expected }) => { + expect(resolveTaskScriptPath(env)).toBe(expected); }); }); diff --git a/src/daemon/systemd.test.ts b/src/daemon/systemd.test.ts index 5a375e5716..77dec0d06f 100644 --- a/src/daemon/systemd.test.ts +++ b/src/daemon/systemd.test.ts @@ -61,49 +61,44 @@ describe("systemd runtime parsing", () => { }); describe("resolveSystemdUserUnitPath", () => { - it("uses default service name when OPENCLAW_PROFILE is unset", () => { - const env = { HOME: "/home/test" }; - expect(resolveSystemdUserUnitPath(env)).toBe( - "/home/test/.config/systemd/user/openclaw-gateway.service", - ); - }); - - it("uses profile-specific service name when OPENCLAW_PROFILE is set to a custom value", () => { - const env = { HOME: "/home/test", OPENCLAW_PROFILE: "jbphoenix" }; - expect(resolveSystemdUserUnitPath(env)).toBe( - "/home/test/.config/systemd/user/openclaw-gateway-jbphoenix.service", - ); - }); - - it("prefers OPENCLAW_SYSTEMD_UNIT over OPENCLAW_PROFILE", () => { - const env = { - HOME: "/home/test", - OPENCLAW_PROFILE: "jbphoenix", - OPENCLAW_SYSTEMD_UNIT: "custom-unit", - }; - expect(resolveSystemdUserUnitPath(env)).toBe( - "/home/test/.config/systemd/user/custom-unit.service", - ); - }); - - it("handles OPENCLAW_SYSTEMD_UNIT with .service suffix", () => { - const env = { - HOME: "/home/test", - OPENCLAW_SYSTEMD_UNIT: "custom-unit.service", - }; - expect(resolveSystemdUserUnitPath(env)).toBe( - "/home/test/.config/systemd/user/custom-unit.service", - ); - }); - - it("trims whitespace from OPENCLAW_SYSTEMD_UNIT", () => { - const env = { - HOME: "/home/test", - OPENCLAW_SYSTEMD_UNIT: " custom-unit ", - }; - expect(resolveSystemdUserUnitPath(env)).toBe( - "/home/test/.config/systemd/user/custom-unit.service", - ); + it.each([ + { + name: "uses default service name when OPENCLAW_PROFILE is unset", + env: { HOME: "/home/test" }, + expected: "/home/test/.config/systemd/user/openclaw-gateway.service", + }, + { + name: "uses profile-specific service name when OPENCLAW_PROFILE is set to a custom value", + env: { HOME: "/home/test", OPENCLAW_PROFILE: "jbphoenix" }, + expected: "/home/test/.config/systemd/user/openclaw-gateway-jbphoenix.service", + }, + { + name: "prefers OPENCLAW_SYSTEMD_UNIT over OPENCLAW_PROFILE", + env: { + HOME: "/home/test", + OPENCLAW_PROFILE: "jbphoenix", + OPENCLAW_SYSTEMD_UNIT: "custom-unit", + }, + expected: "/home/test/.config/systemd/user/custom-unit.service", + }, + { + name: "handles OPENCLAW_SYSTEMD_UNIT with .service suffix", + env: { + HOME: "/home/test", + OPENCLAW_SYSTEMD_UNIT: "custom-unit.service", + }, + expected: "/home/test/.config/systemd/user/custom-unit.service", + }, + { + name: "trims whitespace from OPENCLAW_SYSTEMD_UNIT", + env: { + HOME: "/home/test", + OPENCLAW_SYSTEMD_UNIT: " custom-unit ", + }, + expected: "/home/test/.config/systemd/user/custom-unit.service", + }, + ])("$name", ({ env, expected }) => { + expect(resolveSystemdUserUnitPath(env)).toBe(expected); }); });