import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { detectMarkerLineWithGateway, findExtraGatewayServices } from "./inspect.js"; const { execSchtasksMock } = vi.hoisted(() => ({ execSchtasksMock: vi.fn(), })); vi.mock("./schtasks-exec.js", () => ({ execSchtasks: (...args: unknown[]) => execSchtasksMock(...args), })); // Real content from the openclaw-gateway.service unit file (the canonical gateway unit). const GATEWAY_SERVICE_CONTENTS = `\ [Unit] Description=OpenClaw Gateway (v2026.3.8) After=network-online.target Wants=network-online.target [Service] ExecStart=/usr/bin/node /home/openclaw/.npm-global/lib/node_modules/openclaw/dist/entry.js gateway --port 18789 Restart=always Environment=OPENCLAW_SERVICE_MARKER=openclaw Environment=OPENCLAW_SERVICE_KIND=gateway Environment=OPENCLAW_SERVICE_VERSION=2026.3.8 [Install] WantedBy=default.target `; // Real content from the openclaw-test.service unit file (a non-gateway openclaw service). const TEST_SERVICE_CONTENTS = `\ [Unit] Description=OpenClaw test service After=default.target [Service] Type=simple ExecStart=/bin/sh -c 'while true; do sleep 60; done' Restart=on-failure [Install] WantedBy=default.target `; const CLAWDBOT_GATEWAY_CONTENTS = `\ [Unit] Description=Clawdbot Gateway [Service] ExecStart=/usr/bin/node /opt/clawdbot/dist/entry.js gateway --port 18789 Environment=HOME=/home/clawdbot `; describe("detectMarkerLineWithGateway", () => { it("returns null for openclaw-test.service (openclaw only in description, no gateway on same line)", () => { expect(detectMarkerLineWithGateway(TEST_SERVICE_CONTENTS)).toBeNull(); }); it("returns openclaw for the canonical gateway unit (ExecStart has both openclaw and gateway)", () => { expect(detectMarkerLineWithGateway(GATEWAY_SERVICE_CONTENTS)).toBe("openclaw"); }); it("returns clawdbot for a clawdbot gateway unit", () => { expect(detectMarkerLineWithGateway(CLAWDBOT_GATEWAY_CONTENTS)).toBe("clawdbot"); }); it("handles line continuations — marker and gateway split across physical lines", () => { const contents = `[Service]\nExecStart=/usr/bin/node /opt/openclaw/dist/entry.js \\\n gateway --port 18789\n`; expect(detectMarkerLineWithGateway(contents)).toBe("openclaw"); }); }); describe("findExtraGatewayServices (linux / scanSystemdDir) — real filesystem", () => { // These tests write real .service files to a temp dir and call findExtraGatewayServices // with that dir as HOME. No platform mocking or fs mocking needed. // Only runs on Linux/macOS where the linux branch of findExtraGatewayServices is active. const isLinux = process.platform === "linux"; it.skipIf(!isLinux)("does not report openclaw-test.service as a gateway service", async () => { const tmpHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-")); const systemdDir = path.join(tmpHome, ".config", "systemd", "user"); try { await fs.mkdir(systemdDir, { recursive: true }); await fs.writeFile(path.join(systemdDir, "openclaw-test.service"), TEST_SERVICE_CONTENTS); const result = await findExtraGatewayServices({ HOME: tmpHome }); expect(result).toEqual([]); } finally { await fs.rm(tmpHome, { recursive: true, force: true }); } }); it.skipIf(!isLinux)( "does not report the canonical openclaw-gateway.service as an extra service", async () => { const tmpHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-")); const systemdDir = path.join(tmpHome, ".config", "systemd", "user"); try { await fs.mkdir(systemdDir, { recursive: true }); await fs.writeFile( path.join(systemdDir, "openclaw-gateway.service"), GATEWAY_SERVICE_CONTENTS, ); const result = await findExtraGatewayServices({ HOME: tmpHome }); expect(result).toEqual([]); } finally { await fs.rm(tmpHome, { recursive: true, force: true }); } }, ); it.skipIf(!isLinux)( "reports a legacy clawdbot-gateway service as an extra gateway service", async () => { const tmpHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-")); const systemdDir = path.join(tmpHome, ".config", "systemd", "user"); const unitPath = path.join(systemdDir, "clawdbot-gateway.service"); try { await fs.mkdir(systemdDir, { recursive: true }); await fs.writeFile(unitPath, CLAWDBOT_GATEWAY_CONTENTS); const result = await findExtraGatewayServices({ HOME: tmpHome }); expect(result).toEqual([ { platform: "linux", label: "clawdbot-gateway.service", detail: `unit: ${unitPath}`, scope: "user", marker: "clawdbot", legacy: true, }, ]); } finally { await fs.rm(tmpHome, { recursive: true, force: true }); } }, ); }); describe("findExtraGatewayServices (win32)", () => { const originalPlatform = process.platform; beforeEach(() => { Object.defineProperty(process, "platform", { configurable: true, value: "win32", }); execSchtasksMock.mockReset(); }); afterEach(() => { Object.defineProperty(process, "platform", { configurable: true, value: originalPlatform, }); }); it("skips schtasks queries unless deep mode is enabled", async () => { const result = await findExtraGatewayServices({}); expect(result).toEqual([]); expect(execSchtasksMock).not.toHaveBeenCalled(); }); it("returns empty results when schtasks query fails", async () => { execSchtasksMock.mockResolvedValueOnce({ code: 1, stdout: "", stderr: "error", }); const result = await findExtraGatewayServices({}, { deep: true }); expect(result).toEqual([]); }); it("collects only non-openclaw marker tasks from schtasks output", async () => { execSchtasksMock.mockResolvedValueOnce({ code: 0, stdout: [ "TaskName: OpenClaw Gateway", "Task To Run: C:\\Program Files\\OpenClaw\\openclaw.exe gateway run", "", "TaskName: Clawdbot Legacy", "Task To Run: C:\\clawdbot\\clawdbot.exe run", "", "TaskName: Other Task", "Task To Run: C:\\tools\\helper.exe", "", ].join("\n"), stderr: "", }); const result = await findExtraGatewayServices({}, { deep: true }); expect(result).toEqual([ { platform: "win32", label: "Clawdbot Legacy", detail: "task: Clawdbot Legacy, run: C:\\clawdbot\\clawdbot.exe run", scope: "system", marker: "clawdbot", legacy: true, }, ]); }); });