mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
test(daemon): dedupe service path cases and bootstrap failures
This commit is contained in:
@@ -15,6 +15,7 @@ const state = vi.hoisted(() => ({
|
||||
dirs: new Set<string>(),
|
||||
files: new Map<string, string>(),
|
||||
}));
|
||||
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<string, string | undefined> = {
|
||||
function createDefaultLaunchdEnv(): Record<string, string | undefined> {
|
||||
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<string, string | undefined> = {
|
||||
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<string, string | undefined> = {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user