import { beforeEach, describe, expect, it, vi } from "vitest"; import type { GatewayServiceRuntime } from "../../daemon/service-runtime.js"; import { runNodeDaemonStatus } from "./daemon.js"; const mocks = vi.hoisted(() => { const service = { label: "Node service", loadedText: "loaded", notLoadedText: "not loaded", stage: vi.fn(), install: vi.fn(), uninstall: vi.fn(), stop: vi.fn(), restart: vi.fn(), isLoaded: vi.fn(async () => true), readCommand: vi.fn(async () => null), readRuntime: vi.fn<() => Promise>(async () => ({ status: "running" })), }; return { runtime: { log: vi.fn<(line: string) => void>(), error: vi.fn<(line: string) => void>(), writeJson: vi.fn(), exit: vi.fn(), }, service, }; }); vi.mock("../../runtime.js", () => ({ defaultRuntime: mocks.runtime, })); vi.mock("../../daemon/node-service.js", () => ({ resolveNodeService: () => mocks.service, })); vi.mock("../../daemon/runtime-hints.js", () => ({ buildPlatformRuntimeLogHints: () => [ "Logs: node service log", "Restart attempts: node restart log", ], buildPlatformServiceStartHints: () => ["openclaw node install", "openclaw node start"], })); vi.mock("../../terminal/theme.js", async () => { const actual = await vi.importActual("../../terminal/theme.js"); return { ...actual, colorize: (_rich: boolean, _theme: unknown, text: string) => text, }; }); vi.mock("../daemon-cli/shared.js", async () => { const actual = await vi.importActual("../daemon-cli/shared.js"); return { ...actual, createCliStatusTextStyles: () => ({ rich: false, label: (text: string) => text, accent: (text: string) => text, infoText: (text: string) => text, okText: (text: string) => text, warnText: (text: string) => text, errorText: (text: string) => text, }), formatRuntimeStatus: (runtime: GatewayServiceRuntime | undefined) => runtime?.status ?? "", resolveRuntimeStatusColor: () => "", }; }); describe("runNodeDaemonStatus", () => { function stdout(): string { return mocks.runtime.log.mock.calls.map(([line]) => line).join("\n"); } function stderr(): string { return mocks.runtime.error.mock.calls.map(([line]) => line).join("\n"); } beforeEach(() => { mocks.runtime.log.mockClear(); mocks.runtime.error.mockClear(); mocks.runtime.writeJson.mockClear(); mocks.runtime.exit.mockClear(); mocks.service.isLoaded.mockReset().mockResolvedValue(true); mocks.service.readCommand.mockReset().mockResolvedValue(null); mocks.service.readRuntime.mockReset().mockResolvedValue({ status: "running" }); }); it("keeps missing service-unit status on stderr and prints recovery hints on stdout", async () => { mocks.service.readRuntime.mockResolvedValue({ status: "stopped", missingUnit: true }); await runNodeDaemonStatus(); expect(stderr()).toContain("Service unit not found."); expect(stdout()).toContain("Logs: node service log"); expect(stdout()).toContain("Restart attempts: node restart log"); expect(stderr()).not.toContain("Logs: node service log"); expect(stderr()).not.toContain("Restart attempts: node restart log"); }); it("keeps stopped status on stderr and prints recovery hints on stdout", async () => { mocks.service.readRuntime.mockResolvedValue({ status: "stopped" }); await runNodeDaemonStatus(); expect(stderr()).toContain("Service is loaded but not running."); expect(stdout()).toContain("Logs: node service log"); expect(stdout()).toContain("Restart attempts: node restart log"); expect(stderr()).not.toContain("Logs: node service log"); expect(stderr()).not.toContain("Restart attempts: node restart log"); }); });