File size: 5,836 Bytes
f778c12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
// 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<void>,
  ) {
    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();
  });
});