diff --git a/src/daemon/systemd.test.ts b/src/daemon/systemd.test.ts index 9fc8283b84a..b080302a644 100644 --- a/src/daemon/systemd.test.ts +++ b/src/daemon/systemd.test.ts @@ -17,6 +17,38 @@ import { stopSystemdService, } from "./systemd.js"; +type ExecFileError = Error & { + stderr?: string; + code?: string | number; +}; + +const createExecFileError = ( + message: string, + options: { stderr?: string; code?: string | number } = {}, +): ExecFileError => { + const err = new Error(message) as ExecFileError; + err.code = options.code ?? 1; + if (options.stderr) { + err.stderr = options.stderr; + } + return err; +}; + +const createWritableStreamMock = () => { + const write = vi.fn(); + return { + write, + stdout: { write } as unknown as NodeJS.WritableStream, + }; +}; + +const assertRestartSuccess = async (env: NodeJS.ProcessEnv) => { + const { write, stdout } = createWritableStreamMock(); + await restartSystemdService({ stdout, env }); + expect(write).toHaveBeenCalledTimes(1); + expect(String(write.mock.calls[0]?.[0])).toContain("Restarted systemd service"); +}; + describe("systemd availability", () => { beforeEach(() => { execFileMock.mockReset(); @@ -46,15 +78,10 @@ describe("systemd availability", () => { execFileMock .mockImplementationOnce((_cmd, args, _opts, cb) => { expect(args).toEqual(["--user", "status"]); - const err = new Error( - "Failed to connect to user scope bus via local transport", - ) as Error & { - stderr?: string; - code?: number; - }; - err.stderr = - "Failed to connect to user scope bus via local transport: $DBUS_SESSION_BUS_ADDRESS and $XDG_RUNTIME_DIR not defined"; - err.code = 1; + const err = createExecFileError("Failed to connect to user scope bus via local transport", { + stderr: + "Failed to connect to user scope bus via local transport: $DBUS_SESSION_BUS_ADDRESS and $XDG_RUNTIME_DIR not defined", + }); cb(err, "", ""); }) .mockImplementationOnce((_cmd, args, _opts, cb) => { @@ -271,6 +298,10 @@ describe("parseSystemdExecStart", () => { }); describe("systemd service control", () => { + const assertMachineRestartArgs = (args: string[]) => { + expect(args).toEqual(["--machine", "debian@", "--user", "restart", "openclaw-gateway.service"]); + }; + beforeEach(() => { execFileMock.mockReset(); }); @@ -298,13 +329,7 @@ describe("systemd service control", () => { 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"); + await assertRestartSuccess({ OPENCLAW_PROFILE: "work" }); }); it("surfaces stop failures with systemctl detail", async () => { @@ -331,22 +356,10 @@ describe("systemd service control", () => { cb(null, "", ""); }) .mockImplementationOnce((_cmd, args, _opts, cb) => { - expect(args).toEqual([ - "--machine", - "debian@", - "--user", - "restart", - "openclaw-gateway.service", - ]); + assertMachineRestartArgs(args); cb(null, "", ""); }); - const write = vi.fn(); - const stdout = { write } as unknown as NodeJS.WritableStream; - - await restartSystemdService({ stdout, env: { SUDO_USER: "debian" } }); - - expect(write).toHaveBeenCalledTimes(1); - expect(String(write.mock.calls[0]?.[0])).toContain("Restarted systemd service"); + await assertRestartSuccess({ SUDO_USER: "debian" }); }); it("keeps direct --user scope when SUDO_USER is root", async () => { @@ -359,26 +372,17 @@ describe("systemd service control", () => { expect(args).toEqual(["--user", "restart", "openclaw-gateway.service"]); cb(null, "", ""); }); - const write = vi.fn(); - const stdout = { write } as unknown as NodeJS.WritableStream; - - await restartSystemdService({ stdout, env: { SUDO_USER: "root", USER: "root" } }); - - expect(write).toHaveBeenCalledTimes(1); - expect(String(write.mock.calls[0]?.[0])).toContain("Restarted systemd service"); + await assertRestartSuccess({ SUDO_USER: "root", USER: "root" }); }); 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"]); - const err = new Error("Failed to connect to user scope bus") as Error & { - stderr?: string; - code?: number; - }; - err.stderr = - "Failed to connect to user scope bus via local transport: $DBUS_SESSION_BUS_ADDRESS and $XDG_RUNTIME_DIR not defined"; - err.code = 1; + 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", + }); cb(err, "", ""); }) .mockImplementationOnce((_cmd, args, _opts, cb) => { @@ -387,30 +391,15 @@ describe("systemd service control", () => { }) .mockImplementationOnce((_cmd, args, _opts, cb) => { expect(args).toEqual(["--user", "restart", "openclaw-gateway.service"]); - const err = new Error("Failed to connect to user scope bus") as Error & { - stderr?: string; - code?: number; - }; - err.stderr = "Failed to connect to user scope bus"; - err.code = 1; + const err = createExecFileError("Failed to connect to user scope bus", { + stderr: "Failed to connect to user scope bus", + }); cb(err, "", ""); }) .mockImplementationOnce((_cmd, args, _opts, cb) => { - expect(args).toEqual([ - "--machine", - "debian@", - "--user", - "restart", - "openclaw-gateway.service", - ]); + assertMachineRestartArgs(args); cb(null, "", ""); }); - const write = vi.fn(); - const stdout = { write } as unknown as NodeJS.WritableStream; - - await restartSystemdService({ stdout, env: { USER: "debian" } }); - - expect(write).toHaveBeenCalledTimes(1); - expect(String(write.mock.calls[0]?.[0])).toContain("Restarted systemd service"); + await assertRestartSuccess({ USER: "debian" }); }); });