Files
openclaw/src/daemon/systemd.test.ts
2026-02-16 06:36:15 +00:00

154 lines
4.5 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
const execFileMock = vi.hoisted(() => vi.fn());
vi.mock("node:child_process", () => ({
execFile: execFileMock,
}));
import { splitArgsPreservingQuotes } from "./arg-split.js";
import { parseSystemdExecStart } from "./systemd-unit.js";
import {
isSystemdUserServiceAvailable,
parseSystemdShow,
resolveSystemdUserUnitPath,
} from "./systemd.js";
describe("systemd availability", () => {
beforeEach(() => {
execFileMock.mockReset();
});
it("returns true when systemctl --user succeeds", async () => {
execFileMock.mockImplementation((_cmd, _args, _opts, cb) => {
cb(null, "", "");
});
await expect(isSystemdUserServiceAvailable()).resolves.toBe(true);
});
it("returns false when systemd user bus is unavailable", async () => {
execFileMock.mockImplementation((_cmd, _args, _opts, cb) => {
const err = new Error("Failed to connect to bus") as Error & {
stderr?: string;
code?: number;
};
err.stderr = "Failed to connect to bus";
err.code = 1;
cb(err, "", "");
});
await expect(isSystemdUserServiceAvailable()).resolves.toBe(false);
});
});
describe("systemd runtime parsing", () => {
it("parses active state details", () => {
const output = [
"ActiveState=inactive",
"SubState=dead",
"MainPID=0",
"ExecMainStatus=2",
"ExecMainCode=exited",
].join("\n");
expect(parseSystemdShow(output)).toEqual({
activeState: "inactive",
subState: "dead",
execMainStatus: 2,
execMainCode: "exited",
});
});
});
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",
);
});
});
describe("splitArgsPreservingQuotes", () => {
it("splits on whitespace outside quotes", () => {
expect(splitArgsPreservingQuotes('/usr/bin/openclaw gateway start --name "My Bot"')).toEqual([
"/usr/bin/openclaw",
"gateway",
"start",
"--name",
"My Bot",
]);
});
it("supports systemd-style backslash escaping", () => {
expect(
splitArgsPreservingQuotes('openclaw --name "My \\"Bot\\"" --foo bar', {
escapeMode: "backslash",
}),
).toEqual(["openclaw", "--name", 'My "Bot"', "--foo", "bar"]);
});
it("supports schtasks-style escaped quotes while preserving other backslashes", () => {
expect(
splitArgsPreservingQuotes('openclaw --path "C:\\\\Program Files\\\\OpenClaw"', {
escapeMode: "backslash-quote-only",
}),
).toEqual(["openclaw", "--path", "C:\\\\Program Files\\\\OpenClaw"]);
expect(
splitArgsPreservingQuotes('openclaw --label "My \\"Quoted\\" Name"', {
escapeMode: "backslash-quote-only",
}),
).toEqual(["openclaw", "--label", 'My "Quoted" Name']);
});
});
describe("parseSystemdExecStart", () => {
it("preserves quoted arguments", () => {
const execStart = '/usr/bin/openclaw gateway start --name "My Bot"';
expect(parseSystemdExecStart(execStart)).toEqual([
"/usr/bin/openclaw",
"gateway",
"start",
"--name",
"My Bot",
]);
});
});