agentic-forge 0.0.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.
Files changed (110) hide show
  1. package/.gitattributes +24 -0
  2. package/.github/workflows/ci.yml +70 -0
  3. package/.markdownlint-cli2.jsonc +16 -0
  4. package/.prettierignore +3 -0
  5. package/.prettierrc +6 -0
  6. package/.vscode/agentic-forge.code-workspace +26 -0
  7. package/CHANGELOG.md +100 -0
  8. package/CLAUDE.md +158 -0
  9. package/CONTRIBUTING.md +152 -0
  10. package/LICENSE +21 -0
  11. package/README.md +145 -0
  12. package/agentic-forge-banner.png +0 -0
  13. package/biome.json +21 -0
  14. package/package.json +5 -0
  15. package/scripts/copy-assets.js +21 -0
  16. package/src/agents/explorer.md +97 -0
  17. package/src/agents/reviewer.md +137 -0
  18. package/src/checkpoints/manager.ts +119 -0
  19. package/src/claude/.claude/skills/analyze/SKILL.md +241 -0
  20. package/src/claude/.claude/skills/analyze/references/bug.md +62 -0
  21. package/src/claude/.claude/skills/analyze/references/debt.md +76 -0
  22. package/src/claude/.claude/skills/analyze/references/doc.md +67 -0
  23. package/src/claude/.claude/skills/analyze/references/security.md +76 -0
  24. package/src/claude/.claude/skills/analyze/references/style.md +72 -0
  25. package/src/claude/.claude/skills/create-checkpoint/SKILL.md +88 -0
  26. package/src/claude/.claude/skills/create-log/SKILL.md +75 -0
  27. package/src/claude/.claude/skills/fix-analyze/SKILL.md +102 -0
  28. package/src/claude/.claude/skills/git-branch/SKILL.md +71 -0
  29. package/src/claude/.claude/skills/git-commit/SKILL.md +107 -0
  30. package/src/claude/.claude/skills/git-pr/SKILL.md +96 -0
  31. package/src/claude/.claude/skills/orchestrate/SKILL.md +120 -0
  32. package/src/claude/.claude/skills/sdlc-plan/SKILL.md +163 -0
  33. package/src/claude/.claude/skills/sdlc-plan/references/bug.md +115 -0
  34. package/src/claude/.claude/skills/sdlc-plan/references/chore.md +105 -0
  35. package/src/claude/.claude/skills/sdlc-plan/references/feature.md +130 -0
  36. package/src/claude/.claude/skills/sdlc-review/SKILL.md +215 -0
  37. package/src/claude/.claude/skills/workflow-builder/SKILL.md +185 -0
  38. package/src/claude/.claude/skills/workflow-builder/references/REFERENCE.md +487 -0
  39. package/src/claude/.claude/skills/workflow-builder/references/workflow-example.yaml +427 -0
  40. package/src/cli.ts +182 -0
  41. package/src/commands/config-cmd.ts +28 -0
  42. package/src/commands/index.ts +21 -0
  43. package/src/commands/init.ts +96 -0
  44. package/src/commands/release-notes.ts +85 -0
  45. package/src/commands/resume.ts +103 -0
  46. package/src/commands/run.ts +234 -0
  47. package/src/commands/shortcuts.ts +11 -0
  48. package/src/commands/skills-dir.ts +11 -0
  49. package/src/commands/status.ts +112 -0
  50. package/src/commands/update.ts +64 -0
  51. package/src/commands/version.ts +27 -0
  52. package/src/commands/workflows.ts +129 -0
  53. package/src/config.ts +129 -0
  54. package/src/console.ts +790 -0
  55. package/src/executor.ts +354 -0
  56. package/src/git/worktree.ts +236 -0
  57. package/src/logging/logger.ts +95 -0
  58. package/src/orchestrator.ts +815 -0
  59. package/src/parser.ts +225 -0
  60. package/src/progress.ts +306 -0
  61. package/src/prompts/agentic-system.md +31 -0
  62. package/src/ralph-loop.ts +260 -0
  63. package/src/renderer.ts +164 -0
  64. package/src/runner.ts +634 -0
  65. package/src/signal-manager.ts +55 -0
  66. package/src/steps/base.ts +71 -0
  67. package/src/steps/conditional-step.ts +144 -0
  68. package/src/steps/index.ts +15 -0
  69. package/src/steps/parallel-step.ts +213 -0
  70. package/src/steps/prompt-step.ts +121 -0
  71. package/src/steps/ralph-loop-step.ts +186 -0
  72. package/src/steps/serial-step.ts +84 -0
  73. package/src/templates/analysis/bug.md.j2 +35 -0
  74. package/src/templates/analysis/debt.md.j2 +38 -0
  75. package/src/templates/analysis/doc.md.j2 +45 -0
  76. package/src/templates/analysis/security.md.j2 +35 -0
  77. package/src/templates/analysis/style.md.j2 +44 -0
  78. package/src/templates/analysis-summary.md.j2 +58 -0
  79. package/src/templates/checkpoint.md.j2 +27 -0
  80. package/src/templates/implementation-report.md.j2 +81 -0
  81. package/src/templates/memory.md.j2 +16 -0
  82. package/src/templates/plan-bug.md.j2 +42 -0
  83. package/src/templates/plan-chore.md.j2 +27 -0
  84. package/src/templates/plan-feature.md.j2 +41 -0
  85. package/src/templates/progress.json.j2 +16 -0
  86. package/src/templates/ralph-report.md.j2 +45 -0
  87. package/src/types.ts +141 -0
  88. package/src/workflows/analyze-codebase-merge.yaml +328 -0
  89. package/src/workflows/analyze-codebase.yaml +196 -0
  90. package/src/workflows/analyze-single.yaml +56 -0
  91. package/src/workflows/demo.yaml +180 -0
  92. package/src/workflows/one-shot.yaml +54 -0
  93. package/src/workflows/plan-build-review.yaml +160 -0
  94. package/src/workflows/ralph-loop.yaml +73 -0
  95. package/tests/config.test.ts +219 -0
  96. package/tests/console.test.ts +506 -0
  97. package/tests/executor.test.ts +339 -0
  98. package/tests/init.test.ts +86 -0
  99. package/tests/logger.test.ts +110 -0
  100. package/tests/parser.test.ts +290 -0
  101. package/tests/progress.test.ts +345 -0
  102. package/tests/ralph-loop.test.ts +418 -0
  103. package/tests/renderer.test.ts +350 -0
  104. package/tests/runner.test.ts +497 -0
  105. package/tests/setup.test.ts +7 -0
  106. package/tests/signal-manager.test.ts +26 -0
  107. package/tests/steps.test.ts +412 -0
  108. package/tests/worktree.test.ts +411 -0
  109. package/tsconfig.json +18 -0
  110. package/vitest.config.ts +8 -0
@@ -0,0 +1,497 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ ClaudeResult,
4
+ FileNotFoundError,
5
+ MODEL_MAP,
6
+ SessionOutput,
7
+ checkClaudeAvailable,
8
+ extractModelFromMessage,
9
+ extractResultText,
10
+ extractTextFromMessage,
11
+ extractUserText,
12
+ formatModelName,
13
+ getAgenticSystemPrompt,
14
+ getExecutable,
15
+ parseStreamJsonLine,
16
+ } from "../src/runner.js";
17
+
18
+ // --- getExecutable ---
19
+
20
+ describe("getExecutable", () => {
21
+ it("should find an executable that exists", () => {
22
+ // 'node' should exist in the test environment
23
+ const result = getExecutable("node");
24
+ expect(result).toBeTruthy();
25
+ expect(result.length).toBeGreaterThan(0);
26
+ });
27
+
28
+ it("should throw FileNotFoundError for missing executable", () => {
29
+ expect(() => getExecutable("nonexistent_executable_12345")).toThrow(FileNotFoundError);
30
+ expect(() => getExecutable("nonexistent_executable_12345")).toThrow("Executable not found");
31
+ });
32
+
33
+ it("should return a valid path string", () => {
34
+ const result = getExecutable("node");
35
+ // Should contain path separators
36
+ expect(result.includes("/") || result.includes("\\")).toBe(true);
37
+ });
38
+ });
39
+
40
+ // --- MODEL_MAP ---
41
+
42
+ describe("MODEL_MAP", () => {
43
+ it("should contain all expected models", () => {
44
+ expect("sonnet" in MODEL_MAP).toBe(true);
45
+ expect("haiku" in MODEL_MAP).toBe(true);
46
+ expect("opus" in MODEL_MAP).toBe(true);
47
+ });
48
+
49
+ it("should have correct values", () => {
50
+ expect(MODEL_MAP.sonnet).toBe("sonnet");
51
+ expect(MODEL_MAP.haiku).toBe("haiku");
52
+ expect(MODEL_MAP.opus).toBe("opus");
53
+ });
54
+ });
55
+
56
+ // --- parseStreamJsonLine ---
57
+
58
+ describe("parseStreamJsonLine", () => {
59
+ it("should parse valid JSON line", () => {
60
+ const line = '{"type": "assistant", "message": {"content": []}}';
61
+ const result = parseStreamJsonLine(line);
62
+ expect(result).not.toBeNull();
63
+ expect(result?.type).toBe("assistant");
64
+ });
65
+
66
+ it("should parse JSON with leading/trailing whitespace", () => {
67
+ const line = ' {"type": "result", "result": "done"} \n';
68
+ const result = parseStreamJsonLine(line);
69
+ expect(result).not.toBeNull();
70
+ expect(result?.type).toBe("result");
71
+ });
72
+
73
+ it("should return null for non-JSON line", () => {
74
+ const result = parseStreamJsonLine("Some random text output");
75
+ expect(result).toBeNull();
76
+ });
77
+
78
+ it("should return null for empty line", () => {
79
+ const result = parseStreamJsonLine("");
80
+ expect(result).toBeNull();
81
+ });
82
+
83
+ it("should return null for invalid JSON", () => {
84
+ const result = parseStreamJsonLine("{invalid json");
85
+ expect(result).toBeNull();
86
+ });
87
+ });
88
+
89
+ // --- extractTextFromMessage ---
90
+
91
+ describe("extractTextFromMessage", () => {
92
+ it("should extract text from assistant message", () => {
93
+ const data = {
94
+ type: "assistant",
95
+ message: {
96
+ content: [
97
+ { type: "text", text: "Hello, world!" },
98
+ { type: "text", text: "More text" },
99
+ ],
100
+ },
101
+ };
102
+ const results = extractTextFromMessage(data);
103
+ expect(results).toEqual([
104
+ [0, "Hello, world!"],
105
+ [1, "More text"],
106
+ ]);
107
+ });
108
+
109
+ it("should skip non-text blocks", () => {
110
+ const data = {
111
+ type: "assistant",
112
+ message: {
113
+ content: [
114
+ { type: "text", text: "Text content" },
115
+ { type: "tool_use", name: "Read" },
116
+ { type: "text", text: "More text" },
117
+ ],
118
+ },
119
+ };
120
+ const results = extractTextFromMessage(data);
121
+ expect(results).toEqual([
122
+ [0, "Text content"],
123
+ [2, "More text"],
124
+ ]);
125
+ });
126
+
127
+ it("should return empty for non-assistant/non-stream_event messages", () => {
128
+ const data = { type: "result", result: "some result" };
129
+ const results = extractTextFromMessage(data);
130
+ expect(results).toEqual([]);
131
+ });
132
+
133
+ it("should handle empty content array", () => {
134
+ const data = { type: "assistant", message: { content: [] } };
135
+ const results = extractTextFromMessage(data);
136
+ expect(results).toEqual([]);
137
+ });
138
+
139
+ it("should handle missing message key", () => {
140
+ const data = { type: "assistant" };
141
+ const results = extractTextFromMessage(data);
142
+ expect(results).toEqual([]);
143
+ });
144
+
145
+ it("should skip empty text blocks", () => {
146
+ const data = {
147
+ type: "assistant",
148
+ message: {
149
+ content: [
150
+ { type: "text", text: "" },
151
+ { type: "text", text: "Non-empty" },
152
+ ],
153
+ },
154
+ };
155
+ const results = extractTextFromMessage(data);
156
+ expect(results).toEqual([[1, "Non-empty"]]);
157
+ });
158
+
159
+ it("should extract text from stream_event format", () => {
160
+ const data = {
161
+ type: "stream_event",
162
+ event: {
163
+ type: "content_block_delta",
164
+ index: 0,
165
+ delta: { type: "text_delta", text: "Hello" },
166
+ },
167
+ };
168
+ const results = extractTextFromMessage(data);
169
+ expect(results).toEqual([[0, "Hello"]]);
170
+ });
171
+
172
+ it("should skip non-text deltas in stream events", () => {
173
+ const data = {
174
+ type: "stream_event",
175
+ event: {
176
+ type: "content_block_delta",
177
+ index: 0,
178
+ delta: { type: "input_json_delta", partial_json: "{}" },
179
+ },
180
+ };
181
+ const results = extractTextFromMessage(data);
182
+ expect(results).toEqual([]);
183
+ });
184
+
185
+ it("should skip non-delta stream events", () => {
186
+ const data = {
187
+ type: "stream_event",
188
+ event: {
189
+ type: "message_start",
190
+ message: {},
191
+ },
192
+ };
193
+ const results = extractTextFromMessage(data);
194
+ expect(results).toEqual([]);
195
+ });
196
+ });
197
+
198
+ // --- extractUserText ---
199
+
200
+ describe("extractUserText", () => {
201
+ it("should extract text from user message with content array", () => {
202
+ const data = {
203
+ type: "user",
204
+ message: {
205
+ content: [{ type: "text", text: "Hello, Claude!" }],
206
+ },
207
+ };
208
+ const result = extractUserText(data);
209
+ expect(result).toBe("Hello, Claude!");
210
+ });
211
+
212
+ it("should extract text from user message with string content", () => {
213
+ const data = {
214
+ type: "user",
215
+ message: {
216
+ content: ["Simple string prompt"],
217
+ },
218
+ };
219
+ const result = extractUserText(data);
220
+ expect(result).toBe("Simple string prompt");
221
+ });
222
+
223
+ it("should return null for non-user messages", () => {
224
+ const data = { type: "assistant", message: { content: [] } };
225
+ const result = extractUserText(data);
226
+ expect(result).toBeNull();
227
+ });
228
+
229
+ it("should return null for empty content", () => {
230
+ const data = { type: "user", message: { content: [] } };
231
+ const result = extractUserText(data);
232
+ expect(result).toBeNull();
233
+ });
234
+ });
235
+
236
+ // --- extractModelFromMessage ---
237
+
238
+ describe("extractModelFromMessage", () => {
239
+ it("should extract model from assistant message", () => {
240
+ const data = {
241
+ type: "assistant",
242
+ message: {
243
+ model: "claude-sonnet-4-5-20250929",
244
+ content: [{ type: "text", text: "Hello!" }],
245
+ },
246
+ };
247
+ const result = extractModelFromMessage(data);
248
+ expect(result).toBe("claude-sonnet-4-5-20250929");
249
+ });
250
+
251
+ it("should extract model from system message", () => {
252
+ const data = { type: "system", model: "claude-opus-4-5-20251101" };
253
+ const result = extractModelFromMessage(data);
254
+ expect(result).toBe("claude-opus-4-5-20251101");
255
+ });
256
+
257
+ it("should return null for non-message types", () => {
258
+ const data = { type: "user", message: { content: [] } };
259
+ const result = extractModelFromMessage(data);
260
+ expect(result).toBeNull();
261
+ });
262
+
263
+ it("should return null for missing model field", () => {
264
+ const data = { type: "assistant", message: { content: [] } };
265
+ const result = extractModelFromMessage(data);
266
+ expect(result).toBeNull();
267
+ });
268
+ });
269
+
270
+ // --- formatModelName ---
271
+
272
+ describe("formatModelName", () => {
273
+ it("should format Sonnet 4.5", () => {
274
+ expect(formatModelName("claude-sonnet-4-5-20250929")).toBe("sonnet-4.5");
275
+ });
276
+
277
+ it("should format Opus 4.5", () => {
278
+ expect(formatModelName("claude-opus-4-5-20251101")).toBe("opus-4.5");
279
+ });
280
+
281
+ it("should format Haiku 4.0", () => {
282
+ expect(formatModelName("claude-haiku-4-0-20250101")).toBe("haiku-4.0");
283
+ });
284
+
285
+ it("should format Sonnet 3.7 (pattern 2)", () => {
286
+ expect(formatModelName("claude-3-7-sonnet-20250219")).toBe("sonnet-3.7");
287
+ });
288
+
289
+ it("should return empty string for null", () => {
290
+ expect(formatModelName(null)).toBe("");
291
+ });
292
+
293
+ it("should return original for unparseable names", () => {
294
+ expect(formatModelName("unknown-model")).toBe("unknown-model");
295
+ });
296
+
297
+ it("should return empty string for empty string", () => {
298
+ expect(formatModelName("")).toBe("");
299
+ });
300
+
301
+ it("should return original for short names", () => {
302
+ expect(formatModelName("claude-sonnet")).toBe("claude-sonnet");
303
+ });
304
+
305
+ it("should return original for names without a known tier", () => {
306
+ expect(formatModelName("claude-unknown-4-5-20250929")).toBe("claude-unknown-4-5-20250929");
307
+ });
308
+ });
309
+
310
+ // --- extractResultText ---
311
+
312
+ describe("extractResultText", () => {
313
+ it("should extract result from result message", () => {
314
+ const data = { type: "result", result: "Task completed successfully." };
315
+ const result = extractResultText(data);
316
+ expect(result).toBe("Task completed successfully.");
317
+ });
318
+
319
+ it("should return null for non-result messages", () => {
320
+ const data = { type: "assistant", message: { content: [] } };
321
+ const result = extractResultText(data);
322
+ expect(result).toBeNull();
323
+ });
324
+
325
+ it("should return null for result message without result key", () => {
326
+ const data = { type: "result" };
327
+ const result = extractResultText(data);
328
+ expect(result).toBeNull();
329
+ });
330
+ });
331
+
332
+ // --- SessionOutput ---
333
+
334
+ describe("SessionOutput", () => {
335
+ it("should parse stdout with valid JSON in code block", () => {
336
+ const stdout = `
337
+ Some output text here.
338
+
339
+ \`\`\`json
340
+ {
341
+ "sessionId": "abc123",
342
+ "isSuccess": true,
343
+ "context": "Task completed successfully."
344
+ }
345
+ \`\`\`
346
+ `;
347
+ const output = SessionOutput.fromStdout(stdout);
348
+ expect(output.sessionId).toBe("abc123");
349
+ expect(output.isSuccess).toBe(true);
350
+ expect(output.context).toBe("Task completed successfully.");
351
+ });
352
+
353
+ it("should preserve extra keys", () => {
354
+ const stdout = `
355
+ \`\`\`json
356
+ {
357
+ "sessionId": "test",
358
+ "isSuccess": true,
359
+ "context": "Done",
360
+ "customKey": "customValue",
361
+ "anotherKey": 123
362
+ }
363
+ \`\`\`
364
+ `;
365
+ const output = SessionOutput.fromStdout(stdout);
366
+ expect(output.extra.customKey).toBe("customValue");
367
+ expect(output.extra.anotherKey).toBe(123);
368
+ expect(output.rawJson).not.toBeNull();
369
+ });
370
+
371
+ it("should handle stdout without JSON", () => {
372
+ const stdout = "Just regular output without any JSON blocks.";
373
+ const output = SessionOutput.fromStdout(stdout);
374
+ expect(output.sessionId).toBeNull();
375
+ expect(output.isSuccess).toBe(false);
376
+ expect(output.context).toContain("No valid session output JSON");
377
+ });
378
+
379
+ it("should handle empty stdout", () => {
380
+ const output = SessionOutput.fromStdout("");
381
+ expect(output.sessionId).toBeNull();
382
+ expect(output.isSuccess).toBe(false);
383
+ expect(output.context).toContain("No output received");
384
+ });
385
+
386
+ it("should use last valid JSON block when multiple exist", () => {
387
+ const stdout = `
388
+ First JSON:
389
+ \`\`\`json
390
+ {"sessionId": "first", "isSuccess": false, "context": "First attempt"}
391
+ \`\`\`
392
+
393
+ Second JSON:
394
+ \`\`\`json
395
+ {"sessionId": "second", "isSuccess": true, "context": "Final success"}
396
+ \`\`\`
397
+ `;
398
+ const output = SessionOutput.fromStdout(stdout);
399
+ expect(output.sessionId).toBe("second");
400
+ expect(output.isSuccess).toBe(true);
401
+ });
402
+
403
+ it("should parse bare JSON without code blocks", () => {
404
+ const stdout = 'Output: {"sessionId": "bare", "isSuccess": true, "context": "Bare JSON"}';
405
+ const output = SessionOutput.fromStdout(stdout);
406
+ expect(output.sessionId).toBe("bare");
407
+ expect(output.isSuccess).toBe(true);
408
+ });
409
+
410
+ it("should handle invalid JSON in code blocks", () => {
411
+ const stdout = `
412
+ \`\`\`json
413
+ {invalid json here
414
+ \`\`\`
415
+ `;
416
+ const output = SessionOutput.fromStdout(stdout);
417
+ expect(output.sessionId).toBeNull();
418
+ expect(output.isSuccess).toBe(false);
419
+ });
420
+
421
+ it("should skip JSON without required keys", () => {
422
+ const stdout = `
423
+ \`\`\`json
424
+ {"someOtherKey": "value"}
425
+ \`\`\`
426
+ `;
427
+ const output = SessionOutput.fromStdout(stdout);
428
+ expect(output.sessionId).toBeNull();
429
+ expect(output.context).toContain("No valid session output JSON");
430
+ });
431
+ });
432
+
433
+ // --- ClaudeResult ---
434
+
435
+ describe("ClaudeResult", () => {
436
+ it("should report success for returncode 0", () => {
437
+ const result = new ClaudeResult(0, "output", "", "test", null);
438
+ expect(result.success).toBe(true);
439
+ });
440
+
441
+ it("should report failure for non-zero returncode", () => {
442
+ const result = new ClaudeResult(1, "", "error", "test", null);
443
+ expect(result.success).toBe(false);
444
+ });
445
+
446
+ it("should lazily parse session output", () => {
447
+ const result = new ClaudeResult(
448
+ 0,
449
+ '{"sessionId": "lazy", "isSuccess": true, "context": "Test"}',
450
+ "",
451
+ "test",
452
+ null,
453
+ );
454
+
455
+ const output1 = result.sessionOutput;
456
+ const output2 = result.sessionOutput;
457
+
458
+ expect(output1).toBe(output2); // Same reference (cached)
459
+ expect(output1.sessionId).toBe("lazy");
460
+ });
461
+
462
+ it("should have correct string representation for success", () => {
463
+ const result = new ClaudeResult(0, "", "", "test", null, "sonnet");
464
+ const str = result.toString();
465
+ expect(str).toContain("SUCCESS");
466
+ expect(str).toContain("sonnet");
467
+ });
468
+
469
+ it("should have correct string representation for failure", () => {
470
+ const result = new ClaudeResult(1, "", "error", "test", null);
471
+ const str = result.toString();
472
+ expect(str).toContain("FAILED");
473
+ });
474
+ });
475
+
476
+ // --- getAgenticSystemPrompt ---
477
+
478
+ describe("getAgenticSystemPrompt", () => {
479
+ it("should load system prompt when file exists", () => {
480
+ const prompt = getAgenticSystemPrompt();
481
+ // May or may not exist depending on build state
482
+ if (prompt !== null) {
483
+ expect(prompt.length).toBeGreaterThan(0);
484
+ }
485
+ });
486
+ });
487
+
488
+ // --- checkClaudeAvailable ---
489
+
490
+ describe("checkClaudeAvailable", () => {
491
+ it("should return false when claude is not in PATH", () => {
492
+ // In test environment, claude is likely not available
493
+ // This test just verifies the function doesn't throw
494
+ const result = checkClaudeAvailable();
495
+ expect(typeof result).toBe("boolean");
496
+ });
497
+ });
@@ -0,0 +1,7 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ describe("project setup", () => {
4
+ it("scaffolding is complete", () => {
5
+ expect(true).toBe(true);
6
+ });
7
+ });
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { SignalManager } from "../src/signal-manager.js";
3
+
4
+ describe("SignalManager", () => {
5
+ it("should start with shutdownRequested false", () => {
6
+ const manager = new SignalManager();
7
+ expect(manager.shutdownRequested).toBe(false);
8
+ });
9
+
10
+ it("should set shutdownRequested on requestShutdown", () => {
11
+ const manager = new SignalManager();
12
+ manager.requestShutdown();
13
+ expect(manager.shutdownRequested).toBe(true);
14
+ });
15
+
16
+ it("should accept an optional callback", () => {
17
+ let called = false;
18
+ const manager = new SignalManager(() => {
19
+ called = true;
20
+ });
21
+ // Callback is only triggered by signal, not by requestShutdown
22
+ manager.requestShutdown();
23
+ expect(manager.shutdownRequested).toBe(true);
24
+ expect(called).toBe(false);
25
+ });
26
+ });