elsabro 7.0.1 → 7.2.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.
- package/README.md +155 -887
- package/commands/elsabro/execute.md +121 -10
- package/commands/elsabro/party.md +87 -2
- package/commands/elsabro/start.md +9 -3
- package/flow-engine/src/party.js +29 -3
- package/flow-engine/tests/cli.test.js +2 -2
- package/flow-engine/tests/graph.test.js +1 -1
- package/flow-engine/tests/integration.test.js +7 -7
- package/flow-engine/tests/party.test.js +57 -0
- package/flow-engine/tests/runner.test.js +457 -0
- package/flow-engine/tests/skill-install.test.js +374 -0
- package/flows/development-flow.json +42 -5
- package/flows/quick-flow.json +0 -1
- package/hooks/skill-discovery.sh +6 -4
- package/hooks/skill-install.sh +224 -0
- package/package.json +1 -1
- package/references/command-flow.md +25 -20
|
@@ -229,6 +229,463 @@ describe('serializeContext / deserializeContext', () => {
|
|
|
229
229
|
});
|
|
230
230
|
});
|
|
231
231
|
|
|
232
|
+
// ---------- Failure mode flow definitions ----------
|
|
233
|
+
|
|
234
|
+
const flowWithOnError = {
|
|
235
|
+
id: 'onerror_flow',
|
|
236
|
+
version: '1.0.0',
|
|
237
|
+
config: {},
|
|
238
|
+
nodes: [
|
|
239
|
+
{ id: 'start', type: 'entry', runtime_status: 'implemented', next: 'risky_agent' },
|
|
240
|
+
{
|
|
241
|
+
id: 'risky_agent',
|
|
242
|
+
type: 'agent',
|
|
243
|
+
runtime_status: 'implemented',
|
|
244
|
+
agent: 'test-agent',
|
|
245
|
+
inputs: {},
|
|
246
|
+
onError: 'error_exit',
|
|
247
|
+
next: 'success_exit'
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
id: 'success_exit',
|
|
251
|
+
type: 'exit',
|
|
252
|
+
runtime_status: 'implemented',
|
|
253
|
+
status: 'success',
|
|
254
|
+
outputs: { result: 'ok' }
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
id: 'error_exit',
|
|
258
|
+
type: 'exit',
|
|
259
|
+
runtime_status: 'implemented',
|
|
260
|
+
status: 'cancelled',
|
|
261
|
+
outputs: { result: 'handled_error' }
|
|
262
|
+
}
|
|
263
|
+
]
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const flowWithoutOnError = {
|
|
267
|
+
id: 'no_onerror_flow',
|
|
268
|
+
version: '1.0.0',
|
|
269
|
+
config: {},
|
|
270
|
+
nodes: [
|
|
271
|
+
{ id: 'start', type: 'entry', runtime_status: 'implemented', next: 'risky_agent' },
|
|
272
|
+
{
|
|
273
|
+
id: 'risky_agent',
|
|
274
|
+
type: 'agent',
|
|
275
|
+
runtime_status: 'implemented',
|
|
276
|
+
agent: 'test-agent',
|
|
277
|
+
inputs: {},
|
|
278
|
+
next: 'end'
|
|
279
|
+
},
|
|
280
|
+
{ id: 'end', type: 'exit', runtime_status: 'implemented', status: 'success', outputs: {} }
|
|
281
|
+
]
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const flowWithLoop = {
|
|
285
|
+
id: 'loop_flow',
|
|
286
|
+
version: '1.0.0',
|
|
287
|
+
config: {},
|
|
288
|
+
nodes: [
|
|
289
|
+
{ id: 'start', type: 'entry', runtime_status: 'implemented', next: 'loop_node' },
|
|
290
|
+
{
|
|
291
|
+
id: 'loop_node',
|
|
292
|
+
type: 'condition',
|
|
293
|
+
runtime_status: 'implemented',
|
|
294
|
+
condition: '{{inputs.always_true}}',
|
|
295
|
+
true: 'loop_node',
|
|
296
|
+
false: 'end'
|
|
297
|
+
},
|
|
298
|
+
{ id: 'end', type: 'exit', runtime_status: 'implemented', status: 'success', outputs: {} }
|
|
299
|
+
]
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const flowWithParallel = {
|
|
303
|
+
id: 'parallel_flow',
|
|
304
|
+
version: '1.0.0',
|
|
305
|
+
config: {},
|
|
306
|
+
nodes: [
|
|
307
|
+
{ id: 'start', type: 'entry', runtime_status: 'implemented', next: 'par' },
|
|
308
|
+
{
|
|
309
|
+
id: 'par',
|
|
310
|
+
type: 'parallel',
|
|
311
|
+
runtime_status: 'implemented',
|
|
312
|
+
branches: [
|
|
313
|
+
{ id: 'b1', agent: 'agent-a', inputs: {} },
|
|
314
|
+
{ id: 'b2', agent: 'agent-b', inputs: {} }
|
|
315
|
+
],
|
|
316
|
+
timeout: 5000,
|
|
317
|
+
next: 'end'
|
|
318
|
+
},
|
|
319
|
+
{ id: 'end', type: 'exit', runtime_status: 'implemented', status: 'success', outputs: {} }
|
|
320
|
+
]
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const flowWithParallelOnError = {
|
|
324
|
+
id: 'parallel_onerror_flow',
|
|
325
|
+
version: '1.0.0',
|
|
326
|
+
config: {},
|
|
327
|
+
nodes: [
|
|
328
|
+
{ id: 'start', type: 'entry', runtime_status: 'implemented', next: 'par' },
|
|
329
|
+
{
|
|
330
|
+
id: 'par',
|
|
331
|
+
type: 'parallel',
|
|
332
|
+
runtime_status: 'implemented',
|
|
333
|
+
branches: [
|
|
334
|
+
{ id: 'b1', agent: 'agent-a', inputs: {} }
|
|
335
|
+
],
|
|
336
|
+
onError: 'error_exit',
|
|
337
|
+
next: 'end'
|
|
338
|
+
},
|
|
339
|
+
{ id: 'end', type: 'exit', runtime_status: 'implemented', status: 'success', outputs: {} },
|
|
340
|
+
{
|
|
341
|
+
id: 'error_exit',
|
|
342
|
+
type: 'exit',
|
|
343
|
+
runtime_status: 'implemented',
|
|
344
|
+
status: 'cancelled',
|
|
345
|
+
outputs: { result: 'parallel_error_handled' }
|
|
346
|
+
}
|
|
347
|
+
]
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const flowWithMaxIterations = {
|
|
351
|
+
id: 'maxiter_flow',
|
|
352
|
+
version: '1.0.0',
|
|
353
|
+
config: {},
|
|
354
|
+
nodes: [
|
|
355
|
+
{ id: 'start', type: 'entry', runtime_status: 'implemented', next: 'par' },
|
|
356
|
+
{
|
|
357
|
+
id: 'par',
|
|
358
|
+
type: 'parallel',
|
|
359
|
+
runtime_status: 'implemented',
|
|
360
|
+
branches: [{ id: 'b1', agent: 'agent-a', inputs: {} }],
|
|
361
|
+
maxIterations: 1,
|
|
362
|
+
next: 'end'
|
|
363
|
+
},
|
|
364
|
+
{ id: 'end', type: 'exit', runtime_status: 'implemented', status: 'success', outputs: {} }
|
|
365
|
+
]
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const flowWithMaxIterationsHandler = {
|
|
369
|
+
id: 'maxiter_handler_flow',
|
|
370
|
+
version: '1.0.0',
|
|
371
|
+
config: {},
|
|
372
|
+
nodes: [
|
|
373
|
+
{ id: 'start', type: 'entry', runtime_status: 'implemented', next: 'par' },
|
|
374
|
+
{
|
|
375
|
+
id: 'par',
|
|
376
|
+
type: 'parallel',
|
|
377
|
+
runtime_status: 'implemented',
|
|
378
|
+
branches: [{ id: 'b1', agent: 'agent-a', inputs: {} }],
|
|
379
|
+
maxIterations: 1,
|
|
380
|
+
onMaxIterations: 'limit_exit',
|
|
381
|
+
next: 'end'
|
|
382
|
+
},
|
|
383
|
+
{ id: 'end', type: 'exit', runtime_status: 'implemented', status: 'success', outputs: {} },
|
|
384
|
+
{
|
|
385
|
+
id: 'limit_exit',
|
|
386
|
+
type: 'exit',
|
|
387
|
+
runtime_status: 'implemented',
|
|
388
|
+
status: 'cancelled',
|
|
389
|
+
outputs: { result: 'max_iterations_reached' }
|
|
390
|
+
}
|
|
391
|
+
]
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// Note: flowWithBadRef not needed — loadFlow validates references at load time.
|
|
395
|
+
// The runner's node-not-found path (L66-69) is tested via graph mutation after load.
|
|
396
|
+
|
|
397
|
+
// ---------- Failure mode tests ----------
|
|
398
|
+
|
|
399
|
+
describe('Failure modes — onError routing', () => {
|
|
400
|
+
it('routes to onError node when executor throws', async () => {
|
|
401
|
+
const engine = new FlowEngine();
|
|
402
|
+
engine.loadFlow(flowWithOnError);
|
|
403
|
+
|
|
404
|
+
const result = await engine.run({}, {
|
|
405
|
+
onAgent: async () => { throw new Error('agent exploded'); }
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
assert.equal(result.success, false);
|
|
409
|
+
assert.equal(result.outputs.result, 'handled_error');
|
|
410
|
+
assert.ok(result.nodesVisited.includes('error_exit'));
|
|
411
|
+
assert.ok(!result.nodesVisited.includes('success_exit'));
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('stores error message in outputs when using onError', async () => {
|
|
415
|
+
const engine = new FlowEngine();
|
|
416
|
+
engine.loadFlow(flowWithOnError);
|
|
417
|
+
|
|
418
|
+
const result = await engine.run({}, {
|
|
419
|
+
onAgent: async () => { throw new Error('kaboom'); }
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// The risky_agent node should have error info in its outputs
|
|
423
|
+
assert.equal(result.context.nodes.risky_agent.outputs.error, 'kaboom');
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it('propagates error when node has no onError', async () => {
|
|
427
|
+
const engine = new FlowEngine();
|
|
428
|
+
engine.loadFlow(flowWithoutOnError);
|
|
429
|
+
|
|
430
|
+
await assert.rejects(
|
|
431
|
+
engine.run({}, {
|
|
432
|
+
onAgent: async () => { throw new Error('unhandled boom'); }
|
|
433
|
+
}),
|
|
434
|
+
(err) => {
|
|
435
|
+
assert.equal(err.message, 'unhandled boom');
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('saves checkpoint before onError redirect', async () => {
|
|
442
|
+
const engine = new FlowEngine();
|
|
443
|
+
engine.loadFlow(flowWithOnError);
|
|
444
|
+
|
|
445
|
+
const checkpoints = [];
|
|
446
|
+
await engine.run({}, {
|
|
447
|
+
onAgent: async () => { throw new Error('checkpoint test'); },
|
|
448
|
+
onCheckpoint: async (data) => checkpoints.push(data)
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// Find checkpoint for risky_agent → should point to error_exit
|
|
452
|
+
const agentCheckpoint = checkpoints.find(cp => cp.currentNode === 'risky_agent');
|
|
453
|
+
assert.ok(agentCheckpoint, 'checkpoint for risky_agent should exist');
|
|
454
|
+
assert.equal(agentCheckpoint.nextNode, 'error_exit');
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
describe('Failure modes — maxNodes loop guard', () => {
|
|
459
|
+
it('throws RunnerError when exceeding 200 node traversals', async () => {
|
|
460
|
+
const engine = new FlowEngine();
|
|
461
|
+
engine.loadFlow(flowWithLoop);
|
|
462
|
+
|
|
463
|
+
await assert.rejects(
|
|
464
|
+
engine.run({ always_true: true }, {}),
|
|
465
|
+
(err) => {
|
|
466
|
+
assert.equal(err.name, 'RunnerError');
|
|
467
|
+
assert.ok(err.message.includes('200'));
|
|
468
|
+
assert.ok(err.message.includes('infinite loop'));
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('RunnerError includes current node id', async () => {
|
|
475
|
+
const engine = new FlowEngine();
|
|
476
|
+
engine.loadFlow(flowWithLoop);
|
|
477
|
+
|
|
478
|
+
await assert.rejects(
|
|
479
|
+
engine.run({ always_true: true }, {}),
|
|
480
|
+
(err) => {
|
|
481
|
+
assert.equal(err.nodeId, 'loop_node');
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it('checkpoints are saved during loop before guard triggers', async () => {
|
|
488
|
+
const engine = new FlowEngine();
|
|
489
|
+
engine.loadFlow(flowWithLoop);
|
|
490
|
+
|
|
491
|
+
const checkpoints = [];
|
|
492
|
+
await assert.rejects(
|
|
493
|
+
engine.run({ always_true: true }, {
|
|
494
|
+
onCheckpoint: async (data) => checkpoints.push(data)
|
|
495
|
+
})
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
// entry (1) + 199 loop iterations before guard fires on 201st attempt = 200 checkpoints
|
|
499
|
+
assert.ok(checkpoints.length >= 200, `expected >= 200 checkpoints, got ${checkpoints.length}`);
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
describe('Failure modes — parallel callback failures', () => {
|
|
504
|
+
it('parallel node with onError routes there when onParallel rejects', async () => {
|
|
505
|
+
const engine = new FlowEngine();
|
|
506
|
+
engine.loadFlow(flowWithParallelOnError);
|
|
507
|
+
|
|
508
|
+
const result = await engine.run({}, {
|
|
509
|
+
onParallel: async () => { throw new Error('parallel failed'); }
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
assert.equal(result.success, false);
|
|
513
|
+
assert.equal(result.outputs.result, 'parallel_error_handled');
|
|
514
|
+
assert.ok(result.nodesVisited.includes('error_exit'));
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it('parallel node without onError propagates callback rejection', async () => {
|
|
518
|
+
const engine = new FlowEngine();
|
|
519
|
+
engine.loadFlow(flowWithParallel);
|
|
520
|
+
|
|
521
|
+
await assert.rejects(
|
|
522
|
+
engine.run({}, {
|
|
523
|
+
onParallel: async () => { throw new Error('unhandled parallel'); }
|
|
524
|
+
}),
|
|
525
|
+
(err) => {
|
|
526
|
+
assert.equal(err.message, 'unhandled parallel');
|
|
527
|
+
return true;
|
|
528
|
+
}
|
|
529
|
+
);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it('timeout value is passed to onParallel callback', async () => {
|
|
533
|
+
const engine = new FlowEngine();
|
|
534
|
+
engine.loadFlow(flowWithParallel);
|
|
535
|
+
|
|
536
|
+
let capturedArgs = null;
|
|
537
|
+
await engine.run({}, {
|
|
538
|
+
onParallel: async (args) => {
|
|
539
|
+
capturedArgs = args;
|
|
540
|
+
return [{ id: 'b1', result: 'ok' }, { id: 'b2', result: 'ok' }];
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
assert.ok(capturedArgs, 'onParallel should have been called');
|
|
545
|
+
assert.equal(capturedArgs.timeout, 5000);
|
|
546
|
+
assert.equal(capturedArgs.nodeId, 'par');
|
|
547
|
+
assert.equal(capturedArgs.joinType, 'all');
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it('parallel node handles missing onParallel gracefully', async () => {
|
|
551
|
+
const engine = new FlowEngine();
|
|
552
|
+
engine.loadFlow(flowWithParallel);
|
|
553
|
+
|
|
554
|
+
// No onParallel callback provided
|
|
555
|
+
const result = await engine.run({}, {});
|
|
556
|
+
|
|
557
|
+
assert.equal(result.success, true);
|
|
558
|
+
// Branches should be marked as skipped
|
|
559
|
+
const parOutputs = result.context.nodes.par.outputs;
|
|
560
|
+
assert.ok(parOutputs.branches, 'branches should exist in outputs');
|
|
561
|
+
assert.equal(parOutputs.branches.b1.skipped, true);
|
|
562
|
+
assert.equal(parOutputs.branches.b2.skipped, true);
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
describe('Failure modes — context serialization', () => {
|
|
567
|
+
it('serializeContext handles circular references gracefully', () => {
|
|
568
|
+
const ctx = {
|
|
569
|
+
inputs: { task: 'test' },
|
|
570
|
+
nodes: {},
|
|
571
|
+
steps: {},
|
|
572
|
+
state: {},
|
|
573
|
+
_iterations: {}
|
|
574
|
+
};
|
|
575
|
+
// Create a circular reference
|
|
576
|
+
ctx.state.self = ctx.state;
|
|
577
|
+
|
|
578
|
+
const serialized = serializeContext(ctx);
|
|
579
|
+
|
|
580
|
+
// state should be null due to circular ref
|
|
581
|
+
assert.equal(serialized.state, null);
|
|
582
|
+
// Other keys should still be fine
|
|
583
|
+
assert.deepEqual(serialized.inputs, { task: 'test' });
|
|
584
|
+
assert.deepEqual(serialized.nodes, {});
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('serializeContext preserves all standard keys', () => {
|
|
588
|
+
const ctx = {
|
|
589
|
+
inputs: { a: 1 },
|
|
590
|
+
nodes: { n1: { outputs: { x: 2 } } },
|
|
591
|
+
steps: { s1: { output: 'data' } },
|
|
592
|
+
state: { flag: true },
|
|
593
|
+
_iterations: { n1: 3 }
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
const serialized = serializeContext(ctx);
|
|
597
|
+
|
|
598
|
+
assert.deepEqual(serialized.inputs, ctx.inputs);
|
|
599
|
+
assert.deepEqual(serialized.nodes, ctx.nodes);
|
|
600
|
+
assert.deepEqual(serialized.steps, ctx.steps);
|
|
601
|
+
assert.deepEqual(serialized.state, ctx.state);
|
|
602
|
+
assert.deepEqual(serialized._iterations, ctx._iterations);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it('serializeContext ignores unknown keys', () => {
|
|
606
|
+
const ctx = {
|
|
607
|
+
inputs: {},
|
|
608
|
+
nodes: {},
|
|
609
|
+
steps: {},
|
|
610
|
+
state: {},
|
|
611
|
+
_iterations: {},
|
|
612
|
+
extraField: 'should be ignored',
|
|
613
|
+
_internal: { secret: true }
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
const serialized = serializeContext(ctx);
|
|
617
|
+
|
|
618
|
+
assert.equal(serialized.extraField, undefined);
|
|
619
|
+
assert.equal(serialized._internal, undefined);
|
|
620
|
+
assert.ok('inputs' in serialized);
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
describe('Failure modes — maxIterations fallback', () => {
|
|
625
|
+
it('parallel node falls through to next when maxIterations reached without handler', async () => {
|
|
626
|
+
const engine = new FlowEngine();
|
|
627
|
+
engine.loadFlow(flowWithMaxIterations);
|
|
628
|
+
|
|
629
|
+
const result = await engine.run({}, {
|
|
630
|
+
onParallel: async () => [{ id: 'b1', result: 'done' }]
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
// Should reach 'end' via node.next even though maxIterations=1 was hit
|
|
634
|
+
assert.equal(result.success, true);
|
|
635
|
+
assert.ok(result.nodesVisited.includes('end'));
|
|
636
|
+
assert.ok(!result.nodesVisited.includes('limit_exit'));
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it('parallel node routes to onMaxIterations when handler defined', async () => {
|
|
640
|
+
const engine = new FlowEngine();
|
|
641
|
+
engine.loadFlow(flowWithMaxIterationsHandler);
|
|
642
|
+
|
|
643
|
+
const result = await engine.run({}, {
|
|
644
|
+
onParallel: async () => [{ id: 'b1', result: 'done' }]
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
// Should route to limit_exit via onMaxIterations
|
|
648
|
+
assert.equal(result.success, false); // limit_exit is cancelled status
|
|
649
|
+
assert.equal(result.outputs.result, 'max_iterations_reached');
|
|
650
|
+
assert.ok(result.nodesVisited.includes('limit_exit'));
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
describe('Failure modes — runner edge cases', () => {
|
|
655
|
+
it('RunnerError thrown for node not found in graph at runtime', async () => {
|
|
656
|
+
const engine = new FlowEngine();
|
|
657
|
+
engine.loadFlow(miniFlow);
|
|
658
|
+
|
|
659
|
+
// Mutate graph after loading: point entry to a non-existent node
|
|
660
|
+
const startNode = engine.graph.nodes.get('start');
|
|
661
|
+
startNode.next = 'ghost_node';
|
|
662
|
+
|
|
663
|
+
await assert.rejects(
|
|
664
|
+
engine.run({ task: 'test', go: true }, {}),
|
|
665
|
+
(err) => {
|
|
666
|
+
assert.equal(err.name, 'RunnerError');
|
|
667
|
+
assert.ok(err.message.includes('ghost_node'));
|
|
668
|
+
assert.ok(err.message.includes('not found'));
|
|
669
|
+
return true;
|
|
670
|
+
}
|
|
671
|
+
);
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it('RunnerError thrown when no flow loaded', async () => {
|
|
675
|
+
const engine = new FlowEngine();
|
|
676
|
+
// Do NOT call loadFlow
|
|
677
|
+
|
|
678
|
+
await assert.rejects(
|
|
679
|
+
engine.run({}, {}),
|
|
680
|
+
(err) => {
|
|
681
|
+
assert.equal(err.name, 'RunnerError');
|
|
682
|
+
assert.ok(err.message.includes('loadFlow'));
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
);
|
|
686
|
+
});
|
|
687
|
+
});
|
|
688
|
+
|
|
232
689
|
describe('CheckpointManager', () => {
|
|
233
690
|
const tmpDir = path.join(os.tmpdir(), `elsabro-test-cp-${Date.now()}`);
|
|
234
691
|
|