// Daemon response tests cover normalized daemon command response shapes. import { afterEach, describe, expect, it, vi } from "vitest"; import type { GatewayService } from "../../daemon/service.js"; import { defaultRuntime } from "../../runtime.js"; import { createDaemonActionContext, installDaemonServiceAndEmit } from "./response.js"; describe("daemon action JSON hints", () => { afterEach(() => { vi.restoreAllMocks(); }); it("classifies common daemon hint kinds", () => { const hints = [ "openclaw gateway install", "Restart the container or the service that manages it for openclaw-demo-container.", "systemd user services are unavailable; install/enable systemd or run the gateway under your supervisor.", "On a headless server (SSH/no desktop session): run `sudo loginctl enable-linger $(whoami)` to persist your systemd user session across logins.", "If you're in a container, run the gateway in the foreground instead of `openclaw gateway`.", "WSL2 needs systemd enabled: edit /etc/wsl.conf with [boot]\\nsystemd=true", ]; const writeJson = vi.spyOn(defaultRuntime, "writeJson").mockImplementation(() => {}); createDaemonActionContext({ action: "install", json: true }).emit({ ok: false, hints }); expect(writeJson).toHaveBeenCalledWith( expect.objectContaining({ action: "install", hints, hintItems: [ { kind: "install", text: "openclaw gateway install" }, { kind: "container-restart", text: "Restart the container or the service that manages it for openclaw-demo-container.", }, { kind: "systemd-unavailable", text: "systemd user services are unavailable; install/enable systemd or run the gateway under your supervisor.", }, { kind: "systemd-headless", text: "On a headless server (SSH/no desktop session): run `sudo loginctl enable-linger $(whoami)` to persist your systemd user session across logins.", }, { kind: "container-foreground", text: "If you're in a container, run the gateway in the foreground instead of `openclaw gateway`.", }, { kind: "wsl-systemd", text: "WSL2 needs systemd enabled: edit /etc/wsl.conf with [boot]\\nsystemd=true", }, ], }), ); }); it.each([ "openclaw --profile work gateway install", "openclaw --container demo gateway install", "openclaw node install", "openclaw --profile work node install", "openclaw --container demo node install", ])("classifies scoped Gateway and node service install hints: %s", (hint) => { const writeJson = vi.spyOn(defaultRuntime, "writeJson").mockImplementation(() => {}); createDaemonActionContext({ action: "start", json: true }).emit({ ok: false, hints: [hint] }); expect(writeJson).toHaveBeenCalledWith( expect.objectContaining({ hintItems: [{ kind: "install", text: hint }] }), ); }); }); describe("daemon install verification", () => { function createInstallParams( isLoaded: GatewayService["isLoaded"], onVerified?: () => Promise, ) { const service = { label: "systemd user", loadedText: "enabled", notLoadedText: "disabled", isLoaded, } as GatewayService; return { serviceNoun: "Gateway", service, warnings: [], emit: vi.fn(), fail: vi.fn(), install: vi.fn(async () => {}), onVerified, }; } it("fails install when service-manager verification throws", async () => { const params = createInstallParams( vi.fn(async () => { throw new Error("manager access denied"); }), ); await installDaemonServiceAndEmit(params); expect(params.fail).toHaveBeenCalledWith( "Gateway install verification failed: Error: manager access denied", undefined, ); expect(params.emit).not.toHaveBeenCalled(); }); it("fails install when the service is not loaded after installation", async () => { const params = createInstallParams(vi.fn(async () => false)); await installDaemonServiceAndEmit(params); expect(params.fail).toHaveBeenCalledWith( "Gateway install verification failed: service is not enabled.", ); expect(params.emit).not.toHaveBeenCalled(); }); it("emits success only after the service-manager verification succeeds", async () => { const params = createInstallParams(vi.fn(async () => true)); await installDaemonServiceAndEmit(params); expect(params.fail).not.toHaveBeenCalled(); expect(params.emit).toHaveBeenCalledWith( expect.objectContaining({ ok: true, result: "installed", service: expect.objectContaining({ loaded: true }), }), ); }); it("runs onVerified after verification succeeds and before the success emit", async () => { const onVerified = vi.fn(async () => {}); const params = createInstallParams( vi.fn(async () => true), onVerified, ); await installDaemonServiceAndEmit(params); expect(onVerified).toHaveBeenCalledTimes(1); expect(params.fail).not.toHaveBeenCalled(); expect(params.emit).toHaveBeenCalledWith( expect.objectContaining({ ok: true, result: "installed" }), ); }); it("fails with no success emit when onVerified throws", async () => { const params = createInstallParams( vi.fn(async () => true), async () => { throw new Error("post-check boom"); }, ); await installDaemonServiceAndEmit(params); expect(params.fail).toHaveBeenCalledWith( "Gateway post-install check failed: Error: post-check boom", ); expect(params.emit).not.toHaveBeenCalled(); }); });