macroclaw 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,702 +0,0 @@
1
- import { beforeEach, describe, expect, it, mock } from "bun:test";
2
- import { type ServiceDeps, ServiceManager } from "./service";
3
-
4
- const mockExecSync = mock((_cmd: string, _opts?: any) => "");
5
- const mockExistsSync = mock((_path: string) => true);
6
- const mockWriteFileSync = mock((_path: string, _data: string) => {});
7
- const mockMkdirSync = mock((_path: string, _opts?: any) => {});
8
- const mockRmSync = mock((_path: string) => {});
9
- function createManager(overrides?: Partial<ServiceDeps>): ServiceManager {
10
- return new ServiceManager({
11
- existsSync: mockExistsSync,
12
- writeFileSync: mockWriteFileSync,
13
- mkdirSync: mockMkdirSync,
14
- rmSync: mockRmSync,
15
- execSync: mockExecSync,
16
- tmpdir: () => "/tmp",
17
- randomUUID: () => "test-uuid",
18
- userInfo: () => ({ username: "testuser", homedir: "/home/testuser" }),
19
- platform: "linux",
20
- home: "/home/testuser",
21
- ...overrides,
22
- });
23
- }
24
-
25
- const LAUNCHD_RUNNING = `{\n\t"PID" = 12345;\n\t"Label" = "com.macroclaw";\n}`;
26
- const LAUNCHD_STOPPED = `{\n\t"Label" = "com.macroclaw";\n}`;
27
- const SYSTEMD_ACTIVE = "active";
28
- const SYSTEMD_INACTIVE = "inactive";
29
-
30
- beforeEach(() => {
31
- mockExecSync.mockClear();
32
- mockExistsSync.mockClear();
33
- mockWriteFileSync.mockClear();
34
- mockMkdirSync.mockClear();
35
- mockRmSync.mockClear();
36
- mockExecSync.mockImplementation((_cmd: string, _opts?: any) => "");
37
- mockExistsSync.mockImplementation(() => true);
38
- });
39
-
40
- describe("constructor", () => {
41
- it("detects launchd on darwin", () => {
42
- const mgr = createManager({ platform: "darwin" });
43
- expect(mgr.platform).toBe("launchd");
44
- });
45
-
46
- it("detects systemd on linux", () => {
47
- const mgr = createManager({ platform: "linux" });
48
- expect(mgr.platform).toBe("systemd");
49
- });
50
-
51
- it("throws on unsupported platform", () => {
52
- expect(() => createManager({ platform: "win32" })).toThrow(
53
- "Unsupported platform. Only macOS (launchd) and Linux (systemd) are supported.",
54
- );
55
- });
56
- });
57
-
58
- describe("serviceFilePath", () => {
59
- it("returns plist path for launchd", () => {
60
- const mgr = createManager({ platform: "darwin" });
61
- expect(mgr.serviceFilePath).toContain("Library/LaunchAgents/com.macroclaw.plist");
62
- });
63
-
64
- it("returns systemd path for systemd", () => {
65
- const mgr = createManager({ platform: "linux" });
66
- expect(mgr.serviceFilePath).toBe("/etc/systemd/system/macroclaw.service");
67
- });
68
- });
69
-
70
- describe("isInstalled", () => {
71
- it("returns true when service file exists", () => {
72
- mockExistsSync.mockImplementation(() => true);
73
- const mgr = createManager({ platform: "darwin" });
74
- expect(mgr.isInstalled).toBe(true);
75
- });
76
-
77
- it("returns false when service file does not exist", () => {
78
- mockExistsSync.mockImplementation(() => false);
79
- const mgr = createManager();
80
- expect(mgr.isInstalled).toBe(false);
81
- });
82
- });
83
-
84
- describe("isRunning", () => {
85
- it("returns true when launchd service has a PID", () => {
86
- mockExecSync.mockImplementation((cmd: string) => {
87
- if (cmd.startsWith("launchctl list ")) return LAUNCHD_RUNNING;
88
- return "";
89
- });
90
- const mgr = createManager({ platform: "darwin" });
91
- expect(mgr.isRunning).toBe(true);
92
- });
93
-
94
- it("returns false when launchd service has no PID", () => {
95
- mockExecSync.mockImplementation((cmd: string) => {
96
- if (cmd.startsWith("launchctl list ")) return LAUNCHD_STOPPED;
97
- return "";
98
- });
99
- const mgr = createManager({ platform: "darwin" });
100
- expect(mgr.isRunning).toBe(false);
101
- });
102
-
103
- it("returns false when launchctl list throws", () => {
104
- mockExecSync.mockImplementation((cmd: string) => {
105
- if (cmd.startsWith("launchctl list ")) throw new Error("not found");
106
- return "";
107
- });
108
- const mgr = createManager({ platform: "darwin" });
109
- expect(mgr.isRunning).toBe(false);
110
- });
111
-
112
- it("returns true when systemd service is active", () => {
113
- mockExecSync.mockImplementation((cmd: string) => {
114
- if (cmd === "systemctl is-active macroclaw") return SYSTEMD_ACTIVE;
115
- return "";
116
- });
117
- const mgr = createManager();
118
- expect(mgr.isRunning).toBe(true);
119
- });
120
-
121
- it("returns false when systemd service is inactive", () => {
122
- mockExecSync.mockImplementation((cmd: string) => {
123
- if (cmd === "systemctl is-active macroclaw") return SYSTEMD_INACTIVE;
124
- return "";
125
- });
126
- const mgr = createManager();
127
- expect(mgr.isRunning).toBe(false);
128
- });
129
-
130
- it("returns false when systemctl throws", () => {
131
- mockExecSync.mockImplementation((cmd: string) => {
132
- if (cmd === "systemctl is-active macroclaw") throw new Error("not found");
133
- return "";
134
- });
135
- const mgr = createManager();
136
- expect(mgr.isRunning).toBe(false);
137
- });
138
- });
139
-
140
- describe("install", () => {
141
- it("throws when settings.json is missing on macOS", () => {
142
- mockExistsSync.mockImplementation(() => false);
143
- const mgr = createManager({ platform: "darwin" });
144
- expect(() => mgr.install()).toThrow(
145
- "Settings not found. Run `macroclaw setup` first.",
146
- );
147
- });
148
-
149
- it("throws when settings.json is missing on Linux", () => {
150
- mockExistsSync.mockImplementation(() => false);
151
- mockExecSync.mockImplementation((cmd: string) => {
152
- if (cmd.startsWith("id -gn")) return "testuser\n";
153
- return "";
154
- });
155
- const mgr = createManager();
156
- expect(() => mgr.install()).toThrow("Settings not found. Run `macroclaw setup` first.");
157
- });
158
-
159
- it("runs global install and resolves bun, claude and macroclaw paths", () => {
160
- mockExistsSync.mockImplementation(() => true);
161
- mockExecSync.mockImplementation((cmd: string) => {
162
- if (cmd === "which bun") return "/home/testuser/.bun/bin/bun\n";
163
- if (cmd === "which claude") return "/home/testuser/.local/bin/claude\n";
164
- if (cmd === "bun pm bin -g") return "/home/testuser/.bun/bin\n";
165
- if (cmd.startsWith("id -gn")) return "testuser\n";
166
- return "";
167
- });
168
- const mgr = createManager();
169
- mgr.install();
170
- expect(mockExecSync).toHaveBeenCalledWith("bun install -g macroclaw");
171
- expect(mockExecSync).toHaveBeenCalledWith("which bun");
172
- expect(mockExecSync).toHaveBeenCalledWith("which claude");
173
- expect(mockExecSync).toHaveBeenCalledWith("bun pm bin -g");
174
- });
175
-
176
- it("installs launchd service with PATH and OAuth token", () => {
177
- mockExistsSync.mockImplementation(() => true);
178
- mockExecSync.mockImplementation((cmd: string) => {
179
- if (cmd === "which bun") return "/Users/testuser/.bun/bin/bun\n";
180
- if (cmd === "which claude") return "/Users/testuser/.local/bin/claude\n";
181
- if (cmd === "bun pm bin -g") return "/Users/testuser/.bun/bin\n";
182
- return "";
183
- });
184
- const mgr = createManager({ platform: "darwin" });
185
- mgr.install("sk-test-token");
186
- expect(mockMkdirSync).toHaveBeenCalled();
187
- expect(mockWriteFileSync).toHaveBeenCalled();
188
- const writtenContent = mockWriteFileSync.mock.calls[0]![1] as string;
189
- expect(writtenContent).toContain("<string>/Users/testuser/.bun/bin/bun</string>");
190
- expect(writtenContent).toContain("<string>/Users/testuser/.bun/bin/macroclaw</string>");
191
- expect(writtenContent).toContain("<string>start</string>");
192
- expect(writtenContent).toContain("<key>KeepAlive</key>");
193
- expect(writtenContent).toContain(".macroclaw/logs/stdout.log");
194
- expect(writtenContent).toContain("<key>PATH</key>");
195
- expect(writtenContent).toContain("/Users/testuser/.bun/bin:/Users/testuser/.local/bin");
196
- expect(writtenContent).toContain("<key>CLAUDE_CODE_OAUTH_TOKEN</key>");
197
- expect(writtenContent).toContain("<string>sk-test-token</string>");
198
- expect(mockExecSync).toHaveBeenCalledWith(expect.stringContaining("launchctl load"));
199
- });
200
-
201
- it("installs launchd service without token when not provided", () => {
202
- mockExistsSync.mockImplementation(() => true);
203
- mockExecSync.mockImplementation((cmd: string) => {
204
- if (cmd === "which bun") return "/Users/testuser/.bun/bin/bun\n";
205
- if (cmd === "which claude") return "/Users/testuser/.local/bin/claude\n";
206
- if (cmd === "bun pm bin -g") return "/Users/testuser/.bun/bin\n";
207
- return "";
208
- });
209
- const mgr = createManager({ platform: "darwin" });
210
- mgr.install();
211
- const writtenContent = mockWriteFileSync.mock.calls[0]![1] as string;
212
- expect(writtenContent).not.toContain("CLAUDE_CODE_OAUTH_TOKEN");
213
- });
214
-
215
- it("stops running launchd service before reinstalling", () => {
216
- const calls: string[] = [];
217
- mockExistsSync.mockImplementation(() => true);
218
- mockExecSync.mockImplementation((cmd: string) => {
219
- calls.push(cmd);
220
- if (cmd === "which bun") return "/Users/testuser/.bun/bin/bun\n";
221
- if (cmd === "which claude") return "/Users/testuser/.local/bin/claude\n";
222
- if (cmd === "bun pm bin -g") return "/Users/testuser/.bun/bin\n";
223
- if (cmd.startsWith("launchctl list ")) return LAUNCHD_RUNNING;
224
- return "";
225
- });
226
- const mgr = createManager({ platform: "darwin" });
227
- mgr.install();
228
- const unloadIdx = calls.findIndex(c => c.includes("launchctl unload"));
229
- const loadIdx = calls.findIndex(c => c.includes("launchctl load"));
230
- expect(unloadIdx).toBeGreaterThan(-1);
231
- expect(loadIdx).toBeGreaterThan(unloadIdx);
232
- });
233
-
234
- it("skips unload when launchd service is not running", () => {
235
- mockExistsSync.mockImplementation(() => true);
236
- mockExecSync.mockImplementation((cmd: string) => {
237
- if (cmd === "which bun") return "/Users/testuser/.bun/bin/bun\n";
238
- if (cmd === "which claude") return "/Users/testuser/.local/bin/claude\n";
239
- if (cmd === "bun pm bin -g") return "/Users/testuser/.bun/bin\n";
240
- if (cmd.startsWith("launchctl list ")) return LAUNCHD_STOPPED;
241
- return "";
242
- });
243
- const mgr = createManager({ platform: "darwin" });
244
- mgr.install();
245
- expect(mockExecSync).not.toHaveBeenCalledWith(expect.stringContaining("launchctl unload"));
246
- });
247
-
248
- it("installs systemd service with PATH via temp file and sudo cp", () => {
249
- mockExistsSync.mockImplementation(() => true);
250
- mockExecSync.mockImplementation((cmd: string) => {
251
- if (cmd === "which bun") return "/home/testuser/.bun/bin/bun\n";
252
- if (cmd === "which claude") return "/home/testuser/.local/bin/claude\n";
253
- if (cmd === "bun pm bin -g") return "/home/testuser/.bun/bin\n";
254
- if (cmd.startsWith("id -gn")) return "testuser\n";
255
- return "";
256
- });
257
- const mgr = createManager();
258
- mgr.install();
259
-
260
- // Unit written to temp file first
261
- expect(mockWriteFileSync).toHaveBeenCalledWith("/tmp/macroclaw-test-uuid.service", expect.any(String));
262
- const writtenContent = mockWriteFileSync.mock.calls[0]![1] as string;
263
- expect(writtenContent).toContain("ExecStart=/home/testuser/.bun/bin/bun /home/testuser/.bun/bin/macroclaw start");
264
- expect(writtenContent).toContain("User=testuser");
265
- expect(writtenContent).toContain("Group=testuser");
266
- expect(writtenContent).toContain("Environment=HOME=/home/testuser");
267
- expect(writtenContent).toContain("Environment=PATH=/home/testuser/.bun/bin:/home/testuser/.local/bin");
268
- expect(writtenContent).toContain("WorkingDirectory=/home/testuser");
269
-
270
- // Elevated operations use sudo
271
- expect(mockExecSync).toHaveBeenCalledWith("sudo cp /tmp/macroclaw-test-uuid.service /etc/systemd/system/macroclaw.service");
272
- expect(mockExecSync).toHaveBeenCalledWith("sudo systemctl daemon-reload");
273
- expect(mockExecSync).toHaveBeenCalledWith("sudo systemctl enable macroclaw");
274
- expect(mockExecSync).toHaveBeenCalledWith("sudo systemctl start macroclaw");
275
-
276
- // Temp file cleaned up
277
- expect(mockRmSync).toHaveBeenCalledWith("/tmp/macroclaw-test-uuid.service");
278
- });
279
-
280
- it("cleans up temp file even when sudo cp fails", () => {
281
- mockExistsSync.mockImplementation(() => true);
282
- mockExecSync.mockImplementation((cmd: string) => {
283
- if (cmd.startsWith("id -gn")) return "testuser\n";
284
- if (cmd === "which bun") return "/home/testuser/.bun/bin/bun\n";
285
- if (cmd === "which claude") return "/home/testuser/.local/bin/claude\n";
286
- if (cmd === "bun pm bin -g") return "/home/testuser/.bun/bin\n";
287
- if (cmd.startsWith("sudo cp")) throw new Error("Permission denied");
288
- return "";
289
- });
290
- const mgr = createManager();
291
- expect(() => mgr.install()).toThrow("Permission denied");
292
- expect(mockRmSync).toHaveBeenCalledWith("/tmp/macroclaw-test-uuid.service");
293
- });
294
-
295
- it("uses os userInfo identity, not environment variables", () => {
296
- mockExistsSync.mockImplementation(() => true);
297
- mockExecSync.mockImplementation((cmd: string) => {
298
- if (cmd === "which bun") return "/usr/local/bin/bun\n";
299
- if (cmd === "which claude") return "/usr/local/bin/claude\n";
300
- if (cmd === "bun pm bin -g") return "/usr/local/bin\n";
301
- if (cmd === "id -gn deploy") return "deploy\n";
302
- return "";
303
- });
304
- const mgr = createManager({
305
- userInfo: () => ({ username: "deploy", homedir: "/srv/deploy" }),
306
- });
307
- mgr.install();
308
- const writtenContent = mockWriteFileSync.mock.calls[0]![1] as string;
309
- expect(writtenContent).toContain("User=deploy");
310
- expect(writtenContent).toContain("Group=deploy");
311
- expect(writtenContent).toContain("Environment=HOME=/srv/deploy");
312
- expect(writtenContent).toContain("WorkingDirectory=/srv/deploy");
313
- expect(mockExistsSync).toHaveBeenCalledWith("/srv/deploy/.macroclaw/settings.json");
314
- });
315
-
316
- it("does not require sudo for bun install -g", () => {
317
- mockExistsSync.mockImplementation(() => true);
318
- mockExecSync.mockImplementation((cmd: string) => {
319
- if (cmd === "which bun") return "/home/testuser/.bun/bin/bun\n";
320
- if (cmd === "which claude") return "/home/testuser/.local/bin/claude\n";
321
- if (cmd === "bun pm bin -g") return "/home/testuser/.bun/bin\n";
322
- if (cmd.startsWith("id -gn")) return "testuser\n";
323
- return "";
324
- });
325
- const mgr = createManager();
326
- mgr.install();
327
- // bun install should NOT be prefixed with sudo
328
- expect(mockExecSync).toHaveBeenCalledWith("bun install -g macroclaw");
329
- expect(mockExecSync).not.toHaveBeenCalledWith("sudo bun install -g macroclaw");
330
- });
331
-
332
- it("throws when bun path cannot be resolved", () => {
333
- mockExistsSync.mockImplementation(() => true);
334
- mockExecSync.mockImplementation((cmd: string) => {
335
- if (cmd.startsWith("id -gn")) return "testuser\n";
336
- if (cmd === "which bun") throw new Error("not found");
337
- return "";
338
- });
339
- const mgr = createManager();
340
- expect(() => mgr.install()).toThrow("Could not resolve bun path. Is it installed?");
341
- });
342
-
343
- it("throws when macroclaw not found in global bin", () => {
344
- mockExistsSync.mockImplementation((path: string) => {
345
- if (path.endsWith("/macroclaw")) return false;
346
- return true;
347
- });
348
- mockExecSync.mockImplementation((cmd: string) => {
349
- if (cmd.startsWith("id -gn")) return "testuser\n";
350
- if (cmd === "which bun") return "/home/testuser/.bun/bin/bun\n";
351
- if (cmd === "which claude") return "/home/testuser/.local/bin/claude\n";
352
- if (cmd === "bun pm bin -g") return "/home/testuser/.bun/bin\n";
353
- return "";
354
- });
355
- const mgr = createManager();
356
- expect(() => mgr.install()).toThrow("Could not find macroclaw in /home/testuser/.bun/bin");
357
- });
358
-
359
- it("macOS install does not use sudo", () => {
360
- mockExistsSync.mockImplementation(() => true);
361
- mockExecSync.mockImplementation((cmd: string) => {
362
- if (cmd === "which bun") return "/opt/homebrew/bin/bun\n";
363
- if (cmd === "which claude") return "/opt/homebrew/bin/claude\n";
364
- if (cmd === "bun pm bin -g") return "/opt/homebrew/bin\n";
365
- return "";
366
- });
367
- const mgr = createManager({ platform: "darwin" });
368
- mgr.install();
369
- for (const call of mockExecSync.mock.calls) {
370
- expect(call[0]).not.toMatch(/^sudo /);
371
- }
372
- });
373
- });
374
-
375
- describe("uninstall", () => {
376
- it("throws when service is not installed", () => {
377
- mockExistsSync.mockImplementation(() => false);
378
- const mgr = createManager({ platform: "darwin" });
379
- expect(() => mgr.uninstall()).toThrow(
380
- "Service not installed. Run `macroclaw service install` first.",
381
- );
382
- });
383
-
384
- it("uninstalls running launchd service", () => {
385
- mockExistsSync.mockImplementation(() => true);
386
- mockExecSync.mockImplementation((cmd: string) => {
387
- if (cmd.startsWith("launchctl list ")) return LAUNCHD_RUNNING;
388
- return "";
389
- });
390
- const mgr = createManager({ platform: "darwin" });
391
- mgr.uninstall();
392
- expect(mockExecSync).toHaveBeenCalledWith(expect.stringContaining("launchctl unload"));
393
- expect(mockRmSync).toHaveBeenCalled();
394
- });
395
-
396
- it("uninstalls stopped launchd service without unloading", () => {
397
- mockExistsSync.mockImplementation(() => true);
398
- mockExecSync.mockImplementation((cmd: string) => {
399
- if (cmd.startsWith("launchctl list ")) return LAUNCHD_STOPPED;
400
- return "";
401
- });
402
- const mgr = createManager({ platform: "darwin" });
403
- mgr.uninstall();
404
- expect(mockExecSync).not.toHaveBeenCalledWith(expect.stringContaining("launchctl unload"));
405
- expect(mockRmSync).toHaveBeenCalled();
406
- });
407
-
408
- it("uninstalls running systemd service via sudo", () => {
409
- mockExistsSync.mockImplementation(() => true);
410
- mockExecSync.mockImplementation((cmd: string) => {
411
- if (cmd === "systemctl is-active macroclaw") return SYSTEMD_ACTIVE;
412
- return "";
413
- });
414
- const mgr = createManager();
415
- mgr.uninstall();
416
- expect(mockExecSync).toHaveBeenCalledWith("sudo systemctl stop macroclaw");
417
- expect(mockExecSync).toHaveBeenCalledWith("sudo systemctl disable macroclaw");
418
- expect(mockExecSync).toHaveBeenCalledWith("sudo rm /etc/systemd/system/macroclaw.service");
419
- expect(mockExecSync).toHaveBeenCalledWith("sudo systemctl daemon-reload");
420
- });
421
-
422
- it("uninstalls stopped systemd service without stopping", () => {
423
- mockExistsSync.mockImplementation(() => true);
424
- mockExecSync.mockImplementation((cmd: string) => {
425
- if (cmd === "systemctl is-active macroclaw") return SYSTEMD_INACTIVE;
426
- return "";
427
- });
428
- const mgr = createManager();
429
- mgr.uninstall();
430
- expect(mockExecSync).not.toHaveBeenCalledWith("sudo systemctl stop macroclaw");
431
- expect(mockExecSync).toHaveBeenCalledWith("sudo systemctl disable macroclaw");
432
- });
433
- });
434
-
435
- describe("start", () => {
436
- it("throws when service is not installed", () => {
437
- mockExistsSync.mockImplementation(() => false);
438
- const mgr = createManager({ platform: "darwin" });
439
- expect(() => mgr.start()).toThrow(
440
- "Service not installed. Run `macroclaw service install` first.",
441
- );
442
- });
443
-
444
- it("throws when service is already running (launchd)", () => {
445
- mockExistsSync.mockImplementation(() => true);
446
- mockExecSync.mockImplementation((cmd: string) => {
447
- if (cmd.startsWith("launchctl list ")) return LAUNCHD_RUNNING;
448
- return "";
449
- });
450
- const mgr = createManager({ platform: "darwin" });
451
- expect(() => mgr.start()).toThrow("Service is already running.");
452
- });
453
-
454
- it("throws when service is already running (systemd)", () => {
455
- mockExistsSync.mockImplementation(() => true);
456
- mockExecSync.mockImplementation((cmd: string) => {
457
- if (cmd === "systemctl is-active macroclaw") return SYSTEMD_ACTIVE;
458
- return "";
459
- });
460
- const mgr = createManager();
461
- expect(() => mgr.start()).toThrow("Service is already running.");
462
- });
463
-
464
- it("starts launchd service", () => {
465
- mockExistsSync.mockImplementation(() => true);
466
- mockExecSync.mockImplementation((cmd: string) => {
467
- if (cmd.startsWith("launchctl list ")) throw new Error("not loaded");
468
- return "";
469
- });
470
- const mgr = createManager({ platform: "darwin" });
471
- mgr.start();
472
- expect(mockExecSync).toHaveBeenCalledWith(expect.stringContaining("launchctl load"));
473
- });
474
-
475
- it("starts systemd service via sudo", () => {
476
- mockExistsSync.mockImplementation(() => true);
477
- mockExecSync.mockImplementation((cmd: string) => {
478
- if (cmd === "systemctl is-active macroclaw") return SYSTEMD_INACTIVE;
479
- return "";
480
- });
481
- const mgr = createManager();
482
- mgr.start();
483
- expect(mockExecSync).toHaveBeenCalledWith("sudo systemctl start macroclaw");
484
- });
485
- });
486
-
487
- describe("stop", () => {
488
- it("throws when service is not installed", () => {
489
- mockExistsSync.mockImplementation(() => false);
490
- const mgr = createManager({ platform: "darwin" });
491
- expect(() => mgr.stop()).toThrow(
492
- "Service not installed. Run `macroclaw service install` first.",
493
- );
494
- });
495
-
496
- it("throws when service is not running (launchd)", () => {
497
- mockExistsSync.mockImplementation(() => true);
498
- mockExecSync.mockImplementation((cmd: string) => {
499
- if (cmd.startsWith("launchctl list ")) return LAUNCHD_STOPPED;
500
- return "";
501
- });
502
- const mgr = createManager({ platform: "darwin" });
503
- expect(() => mgr.stop()).toThrow("Service is not running.");
504
- });
505
-
506
- it("throws when service is not running (systemd)", () => {
507
- mockExistsSync.mockImplementation(() => true);
508
- mockExecSync.mockImplementation((cmd: string) => {
509
- if (cmd === "systemctl is-active macroclaw") return SYSTEMD_INACTIVE;
510
- return "";
511
- });
512
- const mgr = createManager();
513
- expect(() => mgr.stop()).toThrow("Service is not running.");
514
- });
515
-
516
- it("stops launchd service", () => {
517
- mockExistsSync.mockImplementation(() => true);
518
- mockExecSync.mockImplementation((cmd: string) => {
519
- if (cmd.startsWith("launchctl list ")) return LAUNCHD_RUNNING;
520
- return "";
521
- });
522
- const mgr = createManager({ platform: "darwin" });
523
- mgr.stop();
524
- expect(mockExecSync).toHaveBeenCalledWith(expect.stringContaining("launchctl unload"));
525
- });
526
-
527
- it("stops systemd service via sudo", () => {
528
- mockExistsSync.mockImplementation(() => true);
529
- mockExecSync.mockImplementation((cmd: string) => {
530
- if (cmd === "systemctl is-active macroclaw") return SYSTEMD_ACTIVE;
531
- return "";
532
- });
533
- const mgr = createManager();
534
- mgr.stop();
535
- expect(mockExecSync).toHaveBeenCalledWith("sudo systemctl stop macroclaw");
536
- });
537
- });
538
-
539
- describe("update", () => {
540
- it("throws when service is not installed", () => {
541
- mockExistsSync.mockImplementation(() => false);
542
- const mgr = createManager({ platform: "darwin" });
543
- expect(() => mgr.update()).toThrow(
544
- "Service not installed. Run `macroclaw service install` first.",
545
- );
546
- });
547
-
548
- it("updates systemd: stops if running, reinstalls, starts", () => {
549
- mockExistsSync.mockImplementation(() => true);
550
- mockExecSync.mockImplementation((cmd: string) => {
551
- if (cmd === "systemctl is-active macroclaw") return SYSTEMD_ACTIVE;
552
- return "";
553
- });
554
- const mgr = createManager();
555
- mgr.update();
556
- expect(mockExecSync).toHaveBeenCalledWith("sudo systemctl stop macroclaw");
557
- expect(mockExecSync).toHaveBeenCalledWith("bun install -g macroclaw@latest");
558
- expect(mockExecSync).not.toHaveBeenCalledWith("sudo bun install -g macroclaw@latest");
559
- expect(mockExecSync).toHaveBeenCalledWith("sudo systemctl start macroclaw");
560
- });
561
-
562
- it("updates systemd: skips stop when not running", () => {
563
- mockExistsSync.mockImplementation(() => true);
564
- mockExecSync.mockImplementation((cmd: string) => {
565
- if (cmd === "systemctl is-active macroclaw") return SYSTEMD_INACTIVE;
566
- return "";
567
- });
568
- const mgr = createManager();
569
- mgr.update();
570
- expect(mockExecSync).not.toHaveBeenCalledWith("sudo systemctl stop macroclaw");
571
- expect(mockExecSync).toHaveBeenCalledWith("bun install -g macroclaw@latest");
572
- expect(mockExecSync).toHaveBeenCalledWith("sudo systemctl start macroclaw");
573
- });
574
-
575
- it("updates launchd without sudo", () => {
576
- mockExistsSync.mockImplementation(() => true);
577
- mockExecSync.mockImplementation((cmd: string) => {
578
- if (cmd.startsWith("launchctl list ")) return LAUNCHD_RUNNING;
579
- return "";
580
- });
581
- const mgr = createManager({ platform: "darwin" });
582
- mgr.update();
583
- expect(mockExecSync).toHaveBeenCalledWith(expect.stringContaining("launchctl unload"));
584
- expect(mockExecSync).toHaveBeenCalledWith("bun install -g macroclaw@latest");
585
- expect(mockExecSync).toHaveBeenCalledWith(expect.stringContaining("launchctl load"));
586
- for (const call of mockExecSync.mock.calls) {
587
- expect(call[0]).not.toMatch(/^sudo /);
588
- }
589
- });
590
- });
591
-
592
- describe("status", () => {
593
- it("returns not installed, not running when service file missing", () => {
594
- mockExistsSync.mockImplementation(() => false);
595
- mockExecSync.mockImplementation((cmd: string) => {
596
- if (cmd === "systemctl is-active macroclaw") throw new Error("not found");
597
- return "";
598
- });
599
- const mgr = createManager();
600
- const s = mgr.status();
601
- expect(s.installed).toBe(false);
602
- expect(s.running).toBe(false);
603
- expect(s.platform).toBe("systemd");
604
- expect(s.pid).toBeUndefined();
605
- expect(s.uptime).toBeUndefined();
606
- });
607
-
608
- it("returns installed but not running for systemd", () => {
609
- mockExistsSync.mockImplementation(() => true);
610
- mockExecSync.mockImplementation((cmd: string) => {
611
- if (cmd === "systemctl is-active macroclaw") return SYSTEMD_INACTIVE;
612
- return "";
613
- });
614
- const mgr = createManager();
615
- const s = mgr.status();
616
- expect(s.installed).toBe(true);
617
- expect(s.running).toBe(false);
618
- expect(s.pid).toBeUndefined();
619
- });
620
-
621
- it("returns pid and uptime for running systemd service", () => {
622
- mockExistsSync.mockImplementation(() => true);
623
- mockExecSync.mockImplementation((cmd: string) => {
624
- if (cmd === "systemctl is-active macroclaw") return SYSTEMD_ACTIVE;
625
- if (cmd.startsWith("systemctl show macroclaw")) return "MainPID=42\nActiveEnterTimestamp=Thu 2026-03-12 10:00:00 UTC";
626
- return "";
627
- });
628
- const mgr = createManager();
629
- const s = mgr.status();
630
- expect(s.installed).toBe(true);
631
- expect(s.running).toBe(true);
632
- expect(s.pid).toBe(42);
633
- expect(s.uptime).toBe("Thu 2026-03-12 10:00:00 UTC");
634
- });
635
-
636
- it("returns pid for running launchd service", () => {
637
- mockExistsSync.mockImplementation(() => true);
638
- mockExecSync.mockImplementation((cmd: string) => {
639
- if (cmd.startsWith("launchctl list ")) return LAUNCHD_RUNNING;
640
- return "";
641
- });
642
- const mgr = createManager({ platform: "darwin" });
643
- const s = mgr.status();
644
- expect(s.installed).toBe(true);
645
- expect(s.running).toBe(true);
646
- expect(s.platform).toBe("launchd");
647
- expect(s.pid).toBe(12345);
648
- });
649
-
650
- it("handles systemctl show failure gracefully", () => {
651
- mockExistsSync.mockImplementation(() => true);
652
- mockExecSync.mockImplementation((cmd: string) => {
653
- if (cmd === "systemctl is-active macroclaw") return SYSTEMD_ACTIVE;
654
- if (cmd.startsWith("systemctl show")) throw new Error("failed");
655
- return "";
656
- });
657
- const mgr = createManager();
658
- const s = mgr.status();
659
- expect(s.running).toBe(true);
660
- expect(s.pid).toBeUndefined();
661
- expect(s.uptime).toBeUndefined();
662
- });
663
-
664
- it("handles launchctl list failure gracefully during status", () => {
665
- mockExistsSync.mockImplementation(() => true);
666
- let callCount = 0;
667
- mockExecSync.mockImplementation((cmd: string) => {
668
- if (cmd.startsWith("launchctl list ")) {
669
- callCount++;
670
- if (callCount === 1) return LAUNCHD_RUNNING;
671
- throw new Error("failed");
672
- }
673
- return "";
674
- });
675
- const mgr = createManager({ platform: "darwin" });
676
- const s = mgr.status();
677
- expect(s.running).toBe(true);
678
- expect(s.pid).toBeUndefined();
679
- });
680
- });
681
-
682
- describe("logs", () => {
683
- it("returns journalctl command for systemd", () => {
684
- const mgr = createManager();
685
- expect(mgr.logs()).toBe("journalctl -u macroclaw -n 50 --no-pager");
686
- });
687
-
688
- it("returns journalctl follow command for systemd", () => {
689
- const mgr = createManager();
690
- expect(mgr.logs(true)).toBe("journalctl -u macroclaw -f");
691
- });
692
-
693
- it("returns tail command for launchd", () => {
694
- const mgr = createManager({ platform: "darwin" });
695
- expect(mgr.logs()).toBe("tail -n 50 /home/testuser/.macroclaw/logs/stdout.log");
696
- });
697
-
698
- it("returns tail follow command for launchd", () => {
699
- const mgr = createManager({ platform: "darwin" });
700
- expect(mgr.logs(true)).toBe("tail -f /home/testuser/.macroclaw/logs/stdout.log /home/testuser/.macroclaw/logs/stderr.log");
701
- });
702
- });