mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-13 19:10:39 +00:00
211 lines
6.4 KiB
TypeScript
211 lines
6.4 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,
|
|
restartSystemdService,
|
|
resolveSystemdUserUnitPath,
|
|
stopSystemdService,
|
|
} 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",
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("systemd service control", () => {
|
|
beforeEach(() => {
|
|
execFileMock.mockReset();
|
|
});
|
|
|
|
it("stops the resolved user unit", async () => {
|
|
execFileMock
|
|
.mockImplementationOnce((_cmd, _args, _opts, cb) => cb(null, "", ""))
|
|
.mockImplementationOnce((_cmd, args, _opts, cb) => {
|
|
expect(args).toEqual(["--user", "stop", "openclaw-gateway.service"]);
|
|
cb(null, "", "");
|
|
});
|
|
const write = vi.fn();
|
|
const stdout = { write } as unknown as NodeJS.WritableStream;
|
|
|
|
await stopSystemdService({ stdout, env: {} });
|
|
|
|
expect(write).toHaveBeenCalledTimes(1);
|
|
expect(String(write.mock.calls[0]?.[0])).toContain("Stopped systemd service");
|
|
});
|
|
|
|
it("restarts a profile-specific user unit", async () => {
|
|
execFileMock
|
|
.mockImplementationOnce((_cmd, _args, _opts, cb) => cb(null, "", ""))
|
|
.mockImplementationOnce((_cmd, args, _opts, cb) => {
|
|
expect(args).toEqual(["--user", "restart", "openclaw-gateway-work.service"]);
|
|
cb(null, "", "");
|
|
});
|
|
const write = vi.fn();
|
|
const stdout = { write } as unknown as NodeJS.WritableStream;
|
|
|
|
await restartSystemdService({ stdout, env: { OPENCLAW_PROFILE: "work" } });
|
|
|
|
expect(write).toHaveBeenCalledTimes(1);
|
|
expect(String(write.mock.calls[0]?.[0])).toContain("Restarted systemd service");
|
|
});
|
|
|
|
it("surfaces stop failures with systemctl detail", async () => {
|
|
execFileMock
|
|
.mockImplementationOnce((_cmd, _args, _opts, cb) => cb(null, "", ""))
|
|
.mockImplementationOnce((_cmd, _args, _opts, cb) => {
|
|
const err = new Error("stop failed") as Error & { code?: number };
|
|
err.code = 1;
|
|
cb(err, "", "permission denied");
|
|
});
|
|
|
|
await expect(
|
|
stopSystemdService({
|
|
stdout: { write: vi.fn() } as unknown as NodeJS.WritableStream,
|
|
env: {},
|
|
}),
|
|
).rejects.toThrow("systemctl stop failed: permission denied");
|
|
});
|
|
});
|