@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.
- package/dist/adapters/types.d.ts +2 -0
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/brain/brain.d.ts +5 -1
- package/dist/brain/brain.d.ts.map +1 -1
- package/dist/brain/brain.js +97 -10
- package/dist/brain/brain.js.map +1 -1
- package/dist/brain/intelligence.d.ts.map +1 -1
- package/dist/brain/intelligence.js +4 -0
- package/dist/brain/intelligence.js.map +1 -1
- package/dist/brain/types.d.ts +1 -1
- package/dist/brain/types.d.ts.map +1 -1
- package/dist/dream/cron-manager.d.ts +10 -0
- package/dist/dream/cron-manager.d.ts.map +1 -0
- package/dist/dream/cron-manager.js +122 -0
- package/dist/dream/cron-manager.js.map +1 -0
- package/dist/dream/dream-engine.d.ts +34 -0
- package/dist/dream/dream-engine.d.ts.map +1 -0
- package/dist/dream/dream-engine.js +88 -0
- package/dist/dream/dream-engine.js.map +1 -0
- package/dist/dream/dream-ops.d.ts +8 -0
- package/dist/dream/dream-ops.d.ts.map +1 -0
- package/dist/dream/dream-ops.js +49 -0
- package/dist/dream/dream-ops.js.map +1 -0
- package/dist/dream/index.d.ts +7 -0
- package/dist/dream/index.d.ts.map +1 -0
- package/dist/dream/index.js +5 -0
- package/dist/dream/index.js.map +1 -0
- package/dist/dream/schema.d.ts +3 -0
- package/dist/dream/schema.d.ts.map +1 -0
- package/dist/dream/schema.js +16 -0
- package/dist/dream/schema.js.map +1 -0
- package/dist/embeddings/index.d.ts +5 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/index.js +3 -0
- package/dist/embeddings/index.js.map +1 -0
- package/dist/embeddings/openai-provider.d.ts +31 -0
- package/dist/embeddings/openai-provider.d.ts.map +1 -0
- package/dist/embeddings/openai-provider.js +120 -0
- package/dist/embeddings/openai-provider.js.map +1 -0
- package/dist/embeddings/pipeline.d.ts +36 -0
- package/dist/embeddings/pipeline.d.ts.map +1 -0
- package/dist/embeddings/pipeline.js +78 -0
- package/dist/embeddings/pipeline.js.map +1 -0
- package/dist/embeddings/types.d.ts +62 -0
- package/dist/embeddings/types.d.ts.map +1 -0
- package/dist/embeddings/types.js +3 -0
- package/dist/embeddings/types.js.map +1 -0
- package/dist/engine/bin/soleri-engine.js +4 -1
- package/dist/engine/bin/soleri-engine.js.map +1 -1
- package/dist/engine/module-manifest.d.ts.map +1 -1
- package/dist/engine/module-manifest.js +20 -0
- package/dist/engine/module-manifest.js.map +1 -1
- package/dist/engine/register-engine.d.ts.map +1 -1
- package/dist/engine/register-engine.js +12 -0
- package/dist/engine/register-engine.js.map +1 -1
- package/dist/flows/chain-types.d.ts +8 -8
- package/dist/flows/dispatch-registry.d.ts +15 -1
- package/dist/flows/dispatch-registry.d.ts.map +1 -1
- package/dist/flows/dispatch-registry.js +28 -1
- package/dist/flows/dispatch-registry.js.map +1 -1
- package/dist/flows/executor.d.ts +20 -2
- package/dist/flows/executor.d.ts.map +1 -1
- package/dist/flows/executor.js +79 -1
- package/dist/flows/executor.js.map +1 -1
- package/dist/flows/index.d.ts +2 -1
- package/dist/flows/index.d.ts.map +1 -1
- package/dist/flows/index.js.map +1 -1
- package/dist/flows/types.d.ts +43 -21
- package/dist/flows/types.d.ts.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/planning/plan-lifecycle.d.ts.map +1 -1
- package/dist/planning/plan-lifecycle.js +4 -2
- package/dist/planning/plan-lifecycle.js.map +1 -1
- package/dist/planning/planner-types.d.ts +1 -1
- package/dist/planning/planner-types.d.ts.map +1 -1
- package/dist/plugins/types.d.ts +31 -31
- package/dist/runtime/admin-ops.d.ts.map +1 -1
- package/dist/runtime/admin-ops.js +15 -0
- package/dist/runtime/admin-ops.js.map +1 -1
- package/dist/runtime/admin-setup-ops.js +2 -2
- package/dist/runtime/admin-setup-ops.js.map +1 -1
- package/dist/runtime/embedding-ops.d.ts +12 -0
- package/dist/runtime/embedding-ops.d.ts.map +1 -0
- package/dist/runtime/embedding-ops.js +96 -0
- package/dist/runtime/embedding-ops.js.map +1 -0
- package/dist/runtime/facades/embedding-facade.d.ts +7 -0
- package/dist/runtime/facades/embedding-facade.d.ts.map +1 -0
- package/dist/runtime/facades/embedding-facade.js +8 -0
- package/dist/runtime/facades/embedding-facade.js.map +1 -0
- package/dist/runtime/facades/index.d.ts.map +1 -1
- package/dist/runtime/facades/index.js +12 -0
- package/dist/runtime/facades/index.js.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.js +120 -0
- package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
- package/dist/runtime/feature-flags.d.ts.map +1 -1
- package/dist/runtime/feature-flags.js +4 -0
- package/dist/runtime/feature-flags.js.map +1 -1
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
- package/dist/runtime/orchestrate-ops.js +146 -12
- package/dist/runtime/orchestrate-ops.js.map +1 -1
- package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
- package/dist/runtime/planning-extra-ops.js +51 -0
- package/dist/runtime/planning-extra-ops.js.map +1 -1
- package/dist/runtime/preflight.d.ts +32 -0
- package/dist/runtime/preflight.d.ts.map +1 -0
- package/dist/runtime/preflight.js +29 -0
- package/dist/runtime/preflight.js.map +1 -0
- package/dist/runtime/quality-signals.d.ts +6 -1
- package/dist/runtime/quality-signals.d.ts.map +1 -1
- package/dist/runtime/quality-signals.js +41 -5
- package/dist/runtime/quality-signals.js.map +1 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +33 -2
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/types.d.ts +27 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/skills/step-tracker.d.ts +39 -0
- package/dist/skills/step-tracker.d.ts.map +1 -0
- package/dist/skills/step-tracker.js +105 -0
- package/dist/skills/step-tracker.js.map +1 -0
- package/dist/skills/sync-skills.d.ts +3 -2
- package/dist/skills/sync-skills.d.ts.map +1 -1
- package/dist/skills/sync-skills.js +42 -8
- package/dist/skills/sync-skills.js.map +1 -1
- package/dist/subagent/dispatcher.d.ts +4 -3
- package/dist/subagent/dispatcher.d.ts.map +1 -1
- package/dist/subagent/dispatcher.js +57 -35
- package/dist/subagent/dispatcher.js.map +1 -1
- package/dist/subagent/index.d.ts +1 -0
- package/dist/subagent/index.d.ts.map +1 -1
- package/dist/subagent/index.js.map +1 -1
- package/dist/subagent/orphan-reaper.d.ts +51 -4
- package/dist/subagent/orphan-reaper.d.ts.map +1 -1
- package/dist/subagent/orphan-reaper.js +103 -3
- package/dist/subagent/orphan-reaper.js.map +1 -1
- package/dist/subagent/types.d.ts +7 -0
- package/dist/subagent/types.d.ts.map +1 -1
- package/dist/subagent/workspace-resolver.d.ts +2 -0
- package/dist/subagent/workspace-resolver.d.ts.map +1 -1
- package/dist/subagent/workspace-resolver.js +3 -1
- package/dist/subagent/workspace-resolver.js.map +1 -1
- package/dist/vault/vault-entries.d.ts +18 -0
- package/dist/vault/vault-entries.d.ts.map +1 -1
- package/dist/vault/vault-entries.js +73 -0
- package/dist/vault/vault-entries.js.map +1 -1
- package/dist/vault/vault-manager.d.ts.map +1 -1
- package/dist/vault/vault-manager.js +1 -0
- package/dist/vault/vault-manager.js.map +1 -1
- package/dist/vault/vault-schema.d.ts.map +1 -1
- package/dist/vault/vault-schema.js +14 -0
- package/dist/vault/vault-schema.js.map +1 -1
- package/dist/vault/vault.d.ts +1 -0
- package/dist/vault/vault.d.ts.map +1 -1
- package/dist/vault/vault.js.map +1 -1
- package/package.json +3 -5
- package/src/__tests__/cron-manager.test.ts +132 -0
- package/src/__tests__/deviation-detection.test.ts +234 -0
- package/src/__tests__/embeddings.test.ts +536 -0
- package/src/__tests__/preflight.test.ts +97 -0
- package/src/__tests__/step-persistence.test.ts +324 -0
- package/src/__tests__/step-tracker.test.ts +260 -0
- package/src/__tests__/subagent/dispatcher.test.ts +122 -4
- package/src/__tests__/subagent/orphan-reaper.test.ts +148 -12
- package/src/__tests__/subagent/process-lifecycle.test.ts +422 -0
- package/src/__tests__/subagent/workspace-resolver.test.ts +6 -1
- package/src/adapters/types.ts +2 -0
- package/src/brain/brain.ts +117 -9
- package/src/brain/intelligence.ts +4 -0
- package/src/brain/types.ts +6 -1
- package/src/dream/cron-manager.ts +137 -0
- package/src/dream/dream-engine.ts +119 -0
- package/src/dream/dream-ops.ts +56 -0
- package/src/dream/dream.test.ts +182 -0
- package/src/dream/index.ts +6 -0
- package/src/dream/schema.ts +17 -0
- package/src/embeddings/openai-provider.ts +158 -0
- package/src/embeddings/pipeline.ts +126 -0
- package/src/embeddings/types.ts +67 -0
- package/src/engine/bin/soleri-engine.ts +4 -1
- package/src/engine/module-manifest.test.ts +4 -4
- package/src/engine/module-manifest.ts +20 -0
- package/src/engine/register-engine.ts +12 -0
- package/src/flows/dispatch-registry.ts +44 -1
- package/src/flows/executor.ts +93 -2
- package/src/flows/index.ts +2 -0
- package/src/flows/types.ts +39 -1
- package/src/index.ts +11 -0
- package/src/planning/goal-ancestry.test.ts +3 -5
- package/src/planning/plan-lifecycle.ts +5 -2
- package/src/planning/planner-types.ts +1 -1
- package/src/planning/planner.test.ts +73 -3
- package/src/runtime/admin-ops.test.ts +2 -2
- package/src/runtime/admin-ops.ts +17 -0
- package/src/runtime/admin-setup-ops.ts +2 -2
- package/src/runtime/embedding-ops.ts +116 -0
- package/src/runtime/facades/admin-facade.test.ts +31 -0
- package/src/runtime/facades/embedding-facade.ts +11 -0
- package/src/runtime/facades/index.ts +12 -0
- package/src/runtime/facades/orchestrate-facade.test.ts +16 -0
- package/src/runtime/facades/orchestrate-facade.ts +146 -0
- package/src/runtime/feature-flags.ts +4 -0
- package/src/runtime/orchestrate-ops.test.ts +182 -2
- package/src/runtime/orchestrate-ops.ts +170 -13
- package/src/runtime/planning-extra-ops.ts +77 -0
- package/src/runtime/preflight.ts +53 -0
- package/src/runtime/quality-signals.test.ts +182 -8
- package/src/runtime/quality-signals.ts +44 -5
- package/src/runtime/runtime.ts +41 -2
- package/src/runtime/types.ts +20 -0
- package/src/skills/__tests__/sync-skills.test.ts +132 -0
- package/src/skills/step-tracker.ts +162 -0
- package/src/skills/sync-skills.ts +54 -9
- package/src/subagent/dispatcher.ts +62 -39
- package/src/subagent/index.ts +1 -0
- package/src/subagent/orphan-reaper.test.ts +135 -0
- package/src/subagent/orphan-reaper.ts +130 -7
- package/src/subagent/types.ts +10 -0
- package/src/subagent/workspace-resolver.ts +3 -1
- package/src/vault/vault-entries.ts +112 -0
- package/src/vault/vault-manager.ts +1 -0
- package/src/vault/vault-scaling.test.ts +3 -2
- package/src/vault/vault-schema.ts +15 -0
- package/src/vault/vault.ts +1 -0
- package/vitest.config.ts +2 -1
- package/dist/brain/strength-scorer.d.ts +0 -31
- package/dist/brain/strength-scorer.d.ts.map +0 -1
- package/dist/brain/strength-scorer.js +0 -264
- package/dist/brain/strength-scorer.js.map +0 -1
- package/dist/engine/index.d.ts +0 -21
- package/dist/engine/index.d.ts.map +0 -1
- package/dist/engine/index.js +0 -18
- package/dist/engine/index.js.map +0 -1
- package/dist/hooks/index.d.ts +0 -2
- package/dist/hooks/index.d.ts.map +0 -1
- package/dist/hooks/index.js +0 -2
- package/dist/hooks/index.js.map +0 -1
- package/dist/persona/index.d.ts +0 -5
- package/dist/persona/index.d.ts.map +0 -1
- package/dist/persona/index.js +0 -4
- package/dist/persona/index.js.map +0 -1
- package/dist/vault/vault-interfaces.d.ts +0 -153
- package/dist/vault/vault-interfaces.d.ts.map +0 -1
- package/dist/vault/vault-interfaces.js +0 -2
- 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
|
|
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
|
|
161
|
-
expect(
|
|
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
|
|
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
|
|
55
|
-
expect(reaped).
|
|
56
|
-
expect(
|
|
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
|
|
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
|
|
136
|
-
expect(reaped).
|
|
137
|
-
expect(
|
|
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
|
});
|