@auto-engineer/pipeline 1.65.0 → 1.67.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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +6 -6
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +135 -0
- package/dist/src/builder/define-v2.d.ts +101 -0
- package/dist/src/builder/define-v2.d.ts.map +1 -0
- package/dist/src/builder/define-v2.js +209 -0
- package/dist/src/builder/define-v2.js.map +1 -0
- package/dist/src/engine/command-dispatcher.d.ts +31 -0
- package/dist/src/engine/command-dispatcher.d.ts.map +1 -0
- package/dist/src/engine/command-dispatcher.js +26 -0
- package/dist/src/engine/command-dispatcher.js.map +1 -0
- package/dist/src/engine/event-router.d.ts +21 -0
- package/dist/src/engine/event-router.d.ts.map +1 -0
- package/dist/src/engine/event-router.js +22 -0
- package/dist/src/engine/event-router.js.map +1 -0
- package/dist/src/engine/index.d.ts +15 -0
- package/dist/src/engine/index.d.ts.map +1 -0
- package/dist/src/engine/index.js +15 -0
- package/dist/src/engine/index.js.map +1 -0
- package/dist/src/engine/pipeline-engine.d.ts +37 -0
- package/dist/src/engine/pipeline-engine.d.ts.map +1 -0
- package/dist/src/engine/pipeline-engine.js +53 -0
- package/dist/src/engine/pipeline-engine.js.map +1 -0
- package/dist/src/engine/projections/item-status.d.ts +9 -0
- package/dist/src/engine/projections/item-status.d.ts.map +1 -0
- package/dist/src/engine/projections/item-status.js +9 -0
- package/dist/src/engine/projections/item-status.js.map +1 -0
- package/dist/src/engine/projections/latest-run.d.ts +9 -0
- package/dist/src/engine/projections/latest-run.d.ts.map +1 -0
- package/dist/src/engine/projections/latest-run.js +9 -0
- package/dist/src/engine/projections/latest-run.js.map +1 -0
- package/dist/src/engine/projections/message-log.d.ts +9 -0
- package/dist/src/engine/projections/message-log.d.ts.map +1 -0
- package/dist/src/engine/projections/message-log.js +10 -0
- package/dist/src/engine/projections/message-log.js.map +1 -0
- package/dist/src/engine/projections/node-status.d.ts +9 -0
- package/dist/src/engine/projections/node-status.d.ts.map +1 -0
- package/dist/src/engine/projections/node-status.js +9 -0
- package/dist/src/engine/projections/node-status.js.map +1 -0
- package/dist/src/engine/projections/stats.d.ts +9 -0
- package/dist/src/engine/projections/stats.d.ts.map +1 -0
- package/dist/src/engine/projections/stats.js +9 -0
- package/dist/src/engine/projections/stats.js.map +1 -0
- package/dist/src/engine/sqlite-consumer.d.ts +11 -0
- package/dist/src/engine/sqlite-consumer.d.ts.map +1 -0
- package/dist/src/engine/sqlite-consumer.js +27 -0
- package/dist/src/engine/sqlite-consumer.js.map +1 -0
- package/dist/src/engine/sqlite-store.d.ts +10 -0
- package/dist/src/engine/sqlite-store.d.ts.map +1 -0
- package/dist/src/engine/sqlite-store.js +14 -0
- package/dist/src/engine/sqlite-store.js.map +1 -0
- package/dist/src/engine/workflow-processor.d.ts +20 -0
- package/dist/src/engine/workflow-processor.d.ts.map +1 -0
- package/dist/src/engine/workflow-processor.js +36 -0
- package/dist/src/engine/workflow-processor.js.map +1 -0
- package/dist/src/engine/workflows/await-workflow.d.ts +33 -0
- package/dist/src/engine/workflows/await-workflow.d.ts.map +1 -0
- package/dist/src/engine/workflows/await-workflow.js +45 -0
- package/dist/src/engine/workflows/await-workflow.js.map +1 -0
- package/dist/src/engine/workflows/phased-workflow.d.ts +64 -0
- package/dist/src/engine/workflows/phased-workflow.d.ts.map +1 -0
- package/dist/src/engine/workflows/phased-workflow.js +103 -0
- package/dist/src/engine/workflows/phased-workflow.js.map +1 -0
- package/dist/src/engine/workflows/settled-workflow.d.ts +62 -0
- package/dist/src/engine/workflows/settled-workflow.d.ts.map +1 -0
- package/dist/src/engine/workflows/settled-workflow.js +92 -0
- package/dist/src/engine/workflows/settled-workflow.js.map +1 -0
- package/dist/src/graph/types.d.ts +1 -1
- package/dist/src/graph/types.d.ts.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/server/pipeline-server-v2.d.ts +48 -0
- package/dist/src/server/pipeline-server-v2.d.ts.map +1 -0
- package/dist/src/server/pipeline-server-v2.js +61 -0
- package/dist/src/server/pipeline-server-v2.js.map +1 -0
- package/dist/src/server/pipeline-server.d.ts +5 -1
- package/dist/src/server/pipeline-server.d.ts.map +1 -1
- package/dist/src/server/pipeline-server.js +71 -10
- package/dist/src/server/pipeline-server.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/ketchup-plan.md +13 -0
- package/package.json +3 -3
- package/src/builder/define-v2.specs.ts +236 -0
- package/src/builder/define-v2.ts +351 -0
- package/src/engine/command-dispatcher.specs.ts +62 -0
- package/src/engine/command-dispatcher.ts +46 -0
- package/src/engine/event-router.specs.ts +75 -0
- package/src/engine/event-router.ts +36 -0
- package/src/engine/index.ts +39 -0
- package/src/engine/pipeline-engine-e2e.specs.ts +776 -0
- package/src/engine/pipeline-engine.integration.specs.ts +126 -0
- package/src/engine/pipeline-engine.specs.ts +70 -0
- package/src/engine/pipeline-engine.ts +82 -0
- package/src/engine/projections/item-status.ts +11 -0
- package/src/engine/projections/latest-run.ts +10 -0
- package/src/engine/projections/message-log.ts +11 -0
- package/src/engine/projections/node-status.ts +10 -0
- package/src/engine/projections/projections.specs.ts +176 -0
- package/src/engine/projections/stats.ts +10 -0
- package/src/engine/sqlite-consumer.specs.ts +42 -0
- package/src/engine/sqlite-consumer.ts +34 -0
- package/src/engine/sqlite-store.specs.ts +46 -0
- package/src/engine/sqlite-store.ts +21 -0
- package/src/engine/workflow-processor.specs.ts +37 -0
- package/src/engine/workflow-processor.ts +57 -0
- package/src/engine/workflows/await-workflow.specs.ts +104 -0
- package/src/engine/workflows/await-workflow.ts +66 -0
- package/src/engine/workflows/phased-workflow.specs.ts +383 -0
- package/src/engine/workflows/phased-workflow.ts +153 -0
- package/src/engine/workflows/settled-workflow.specs.ts +364 -0
- package/src/engine/workflows/settled-workflow.ts +139 -0
- package/src/graph/types.ts +1 -1
- package/src/index.ts +2 -0
- package/src/server/pipeline-server-v2.specs.ts +91 -0
- package/src/server/pipeline-server-v2.ts +70 -0
- package/src/server/pipeline-server.specs.ts +327 -134
- package/src/server/pipeline-server.ts +77 -11
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { readMessagesBatch, sqliteConnection } from '@event-driven-io/emmett-sqlite';
|
|
5
|
+
import { nanoid } from 'nanoid';
|
|
1
6
|
import { define } from '../builder/define';
|
|
2
7
|
import { PipelineServer } from './pipeline-server';
|
|
3
8
|
|
|
@@ -427,7 +432,7 @@ describe('PipelineServer', () => {
|
|
|
427
432
|
await server.stop();
|
|
428
433
|
});
|
|
429
434
|
|
|
430
|
-
it('should have status from computeSettledStats on settled nodes
|
|
435
|
+
it('should have status from computeSettledStats on settled nodes in current session', async () => {
|
|
431
436
|
const handler = {
|
|
432
437
|
name: 'CheckTests',
|
|
433
438
|
events: ['TestsPassed'],
|
|
@@ -444,7 +449,7 @@ describe('PipelineServer', () => {
|
|
|
444
449
|
server.registerPipeline(pipeline);
|
|
445
450
|
await server.start();
|
|
446
451
|
|
|
447
|
-
|
|
452
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
448
453
|
method: 'POST',
|
|
449
454
|
headers: { 'Content-Type': 'application/json' },
|
|
450
455
|
body: JSON.stringify({ type: 'CheckTests', data: {} }),
|
|
@@ -452,9 +457,7 @@ describe('PipelineServer', () => {
|
|
|
452
457
|
|
|
453
458
|
await new Promise((r) => setTimeout(r, 100));
|
|
454
459
|
|
|
455
|
-
const data = await fetchAs<PipelineResponse>(
|
|
456
|
-
`http://localhost:${server.port}/pipeline?correlationId=${commandResponse.correlationId}`,
|
|
457
|
-
);
|
|
460
|
+
const data = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
458
461
|
const settledNode = data.nodes.find((n) => n.id === 'settled:CheckTests');
|
|
459
462
|
expect(settledNode?.status).toBeDefined();
|
|
460
463
|
expect(settledNode?.pendingCount).toBeDefined();
|
|
@@ -481,7 +484,7 @@ describe('PipelineServer', () => {
|
|
|
481
484
|
server.registerPipeline(pipeline);
|
|
482
485
|
await server.start();
|
|
483
486
|
|
|
484
|
-
|
|
487
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
485
488
|
method: 'POST',
|
|
486
489
|
headers: { 'Content-Type': 'application/json' },
|
|
487
490
|
body: JSON.stringify({ type: 'SlowCmd', data: {} }),
|
|
@@ -489,9 +492,7 @@ describe('PipelineServer', () => {
|
|
|
489
492
|
|
|
490
493
|
await new Promise((r) => setTimeout(r, 50));
|
|
491
494
|
|
|
492
|
-
const data = await fetchAs<PipelineResponse>(
|
|
493
|
-
`http://localhost:${server.port}/pipeline?correlationId=${commandResponse.correlationId}`,
|
|
494
|
-
);
|
|
495
|
+
const data = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
495
496
|
const cmdNode = data.nodes.find((n) => n.id === 'cmd:SlowCmd');
|
|
496
497
|
expect(cmdNode?.status).toBe('running');
|
|
497
498
|
|
|
@@ -511,7 +512,7 @@ describe('PipelineServer', () => {
|
|
|
511
512
|
server.registerPipeline(pipeline);
|
|
512
513
|
await server.start();
|
|
513
514
|
|
|
514
|
-
|
|
515
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
515
516
|
method: 'POST',
|
|
516
517
|
headers: { 'Content-Type': 'application/json' },
|
|
517
518
|
body: JSON.stringify({ type: 'SuccessCmd', data: {} }),
|
|
@@ -519,9 +520,7 @@ describe('PipelineServer', () => {
|
|
|
519
520
|
|
|
520
521
|
await new Promise((r) => setTimeout(r, 100));
|
|
521
522
|
|
|
522
|
-
const data = await fetchAs<PipelineResponse>(
|
|
523
|
-
`http://localhost:${server.port}/pipeline?correlationId=${commandResponse.correlationId}`,
|
|
524
|
-
);
|
|
523
|
+
const data = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
525
524
|
const cmdNode = data.nodes.find((n) => n.id === 'cmd:SuccessCmd');
|
|
526
525
|
expect(cmdNode?.status).toBe('success');
|
|
527
526
|
await server.stop();
|
|
@@ -539,7 +538,7 @@ describe('PipelineServer', () => {
|
|
|
539
538
|
server.registerPipeline(pipeline);
|
|
540
539
|
await server.start();
|
|
541
540
|
|
|
542
|
-
|
|
541
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
543
542
|
method: 'POST',
|
|
544
543
|
headers: { 'Content-Type': 'application/json' },
|
|
545
544
|
body: JSON.stringify({ type: 'FailCmd', data: {} }),
|
|
@@ -547,15 +546,13 @@ describe('PipelineServer', () => {
|
|
|
547
546
|
|
|
548
547
|
await new Promise((r) => setTimeout(r, 100));
|
|
549
548
|
|
|
550
|
-
const data = await fetchAs<PipelineResponse>(
|
|
551
|
-
`http://localhost:${server.port}/pipeline?correlationId=${commandResponse.correlationId}`,
|
|
552
|
-
);
|
|
549
|
+
const data = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
553
550
|
const cmdNode = data.nodes.find((n) => n.id === 'cmd:FailCmd');
|
|
554
551
|
expect(cmdNode?.status).toBe('error');
|
|
555
552
|
await server.stop();
|
|
556
553
|
});
|
|
557
554
|
|
|
558
|
-
it('should broadcast PipelineRunStarted event
|
|
555
|
+
it('should broadcast PipelineRunStarted event once per session on server start', async () => {
|
|
559
556
|
const handler = {
|
|
560
557
|
name: 'StartCmd',
|
|
561
558
|
events: ['Started'],
|
|
@@ -567,7 +564,7 @@ describe('PipelineServer', () => {
|
|
|
567
564
|
server.registerPipeline(pipeline);
|
|
568
565
|
await server.start();
|
|
569
566
|
|
|
570
|
-
|
|
567
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
571
568
|
method: 'POST',
|
|
572
569
|
headers: { 'Content-Type': 'application/json' },
|
|
573
570
|
body: JSON.stringify({ type: 'StartCmd', data: {} }),
|
|
@@ -578,12 +575,9 @@ describe('PipelineServer', () => {
|
|
|
578
575
|
const msgs = await fetchAs<StoredMessage[]>(`http://localhost:${server.port}/messages`);
|
|
579
576
|
const pipelineRunStarted = msgs.find((m) => m.message.type === 'PipelineRunStarted');
|
|
580
577
|
expect(pipelineRunStarted).toBeDefined();
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
);
|
|
584
|
-
expect((pipelineRunStarted?.message as { data?: { triggerCommand?: string } }).data?.triggerCommand).toBe(
|
|
585
|
-
'StartCmd',
|
|
586
|
-
);
|
|
578
|
+
const prsMessage = pipelineRunStarted?.message as { correlationId?: string; data?: { triggerCommand?: string } };
|
|
579
|
+
expect(prsMessage.correlationId).toMatch(/^session-/);
|
|
580
|
+
expect(prsMessage.data?.triggerCommand).toBe('PipelineStarted');
|
|
587
581
|
await server.stop();
|
|
588
582
|
});
|
|
589
583
|
|
|
@@ -626,7 +620,7 @@ describe('PipelineServer', () => {
|
|
|
626
620
|
server.registerPipeline(pipeline);
|
|
627
621
|
await server.start();
|
|
628
622
|
|
|
629
|
-
|
|
623
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
630
624
|
method: 'POST',
|
|
631
625
|
headers: { 'Content-Type': 'application/json' },
|
|
632
626
|
body: JSON.stringify({ type: 'RunCmd', data: {} }),
|
|
@@ -647,7 +641,7 @@ describe('PipelineServer', () => {
|
|
|
647
641
|
expect(runningEvent).toBeDefined();
|
|
648
642
|
expect((runningEvent?.message as NodeStatusChangedMessage).data?.nodeId).toBe('cmd:RunCmd');
|
|
649
643
|
expect((runningEvent?.message as NodeStatusChangedMessage).data?.previousStatus).toBe('idle');
|
|
650
|
-
expect((runningEvent?.message as NodeStatusChangedMessage).correlationId).
|
|
644
|
+
expect((runningEvent?.message as NodeStatusChangedMessage).correlationId).toMatch(/^session-/);
|
|
651
645
|
await server.stop();
|
|
652
646
|
});
|
|
653
647
|
|
|
@@ -663,7 +657,7 @@ describe('PipelineServer', () => {
|
|
|
663
657
|
server.registerPipeline(pipeline);
|
|
664
658
|
await server.start();
|
|
665
659
|
|
|
666
|
-
|
|
660
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
667
661
|
method: 'POST',
|
|
668
662
|
headers: { 'Content-Type': 'application/json' },
|
|
669
663
|
body: JSON.stringify({ type: 'CompleteCmd', data: {} }),
|
|
@@ -684,7 +678,7 @@ describe('PipelineServer', () => {
|
|
|
684
678
|
expect(successEvent).toBeDefined();
|
|
685
679
|
expect((successEvent?.message as NodeStatusChangedMessage).data?.nodeId).toBe('cmd:CompleteCmd');
|
|
686
680
|
expect((successEvent?.message as NodeStatusChangedMessage).data?.previousStatus).toBe('running');
|
|
687
|
-
expect((successEvent?.message as NodeStatusChangedMessage).correlationId).
|
|
681
|
+
expect((successEvent?.message as NodeStatusChangedMessage).correlationId).toMatch(/^session-/);
|
|
688
682
|
await server.stop();
|
|
689
683
|
});
|
|
690
684
|
|
|
@@ -700,7 +694,7 @@ describe('PipelineServer', () => {
|
|
|
700
694
|
server.registerPipeline(pipeline);
|
|
701
695
|
await server.start();
|
|
702
696
|
|
|
703
|
-
|
|
697
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
704
698
|
method: 'POST',
|
|
705
699
|
headers: { 'Content-Type': 'application/json' },
|
|
706
700
|
body: JSON.stringify({ type: 'PersistCmd', data: {} }),
|
|
@@ -708,20 +702,16 @@ describe('PipelineServer', () => {
|
|
|
708
702
|
|
|
709
703
|
await new Promise((r) => setTimeout(r, 100));
|
|
710
704
|
|
|
711
|
-
const firstCall = await fetchAs<PipelineResponse>(
|
|
712
|
-
`http://localhost:${server.port}/pipeline?correlationId=${commandResponse.correlationId}`,
|
|
713
|
-
);
|
|
705
|
+
const firstCall = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
714
706
|
expect(firstCall.nodes.find((n) => n.id === 'cmd:PersistCmd')?.status).toBe('success');
|
|
715
707
|
|
|
716
|
-
const secondCall = await fetchAs<PipelineResponse>(
|
|
717
|
-
`http://localhost:${server.port}/pipeline?correlationId=${commandResponse.correlationId}`,
|
|
718
|
-
);
|
|
708
|
+
const secondCall = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
719
709
|
expect(secondCall.nodes.find((n) => n.id === 'cmd:PersistCmd')?.status).toBe('success');
|
|
720
710
|
|
|
721
711
|
await server.stop();
|
|
722
712
|
});
|
|
723
713
|
|
|
724
|
-
it('should track
|
|
714
|
+
it('should track all commands under the same session', async () => {
|
|
725
715
|
const handler = {
|
|
726
716
|
name: 'IndependentCmd',
|
|
727
717
|
events: ['IndependentDone'],
|
|
@@ -731,15 +721,16 @@ describe('PipelineServer', () => {
|
|
|
731
721
|
const server = new PipelineServer({ port: 0 });
|
|
732
722
|
server.registerCommandHandlers([handler]);
|
|
733
723
|
server.registerPipeline(pipeline);
|
|
724
|
+
server.registerItemKeyExtractor('IndependentCmd', (_d) => undefined);
|
|
734
725
|
await server.start();
|
|
735
726
|
|
|
736
|
-
|
|
727
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
737
728
|
method: 'POST',
|
|
738
729
|
headers: { 'Content-Type': 'application/json' },
|
|
739
730
|
body: JSON.stringify({ type: 'IndependentCmd', data: {} }),
|
|
740
731
|
});
|
|
741
732
|
|
|
742
|
-
|
|
733
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
743
734
|
method: 'POST',
|
|
744
735
|
headers: { 'Content-Type': 'application/json' },
|
|
745
736
|
body: JSON.stringify({ type: 'IndependentCmd', data: {} }),
|
|
@@ -747,22 +738,15 @@ describe('PipelineServer', () => {
|
|
|
747
738
|
|
|
748
739
|
await new Promise((r) => setTimeout(r, 100));
|
|
749
740
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
);
|
|
755
|
-
const pipeline2 = await fetchAs<PipelineResponse>(
|
|
756
|
-
`http://localhost:${server.port}/pipeline?correlationId=${run2.correlationId}`,
|
|
757
|
-
);
|
|
758
|
-
|
|
759
|
-
expect(pipeline1.nodes.find((n) => n.id === 'cmd:IndependentCmd')?.status).toBe('success');
|
|
760
|
-
expect(pipeline2.nodes.find((n) => n.id === 'cmd:IndependentCmd')?.status).toBe('success');
|
|
741
|
+
const data = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
742
|
+
const cmdNode = data.nodes.find((n) => n.id === 'cmd:IndependentCmd');
|
|
743
|
+
expect(cmdNode?.status).toBe('success');
|
|
744
|
+
expect(cmdNode?.endedCount).toBe(2);
|
|
761
745
|
|
|
762
746
|
await server.stop();
|
|
763
747
|
});
|
|
764
748
|
|
|
765
|
-
it('should show
|
|
749
|
+
it('should show session status when no correlationId query param provided', async () => {
|
|
766
750
|
const handler = {
|
|
767
751
|
name: 'IdleCmd',
|
|
768
752
|
events: ['IdleDone'],
|
|
@@ -774,7 +758,7 @@ describe('PipelineServer', () => {
|
|
|
774
758
|
server.registerPipeline(pipeline);
|
|
775
759
|
await server.start();
|
|
776
760
|
|
|
777
|
-
await
|
|
761
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
778
762
|
method: 'POST',
|
|
779
763
|
headers: { 'Content-Type': 'application/json' },
|
|
780
764
|
body: JSON.stringify({ type: 'IdleCmd', data: {} }),
|
|
@@ -782,14 +766,14 @@ describe('PipelineServer', () => {
|
|
|
782
766
|
|
|
783
767
|
await new Promise((r) => setTimeout(r, 100));
|
|
784
768
|
|
|
785
|
-
const
|
|
786
|
-
const cmdNode =
|
|
787
|
-
expect(cmdNode?.status).toBe('
|
|
769
|
+
const data = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
770
|
+
const cmdNode = data.nodes.find((n) => n.id === 'cmd:IdleCmd');
|
|
771
|
+
expect(cmdNode?.status).toBe('success');
|
|
788
772
|
|
|
789
773
|
await server.stop();
|
|
790
774
|
});
|
|
791
775
|
|
|
792
|
-
it('should return latestRun
|
|
776
|
+
it('should return latestRun as the session id', async () => {
|
|
793
777
|
const handler = {
|
|
794
778
|
name: 'LatestCmd',
|
|
795
779
|
events: ['LatestDone'],
|
|
@@ -801,7 +785,7 @@ describe('PipelineServer', () => {
|
|
|
801
785
|
server.registerPipeline(pipeline);
|
|
802
786
|
await server.start();
|
|
803
787
|
|
|
804
|
-
|
|
788
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
805
789
|
method: 'POST',
|
|
806
790
|
headers: { 'Content-Type': 'application/json' },
|
|
807
791
|
body: JSON.stringify({ type: 'LatestCmd', data: {} }),
|
|
@@ -809,7 +793,7 @@ describe('PipelineServer', () => {
|
|
|
809
793
|
|
|
810
794
|
await new Promise((r) => setTimeout(r, 50));
|
|
811
795
|
|
|
812
|
-
|
|
796
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
813
797
|
method: 'POST',
|
|
814
798
|
headers: { 'Content-Type': 'application/json' },
|
|
815
799
|
body: JSON.stringify({ type: 'LatestCmd', data: {} }),
|
|
@@ -818,8 +802,7 @@ describe('PipelineServer', () => {
|
|
|
818
802
|
await new Promise((r) => setTimeout(r, 50));
|
|
819
803
|
|
|
820
804
|
const data = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
821
|
-
expect(data.latestRun).
|
|
822
|
-
expect(data.latestRun).not.toBe(run1.correlationId);
|
|
805
|
+
expect(data.latestRun).toMatch(/^session-/);
|
|
823
806
|
|
|
824
807
|
await server.stop();
|
|
825
808
|
});
|
|
@@ -1553,7 +1536,7 @@ describe('PipelineServer', () => {
|
|
|
1553
1536
|
server.registerItemKeyExtractor('ImplementSlice', (d) => (d as { slicePath?: string }).slicePath);
|
|
1554
1537
|
await server.start();
|
|
1555
1538
|
|
|
1556
|
-
|
|
1539
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
1557
1540
|
method: 'POST',
|
|
1558
1541
|
headers: { 'Content-Type': 'application/json' },
|
|
1559
1542
|
body: JSON.stringify({ type: 'ImplementSlice', data: { slicePath: '/server/slice-1' } }),
|
|
@@ -1561,9 +1544,7 @@ describe('PipelineServer', () => {
|
|
|
1561
1544
|
|
|
1562
1545
|
await new Promise((r) => setTimeout(r, 100));
|
|
1563
1546
|
|
|
1564
|
-
const data = await fetchAs<PipelineResponse>(
|
|
1565
|
-
`http://localhost:${server.port}/pipeline?correlationId=${commandResponse.correlationId}`,
|
|
1566
|
-
);
|
|
1547
|
+
const data = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
1567
1548
|
const cmdNode = data.nodes.find((n) => n.id === 'cmd:ImplementSlice');
|
|
1568
1549
|
expect(cmdNode?.pendingCount).toBe(0);
|
|
1569
1550
|
expect(cmdNode?.endedCount).toBe(1);
|
|
@@ -1584,43 +1565,27 @@ describe('PipelineServer', () => {
|
|
|
1584
1565
|
server.registerItemKeyExtractor('ImplementSlice', (d) => (d as { slicePath?: string }).slicePath);
|
|
1585
1566
|
await server.start();
|
|
1586
1567
|
|
|
1587
|
-
const correlationId = `corr-parallel-test`;
|
|
1588
|
-
|
|
1589
1568
|
await Promise.all([
|
|
1590
1569
|
fetch(`http://localhost:${server.port}/command`, {
|
|
1591
1570
|
method: 'POST',
|
|
1592
1571
|
headers: { 'Content-Type': 'application/json' },
|
|
1593
|
-
body: JSON.stringify({
|
|
1594
|
-
type: 'ImplementSlice',
|
|
1595
|
-
data: { slicePath: '/server/slice-1' },
|
|
1596
|
-
correlationId,
|
|
1597
|
-
}),
|
|
1572
|
+
body: JSON.stringify({ type: 'ImplementSlice', data: { slicePath: '/server/slice-1' } }),
|
|
1598
1573
|
}),
|
|
1599
1574
|
fetch(`http://localhost:${server.port}/command`, {
|
|
1600
1575
|
method: 'POST',
|
|
1601
1576
|
headers: { 'Content-Type': 'application/json' },
|
|
1602
|
-
body: JSON.stringify({
|
|
1603
|
-
type: 'ImplementSlice',
|
|
1604
|
-
data: { slicePath: '/server/slice-2' },
|
|
1605
|
-
correlationId,
|
|
1606
|
-
}),
|
|
1577
|
+
body: JSON.stringify({ type: 'ImplementSlice', data: { slicePath: '/server/slice-2' } }),
|
|
1607
1578
|
}),
|
|
1608
1579
|
fetch(`http://localhost:${server.port}/command`, {
|
|
1609
1580
|
method: 'POST',
|
|
1610
1581
|
headers: { 'Content-Type': 'application/json' },
|
|
1611
|
-
body: JSON.stringify({
|
|
1612
|
-
type: 'ImplementSlice',
|
|
1613
|
-
data: { slicePath: '/server/slice-3' },
|
|
1614
|
-
correlationId,
|
|
1615
|
-
}),
|
|
1582
|
+
body: JSON.stringify({ type: 'ImplementSlice', data: { slicePath: '/server/slice-3' } }),
|
|
1616
1583
|
}),
|
|
1617
1584
|
]);
|
|
1618
1585
|
|
|
1619
1586
|
await new Promise((r) => setTimeout(r, 100));
|
|
1620
1587
|
|
|
1621
|
-
const data = await fetchAs<PipelineResponse>(
|
|
1622
|
-
`http://localhost:${server.port}/pipeline?correlationId=${correlationId}`,
|
|
1623
|
-
);
|
|
1588
|
+
const data = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
1624
1589
|
const cmdNode = data.nodes.find((n) => n.id === 'cmd:ImplementSlice');
|
|
1625
1590
|
expect(cmdNode?.pendingCount).toBe(0);
|
|
1626
1591
|
expect(cmdNode?.endedCount).toBe(3);
|
|
@@ -1647,32 +1612,27 @@ describe('PipelineServer', () => {
|
|
|
1647
1612
|
server.registerItemKeyExtractor('SlowSlice', (d) => (d as { id?: string }).id);
|
|
1648
1613
|
await server.start();
|
|
1649
1614
|
|
|
1650
|
-
const correlationId = `corr-slow-test`;
|
|
1651
|
-
|
|
1652
1615
|
void fetch(`http://localhost:${server.port}/command`, {
|
|
1653
1616
|
method: 'POST',
|
|
1654
1617
|
headers: { 'Content-Type': 'application/json' },
|
|
1655
|
-
body: JSON.stringify({ type: 'SlowSlice', data: { id: 'item-1' }
|
|
1618
|
+
body: JSON.stringify({ type: 'SlowSlice', data: { id: 'item-1' } }),
|
|
1656
1619
|
});
|
|
1657
1620
|
void fetch(`http://localhost:${server.port}/command`, {
|
|
1658
1621
|
method: 'POST',
|
|
1659
1622
|
headers: { 'Content-Type': 'application/json' },
|
|
1660
|
-
body: JSON.stringify({ type: 'SlowSlice', data: { id: 'item-2' }
|
|
1623
|
+
body: JSON.stringify({ type: 'SlowSlice', data: { id: 'item-2' } }),
|
|
1661
1624
|
});
|
|
1662
1625
|
void fetch(`http://localhost:${server.port}/command`, {
|
|
1663
1626
|
method: 'POST',
|
|
1664
1627
|
headers: { 'Content-Type': 'application/json' },
|
|
1665
|
-
body: JSON.stringify({ type: 'SlowSlice', data: { id: 'item-3' }
|
|
1628
|
+
body: JSON.stringify({ type: 'SlowSlice', data: { id: 'item-3' } }),
|
|
1666
1629
|
});
|
|
1667
1630
|
|
|
1668
|
-
// Wait for all 3 handlers to start (deterministic instead of timeout)
|
|
1669
1631
|
while (resolveHandlers.length < 3) {
|
|
1670
1632
|
await new Promise((r) => setTimeout(r, 10));
|
|
1671
1633
|
}
|
|
1672
1634
|
|
|
1673
|
-
const midwayData = await fetchAs<PipelineResponse>(
|
|
1674
|
-
`http://localhost:${server.port}/pipeline?correlationId=${correlationId}`,
|
|
1675
|
-
);
|
|
1635
|
+
const midwayData = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
1676
1636
|
const midwayNode = midwayData.nodes.find((n) => n.id === 'cmd:SlowSlice');
|
|
1677
1637
|
expect(midwayNode?.pendingCount).toBe(3);
|
|
1678
1638
|
expect(midwayNode?.endedCount).toBe(0);
|
|
@@ -1681,9 +1641,7 @@ describe('PipelineServer', () => {
|
|
|
1681
1641
|
resolveHandlers.forEach((r) => r());
|
|
1682
1642
|
await new Promise((r) => setTimeout(r, 50));
|
|
1683
1643
|
|
|
1684
|
-
const finalData = await fetchAs<PipelineResponse>(
|
|
1685
|
-
`http://localhost:${server.port}/pipeline?correlationId=${correlationId}`,
|
|
1686
|
-
);
|
|
1644
|
+
const finalData = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
1687
1645
|
const finalNode = finalData.nodes.find((n) => n.id === 'cmd:SlowSlice');
|
|
1688
1646
|
expect(finalNode?.pendingCount).toBe(0);
|
|
1689
1647
|
expect(finalNode?.endedCount).toBe(3);
|
|
@@ -1710,31 +1668,27 @@ describe('PipelineServer', () => {
|
|
|
1710
1668
|
server.registerItemKeyExtractor('MixedSlice', (d) => (d as { id?: string }).id);
|
|
1711
1669
|
await server.start();
|
|
1712
1670
|
|
|
1713
|
-
const correlationId = `corr-mixed-test`;
|
|
1714
|
-
|
|
1715
1671
|
await Promise.all([
|
|
1716
1672
|
fetch(`http://localhost:${server.port}/command`, {
|
|
1717
1673
|
method: 'POST',
|
|
1718
1674
|
headers: { 'Content-Type': 'application/json' },
|
|
1719
|
-
body: JSON.stringify({ type: 'MixedSlice', data: { id: 'pass-1' }
|
|
1675
|
+
body: JSON.stringify({ type: 'MixedSlice', data: { id: 'pass-1' } }),
|
|
1720
1676
|
}),
|
|
1721
1677
|
fetch(`http://localhost:${server.port}/command`, {
|
|
1722
1678
|
method: 'POST',
|
|
1723
1679
|
headers: { 'Content-Type': 'application/json' },
|
|
1724
|
-
body: JSON.stringify({ type: 'MixedSlice', data: { id: 'fail-1', shouldFail: true }
|
|
1680
|
+
body: JSON.stringify({ type: 'MixedSlice', data: { id: 'fail-1', shouldFail: true } }),
|
|
1725
1681
|
}),
|
|
1726
1682
|
fetch(`http://localhost:${server.port}/command`, {
|
|
1727
1683
|
method: 'POST',
|
|
1728
1684
|
headers: { 'Content-Type': 'application/json' },
|
|
1729
|
-
body: JSON.stringify({ type: 'MixedSlice', data: { id: 'pass-2' }
|
|
1685
|
+
body: JSON.stringify({ type: 'MixedSlice', data: { id: 'pass-2' } }),
|
|
1730
1686
|
}),
|
|
1731
1687
|
]);
|
|
1732
1688
|
|
|
1733
1689
|
await new Promise((r) => setTimeout(r, 100));
|
|
1734
1690
|
|
|
1735
|
-
const data = await fetchAs<PipelineResponse>(
|
|
1736
|
-
`http://localhost:${server.port}/pipeline?correlationId=${correlationId}`,
|
|
1737
|
-
);
|
|
1691
|
+
const data = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
1738
1692
|
const cmdNode = data.nodes.find((n) => n.id === 'cmd:MixedSlice');
|
|
1739
1693
|
expect(cmdNode?.pendingCount).toBe(0);
|
|
1740
1694
|
expect(cmdNode?.endedCount).toBe(3);
|
|
@@ -1763,31 +1717,26 @@ describe('PipelineServer', () => {
|
|
|
1763
1717
|
server.registerItemKeyExtractor('RetrySlice', (d) => (d as { slicePath?: string }).slicePath);
|
|
1764
1718
|
await server.start();
|
|
1765
1719
|
|
|
1766
|
-
const correlationId = `corr-retry-test`;
|
|
1767
1720
|
const slicePath = '/server/retry-slice';
|
|
1768
1721
|
|
|
1769
1722
|
await fetch(`http://localhost:${server.port}/command`, {
|
|
1770
1723
|
method: 'POST',
|
|
1771
1724
|
headers: { 'Content-Type': 'application/json' },
|
|
1772
|
-
body: JSON.stringify({ type: 'RetrySlice', data: { slicePath }
|
|
1725
|
+
body: JSON.stringify({ type: 'RetrySlice', data: { slicePath } }),
|
|
1773
1726
|
});
|
|
1774
1727
|
await new Promise((r) => setTimeout(r, 50));
|
|
1775
1728
|
|
|
1776
|
-
const afterFailure = await fetchAs<PipelineResponse>(
|
|
1777
|
-
`http://localhost:${server.port}/pipeline?correlationId=${correlationId}`,
|
|
1778
|
-
);
|
|
1729
|
+
const afterFailure = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
1779
1730
|
expect(afterFailure.nodes.find((n) => n.id === 'cmd:RetrySlice')?.status).toBe('error');
|
|
1780
1731
|
|
|
1781
1732
|
await fetch(`http://localhost:${server.port}/command`, {
|
|
1782
1733
|
method: 'POST',
|
|
1783
1734
|
headers: { 'Content-Type': 'application/json' },
|
|
1784
|
-
body: JSON.stringify({ type: 'RetrySlice', data: { slicePath }
|
|
1735
|
+
body: JSON.stringify({ type: 'RetrySlice', data: { slicePath } }),
|
|
1785
1736
|
});
|
|
1786
1737
|
await new Promise((r) => setTimeout(r, 50));
|
|
1787
1738
|
|
|
1788
|
-
const afterRetry = await fetchAs<PipelineResponse>(
|
|
1789
|
-
`http://localhost:${server.port}/pipeline?correlationId=${correlationId}`,
|
|
1790
|
-
);
|
|
1739
|
+
const afterRetry = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
1791
1740
|
const node = afterRetry.nodes.find((n) => n.id === 'cmd:RetrySlice');
|
|
1792
1741
|
expect(node?.status).toBe('success');
|
|
1793
1742
|
expect(node?.pendingCount).toBe(0);
|
|
@@ -1809,12 +1758,10 @@ describe('PipelineServer', () => {
|
|
|
1809
1758
|
server.registerItemKeyExtractor('CountSlice', (d) => (d as { id?: string }).id);
|
|
1810
1759
|
await server.start();
|
|
1811
1760
|
|
|
1812
|
-
const correlationId = `corr-counts-event-test`;
|
|
1813
|
-
|
|
1814
1761
|
await fetch(`http://localhost:${server.port}/command`, {
|
|
1815
1762
|
method: 'POST',
|
|
1816
1763
|
headers: { 'Content-Type': 'application/json' },
|
|
1817
|
-
body: JSON.stringify({ type: 'CountSlice', data: { id: 'item-1' }
|
|
1764
|
+
body: JSON.stringify({ type: 'CountSlice', data: { id: 'item-1' } }),
|
|
1818
1765
|
});
|
|
1819
1766
|
|
|
1820
1767
|
await new Promise((r) => setTimeout(r, 100));
|
|
@@ -1854,19 +1801,15 @@ describe('PipelineServer', () => {
|
|
|
1854
1801
|
server.registerPipeline(pipeline);
|
|
1855
1802
|
await server.start();
|
|
1856
1803
|
|
|
1857
|
-
const correlationId = `corr-no-extractor-test`;
|
|
1858
|
-
|
|
1859
1804
|
await fetch(`http://localhost:${server.port}/command`, {
|
|
1860
1805
|
method: 'POST',
|
|
1861
1806
|
headers: { 'Content-Type': 'application/json' },
|
|
1862
|
-
body: JSON.stringify({ type: 'NoExtractorCmd', data: {}
|
|
1807
|
+
body: JSON.stringify({ type: 'NoExtractorCmd', data: {} }),
|
|
1863
1808
|
});
|
|
1864
1809
|
|
|
1865
1810
|
await new Promise((r) => setTimeout(r, 100));
|
|
1866
1811
|
|
|
1867
|
-
const data = await fetchAs<PipelineResponse>(
|
|
1868
|
-
`http://localhost:${server.port}/pipeline?correlationId=${correlationId}`,
|
|
1869
|
-
);
|
|
1812
|
+
const data = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
1870
1813
|
const cmdNode = data.nodes.find((n) => n.id === 'cmd:NoExtractorCmd');
|
|
1871
1814
|
expect(cmdNode?.pendingCount).toBe(0);
|
|
1872
1815
|
expect(cmdNode?.endedCount).toBe(1);
|
|
@@ -1875,7 +1818,7 @@ describe('PipelineServer', () => {
|
|
|
1875
1818
|
await server.stop();
|
|
1876
1819
|
});
|
|
1877
1820
|
|
|
1878
|
-
it('should show
|
|
1821
|
+
it('should show zero counts for commands not yet executed in session', async () => {
|
|
1879
1822
|
const handler = {
|
|
1880
1823
|
name: 'IdleCountCmd',
|
|
1881
1824
|
events: ['IdleCountDone'],
|
|
@@ -1889,7 +1832,6 @@ describe('PipelineServer', () => {
|
|
|
1889
1832
|
|
|
1890
1833
|
const data = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
1891
1834
|
const cmdNode = data.nodes.find((n) => n.id === 'cmd:IdleCountCmd');
|
|
1892
|
-
expect(cmdNode?.status).toBe('idle');
|
|
1893
1835
|
expect(cmdNode?.pendingCount).toBe(0);
|
|
1894
1836
|
expect(cmdNode?.endedCount).toBe(0);
|
|
1895
1837
|
|
|
@@ -1915,30 +1857,24 @@ describe('PipelineServer', () => {
|
|
|
1915
1857
|
server.registerPipeline(pipeline);
|
|
1916
1858
|
await server.start();
|
|
1917
1859
|
|
|
1918
|
-
const correlationId = `corr-retry-no-extractor-bug`;
|
|
1919
|
-
|
|
1920
1860
|
await fetch(`http://localhost:${server.port}/command`, {
|
|
1921
1861
|
method: 'POST',
|
|
1922
1862
|
headers: { 'Content-Type': 'application/json' },
|
|
1923
|
-
body: JSON.stringify({ type: 'RetryNoExtractor', data: { targetDir: '/slice1' }
|
|
1863
|
+
body: JSON.stringify({ type: 'RetryNoExtractor', data: { targetDir: '/slice1' } }),
|
|
1924
1864
|
});
|
|
1925
1865
|
await new Promise((r) => setTimeout(r, 50));
|
|
1926
1866
|
|
|
1927
|
-
const afterFailure = await fetchAs<PipelineResponse>(
|
|
1928
|
-
`http://localhost:${server.port}/pipeline?correlationId=${correlationId}`,
|
|
1929
|
-
);
|
|
1867
|
+
const afterFailure = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
1930
1868
|
expect(afterFailure.nodes.find((n) => n.id === 'cmd:RetryNoExtractor')?.status).toBe('error');
|
|
1931
1869
|
|
|
1932
1870
|
await fetch(`http://localhost:${server.port}/command`, {
|
|
1933
1871
|
method: 'POST',
|
|
1934
1872
|
headers: { 'Content-Type': 'application/json' },
|
|
1935
|
-
body: JSON.stringify({ type: 'RetryNoExtractor', data: { targetDir: '/slice1' }
|
|
1873
|
+
body: JSON.stringify({ type: 'RetryNoExtractor', data: { targetDir: '/slice1' } }),
|
|
1936
1874
|
});
|
|
1937
1875
|
await new Promise((r) => setTimeout(r, 50));
|
|
1938
1876
|
|
|
1939
|
-
const afterRetry = await fetchAs<PipelineResponse>(
|
|
1940
|
-
`http://localhost:${server.port}/pipeline?correlationId=${correlationId}`,
|
|
1941
|
-
);
|
|
1877
|
+
const afterRetry = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
1942
1878
|
const node = afterRetry.nodes.find((n) => n.id === 'cmd:RetryNoExtractor');
|
|
1943
1879
|
expect(node?.status).toBe('error');
|
|
1944
1880
|
expect(node?.endedCount).toBe(2);
|
|
@@ -2207,4 +2143,261 @@ describe('PipelineServer', () => {
|
|
|
2207
2143
|
await server.stop();
|
|
2208
2144
|
});
|
|
2209
2145
|
});
|
|
2146
|
+
|
|
2147
|
+
describe('SQLite persistence', () => {
|
|
2148
|
+
it('should persist events to SQLite when storeFileName is set', async () => {
|
|
2149
|
+
const tmpFile = path.join(os.tmpdir(), `pipeline-test-${nanoid()}.db`);
|
|
2150
|
+
const handler = {
|
|
2151
|
+
name: 'TestCmd',
|
|
2152
|
+
events: ['TestDone'],
|
|
2153
|
+
handle: async () => ({ type: 'TestDone', data: {} }),
|
|
2154
|
+
};
|
|
2155
|
+
const pipeline = define('test').on('Trigger').emit('TestCmd', {}).build();
|
|
2156
|
+
const server = new PipelineServer({ port: 0, storeFileName: tmpFile });
|
|
2157
|
+
server.registerCommandHandlers([handler]);
|
|
2158
|
+
server.registerPipeline(pipeline);
|
|
2159
|
+
await server.start();
|
|
2160
|
+
|
|
2161
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
2162
|
+
method: 'POST',
|
|
2163
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2164
|
+
body: JSON.stringify({ type: 'TestCmd', data: {} }),
|
|
2165
|
+
});
|
|
2166
|
+
|
|
2167
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
2168
|
+
await server.stop();
|
|
2169
|
+
|
|
2170
|
+
const connection = sqliteConnection({ fileName: tmpFile });
|
|
2171
|
+
const { messages } = await readMessagesBatch(connection, {
|
|
2172
|
+
after: 0n,
|
|
2173
|
+
batchSize: 1000,
|
|
2174
|
+
});
|
|
2175
|
+
connection.close();
|
|
2176
|
+
|
|
2177
|
+
expect(messages.length).toBeGreaterThan(0);
|
|
2178
|
+
expect(messages.some((m) => m.type === 'CommandDispatched')).toBe(true);
|
|
2179
|
+
expect(messages.some((m) => m.type === 'PipelineRunStarted')).toBe(true);
|
|
2180
|
+
|
|
2181
|
+
fs.unlinkSync(tmpFile);
|
|
2182
|
+
});
|
|
2183
|
+
|
|
2184
|
+
it('should restore pipeline state after server restart', async () => {
|
|
2185
|
+
const tmpFile = path.join(os.tmpdir(), `pipeline-test-${nanoid()}.db`);
|
|
2186
|
+
const handler = {
|
|
2187
|
+
name: 'RestartCmd',
|
|
2188
|
+
events: ['RestartDone'],
|
|
2189
|
+
handle: async () => ({ type: 'RestartDone', data: {} }),
|
|
2190
|
+
};
|
|
2191
|
+
const pipeline = define('test').on('Trigger').emit('RestartCmd', {}).build();
|
|
2192
|
+
|
|
2193
|
+
const server1 = new PipelineServer({ port: 0, storeFileName: tmpFile });
|
|
2194
|
+
server1.registerCommandHandlers([handler]);
|
|
2195
|
+
server1.registerPipeline(pipeline);
|
|
2196
|
+
await server1.start();
|
|
2197
|
+
|
|
2198
|
+
const beforeRestart = await fetchAs<PipelineResponse>(`http://localhost:${server1.port}/pipeline`);
|
|
2199
|
+
const session1 = beforeRestart.latestRun!;
|
|
2200
|
+
|
|
2201
|
+
await fetch(`http://localhost:${server1.port}/command`, {
|
|
2202
|
+
method: 'POST',
|
|
2203
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2204
|
+
body: JSON.stringify({ type: 'RestartCmd', data: {} }),
|
|
2205
|
+
});
|
|
2206
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
2207
|
+
|
|
2208
|
+
const beforeStop = await fetchAs<PipelineResponse>(`http://localhost:${server1.port}/pipeline`);
|
|
2209
|
+
expect(beforeStop.nodes.find((n) => n.id === 'cmd:RestartCmd')?.status).toBe('success');
|
|
2210
|
+
|
|
2211
|
+
await server1.stop();
|
|
2212
|
+
|
|
2213
|
+
const server2 = new PipelineServer({ port: 0, storeFileName: tmpFile });
|
|
2214
|
+
server2.registerCommandHandlers([handler]);
|
|
2215
|
+
server2.registerPipeline(pipeline);
|
|
2216
|
+
await server2.start();
|
|
2217
|
+
|
|
2218
|
+
const afterRestart = await fetchAs<PipelineResponse>(
|
|
2219
|
+
`http://localhost:${server2.port}/pipeline?correlationId=${session1}`,
|
|
2220
|
+
);
|
|
2221
|
+
expect(afterRestart.nodes.find((n) => n.id === 'cmd:RestartCmd')?.status).toBe('success');
|
|
2222
|
+
expect(afterRestart.nodes.find((n) => n.id === 'cmd:RestartCmd')?.endedCount).toBe(1);
|
|
2223
|
+
|
|
2224
|
+
await server2.stop();
|
|
2225
|
+
fs.unlinkSync(tmpFile);
|
|
2226
|
+
});
|
|
2227
|
+
|
|
2228
|
+
it('should create new session on restart while old session remains queryable', async () => {
|
|
2229
|
+
const tmpFile = path.join(os.tmpdir(), `pipeline-test-${nanoid()}.db`);
|
|
2230
|
+
const handler = {
|
|
2231
|
+
name: 'SessionCmd',
|
|
2232
|
+
events: ['SessionDone'],
|
|
2233
|
+
handle: async () => ({ type: 'SessionDone', data: {} }),
|
|
2234
|
+
};
|
|
2235
|
+
const pipeline = define('test').on('Trigger').emit('SessionCmd', {}).build();
|
|
2236
|
+
|
|
2237
|
+
const server1 = new PipelineServer({ port: 0, storeFileName: tmpFile });
|
|
2238
|
+
server1.registerCommandHandlers([handler]);
|
|
2239
|
+
server1.registerPipeline(pipeline);
|
|
2240
|
+
await server1.start();
|
|
2241
|
+
|
|
2242
|
+
const initial = await fetchAs<PipelineResponse>(`http://localhost:${server1.port}/pipeline`);
|
|
2243
|
+
const session1 = initial.latestRun!;
|
|
2244
|
+
|
|
2245
|
+
await fetch(`http://localhost:${server1.port}/command`, {
|
|
2246
|
+
method: 'POST',
|
|
2247
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2248
|
+
body: JSON.stringify({ type: 'SessionCmd', data: {} }),
|
|
2249
|
+
});
|
|
2250
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
2251
|
+
|
|
2252
|
+
await server1.stop();
|
|
2253
|
+
|
|
2254
|
+
const server2 = new PipelineServer({ port: 0, storeFileName: tmpFile });
|
|
2255
|
+
server2.registerCommandHandlers([handler]);
|
|
2256
|
+
server2.registerPipeline(pipeline);
|
|
2257
|
+
await server2.start();
|
|
2258
|
+
|
|
2259
|
+
const restored = await fetchAs<PipelineResponse>(`http://localhost:${server2.port}/pipeline`);
|
|
2260
|
+
expect(restored.latestRun).toBe(session1);
|
|
2261
|
+
|
|
2262
|
+
await fetch(`http://localhost:${server2.port}/command`, {
|
|
2263
|
+
method: 'POST',
|
|
2264
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2265
|
+
body: JSON.stringify({ type: 'RestartPipeline', data: {} }),
|
|
2266
|
+
});
|
|
2267
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
2268
|
+
|
|
2269
|
+
const afterNewSession = await fetchAs<PipelineResponse>(`http://localhost:${server2.port}/pipeline`);
|
|
2270
|
+
const session2 = afterNewSession.latestRun!;
|
|
2271
|
+
expect(session2).not.toBe(session1);
|
|
2272
|
+
|
|
2273
|
+
const oldSessionData = await fetchAs<PipelineResponse>(
|
|
2274
|
+
`http://localhost:${server2.port}/pipeline?correlationId=${session1}`,
|
|
2275
|
+
);
|
|
2276
|
+
expect(oldSessionData.nodes.find((n) => n.id === 'cmd:SessionCmd')?.status).toBe('success');
|
|
2277
|
+
|
|
2278
|
+
const newSessionData = await fetchAs<PipelineResponse>(
|
|
2279
|
+
`http://localhost:${server2.port}/pipeline?correlationId=${session2}`,
|
|
2280
|
+
);
|
|
2281
|
+
expect(newSessionData.nodes.find((n) => n.id === 'cmd:SessionCmd')?.status).toBe('idle');
|
|
2282
|
+
|
|
2283
|
+
await server2.stop();
|
|
2284
|
+
fs.unlinkSync(tmpFile);
|
|
2285
|
+
});
|
|
2286
|
+
});
|
|
2287
|
+
|
|
2288
|
+
describe('RestartPipeline command', () => {
|
|
2289
|
+
it('should emit PipelineRestarted event when RestartPipeline command is sent', async () => {
|
|
2290
|
+
const server = new PipelineServer({ port: 0 });
|
|
2291
|
+
await server.start();
|
|
2292
|
+
|
|
2293
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
2294
|
+
method: 'POST',
|
|
2295
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2296
|
+
body: JSON.stringify({ type: 'RestartPipeline', data: {} }),
|
|
2297
|
+
});
|
|
2298
|
+
|
|
2299
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
2300
|
+
|
|
2301
|
+
const msgs = await fetchAs<StoredMessage[]>(`http://localhost:${server.port}/messages`);
|
|
2302
|
+
const restartedEvent = msgs.find((m) => m.message.type === 'PipelineRestarted');
|
|
2303
|
+
expect(restartedEvent).toBeDefined();
|
|
2304
|
+
|
|
2305
|
+
await server.stop();
|
|
2306
|
+
});
|
|
2307
|
+
});
|
|
2308
|
+
|
|
2309
|
+
describe('session-based status tracking', () => {
|
|
2310
|
+
it('should not overwrite session status when sub-commands use different correlationIds', async () => {
|
|
2311
|
+
const parentHandler = {
|
|
2312
|
+
name: 'ParentCmd',
|
|
2313
|
+
events: ['ParentDone'],
|
|
2314
|
+
handle: async (
|
|
2315
|
+
_cmd: { data: Record<string, unknown> },
|
|
2316
|
+
ctx?: { sendCommand: (type: string, data: unknown, correlationId?: string) => Promise<void> },
|
|
2317
|
+
) => {
|
|
2318
|
+
if (ctx !== undefined) {
|
|
2319
|
+
await ctx.sendCommand('ChildCmd', { index: 0 }, 'sub-0');
|
|
2320
|
+
await ctx.sendCommand('ChildCmd', { index: 1 }, 'sub-1');
|
|
2321
|
+
await ctx.sendCommand('ChildCmd', { index: 2 }, 'sub-2');
|
|
2322
|
+
}
|
|
2323
|
+
return { type: 'ParentDone', data: {} };
|
|
2324
|
+
},
|
|
2325
|
+
};
|
|
2326
|
+
const childHandler = {
|
|
2327
|
+
name: 'ChildCmd',
|
|
2328
|
+
events: ['ChildDone'],
|
|
2329
|
+
handle: async () => ({ type: 'ChildDone', data: {} }),
|
|
2330
|
+
};
|
|
2331
|
+
const pipeline = define('test').on('Trigger').emit('ParentCmd', {}).build();
|
|
2332
|
+
const server = new PipelineServer({ port: 0 });
|
|
2333
|
+
server.registerCommandHandlers([parentHandler, childHandler]);
|
|
2334
|
+
server.registerPipeline(pipeline);
|
|
2335
|
+
await server.start();
|
|
2336
|
+
|
|
2337
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
2338
|
+
method: 'POST',
|
|
2339
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2340
|
+
body: JSON.stringify({ type: 'ParentCmd', data: {} }),
|
|
2341
|
+
});
|
|
2342
|
+
|
|
2343
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
2344
|
+
|
|
2345
|
+
const data = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
2346
|
+
expect(data.latestRun).not.toBe('sub-0');
|
|
2347
|
+
expect(data.latestRun).not.toBe('sub-1');
|
|
2348
|
+
expect(data.latestRun).not.toBe('sub-2');
|
|
2349
|
+
const parentNode = data.nodes.find((n) => n.id === 'cmd:ParentCmd');
|
|
2350
|
+
expect(parentNode?.status).toBe('success');
|
|
2351
|
+
|
|
2352
|
+
await server.stop();
|
|
2353
|
+
});
|
|
2354
|
+
|
|
2355
|
+
it('should count sub-command items under unified session view', async () => {
|
|
2356
|
+
const parentHandler = {
|
|
2357
|
+
name: 'ParentCmd',
|
|
2358
|
+
events: ['ParentDone'],
|
|
2359
|
+
handle: async (
|
|
2360
|
+
_cmd: { data: Record<string, unknown> },
|
|
2361
|
+
ctx?: { sendCommand: (type: string, data: unknown, correlationId?: string) => Promise<void> },
|
|
2362
|
+
) => {
|
|
2363
|
+
if (ctx !== undefined) {
|
|
2364
|
+
await ctx.sendCommand('ChildCmd', { index: 0 }, 'sub-0');
|
|
2365
|
+
await ctx.sendCommand('ChildCmd', { index: 1 }, 'sub-1');
|
|
2366
|
+
await ctx.sendCommand('ChildCmd', { index: 2 }, 'sub-2');
|
|
2367
|
+
}
|
|
2368
|
+
return { type: 'ParentDone', data: {} };
|
|
2369
|
+
},
|
|
2370
|
+
};
|
|
2371
|
+
const childHandler = {
|
|
2372
|
+
name: 'ChildCmd',
|
|
2373
|
+
events: ['ChildDone'],
|
|
2374
|
+
handle: async () => ({ type: 'ChildDone', data: {} }),
|
|
2375
|
+
};
|
|
2376
|
+
const pipeline = define('test')
|
|
2377
|
+
.on('Trigger')
|
|
2378
|
+
.emit('ParentCmd', {})
|
|
2379
|
+
.on('ChildDone')
|
|
2380
|
+
.handle(async () => {})
|
|
2381
|
+
.build();
|
|
2382
|
+
const server = new PipelineServer({ port: 0 });
|
|
2383
|
+
server.registerCommandHandlers([parentHandler, childHandler]);
|
|
2384
|
+
server.registerPipeline(pipeline);
|
|
2385
|
+
server.registerItemKeyExtractor('ChildCmd', (d) => String((d as { index: number }).index));
|
|
2386
|
+
await server.start();
|
|
2387
|
+
|
|
2388
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
2389
|
+
method: 'POST',
|
|
2390
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2391
|
+
body: JSON.stringify({ type: 'ParentCmd', data: {} }),
|
|
2392
|
+
});
|
|
2393
|
+
|
|
2394
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
2395
|
+
|
|
2396
|
+
const data = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
2397
|
+
const childNode = data.nodes.find((n) => n.id === 'cmd:ChildCmd');
|
|
2398
|
+
expect(childNode?.endedCount).toBe(3);
|
|
2399
|
+
|
|
2400
|
+
await server.stop();
|
|
2401
|
+
});
|
|
2402
|
+
});
|
|
2210
2403
|
});
|