mirror of https://github.com/openclaw/openclaw.git
test: share systemd service test helpers
This commit is contained in:
parent
26578a18c8
commit
4fe59edd84
|
|
@ -25,6 +25,10 @@ type ExecFileError = Error & {
|
|||
code?: string | number;
|
||||
};
|
||||
|
||||
const TEST_SERVICE_HOME = "/home/test";
|
||||
const TEST_MANAGED_HOME = "/tmp/openclaw-test-home";
|
||||
const GATEWAY_SERVICE = "openclaw-gateway.service";
|
||||
|
||||
const createExecFileError = (
|
||||
message: string,
|
||||
options: { stderr?: string; code?: string | number } = {},
|
||||
|
|
@ -58,6 +62,48 @@ function pathLikeToString(pathname: unknown): string {
|
|||
return "";
|
||||
}
|
||||
|
||||
function assertUserSystemctlArgs(args: string[], ...command: string[]) {
|
||||
expect(args).toEqual(["--user", ...command]);
|
||||
}
|
||||
|
||||
function assertMachineUserSystemctlArgs(args: string[], user: string, ...command: string[]) {
|
||||
expect(args).toEqual(["--machine", `${user}@`, "--user", ...command]);
|
||||
}
|
||||
|
||||
async function readManagedServiceEnabled(env: NodeJS.ProcessEnv = { HOME: TEST_MANAGED_HOME }) {
|
||||
const { isSystemdServiceEnabled } = await import("./systemd.js");
|
||||
vi.spyOn(fs, "access").mockResolvedValue(undefined);
|
||||
return isSystemdServiceEnabled({ env });
|
||||
}
|
||||
|
||||
function mockReadGatewayServiceFile(
|
||||
unitLines: string[],
|
||||
extraFiles: Record<string, string | Error> = {},
|
||||
) {
|
||||
return vi.spyOn(fs, "readFile").mockImplementation(async (pathname) => {
|
||||
const pathValue = pathLikeToString(pathname);
|
||||
if (pathValue.endsWith(`/${GATEWAY_SERVICE}`)) {
|
||||
return unitLines.join("\n");
|
||||
}
|
||||
const extraFile = extraFiles[pathValue];
|
||||
if (typeof extraFile === "string") {
|
||||
return extraFile;
|
||||
}
|
||||
if (extraFile instanceof Error) {
|
||||
throw extraFile;
|
||||
}
|
||||
throw new Error(`unexpected readFile path: ${pathValue}`);
|
||||
});
|
||||
}
|
||||
|
||||
async function expectExecStartWithoutEnvironment(envFileLine: string) {
|
||||
mockReadGatewayServiceFile(["[Service]", "ExecStart=/usr/bin/openclaw gateway run", envFileLine]);
|
||||
|
||||
const command = await readSystemdServiceExecStart({ HOME: TEST_SERVICE_HOME });
|
||||
expect(command?.programArguments).toEqual(["/usr/bin/openclaw", "gateway", "run"]);
|
||||
expect(command?.environment).toBeUndefined();
|
||||
}
|
||||
|
||||
const assertRestartSuccess = async (env: NodeJS.ProcessEnv) => {
|
||||
const { write, stdout } = createWritableStreamMock();
|
||||
await restartSystemdService({ stdout, env });
|
||||
|
|
@ -118,24 +164,18 @@ describe("systemd availability", () => {
|
|||
});
|
||||
|
||||
describe("isSystemdServiceEnabled", () => {
|
||||
const mockManagedUnitPresent = () => {
|
||||
vi.spyOn(fs, "access").mockResolvedValue(undefined);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
execFileMock.mockReset();
|
||||
});
|
||||
|
||||
it("returns false when systemctl is not present", async () => {
|
||||
const { isSystemdServiceEnabled } = await import("./systemd.js");
|
||||
mockManagedUnitPresent();
|
||||
execFileMock.mockImplementation((_cmd, _args, _opts, cb) => {
|
||||
const err = new Error("spawn systemctl EACCES") as Error & { code?: string };
|
||||
err.code = "EACCES";
|
||||
cb(err, "", "");
|
||||
});
|
||||
const result = await isSystemdServiceEnabled({ env: { HOME: "/tmp/openclaw-test-home" } });
|
||||
const result = await readManagedServiceEnabled();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
|
|
@ -152,55 +192,45 @@ describe("isSystemdServiceEnabled", () => {
|
|||
});
|
||||
|
||||
it("calls systemctl is-enabled when systemctl is present", async () => {
|
||||
const { isSystemdServiceEnabled } = await import("./systemd.js");
|
||||
mockManagedUnitPresent();
|
||||
execFileMock.mockImplementationOnce((_cmd, args, _opts, cb) => {
|
||||
expect(args).toEqual(["--user", "is-enabled", "openclaw-gateway.service"]);
|
||||
assertUserSystemctlArgs(args, "is-enabled", GATEWAY_SERVICE);
|
||||
cb(null, "enabled", "");
|
||||
});
|
||||
const result = await isSystemdServiceEnabled({ env: { HOME: "/tmp/openclaw-test-home" } });
|
||||
const result = await readManagedServiceEnabled();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when systemctl reports disabled", async () => {
|
||||
const { isSystemdServiceEnabled } = await import("./systemd.js");
|
||||
mockManagedUnitPresent();
|
||||
execFileMock.mockImplementationOnce((_cmd, _args, _opts, cb) => {
|
||||
const err = new Error("disabled") as Error & { code?: number };
|
||||
err.code = 1;
|
||||
cb(err, "disabled", "");
|
||||
});
|
||||
const result = await isSystemdServiceEnabled({ env: { HOME: "/tmp/openclaw-test-home" } });
|
||||
const result = await readManagedServiceEnabled();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for the WSL2 Ubuntu 24.04 wrapper-only is-enabled failure", async () => {
|
||||
const { isSystemdServiceEnabled } = await import("./systemd.js");
|
||||
mockManagedUnitPresent();
|
||||
execFileMock.mockImplementationOnce((_cmd, args, _opts, cb) => {
|
||||
expect(args).toEqual(["--user", "is-enabled", "openclaw-gateway.service"]);
|
||||
assertUserSystemctlArgs(args, "is-enabled", GATEWAY_SERVICE);
|
||||
const err = new Error(
|
||||
"Command failed: systemctl --user is-enabled openclaw-gateway.service",
|
||||
`Command failed: systemctl --user is-enabled ${GATEWAY_SERVICE}`,
|
||||
) as Error & { code?: number };
|
||||
err.code = 1;
|
||||
cb(err, "", "");
|
||||
});
|
||||
|
||||
await expect(
|
||||
isSystemdServiceEnabled({ env: { HOME: "/tmp/openclaw-test-home" } }),
|
||||
).rejects.toThrow(
|
||||
"systemctl is-enabled unavailable: Command failed: systemctl --user is-enabled openclaw-gateway.service",
|
||||
await expect(readManagedServiceEnabled()).rejects.toThrow(
|
||||
`systemctl is-enabled unavailable: Command failed: systemctl --user is-enabled ${GATEWAY_SERVICE}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns false when is-enabled cannot connect to the user bus without machine fallback", async () => {
|
||||
const { isSystemdServiceEnabled } = await import("./systemd.js");
|
||||
mockManagedUnitPresent();
|
||||
vi.spyOn(os, "userInfo").mockImplementationOnce(() => {
|
||||
throw new Error("no user info");
|
||||
});
|
||||
execFileMock.mockImplementationOnce((_cmd, args, _opts, cb) => {
|
||||
expect(args).toEqual(["--user", "is-enabled", "openclaw-gateway.service"]);
|
||||
assertUserSystemctlArgs(args, "is-enabled", GATEWAY_SERVICE);
|
||||
cb(
|
||||
createExecFileError("Failed to connect to bus", { stderr: "Failed to connect to bus" }),
|
||||
"",
|
||||
|
|
@ -209,18 +239,14 @@ describe("isSystemdServiceEnabled", () => {
|
|||
});
|
||||
|
||||
await expect(
|
||||
isSystemdServiceEnabled({
|
||||
env: { HOME: "/tmp/openclaw-test-home", USER: "", LOGNAME: "" },
|
||||
}),
|
||||
readManagedServiceEnabled({ HOME: TEST_MANAGED_HOME, USER: "", LOGNAME: "" }),
|
||||
).rejects.toThrow("systemctl is-enabled unavailable: Failed to connect to bus");
|
||||
});
|
||||
|
||||
it("returns false when both direct and machine-scope is-enabled checks report bus unavailability", async () => {
|
||||
const { isSystemdServiceEnabled } = await import("./systemd.js");
|
||||
mockManagedUnitPresent();
|
||||
execFileMock
|
||||
.mockImplementationOnce((_cmd, args, _opts, cb) => {
|
||||
expect(args).toEqual(["--user", "is-enabled", "openclaw-gateway.service"]);
|
||||
assertUserSystemctlArgs(args, "is-enabled", GATEWAY_SERVICE);
|
||||
cb(
|
||||
createExecFileError("Failed to connect to bus", { stderr: "Failed to connect to bus" }),
|
||||
"",
|
||||
|
|
@ -228,13 +254,7 @@ describe("isSystemdServiceEnabled", () => {
|
|||
);
|
||||
})
|
||||
.mockImplementationOnce((_cmd, args, _opts, cb) => {
|
||||
expect(args).toEqual([
|
||||
"--machine",
|
||||
"debian@",
|
||||
"--user",
|
||||
"is-enabled",
|
||||
"openclaw-gateway.service",
|
||||
]);
|
||||
assertMachineUserSystemctlArgs(args, "debian", "is-enabled", GATEWAY_SERVICE);
|
||||
cb(
|
||||
createExecFileError("Failed to connect to user scope bus via local transport", {
|
||||
stderr:
|
||||
|
|
@ -246,32 +266,28 @@ describe("isSystemdServiceEnabled", () => {
|
|||
});
|
||||
|
||||
await expect(
|
||||
isSystemdServiceEnabled({
|
||||
env: { HOME: "/tmp/openclaw-test-home", USER: "debian" },
|
||||
}),
|
||||
readManagedServiceEnabled({ HOME: TEST_MANAGED_HOME, USER: "debian" }),
|
||||
).rejects.toThrow("systemctl is-enabled unavailable: Failed to connect to user scope bus");
|
||||
});
|
||||
|
||||
it("throws when generic wrapper errors report infrastructure failures", async () => {
|
||||
const { isSystemdServiceEnabled } = await import("./systemd.js");
|
||||
mockManagedUnitPresent();
|
||||
execFileMock.mockImplementationOnce((_cmd, args, _opts, cb) => {
|
||||
expect(args).toEqual(["--user", "is-enabled", "openclaw-gateway.service"]);
|
||||
assertUserSystemctlArgs(args, "is-enabled", GATEWAY_SERVICE);
|
||||
const err = new Error(
|
||||
"Command failed: systemctl --user is-enabled openclaw-gateway.service",
|
||||
`Command failed: systemctl --user is-enabled ${GATEWAY_SERVICE}`,
|
||||
) as Error & { code?: number };
|
||||
err.code = 1;
|
||||
cb(err, "", "read-only file system");
|
||||
});
|
||||
|
||||
await expect(
|
||||
isSystemdServiceEnabled({ env: { HOME: "/tmp/openclaw-test-home" } }),
|
||||
).rejects.toThrow("systemctl is-enabled unavailable: read-only file system");
|
||||
await expect(readManagedServiceEnabled()).rejects.toThrow(
|
||||
"systemctl is-enabled unavailable: read-only file system",
|
||||
);
|
||||
});
|
||||
|
||||
it("throws when systemctl is-enabled fails for non-state errors", async () => {
|
||||
const { isSystemdServiceEnabled } = await import("./systemd.js");
|
||||
mockManagedUnitPresent();
|
||||
vi.spyOn(fs, "access").mockResolvedValue(undefined);
|
||||
execFileMock
|
||||
.mockImplementationOnce((_cmd, args, _opts, cb) => {
|
||||
expect(args).toEqual(["--user", "is-enabled", "openclaw-gateway.service"]);
|
||||
|
|
@ -294,7 +310,7 @@ describe("isSystemdServiceEnabled", () => {
|
|||
|
||||
it("returns false when systemctl is-enabled exits with code 4 (not-found)", async () => {
|
||||
const { isSystemdServiceEnabled } = await import("./systemd.js");
|
||||
mockManagedUnitPresent();
|
||||
vi.spyOn(fs, "access").mockResolvedValue(undefined);
|
||||
execFileMock.mockImplementationOnce((_cmd, _args, _opts, cb) => {
|
||||
// On Ubuntu 24.04, `systemctl --user is-enabled <unit>` exits with
|
||||
// code 4 and prints "not-found" to stdout when the unit doesn't exist.
|
||||
|
|
@ -463,82 +479,38 @@ describe("readSystemdServiceExecStart", () => {
|
|||
});
|
||||
|
||||
it("loads OPENCLAW_GATEWAY_TOKEN from EnvironmentFile", async () => {
|
||||
const readFileSpy = vi.spyOn(fs, "readFile").mockImplementation(async (pathname) => {
|
||||
const pathValue = pathLikeToString(pathname);
|
||||
if (pathValue.endsWith("/openclaw-gateway.service")) {
|
||||
return [
|
||||
"[Service]",
|
||||
"ExecStart=/usr/bin/openclaw gateway run",
|
||||
"EnvironmentFile=%h/.openclaw/.env",
|
||||
].join("\n");
|
||||
}
|
||||
if (pathValue === "/home/test/.openclaw/.env") {
|
||||
return "OPENCLAW_GATEWAY_TOKEN=env-file-token\n";
|
||||
}
|
||||
throw new Error(`unexpected readFile path: ${pathValue}`);
|
||||
});
|
||||
const readFileSpy = mockReadGatewayServiceFile(
|
||||
["[Service]", "ExecStart=/usr/bin/openclaw gateway run", "EnvironmentFile=%h/.openclaw/.env"],
|
||||
{ [`${TEST_SERVICE_HOME}/.openclaw/.env`]: "OPENCLAW_GATEWAY_TOKEN=env-file-token\n" },
|
||||
);
|
||||
|
||||
const command = await readSystemdServiceExecStart({ HOME: "/home/test" });
|
||||
const command = await readSystemdServiceExecStart({ HOME: TEST_SERVICE_HOME });
|
||||
expect(command?.environment?.OPENCLAW_GATEWAY_TOKEN).toBe("env-file-token");
|
||||
expect(readFileSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("lets EnvironmentFile override inline Environment values", async () => {
|
||||
vi.spyOn(fs, "readFile").mockImplementation(async (pathname) => {
|
||||
const pathValue = pathLikeToString(pathname);
|
||||
if (pathValue.endsWith("/openclaw-gateway.service")) {
|
||||
return [
|
||||
"[Service]",
|
||||
"ExecStart=/usr/bin/openclaw gateway run",
|
||||
"EnvironmentFile=%h/.openclaw/.env",
|
||||
'Environment="OPENCLAW_GATEWAY_TOKEN=inline-token"',
|
||||
].join("\n");
|
||||
}
|
||||
if (pathValue === "/home/test/.openclaw/.env") {
|
||||
return "OPENCLAW_GATEWAY_TOKEN=env-file-token\n";
|
||||
}
|
||||
throw new Error(`unexpected readFile path: ${pathValue}`);
|
||||
});
|
||||
mockReadGatewayServiceFile(
|
||||
[
|
||||
"[Service]",
|
||||
"ExecStart=/usr/bin/openclaw gateway run",
|
||||
"EnvironmentFile=%h/.openclaw/.env",
|
||||
'Environment="OPENCLAW_GATEWAY_TOKEN=inline-token"',
|
||||
],
|
||||
{ [`${TEST_SERVICE_HOME}/.openclaw/.env`]: "OPENCLAW_GATEWAY_TOKEN=env-file-token\n" },
|
||||
);
|
||||
|
||||
const command = await readSystemdServiceExecStart({ HOME: "/home/test" });
|
||||
const command = await readSystemdServiceExecStart({ HOME: TEST_SERVICE_HOME });
|
||||
expect(command?.environment?.OPENCLAW_GATEWAY_TOKEN).toBe("env-file-token");
|
||||
expect(command?.environmentValueSources?.OPENCLAW_GATEWAY_TOKEN).toBe("file");
|
||||
});
|
||||
|
||||
it("ignores missing optional EnvironmentFile entries", async () => {
|
||||
vi.spyOn(fs, "readFile").mockImplementation(async (pathname) => {
|
||||
const pathValue = pathLikeToString(pathname);
|
||||
if (pathValue.endsWith("/openclaw-gateway.service")) {
|
||||
return [
|
||||
"[Service]",
|
||||
"ExecStart=/usr/bin/openclaw gateway run",
|
||||
"EnvironmentFile=-%h/.openclaw/missing.env",
|
||||
].join("\n");
|
||||
}
|
||||
throw new Error(`missing: ${pathValue}`);
|
||||
});
|
||||
|
||||
const command = await readSystemdServiceExecStart({ HOME: "/home/test" });
|
||||
expect(command?.programArguments).toEqual(["/usr/bin/openclaw", "gateway", "run"]);
|
||||
expect(command?.environment).toBeUndefined();
|
||||
await expectExecStartWithoutEnvironment("EnvironmentFile=-%h/.openclaw/missing.env");
|
||||
});
|
||||
|
||||
it("keeps parsing when non-optional EnvironmentFile entries are missing", async () => {
|
||||
vi.spyOn(fs, "readFile").mockImplementation(async (pathname) => {
|
||||
const pathValue = pathLikeToString(pathname);
|
||||
if (pathValue.endsWith("/openclaw-gateway.service")) {
|
||||
return [
|
||||
"[Service]",
|
||||
"ExecStart=/usr/bin/openclaw gateway run",
|
||||
"EnvironmentFile=%h/.openclaw/missing.env",
|
||||
].join("\n");
|
||||
}
|
||||
throw new Error(`missing: ${pathValue}`);
|
||||
});
|
||||
|
||||
const command = await readSystemdServiceExecStart({ HOME: "/home/test" });
|
||||
expect(command?.programArguments).toEqual(["/usr/bin/openclaw", "gateway", "run"]);
|
||||
expect(command?.environment).toBeUndefined();
|
||||
await expectExecStartWithoutEnvironment("EnvironmentFile=%h/.openclaw/missing.env");
|
||||
});
|
||||
|
||||
it("supports multiple EnvironmentFile entries and quoted paths", async () => {
|
||||
|
|
@ -631,7 +603,7 @@ describe("readSystemdServiceExecStart", () => {
|
|||
|
||||
describe("systemd service control", () => {
|
||||
const assertMachineRestartArgs = (args: string[]) => {
|
||||
expect(args).toEqual(["--machine", "debian@", "--user", "restart", "openclaw-gateway.service"]);
|
||||
assertMachineUserSystemctlArgs(args, "debian", "restart", GATEWAY_SERVICE);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -642,7 +614,7 @@ describe("systemd service control", () => {
|
|||
execFileMock
|
||||
.mockImplementationOnce((_cmd, _args, _opts, cb) => cb(null, "", ""))
|
||||
.mockImplementationOnce((_cmd, args, _opts, cb) => {
|
||||
expect(args).toEqual(["--user", "stop", "openclaw-gateway.service"]);
|
||||
assertUserSystemctlArgs(args, "stop", GATEWAY_SERVICE);
|
||||
cb(null, "", "");
|
||||
});
|
||||
const write = vi.fn();
|
||||
|
|
@ -664,7 +636,7 @@ describe("systemd service control", () => {
|
|||
),
|
||||
)
|
||||
.mockImplementationOnce((_cmd, args, _opts, cb) => {
|
||||
expect(args).toEqual(["--user", "stop", "openclaw-gateway.service"]);
|
||||
assertUserSystemctlArgs(args, "stop", GATEWAY_SERVICE);
|
||||
cb(null, "", "");
|
||||
});
|
||||
|
||||
|
|
@ -678,7 +650,7 @@ describe("systemd service control", () => {
|
|||
execFileMock
|
||||
.mockImplementationOnce((_cmd, _args, _opts, cb) => cb(null, "", ""))
|
||||
.mockImplementationOnce((_cmd, args, _opts, cb) => {
|
||||
expect(args).toEqual(["--user", "restart", "openclaw-gateway-work.service"]);
|
||||
assertUserSystemctlArgs(args, "restart", "openclaw-gateway-work.service");
|
||||
cb(null, "", "");
|
||||
});
|
||||
await assertRestartSuccess({ OPENCLAW_PROFILE: "work" });
|
||||
|
|
@ -724,7 +696,7 @@ describe("systemd service control", () => {
|
|||
it("targets the sudo caller's user scope when SUDO_USER is set", async () => {
|
||||
execFileMock
|
||||
.mockImplementationOnce((_cmd, args, _opts, cb) => {
|
||||
expect(args).toEqual(["--machine", "debian@", "--user", "status"]);
|
||||
assertMachineUserSystemctlArgs(args, "debian", "status");
|
||||
cb(null, "", "");
|
||||
})
|
||||
.mockImplementationOnce((_cmd, args, _opts, cb) => {
|
||||
|
|
@ -737,11 +709,11 @@ describe("systemd service control", () => {
|
|||
it("keeps direct --user scope when SUDO_USER is root", async () => {
|
||||
execFileMock
|
||||
.mockImplementationOnce((_cmd, args, _opts, cb) => {
|
||||
expect(args).toEqual(["--user", "status"]);
|
||||
assertUserSystemctlArgs(args, "status");
|
||||
cb(null, "", "");
|
||||
})
|
||||
.mockImplementationOnce((_cmd, args, _opts, cb) => {
|
||||
expect(args).toEqual(["--user", "restart", "openclaw-gateway.service"]);
|
||||
assertUserSystemctlArgs(args, "restart", GATEWAY_SERVICE);
|
||||
cb(null, "", "");
|
||||
});
|
||||
await assertRestartSuccess({ SUDO_USER: "root", USER: "root" });
|
||||
|
|
@ -750,7 +722,7 @@ describe("systemd service control", () => {
|
|||
it("falls back to machine user scope for restart when user bus env is missing", async () => {
|
||||
execFileMock
|
||||
.mockImplementationOnce((_cmd, args, _opts, cb) => {
|
||||
expect(args).toEqual(["--user", "status"]);
|
||||
assertUserSystemctlArgs(args, "status");
|
||||
const err = createExecFileError("Failed to connect to user scope bus", {
|
||||
stderr:
|
||||
"Failed to connect to user scope bus via local transport: $DBUS_SESSION_BUS_ADDRESS and $XDG_RUNTIME_DIR not defined",
|
||||
|
|
@ -758,11 +730,11 @@ describe("systemd service control", () => {
|
|||
cb(err, "", "");
|
||||
})
|
||||
.mockImplementationOnce((_cmd, args, _opts, cb) => {
|
||||
expect(args).toEqual(["--machine", "debian@", "--user", "status"]);
|
||||
assertMachineUserSystemctlArgs(args, "debian", "status");
|
||||
cb(null, "", "");
|
||||
})
|
||||
.mockImplementationOnce((_cmd, args, _opts, cb) => {
|
||||
expect(args).toEqual(["--user", "restart", "openclaw-gateway.service"]);
|
||||
assertUserSystemctlArgs(args, "restart", GATEWAY_SERVICE);
|
||||
const err = createExecFileError("Failed to connect to user scope bus", {
|
||||
stderr: "Failed to connect to user scope bus",
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue