@soleri/core 9.10.0 → 9.12.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 (248) hide show
  1. package/dist/adapters/types.d.ts +2 -0
  2. package/dist/adapters/types.d.ts.map +1 -1
  3. package/dist/brain/brain.d.ts +5 -1
  4. package/dist/brain/brain.d.ts.map +1 -1
  5. package/dist/brain/brain.js +97 -10
  6. package/dist/brain/brain.js.map +1 -1
  7. package/dist/brain/intelligence.d.ts.map +1 -1
  8. package/dist/brain/intelligence.js +4 -0
  9. package/dist/brain/intelligence.js.map +1 -1
  10. package/dist/brain/types.d.ts +1 -1
  11. package/dist/brain/types.d.ts.map +1 -1
  12. package/dist/dream/cron-manager.d.ts +10 -0
  13. package/dist/dream/cron-manager.d.ts.map +1 -0
  14. package/dist/dream/cron-manager.js +122 -0
  15. package/dist/dream/cron-manager.js.map +1 -0
  16. package/dist/dream/dream-engine.d.ts +34 -0
  17. package/dist/dream/dream-engine.d.ts.map +1 -0
  18. package/dist/dream/dream-engine.js +88 -0
  19. package/dist/dream/dream-engine.js.map +1 -0
  20. package/dist/dream/dream-ops.d.ts +8 -0
  21. package/dist/dream/dream-ops.d.ts.map +1 -0
  22. package/dist/dream/dream-ops.js +49 -0
  23. package/dist/dream/dream-ops.js.map +1 -0
  24. package/dist/dream/index.d.ts +7 -0
  25. package/dist/dream/index.d.ts.map +1 -0
  26. package/dist/dream/index.js +5 -0
  27. package/dist/dream/index.js.map +1 -0
  28. package/dist/dream/schema.d.ts +3 -0
  29. package/dist/dream/schema.d.ts.map +1 -0
  30. package/dist/dream/schema.js +16 -0
  31. package/dist/dream/schema.js.map +1 -0
  32. package/dist/embeddings/index.d.ts +5 -0
  33. package/dist/embeddings/index.d.ts.map +1 -0
  34. package/dist/embeddings/index.js +3 -0
  35. package/dist/embeddings/index.js.map +1 -0
  36. package/dist/embeddings/openai-provider.d.ts +31 -0
  37. package/dist/embeddings/openai-provider.d.ts.map +1 -0
  38. package/dist/embeddings/openai-provider.js +120 -0
  39. package/dist/embeddings/openai-provider.js.map +1 -0
  40. package/dist/embeddings/pipeline.d.ts +36 -0
  41. package/dist/embeddings/pipeline.d.ts.map +1 -0
  42. package/dist/embeddings/pipeline.js +78 -0
  43. package/dist/embeddings/pipeline.js.map +1 -0
  44. package/dist/embeddings/types.d.ts +62 -0
  45. package/dist/embeddings/types.d.ts.map +1 -0
  46. package/dist/embeddings/types.js +3 -0
  47. package/dist/embeddings/types.js.map +1 -0
  48. package/dist/engine/bin/soleri-engine.js +4 -1
  49. package/dist/engine/bin/soleri-engine.js.map +1 -1
  50. package/dist/engine/module-manifest.d.ts.map +1 -1
  51. package/dist/engine/module-manifest.js +20 -0
  52. package/dist/engine/module-manifest.js.map +1 -1
  53. package/dist/engine/register-engine.d.ts.map +1 -1
  54. package/dist/engine/register-engine.js +12 -0
  55. package/dist/engine/register-engine.js.map +1 -1
  56. package/dist/flows/chain-types.d.ts +8 -8
  57. package/dist/flows/dispatch-registry.d.ts +15 -1
  58. package/dist/flows/dispatch-registry.d.ts.map +1 -1
  59. package/dist/flows/dispatch-registry.js +28 -1
  60. package/dist/flows/dispatch-registry.js.map +1 -1
  61. package/dist/flows/executor.d.ts +20 -2
  62. package/dist/flows/executor.d.ts.map +1 -1
  63. package/dist/flows/executor.js +79 -1
  64. package/dist/flows/executor.js.map +1 -1
  65. package/dist/flows/index.d.ts +2 -1
  66. package/dist/flows/index.d.ts.map +1 -1
  67. package/dist/flows/index.js.map +1 -1
  68. package/dist/flows/types.d.ts +43 -21
  69. package/dist/flows/types.d.ts.map +1 -1
  70. package/dist/index.d.ts +5 -0
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +3 -0
  73. package/dist/index.js.map +1 -1
  74. package/dist/planning/plan-lifecycle.d.ts.map +1 -1
  75. package/dist/planning/plan-lifecycle.js +4 -2
  76. package/dist/planning/plan-lifecycle.js.map +1 -1
  77. package/dist/planning/planner-types.d.ts +1 -1
  78. package/dist/planning/planner-types.d.ts.map +1 -1
  79. package/dist/plugins/types.d.ts +31 -31
  80. package/dist/runtime/admin-ops.d.ts.map +1 -1
  81. package/dist/runtime/admin-ops.js +15 -0
  82. package/dist/runtime/admin-ops.js.map +1 -1
  83. package/dist/runtime/admin-setup-ops.js +2 -2
  84. package/dist/runtime/admin-setup-ops.js.map +1 -1
  85. package/dist/runtime/embedding-ops.d.ts +12 -0
  86. package/dist/runtime/embedding-ops.d.ts.map +1 -0
  87. package/dist/runtime/embedding-ops.js +96 -0
  88. package/dist/runtime/embedding-ops.js.map +1 -0
  89. package/dist/runtime/facades/embedding-facade.d.ts +7 -0
  90. package/dist/runtime/facades/embedding-facade.d.ts.map +1 -0
  91. package/dist/runtime/facades/embedding-facade.js +8 -0
  92. package/dist/runtime/facades/embedding-facade.js.map +1 -0
  93. package/dist/runtime/facades/index.d.ts.map +1 -1
  94. package/dist/runtime/facades/index.js +12 -0
  95. package/dist/runtime/facades/index.js.map +1 -1
  96. package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
  97. package/dist/runtime/facades/orchestrate-facade.js +120 -0
  98. package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
  99. package/dist/runtime/feature-flags.d.ts.map +1 -1
  100. package/dist/runtime/feature-flags.js +4 -0
  101. package/dist/runtime/feature-flags.js.map +1 -1
  102. package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
  103. package/dist/runtime/orchestrate-ops.js +146 -12
  104. package/dist/runtime/orchestrate-ops.js.map +1 -1
  105. package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
  106. package/dist/runtime/planning-extra-ops.js +51 -0
  107. package/dist/runtime/planning-extra-ops.js.map +1 -1
  108. package/dist/runtime/preflight.d.ts +32 -0
  109. package/dist/runtime/preflight.d.ts.map +1 -0
  110. package/dist/runtime/preflight.js +29 -0
  111. package/dist/runtime/preflight.js.map +1 -0
  112. package/dist/runtime/quality-signals.d.ts +6 -1
  113. package/dist/runtime/quality-signals.d.ts.map +1 -1
  114. package/dist/runtime/quality-signals.js +41 -5
  115. package/dist/runtime/quality-signals.js.map +1 -1
  116. package/dist/runtime/runtime.d.ts.map +1 -1
  117. package/dist/runtime/runtime.js +33 -2
  118. package/dist/runtime/runtime.js.map +1 -1
  119. package/dist/runtime/types.d.ts +27 -0
  120. package/dist/runtime/types.d.ts.map +1 -1
  121. package/dist/skills/step-tracker.d.ts +39 -0
  122. package/dist/skills/step-tracker.d.ts.map +1 -0
  123. package/dist/skills/step-tracker.js +105 -0
  124. package/dist/skills/step-tracker.js.map +1 -0
  125. package/dist/skills/sync-skills.d.ts +3 -2
  126. package/dist/skills/sync-skills.d.ts.map +1 -1
  127. package/dist/skills/sync-skills.js +42 -8
  128. package/dist/skills/sync-skills.js.map +1 -1
  129. package/dist/subagent/dispatcher.d.ts +4 -3
  130. package/dist/subagent/dispatcher.d.ts.map +1 -1
  131. package/dist/subagent/dispatcher.js +57 -35
  132. package/dist/subagent/dispatcher.js.map +1 -1
  133. package/dist/subagent/index.d.ts +1 -0
  134. package/dist/subagent/index.d.ts.map +1 -1
  135. package/dist/subagent/index.js.map +1 -1
  136. package/dist/subagent/orphan-reaper.d.ts +51 -4
  137. package/dist/subagent/orphan-reaper.d.ts.map +1 -1
  138. package/dist/subagent/orphan-reaper.js +103 -3
  139. package/dist/subagent/orphan-reaper.js.map +1 -1
  140. package/dist/subagent/types.d.ts +7 -0
  141. package/dist/subagent/types.d.ts.map +1 -1
  142. package/dist/subagent/workspace-resolver.d.ts +2 -0
  143. package/dist/subagent/workspace-resolver.d.ts.map +1 -1
  144. package/dist/subagent/workspace-resolver.js +3 -1
  145. package/dist/subagent/workspace-resolver.js.map +1 -1
  146. package/dist/vault/vault-entries.d.ts +18 -0
  147. package/dist/vault/vault-entries.d.ts.map +1 -1
  148. package/dist/vault/vault-entries.js +73 -0
  149. package/dist/vault/vault-entries.js.map +1 -1
  150. package/dist/vault/vault-manager.d.ts.map +1 -1
  151. package/dist/vault/vault-manager.js +1 -0
  152. package/dist/vault/vault-manager.js.map +1 -1
  153. package/dist/vault/vault-schema.d.ts.map +1 -1
  154. package/dist/vault/vault-schema.js +14 -0
  155. package/dist/vault/vault-schema.js.map +1 -1
  156. package/dist/vault/vault.d.ts +1 -0
  157. package/dist/vault/vault.d.ts.map +1 -1
  158. package/dist/vault/vault.js.map +1 -1
  159. package/package.json +3 -5
  160. package/src/__tests__/cron-manager.test.ts +132 -0
  161. package/src/__tests__/deviation-detection.test.ts +234 -0
  162. package/src/__tests__/embeddings.test.ts +536 -0
  163. package/src/__tests__/preflight.test.ts +97 -0
  164. package/src/__tests__/step-persistence.test.ts +324 -0
  165. package/src/__tests__/step-tracker.test.ts +260 -0
  166. package/src/__tests__/subagent/dispatcher.test.ts +122 -4
  167. package/src/__tests__/subagent/orphan-reaper.test.ts +148 -12
  168. package/src/__tests__/subagent/process-lifecycle.test.ts +422 -0
  169. package/src/__tests__/subagent/workspace-resolver.test.ts +6 -1
  170. package/src/adapters/types.ts +2 -0
  171. package/src/brain/brain.ts +117 -9
  172. package/src/brain/intelligence.ts +4 -0
  173. package/src/brain/types.ts +6 -1
  174. package/src/dream/cron-manager.ts +137 -0
  175. package/src/dream/dream-engine.ts +119 -0
  176. package/src/dream/dream-ops.ts +56 -0
  177. package/src/dream/dream.test.ts +182 -0
  178. package/src/dream/index.ts +6 -0
  179. package/src/dream/schema.ts +17 -0
  180. package/src/embeddings/openai-provider.ts +158 -0
  181. package/src/embeddings/pipeline.ts +126 -0
  182. package/src/embeddings/types.ts +67 -0
  183. package/src/engine/bin/soleri-engine.ts +4 -1
  184. package/src/engine/module-manifest.test.ts +4 -4
  185. package/src/engine/module-manifest.ts +20 -0
  186. package/src/engine/register-engine.ts +12 -0
  187. package/src/flows/dispatch-registry.ts +44 -1
  188. package/src/flows/executor.ts +93 -2
  189. package/src/flows/index.ts +2 -0
  190. package/src/flows/types.ts +39 -1
  191. package/src/index.ts +11 -0
  192. package/src/planning/goal-ancestry.test.ts +3 -5
  193. package/src/planning/plan-lifecycle.ts +5 -2
  194. package/src/planning/planner-types.ts +1 -1
  195. package/src/planning/planner.test.ts +73 -3
  196. package/src/runtime/admin-ops.test.ts +2 -2
  197. package/src/runtime/admin-ops.ts +17 -0
  198. package/src/runtime/admin-setup-ops.ts +2 -2
  199. package/src/runtime/embedding-ops.ts +116 -0
  200. package/src/runtime/facades/admin-facade.test.ts +31 -0
  201. package/src/runtime/facades/embedding-facade.ts +11 -0
  202. package/src/runtime/facades/index.ts +12 -0
  203. package/src/runtime/facades/orchestrate-facade.test.ts +16 -0
  204. package/src/runtime/facades/orchestrate-facade.ts +146 -0
  205. package/src/runtime/feature-flags.ts +4 -0
  206. package/src/runtime/orchestrate-ops.test.ts +182 -2
  207. package/src/runtime/orchestrate-ops.ts +170 -13
  208. package/src/runtime/planning-extra-ops.ts +77 -0
  209. package/src/runtime/preflight.ts +53 -0
  210. package/src/runtime/quality-signals.test.ts +182 -8
  211. package/src/runtime/quality-signals.ts +44 -5
  212. package/src/runtime/runtime.ts +41 -2
  213. package/src/runtime/types.ts +20 -0
  214. package/src/skills/__tests__/sync-skills.test.ts +132 -0
  215. package/src/skills/step-tracker.ts +162 -0
  216. package/src/skills/sync-skills.ts +54 -9
  217. package/src/subagent/dispatcher.ts +62 -39
  218. package/src/subagent/index.ts +1 -0
  219. package/src/subagent/orphan-reaper.test.ts +135 -0
  220. package/src/subagent/orphan-reaper.ts +130 -7
  221. package/src/subagent/types.ts +10 -0
  222. package/src/subagent/workspace-resolver.ts +3 -1
  223. package/src/vault/vault-entries.ts +112 -0
  224. package/src/vault/vault-manager.ts +1 -0
  225. package/src/vault/vault-scaling.test.ts +3 -2
  226. package/src/vault/vault-schema.ts +15 -0
  227. package/src/vault/vault.ts +1 -0
  228. package/vitest.config.ts +2 -1
  229. package/dist/brain/strength-scorer.d.ts +0 -31
  230. package/dist/brain/strength-scorer.d.ts.map +0 -1
  231. package/dist/brain/strength-scorer.js +0 -264
  232. package/dist/brain/strength-scorer.js.map +0 -1
  233. package/dist/engine/index.d.ts +0 -21
  234. package/dist/engine/index.d.ts.map +0 -1
  235. package/dist/engine/index.js +0 -18
  236. package/dist/engine/index.js.map +0 -1
  237. package/dist/hooks/index.d.ts +0 -2
  238. package/dist/hooks/index.d.ts.map +0 -1
  239. package/dist/hooks/index.js +0 -2
  240. package/dist/hooks/index.js.map +0 -1
  241. package/dist/persona/index.d.ts +0 -5
  242. package/dist/persona/index.d.ts.map +0 -1
  243. package/dist/persona/index.js +0 -4
  244. package/dist/persona/index.js.map +0 -1
  245. package/dist/vault/vault-interfaces.d.ts +0 -153
  246. package/dist/vault/vault-interfaces.d.ts.map +0 -1
  247. package/dist/vault/vault-interfaces.js +0 -2
  248. package/dist/vault/vault-interfaces.js.map +0 -1
@@ -154,11 +154,11 @@ describe('SubagentDispatcher', () => {
154
154
  dispatcher.cleanup();
155
155
  });
156
156
 
157
- it('reapOrphans() returns orphaned results when processes are dead', () => {
157
+ it('reapOrphans() returns ReapResult with empty arrays when no processes tracked', () => {
158
158
  // reapOrphans delegates to the internal OrphanReaper
159
- // Without registering processes, it should return empty
160
- const orphaned = dispatcher.reapOrphans();
161
- expect(orphaned).toEqual([]);
159
+ // Without registering processes, it should return empty reaped/alive
160
+ const result = dispatcher.reapOrphans();
161
+ expect(result).toEqual({ reaped: [], alive: [] });
162
162
  });
163
163
 
164
164
  it('dispatch() stops on first failure in sequential mode', async () => {
@@ -192,4 +192,122 @@ describe('SubagentDispatcher', () => {
192
192
  expect(updates.length).toBeGreaterThanOrEqual(1);
193
193
  expect(updates[0][0]).toBe('cb-test');
194
194
  });
195
+
196
+ // ── Timeout + process killing ─────────────────────────────────────
197
+
198
+ it('dispatch() returns timeout error when task exceeds timeout', async () => {
199
+ vi.useFakeTimers();
200
+
201
+ (mockAdapter.execute as ReturnType<typeof vi.fn>).mockImplementation(
202
+ () => new Promise(() => {}), // never resolves
203
+ );
204
+
205
+ const tasks = [makeTask({ taskId: 'timeout-task' })];
206
+ const dispatchPromise = dispatcher.dispatch(tasks, { parallel: false, timeout: 1000 });
207
+
208
+ await vi.advanceTimersByTimeAsync(1000);
209
+
210
+ const result = await dispatchPromise;
211
+ expect(result.failed).toBe(1);
212
+ expect(result.results[0].error).toBe('Task timed out');
213
+
214
+ vi.useRealTimers();
215
+ });
216
+
217
+ it('dispatch() passes onMeta callback to adapter for pid reporting', async () => {
218
+ let capturedOnMeta: ((meta: Record<string, unknown>) => void) | undefined;
219
+
220
+ (mockAdapter.execute as ReturnType<typeof vi.fn>).mockImplementation(async (ctx) => {
221
+ capturedOnMeta = ctx.onMeta;
222
+ // Simulate adapter reporting its PID
223
+ ctx.onMeta?.({ pid: 42 });
224
+ return { exitCode: 0, summary: 'done' };
225
+ });
226
+
227
+ const tasks = [makeTask({ taskId: 'meta-test' })];
228
+ const result = await dispatcher.dispatch(tasks, { parallel: false });
229
+
230
+ expect(capturedOnMeta).toBeDefined();
231
+ expect(result.results[0].pid).toBe(42);
232
+ expect(result.completed).toBe(1);
233
+ });
234
+
235
+ it('dispatch() kills child process on timeout when pid is reported', async () => {
236
+ vi.useFakeTimers();
237
+
238
+ const killSpy = vi.spyOn(process, 'kill');
239
+ let sigkillSent = false;
240
+ killSpy.mockImplementation((_pid: number, signal?: string | number) => {
241
+ if (signal === 0) {
242
+ // Process alive until SIGKILL
243
+ if (sigkillSent) {
244
+ const err = new Error('No such process') as NodeJS.ErrnoException;
245
+ err.code = 'ESRCH';
246
+ throw err;
247
+ }
248
+ return true;
249
+ }
250
+ if (signal === 'SIGTERM') return true;
251
+ if (signal === 'SIGKILL') {
252
+ sigkillSent = true;
253
+ return true;
254
+ }
255
+ return true;
256
+ });
257
+
258
+ (mockAdapter.execute as ReturnType<typeof vi.fn>).mockImplementation(async (ctx) => {
259
+ // Report PID immediately
260
+ ctx.onMeta?.({ pid: 9876 });
261
+ // Then hang forever (simulate stuck process)
262
+ return new Promise(() => {});
263
+ });
264
+
265
+ const tasks = [makeTask({ taskId: 'kill-test' })];
266
+ const dispatchPromise = dispatcher.dispatch(tasks, { parallel: false, timeout: 500 });
267
+
268
+ // Trigger the timeout
269
+ await vi.advanceTimersByTimeAsync(500);
270
+
271
+ const result = await dispatchPromise;
272
+ expect(result.failed).toBe(1);
273
+ expect(result.results[0].error).toBe('Task timed out');
274
+ expect(result.results[0].pid).toBe(9876);
275
+
276
+ // Verify SIGTERM was sent
277
+ expect(killSpy).toHaveBeenCalledWith(9876, 'SIGTERM');
278
+
279
+ // Advance past the 5s grace period to trigger SIGKILL escalation
280
+ await vi.advanceTimersByTimeAsync(5_000);
281
+ expect(killSpy).toHaveBeenCalledWith(9876, 'SIGKILL');
282
+
283
+ killSpy.mockRestore();
284
+ vi.useRealTimers();
285
+ });
286
+
287
+ it('dispatch() does not attempt kill when no pid is reported', async () => {
288
+ vi.useFakeTimers();
289
+
290
+ const killSpy = vi.spyOn(process, 'kill');
291
+ killSpy.mockImplementation(() => true);
292
+
293
+ (mockAdapter.execute as ReturnType<typeof vi.fn>).mockImplementation(
294
+ () => new Promise(() => {}), // never resolves, no pid reported
295
+ );
296
+
297
+ const tasks = [makeTask({ taskId: 'no-pid-task' })];
298
+ const dispatchPromise = dispatcher.dispatch(tasks, { parallel: false, timeout: 500 });
299
+
300
+ await vi.advanceTimersByTimeAsync(500);
301
+
302
+ const result = await dispatchPromise;
303
+ expect(result.failed).toBe(1);
304
+ expect(result.results[0].error).toBe('Task timed out');
305
+
306
+ // No SIGTERM or SIGKILL should have been sent (only signal-0 checks from reaper are possible)
307
+ const termCalls = killSpy.mock.calls.filter((c) => c[1] === 'SIGTERM' || c[1] === 'SIGKILL');
308
+ expect(termCalls).toHaveLength(0);
309
+
310
+ killSpy.mockRestore();
311
+ vi.useRealTimers();
312
+ });
195
313
  });
@@ -28,7 +28,7 @@ describe('OrphanReaper', () => {
28
28
  expect(reaper.listTracked()).toHaveLength(0);
29
29
  });
30
30
 
31
- it('reap() returns empty when all processes are alive', () => {
31
+ it('reap() returns empty reaped when all processes are alive', () => {
32
32
  // process.kill(pid, 0) succeeds = process alive
33
33
  killSpy.mockImplementation(() => true);
34
34
 
@@ -36,8 +36,9 @@ describe('OrphanReaper', () => {
36
36
  reaper.register(1234, 'task-1');
37
37
  reaper.register(5678, 'task-2');
38
38
 
39
- const reaped = reaper.reap();
40
- expect(reaped).toHaveLength(0);
39
+ const result = reaper.reap();
40
+ expect(result.reaped).toHaveLength(0);
41
+ expect(result.alive).toEqual(['task-1', 'task-2']);
41
42
  expect(reaper.listTracked()).toHaveLength(2);
42
43
  });
43
44
 
@@ -51,10 +52,9 @@ describe('OrphanReaper', () => {
51
52
  const reaper = new OrphanReaper();
52
53
  reaper.register(1234, 'task-1');
53
54
 
54
- const reaped = reaper.reap();
55
- expect(reaped).toHaveLength(1);
56
- expect(reaped[0].taskId).toBe('task-1');
57
- expect(reaped[0].pid).toBe(1234);
55
+ const result = reaper.reap();
56
+ expect(result.reaped).toEqual(['task-1']);
57
+ expect(result.alive).toHaveLength(0);
58
58
  // Dead process should be removed from tracking
59
59
  expect(reaper.isTracked(1234)).toBe(false);
60
60
  });
@@ -84,8 +84,9 @@ describe('OrphanReaper', () => {
84
84
  const reaper = new OrphanReaper();
85
85
  reaper.register(1234, 'task-1');
86
86
 
87
- const reaped = reaper.reap();
88
- expect(reaped).toHaveLength(0);
87
+ const result = reaper.reap();
88
+ expect(result.reaped).toHaveLength(0);
89
+ expect(result.alive).toEqual(['task-1']);
89
90
  expect(reaper.isTracked(1234)).toBe(true);
90
91
  });
91
92
 
@@ -132,10 +133,145 @@ describe('OrphanReaper', () => {
132
133
  reaper.register(1234, 'task-alive');
133
134
  reaper.register(5678, 'task-dead');
134
135
 
135
- const reaped = reaper.reap();
136
- expect(reaped).toHaveLength(1);
137
- expect(reaped[0].taskId).toBe('task-dead');
136
+ const result = reaper.reap();
137
+ expect(result.reaped).toEqual(['task-dead']);
138
+ expect(result.alive).toEqual(['task-alive']);
138
139
  expect(reaper.isTracked(1234)).toBe(true);
139
140
  expect(reaper.isTracked(5678)).toBe(false);
140
141
  });
142
+
143
+ // ── isAlive (public) ──────────────────────────────────────────────
144
+
145
+ it('isAlive() returns true when process.kill(pid, 0) succeeds', () => {
146
+ killSpy.mockImplementation(() => true);
147
+ const reaper = new OrphanReaper();
148
+ expect(reaper.isAlive(1234)).toBe(true);
149
+ });
150
+
151
+ it('isAlive() returns true for EPERM (process exists, no permission)', () => {
152
+ killSpy.mockImplementation(() => {
153
+ const err = new Error('Operation not permitted') as NodeJS.ErrnoException;
154
+ err.code = 'EPERM';
155
+ throw err;
156
+ });
157
+ const reaper = new OrphanReaper();
158
+ expect(reaper.isAlive(1234)).toBe(true);
159
+ });
160
+
161
+ it('isAlive() returns false for ESRCH (process dead)', () => {
162
+ killSpy.mockImplementation(() => {
163
+ const err = new Error('No such process') as NodeJS.ErrnoException;
164
+ err.code = 'ESRCH';
165
+ throw err;
166
+ });
167
+ const reaper = new OrphanReaper();
168
+ expect(reaper.isAlive(1234)).toBe(false);
169
+ });
170
+
171
+ // ── killProcess ───────────────────────────────────────────────────
172
+
173
+ describe('killProcess', () => {
174
+ it('returns killed=true with SIGTERM when process is already dead', async () => {
175
+ killSpy.mockImplementation(() => {
176
+ const err = new Error('No such process') as NodeJS.ErrnoException;
177
+ err.code = 'ESRCH';
178
+ throw err;
179
+ });
180
+
181
+ const reaper = new OrphanReaper();
182
+ const result = await reaper.killProcess(9999);
183
+ expect(result.killed).toBe(true);
184
+ expect(result.signal).toBe('SIGTERM');
185
+ });
186
+
187
+ it('sends SIGTERM and returns killed=true when process dies from SIGTERM', async () => {
188
+ let termSent = false;
189
+ killSpy.mockImplementation((_pid: number, signal?: string | number) => {
190
+ if (signal === 0) {
191
+ // isAlive check: alive before SIGTERM, dead after
192
+ if (termSent) {
193
+ const err = new Error('No such process') as NodeJS.ErrnoException;
194
+ err.code = 'ESRCH';
195
+ throw err;
196
+ }
197
+ return true;
198
+ }
199
+ if (signal === 'SIGTERM') {
200
+ termSent = true;
201
+ return true;
202
+ }
203
+ return true;
204
+ });
205
+
206
+ const reaper = new OrphanReaper();
207
+ // Use escalate=false to avoid the 5s wait
208
+ const result = await reaper.killProcess(1234, false);
209
+ expect(result.killed).toBe(true);
210
+ expect(result.signal).toBe('SIGTERM');
211
+ });
212
+
213
+ it('escalates to SIGKILL when SIGTERM does not kill within grace period', async () => {
214
+ vi.useFakeTimers();
215
+
216
+ let sigkillSent = false;
217
+ killSpy.mockImplementation((_pid: number, signal?: string | number) => {
218
+ if (signal === 0) {
219
+ // Process stays alive until SIGKILL
220
+ if (sigkillSent) {
221
+ const err = new Error('No such process') as NodeJS.ErrnoException;
222
+ err.code = 'ESRCH';
223
+ throw err;
224
+ }
225
+ return true;
226
+ }
227
+ if (signal === 'SIGTERM') return true;
228
+ if (signal === 'SIGKILL') {
229
+ sigkillSent = true;
230
+ return true;
231
+ }
232
+ return true;
233
+ });
234
+
235
+ const reaper = new OrphanReaper();
236
+ const promise = reaper.killProcess(1234, true);
237
+
238
+ // Advance past the 5s grace period
239
+ await vi.advanceTimersByTimeAsync(5_000);
240
+
241
+ const result = await promise;
242
+ expect(result.killed).toBe(true);
243
+ expect(result.signal).toBe('SIGKILL');
244
+
245
+ vi.useRealTimers();
246
+ });
247
+
248
+ it('handles SIGTERM throwing (process dies between check and signal)', async () => {
249
+ let firstCheck = true;
250
+ killSpy.mockImplementation((_pid: number, signal?: string | number) => {
251
+ if (signal === 0) {
252
+ if (firstCheck) {
253
+ firstCheck = false;
254
+ return true; // alive on first check
255
+ }
256
+ // Dead after
257
+ const err = new Error('No such process') as NodeJS.ErrnoException;
258
+ err.code = 'ESRCH';
259
+ throw err;
260
+ }
261
+ if (signal === 'SIGTERM') {
262
+ // Process died between isAlive check and SIGTERM send
263
+ const err = new Error('No such process') as NodeJS.ErrnoException;
264
+ err.code = 'ESRCH';
265
+ throw err;
266
+ }
267
+ return true;
268
+ });
269
+
270
+ const reaper = new OrphanReaper();
271
+ const result = await reaper.killProcess(1234, false);
272
+ // Process is dead, so killed=true
273
+ expect(result.killed).toBe(true);
274
+ expect(result.signal).toBe('SIGTERM');
275
+ });
276
+ });
141
277
  });