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 `; const COMPANION_SERVICE_CONTENTS = `\ [Unit] Description=OpenClaw companion worker After=openclaw-gateway.service Requires=openclaw-gateway.service [Service] ExecStart=/usr/bin/node /opt/openclaw-worker/dist/index.js worker `; const CUSTOM_OPENCLAW_GATEWAY_CONTENTS = `\ [Unit] Description=Custom OpenClaw gateway [Service] ExecStart=/usr/bin/node /opt/openclaw/dist/entry.js gateway --port 18888 `; 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"); }); it("ignores dependency-only references to the gateway unit", () => { expect(detectMarkerLineWithGateway(COMPANION_SERVICE_CONTENTS)).toBeNull(); }); it("ignores non-gateway ExecStart commands that only pass gateway-named options", () => { const contents = `[Service]\nExecStart=/usr/bin/openclaw-helper --gateway-url http://127.0.0.1:18789 sync\n`; expect(detectMarkerLineWithGateway(contents)).toBeNull(); }); }); 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 }); } }, ); it.skipIf(!isLinux)( "does not report companion units that only depend on the gateway", 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-companion.service"), COMPANION_SERVICE_CONTENTS, ); const result = await findExtraGatewayServices({ HOME: tmpHome }); expect(result).toEqual([]); } finally { await fs.rm(tmpHome, { recursive: true, force: true }); } }, ); it.skipIf(!isLinux)( "reports custom-named gateway units that execute openclaw gateway", 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, "custom-openclaw.service"); try { await fs.mkdir(systemdDir, { recursive: true }); await fs.writeFile(unitPath, CUSTOM_OPENCLAW_GATEWAY_CONTENTS); const result = await findExtraGatewayServices({ HOME: tmpHome }); expect(result).toEqual([ { platform: "linux", label: "custom-openclaw.service", detail: `unit: ${unitPath}`, scope: "user", marker: "openclaw", legacy: false, }, ]); } finally { await fs.rm(tmpHome, { recursive: true, force: true }); } }, ); }); describe("findExtraGatewayServices (darwin / scanLaunchdDir) — real filesystem", () => { const originalPlatform = process.platform; beforeEach(() => { Object.defineProperty(process, "platform", { configurable: true, value: "darwin", }); }); afterEach(() => { Object.defineProperty(process, "platform", { configurable: true, value: originalPlatform, }); }); it("does not report LaunchAgent companions that only mention the gateway label", async () => { const tmpHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-")); const launchdDir = path.join(tmpHome, "Library", "LaunchAgents"); try { await fs.mkdir(launchdDir, { recursive: true }); await fs.writeFile( path.join(launchdDir, "com.example.companion.plist"), ` Labelcom.example.companion KeepAliveOtherJobEnabledai.openclaw.gateway ProgramArguments/usr/local/bin/openclaw-helpersync `, ); const result = await findExtraGatewayServices({ HOME: tmpHome }); expect(result).toEqual([]); } finally { await fs.rm(tmpHome, { recursive: true, force: true }); } }); it("does not report LaunchAgent companions that only pass gateway-named options", async () => { const tmpHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-")); const launchdDir = path.join(tmpHome, "Library", "LaunchAgents"); try { await fs.mkdir(launchdDir, { recursive: true }); await fs.writeFile( path.join(launchdDir, "com.example.companion-options.plist"), ` Labelcom.example.companion-options ProgramArguments/usr/local/bin/openclaw-helper--gateway-urlhttp://127.0.0.1:18789sync `, ); const result = await findExtraGatewayServices({ HOME: tmpHome }); expect(result).toEqual([]); } finally { await fs.rm(tmpHome, { recursive: true, force: true }); } }); it("reports custom LaunchAgents that execute openclaw gateway", async () => { const tmpHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-")); const launchdDir = path.join(tmpHome, "Library", "LaunchAgents"); const plistPath = path.join(launchdDir, "com.example.openclaw-gateway.plist"); try { await fs.mkdir(launchdDir, { recursive: true }); await fs.writeFile( plistPath, ` Labelcom.example.openclaw-gateway ProgramArguments/usr/local/bin/openclawgateway--port18888 `, ); const result = await findExtraGatewayServices({ HOME: tmpHome }); expect(result).toEqual([ { platform: "darwin", label: "com.example.openclaw-gateway", detail: `plist: ${plistPath}`, scope: "user", marker: "openclaw", legacy: false, }, ]); } 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, }, ]); }); });