@runfusion/fusion 0.1.3 → 0.2.1

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.
Files changed (44) hide show
  1. package/dist/bin.js +4155 -1194
  2. package/dist/client/assets/AgentDetailView-BMrHuWGs.css +1 -0
  3. package/dist/client/assets/AgentDetailView-CkuuGA1O.js +28 -0
  4. package/dist/client/assets/AgentsView-CWFLMIDP.js +522 -0
  5. package/dist/client/assets/AgentsView-DETxUmHS.css +1 -0
  6. package/dist/client/assets/ChatView-C_T91ebd.js +1 -0
  7. package/dist/client/assets/DevServerView-ChWTzTvy.js +11 -0
  8. package/dist/client/assets/DevServerView-ZeBGQkLI.css +1 -0
  9. package/dist/client/assets/DirectoryPicker-BMJIT7HD.js +1 -0
  10. package/dist/client/assets/DocumentsView-CjfVl8mZ.js +1 -0
  11. package/dist/client/assets/DocumentsView-Co9to4Zp.css +1 -0
  12. package/dist/client/assets/InsightsView-Egu71gmh.css +1 -0
  13. package/dist/client/assets/InsightsView-Rb735C9_.js +11 -0
  14. package/dist/client/assets/MemoryView-DhinauGs.css +1 -0
  15. package/dist/client/assets/MemoryView-LLc_uNtG.js +2 -0
  16. package/dist/client/assets/NodesView-BYVG2yY-.css +1 -0
  17. package/dist/client/assets/NodesView-BviqBWRA.js +14 -0
  18. package/dist/client/assets/PiExtensionsManager-CPgmJgDk.css +1 -0
  19. package/dist/client/assets/PiExtensionsManager-CriZBkQe.js +11 -0
  20. package/dist/client/assets/PluginManager-BywTPbLB.js +1 -0
  21. package/dist/client/assets/PluginManager-D64RIzmL.css +1 -0
  22. package/dist/client/assets/RoadmapsView-BOYnyMCh.css +1 -0
  23. package/dist/client/assets/RoadmapsView-Dhl--4vY.js +6 -0
  24. package/dist/client/assets/SetupWizardModal-CVtmwoJC.js +1 -0
  25. package/dist/client/assets/SkillsView-CG9y4fsE.js +1 -0
  26. package/dist/client/assets/SkillsView-Cytf009Z.css +1 -0
  27. package/dist/client/assets/folder-open-BVDq27HP.js +6 -0
  28. package/dist/client/assets/index-CikysL-d.js +644 -0
  29. package/dist/client/assets/index-Da1qmtc7.css +1 -0
  30. package/dist/client/assets/upload-BDvpReDO.js +6 -0
  31. package/dist/client/index.html +2 -2
  32. package/dist/extension.js +187 -76
  33. package/dist/pi-claude-cli/src/__tests__/control-handler.test.ts +191 -0
  34. package/dist/pi-claude-cli/src/__tests__/event-bridge.test.ts +1244 -0
  35. package/dist/pi-claude-cli/src/__tests__/mcp-config.test.ts +272 -0
  36. package/dist/pi-claude-cli/src/__tests__/process-manager.test.ts +619 -0
  37. package/dist/pi-claude-cli/src/__tests__/prompt-builder.test.ts +1067 -0
  38. package/dist/pi-claude-cli/src/__tests__/provider.test.ts +1902 -0
  39. package/dist/pi-claude-cli/src/__tests__/stream-parser.test.ts +188 -0
  40. package/dist/pi-claude-cli/src/__tests__/thinking-config.test.ts +141 -0
  41. package/dist/pi-claude-cli/src/__tests__/tool-mapping.test.ts +252 -0
  42. package/package.json +11 -5
  43. package/dist/client/assets/index-BuenKJX0.css +0 -1
  44. package/dist/client/assets/index-CjGu8HRV.js +0 -1250
@@ -0,0 +1,619 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import type { ChildProcess } from "node:child_process";
3
+
4
+ // Mock cross-spawn before importing process-manager
5
+ vi.mock("cross-spawn", () => ({
6
+ default: vi.fn(() => {
7
+ const EventEmitter = require("node:events");
8
+ const proc = new EventEmitter();
9
+ proc.stdin = { write: vi.fn(), end: vi.fn() };
10
+ proc.stdout = new EventEmitter();
11
+ proc.stderr = new EventEmitter();
12
+ proc.killed = false;
13
+ proc.kill = vi.fn(() => {
14
+ proc.killed = true;
15
+ });
16
+ proc.pid = 12345;
17
+ return proc;
18
+ }),
19
+ }));
20
+
21
+ // Mock child_process.execSync for validation tests
22
+ vi.mock("node:child_process", () => ({
23
+ execSync: vi.fn(),
24
+ }));
25
+
26
+ import spawn from "cross-spawn";
27
+ import { execSync } from "node:child_process";
28
+ import { existsSync, readFileSync } from "node:fs";
29
+ import { join } from "node:path";
30
+ import { tmpdir } from "node:os";
31
+ import {
32
+ spawnClaude,
33
+ writeUserMessage,
34
+ cleanupProcess,
35
+ captureStderr,
36
+ validateCliPresence,
37
+ validateCliAuth,
38
+ forceKillProcess,
39
+ registerProcess,
40
+ killAllProcesses,
41
+ cleanupSystemPromptFile,
42
+ } from "../process-manager";
43
+
44
+ describe("spawnClaude", () => {
45
+ beforeEach(() => {
46
+ vi.clearAllMocks();
47
+ });
48
+
49
+ it("spawns claude with all required CLI flags", () => {
50
+ spawnClaude("claude-sonnet-4-5-20250929");
51
+
52
+ expect(spawn).toHaveBeenCalledTimes(1);
53
+ const [cmd, args] = (spawn as any).mock.calls[0];
54
+
55
+ expect(cmd).toBe("claude");
56
+ expect(args).toContain("-p");
57
+ expect(args).toContain("--input-format");
58
+ expect(args).toContain("stream-json");
59
+ expect(args).toContain("--output-format");
60
+ expect(args).toContain("--verbose");
61
+ expect(args).toContain("--include-partial-messages");
62
+ expect(args).not.toContain("--no-session-persistence");
63
+ expect(args).toContain("--model");
64
+ expect(args).toContain("claude-sonnet-4-5-20250929");
65
+ expect(args).toContain("--permission-prompt-tool");
66
+ expect(args).toContain("stdio");
67
+ });
68
+
69
+ it("passes stream-json for both input-format and output-format", () => {
70
+ spawnClaude("claude-sonnet-4-5-20250929");
71
+ const args = (spawn as any).mock.calls[0][1] as string[];
72
+
73
+ const inputFormatIdx = args.indexOf("--input-format");
74
+ expect(args[inputFormatIdx + 1]).toBe("stream-json");
75
+
76
+ const outputFormatIdx = args.indexOf("--output-format");
77
+ expect(args[outputFormatIdx + 1]).toBe("stream-json");
78
+ });
79
+
80
+ it("sets stdio to pipe for stdin, stdout, and stderr", () => {
81
+ spawnClaude("claude-sonnet-4-5-20250929");
82
+ const options = (spawn as any).mock.calls[0][2];
83
+ expect(options.stdio).toEqual(["pipe", "pipe", "pipe"]);
84
+ });
85
+
86
+ it("passes cwd from options when provided", () => {
87
+ spawnClaude("claude-sonnet-4-5-20250929", undefined, {
88
+ cwd: "/custom/path",
89
+ });
90
+ const options = (spawn as any).mock.calls[0][2];
91
+ expect(options.cwd).toBe("/custom/path");
92
+ });
93
+
94
+ it("writes system prompt to temp file and passes path via --append-system-prompt", () => {
95
+ spawnClaude("claude-sonnet-4-5-20250929", "You are a helpful assistant.");
96
+ const args = (spawn as any).mock.calls[0][1] as string[];
97
+
98
+ expect(args).toContain("--append-system-prompt");
99
+ const idx = args.indexOf("--append-system-prompt");
100
+ expect(args[idx + 1]).toContain("pi-claude-cli-sysprompt-");
101
+ });
102
+
103
+ it("temp file contains the system prompt text", () => {
104
+ spawnClaude("claude-sonnet-4-5-20250929", "You are a helpful assistant.");
105
+ const tmpFile = join(
106
+ tmpdir(),
107
+ `pi-claude-cli-sysprompt-${process.pid}.txt`,
108
+ );
109
+ expect(existsSync(tmpFile)).toBe(true);
110
+ expect(readFileSync(tmpFile, "utf-8")).toBe("You are a helpful assistant.");
111
+ });
112
+
113
+ it("does not include --append-system-prompt when no system prompt", () => {
114
+ spawnClaude("claude-sonnet-4-5-20250929");
115
+ const args = (spawn as any).mock.calls[0][1] as string[];
116
+ expect(args).not.toContain("--append-system-prompt");
117
+ });
118
+
119
+ it("returns the spawned ChildProcess", () => {
120
+ const proc = spawnClaude("claude-sonnet-4-5-20250929");
121
+ expect(proc).toBeDefined();
122
+ expect(proc.pid).toBe(12345);
123
+ });
124
+ });
125
+
126
+ describe("effort flag", () => {
127
+ beforeEach(() => {
128
+ vi.clearAllMocks();
129
+ });
130
+
131
+ it("includes --effort and high in args when effort is high", () => {
132
+ spawnClaude("claude-sonnet-4-5-20250929", undefined, { effort: "high" });
133
+ const args = (spawn as any).mock.calls[0][1] as string[];
134
+
135
+ expect(args).toContain("--effort");
136
+ const idx = args.indexOf("--effort");
137
+ expect(args[idx + 1]).toBe("high");
138
+ });
139
+
140
+ it("includes --effort and max in args when effort is max", () => {
141
+ spawnClaude("claude-opus-4-6-20260301", undefined, { effort: "max" });
142
+ const args = (spawn as any).mock.calls[0][1] as string[];
143
+
144
+ expect(args).toContain("--effort");
145
+ const idx = args.indexOf("--effort");
146
+ expect(args[idx + 1]).toBe("max");
147
+ });
148
+
149
+ it("includes --effort and low in args when effort is low", () => {
150
+ spawnClaude("claude-sonnet-4-5-20250929", undefined, { effort: "low" });
151
+ const args = (spawn as any).mock.calls[0][1] as string[];
152
+
153
+ expect(args).toContain("--effort");
154
+ const idx = args.indexOf("--effort");
155
+ expect(args[idx + 1]).toBe("low");
156
+ });
157
+
158
+ it("does NOT include --effort when effort is undefined", () => {
159
+ spawnClaude("claude-sonnet-4-5-20250929", undefined, { cwd: "/some/path" });
160
+ const args = (spawn as any).mock.calls[0][1] as string[];
161
+
162
+ expect(args).not.toContain("--effort");
163
+ });
164
+
165
+ it("does NOT include --effort when options is undefined", () => {
166
+ spawnClaude("claude-sonnet-4-5-20250929");
167
+ const args = (spawn as any).mock.calls[0][1] as string[];
168
+
169
+ expect(args).not.toContain("--effort");
170
+ });
171
+
172
+ it("is backward compatible - existing calls without effort still work", () => {
173
+ spawnClaude("claude-sonnet-4-5-20250929", "system prompt", {
174
+ cwd: "/path",
175
+ });
176
+ const args = (spawn as any).mock.calls[0][1] as string[];
177
+
178
+ expect(args).toContain("--append-system-prompt");
179
+ expect(args).not.toContain("--effort");
180
+ });
181
+ });
182
+
183
+ describe("writeUserMessage", () => {
184
+ it("writes correct NDJSON user message to stdin", () => {
185
+ const mockStdin = { write: vi.fn(), end: vi.fn() };
186
+ const proc = { stdin: mockStdin } as unknown as ChildProcess;
187
+
188
+ writeUserMessage(proc, "Hello Claude");
189
+
190
+ expect(mockStdin.write).toHaveBeenCalledTimes(1);
191
+ const written = mockStdin.write.mock.calls[0][0] as string;
192
+ const parsed = JSON.parse(written.trim());
193
+ expect(parsed.type).toBe("user");
194
+ expect(parsed.message.role).toBe("user");
195
+ expect(parsed.message.content).toBe("Hello Claude");
196
+ });
197
+
198
+ it("appends newline to the JSON", () => {
199
+ const mockStdin = { write: vi.fn(), end: vi.fn() };
200
+ const proc = { stdin: mockStdin } as unknown as ChildProcess;
201
+
202
+ writeUserMessage(proc, "test");
203
+
204
+ const written = mockStdin.write.mock.calls[0][0] as string;
205
+ expect(written.endsWith("\n")).toBe(true);
206
+ });
207
+
208
+ it("does NOT call stdin.end()", () => {
209
+ const mockStdin = { write: vi.fn(), end: vi.fn() };
210
+ const proc = { stdin: mockStdin } as unknown as ChildProcess;
211
+
212
+ writeUserMessage(proc, "test");
213
+
214
+ expect(mockStdin.end).not.toHaveBeenCalled();
215
+ });
216
+
217
+ it("sends string content in NDJSON when given string", () => {
218
+ const mockStdin = { write: vi.fn(), end: vi.fn() };
219
+ const proc = { stdin: mockStdin } as unknown as ChildProcess;
220
+
221
+ writeUserMessage(proc, "hello");
222
+
223
+ const written = mockStdin.write.mock.calls[0][0] as string;
224
+ const parsed = JSON.parse(written.trim());
225
+ expect(typeof parsed.message.content).toBe("string");
226
+ expect(parsed.message.content).toBe("hello");
227
+ });
228
+
229
+ it("sends array content in NDJSON when given ContentBlock[]", () => {
230
+ const mockStdin = { write: vi.fn(), end: vi.fn() };
231
+ const proc = { stdin: mockStdin } as unknown as ChildProcess;
232
+
233
+ const blocks = [
234
+ { type: "text", text: "hello" },
235
+ {
236
+ type: "image",
237
+ source: { type: "base64", media_type: "image/png", data: "abc" },
238
+ },
239
+ ];
240
+ writeUserMessage(proc, blocks as any);
241
+
242
+ const written = mockStdin.write.mock.calls[0][0] as string;
243
+ const parsed = JSON.parse(written.trim());
244
+ expect(Array.isArray(parsed.message.content)).toBe(true);
245
+ expect(parsed.message.content).toEqual(blocks);
246
+ });
247
+ });
248
+
249
+ describe("cleanupProcess", () => {
250
+ beforeEach(() => {
251
+ vi.useFakeTimers();
252
+ });
253
+
254
+ afterEach(() => {
255
+ vi.useRealTimers();
256
+ });
257
+
258
+ it("kills the process with SIGKILL after 500ms grace period", () => {
259
+ const mockProc: any = {
260
+ killed: false,
261
+ exitCode: null,
262
+ kill: vi.fn(() => {
263
+ mockProc.killed = true;
264
+ }),
265
+ };
266
+
267
+ cleanupProcess(mockProc as ChildProcess);
268
+
269
+ // Not killed immediately
270
+ expect(mockProc.kill).not.toHaveBeenCalled();
271
+
272
+ // Not killed at 400ms
273
+ vi.advanceTimersByTime(400);
274
+ expect(mockProc.kill).not.toHaveBeenCalled();
275
+
276
+ // Killed after 500ms grace period
277
+ vi.advanceTimersByTime(100);
278
+ expect(mockProc.kill).toHaveBeenCalledWith("SIGKILL");
279
+ });
280
+
281
+ it("does not kill if process is already killed", () => {
282
+ const proc = {
283
+ killed: true,
284
+ exitCode: null,
285
+ kill: vi.fn(),
286
+ } as unknown as ChildProcess;
287
+
288
+ cleanupProcess(proc);
289
+ vi.advanceTimersByTime(500);
290
+
291
+ expect(proc.kill).not.toHaveBeenCalled();
292
+ });
293
+ });
294
+
295
+ describe("captureStderr", () => {
296
+ it("returns a function that accumulates stderr data", () => {
297
+ const EventEmitter = require("node:events");
298
+ const stderr = new EventEmitter();
299
+ const proc = { stderr } as unknown as ChildProcess;
300
+
301
+ const getStderr = captureStderr(proc);
302
+
303
+ stderr.emit("data", Buffer.from("error line 1\n"));
304
+ stderr.emit("data", Buffer.from("error line 2\n"));
305
+
306
+ expect(getStderr()).toBe("error line 1\nerror line 2\n");
307
+ });
308
+
309
+ it("returns empty string when no stderr data", () => {
310
+ const EventEmitter = require("node:events");
311
+ const stderr = new EventEmitter();
312
+ const proc = { stderr } as unknown as ChildProcess;
313
+
314
+ const getStderr = captureStderr(proc);
315
+ expect(getStderr()).toBe("");
316
+ });
317
+ });
318
+
319
+ describe("validateCliPresence", () => {
320
+ it("does not throw when claude --version succeeds", () => {
321
+ (execSync as any).mockReturnValue(Buffer.from("1.0.0"));
322
+ expect(() => validateCliPresence()).not.toThrow();
323
+ });
324
+
325
+ it("throws with install instructions when claude --version fails", () => {
326
+ (execSync as any).mockImplementation(() => {
327
+ throw new Error("command not found");
328
+ });
329
+
330
+ expect(() => validateCliPresence()).toThrow();
331
+ try {
332
+ validateCliPresence();
333
+ } catch (e: any) {
334
+ expect(e.message).toContain("Claude Code CLI not found");
335
+ expect(e.message).toContain("npm install");
336
+ }
337
+ });
338
+ });
339
+
340
+ describe("validateCliAuth", () => {
341
+ it("returns true when claude auth status succeeds", () => {
342
+ (execSync as any).mockReturnValue(Buffer.from("Logged in"));
343
+ expect(validateCliAuth()).toBe(true);
344
+ });
345
+
346
+ it("returns false and warns when claude auth status fails", () => {
347
+ (execSync as any).mockImplementation(() => {
348
+ throw new Error("not authenticated");
349
+ });
350
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
351
+
352
+ expect(validateCliAuth()).toBe(false);
353
+ expect(warnSpy).toHaveBeenCalledWith(
354
+ expect.stringContaining("not authenticated"),
355
+ );
356
+ warnSpy.mockRestore();
357
+ });
358
+ });
359
+
360
+ describe("CLI flags", () => {
361
+ beforeEach(() => {
362
+ vi.clearAllMocks();
363
+ });
364
+
365
+ it("spawnClaude does NOT include --permission-mode or dontAsk in args", () => {
366
+ spawnClaude("claude-sonnet-4-5-20250929");
367
+ const args = (spawn as any).mock.calls[0][1] as string[];
368
+
369
+ expect(args).not.toContain("--permission-mode");
370
+ expect(args).not.toContain("dontAsk");
371
+ });
372
+
373
+ it("spawnClaude includes --permission-prompt-tool followed by stdio in args", () => {
374
+ spawnClaude("claude-sonnet-4-5-20250929");
375
+ const args = (spawn as any).mock.calls[0][1] as string[];
376
+
377
+ expect(args).toContain("--permission-prompt-tool");
378
+ const idx = args.indexOf("--permission-prompt-tool");
379
+ expect(args[idx + 1]).toBe("stdio");
380
+ });
381
+ });
382
+
383
+ describe("mcp-config flag", () => {
384
+ beforeEach(() => {
385
+ vi.clearAllMocks();
386
+ });
387
+
388
+ it("spawnClaude with mcpConfigPath includes --mcp-config followed by the path", () => {
389
+ spawnClaude("claude-sonnet-4-5-20250929", undefined, {
390
+ mcpConfigPath: "/tmp/mcp-config.json",
391
+ });
392
+ const args = (spawn as any).mock.calls[0][1] as string[];
393
+
394
+ expect(args).toContain("--mcp-config");
395
+ const idx = args.indexOf("--mcp-config");
396
+ expect(args[idx + 1]).toBe("/tmp/mcp-config.json");
397
+ });
398
+
399
+ it("spawnClaude without mcpConfigPath does NOT include --mcp-config in args", () => {
400
+ spawnClaude("claude-sonnet-4-5-20250929");
401
+ const args = (spawn as any).mock.calls[0][1] as string[];
402
+
403
+ expect(args).not.toContain("--mcp-config");
404
+ });
405
+
406
+ it("spawnClaude NEVER includes --strict-mcp-config in args", () => {
407
+ spawnClaude("claude-sonnet-4-5-20250929", undefined, {
408
+ mcpConfigPath: "/tmp/mcp-config.json",
409
+ });
410
+ const args = (spawn as any).mock.calls[0][1] as string[];
411
+
412
+ expect(args).not.toContain("--strict-mcp-config");
413
+ });
414
+
415
+ it("backward compatibility - existing calls with only effort/cwd still work", () => {
416
+ spawnClaude("claude-sonnet-4-5-20250929", "system prompt", {
417
+ cwd: "/path",
418
+ effort: "high",
419
+ });
420
+ const args = (spawn as any).mock.calls[0][1] as string[];
421
+
422
+ expect(args).toContain("--append-system-prompt");
423
+ expect(args).toContain("--effort");
424
+ expect(args).not.toContain("--mcp-config");
425
+ expect(args).toContain("--permission-prompt-tool");
426
+ });
427
+ });
428
+
429
+ describe("forceKillProcess", () => {
430
+ it("calls proc.kill('SIGKILL') on live process", () => {
431
+ const proc = {
432
+ killed: false,
433
+ exitCode: null,
434
+ kill: vi.fn(),
435
+ } as unknown as ChildProcess;
436
+
437
+ forceKillProcess(proc);
438
+
439
+ expect(proc.kill).toHaveBeenCalledWith("SIGKILL");
440
+ });
441
+
442
+ it("no-ops when proc.killed is true", () => {
443
+ const proc = {
444
+ killed: true,
445
+ exitCode: null,
446
+ kill: vi.fn(),
447
+ } as unknown as ChildProcess;
448
+
449
+ forceKillProcess(proc);
450
+
451
+ expect(proc.kill).not.toHaveBeenCalled();
452
+ });
453
+
454
+ it("no-ops when proc.exitCode is not null", () => {
455
+ const proc = {
456
+ killed: false,
457
+ exitCode: 0,
458
+ kill: vi.fn(),
459
+ } as unknown as ChildProcess;
460
+
461
+ forceKillProcess(proc);
462
+
463
+ expect(proc.kill).not.toHaveBeenCalled();
464
+ });
465
+ });
466
+
467
+ describe("process registry", () => {
468
+ beforeEach(() => {
469
+ // Clear registry between tests
470
+ killAllProcesses();
471
+ vi.clearAllMocks();
472
+ });
473
+
474
+ it("registerProcess adds proc and killAllProcesses kills it", () => {
475
+ const EventEmitter = require("node:events");
476
+ const proc = new EventEmitter();
477
+ proc.killed = false;
478
+ proc.exitCode = null;
479
+ proc.kill = vi.fn(() => {
480
+ proc.killed = true;
481
+ });
482
+
483
+ registerProcess(proc as unknown as ChildProcess);
484
+ killAllProcesses();
485
+
486
+ expect(proc.kill).toHaveBeenCalledWith("SIGKILL");
487
+ });
488
+
489
+ it("proc exit event removes from registry", () => {
490
+ const EventEmitter = require("node:events");
491
+ const proc = new EventEmitter();
492
+ proc.killed = false;
493
+ proc.exitCode = null;
494
+ proc.kill = vi.fn(() => {
495
+ proc.killed = true;
496
+ });
497
+
498
+ registerProcess(proc as unknown as ChildProcess);
499
+
500
+ // Simulate natural exit
501
+ proc.exitCode = 0;
502
+ proc.emit("exit", 0, null);
503
+
504
+ // Clear mock to check killAllProcesses doesn't call kill again
505
+ proc.kill.mockClear();
506
+ proc.killed = false;
507
+ proc.exitCode = null;
508
+
509
+ killAllProcesses();
510
+
511
+ // Should NOT have been killed since it was removed on exit
512
+ expect(proc.kill).not.toHaveBeenCalled();
513
+ });
514
+
515
+ it("killAllProcesses clears set and handles already-dead processes", () => {
516
+ const EventEmitter = require("node:events");
517
+ const proc1 = new EventEmitter();
518
+ proc1.killed = true; // already dead
519
+ proc1.exitCode = null;
520
+ proc1.kill = vi.fn();
521
+
522
+ const proc2 = new EventEmitter();
523
+ proc2.killed = false;
524
+ proc2.exitCode = 1; // already exited
525
+ proc2.kill = vi.fn();
526
+
527
+ const proc3 = new EventEmitter();
528
+ proc3.killed = false;
529
+ proc3.exitCode = null; // alive
530
+ proc3.kill = vi.fn(() => {
531
+ proc3.killed = true;
532
+ });
533
+
534
+ registerProcess(proc1 as unknown as ChildProcess);
535
+ registerProcess(proc2 as unknown as ChildProcess);
536
+ registerProcess(proc3 as unknown as ChildProcess);
537
+
538
+ killAllProcesses();
539
+
540
+ // Already dead -- forceKillProcess should no-op
541
+ expect(proc1.kill).not.toHaveBeenCalled();
542
+ expect(proc2.kill).not.toHaveBeenCalled();
543
+ // Live process should be killed
544
+ expect(proc3.kill).toHaveBeenCalledWith("SIGKILL");
545
+
546
+ // Calling again should not kill anything (set was cleared)
547
+ proc3.kill.mockClear();
548
+ proc3.killed = false;
549
+ proc3.exitCode = null;
550
+ killAllProcesses();
551
+ expect(proc3.kill).not.toHaveBeenCalled();
552
+ });
553
+ });
554
+
555
+ describe("resume session flag", () => {
556
+ beforeEach(() => {
557
+ vi.clearAllMocks();
558
+ });
559
+
560
+ it("includes --resume followed by session ID when resumeSessionId is provided", () => {
561
+ spawnClaude("claude-sonnet-4-5-20250929", undefined, {
562
+ resumeSessionId: "session-abc-123",
563
+ });
564
+ const args = (spawn as any).mock.calls[0][1] as string[];
565
+
566
+ expect(args).toContain("--resume");
567
+ const idx = args.indexOf("--resume");
568
+ expect(args[idx + 1]).toBe("session-abc-123");
569
+ });
570
+
571
+ it("does NOT include --resume when resumeSessionId is undefined", () => {
572
+ spawnClaude("claude-sonnet-4-5-20250929");
573
+ const args = (spawn as any).mock.calls[0][1] as string[];
574
+
575
+ expect(args).not.toContain("--resume");
576
+ });
577
+
578
+ it("includes both --resume and --effort when both are provided", () => {
579
+ spawnClaude("claude-sonnet-4-5-20250929", undefined, {
580
+ resumeSessionId: "session-abc",
581
+ effort: "high",
582
+ });
583
+ const args = (spawn as any).mock.calls[0][1] as string[];
584
+
585
+ expect(args).toContain("--resume");
586
+ expect(args).toContain("--effort");
587
+ });
588
+
589
+ it("includes both --resume and --mcp-config when both are provided", () => {
590
+ spawnClaude("claude-sonnet-4-5-20250929", undefined, {
591
+ resumeSessionId: "session-abc",
592
+ mcpConfigPath: "/tmp/mcp.json",
593
+ });
594
+ const args = (spawn as any).mock.calls[0][1] as string[];
595
+
596
+ expect(args).toContain("--resume");
597
+ expect(args).toContain("--mcp-config");
598
+ });
599
+ });
600
+
601
+ describe("cleanupSystemPromptFile", () => {
602
+ const tmpFile = join(tmpdir(), `pi-claude-cli-sysprompt-${process.pid}.txt`);
603
+
604
+ it("deletes the temp file when it exists", () => {
605
+ // Create the file by spawning with a system prompt
606
+ spawnClaude("claude-sonnet-4-5-20250929", "test prompt");
607
+ expect(existsSync(tmpFile)).toBe(true);
608
+
609
+ cleanupSystemPromptFile();
610
+ expect(existsSync(tmpFile)).toBe(false);
611
+ });
612
+
613
+ it("does not throw when file does not exist", () => {
614
+ // Ensure file doesn't exist
615
+ cleanupSystemPromptFile();
616
+ // Call again — should not throw
617
+ expect(() => cleanupSystemPromptFile()).not.toThrow();
618
+ });
619
+ });