@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.
Files changed (120) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +6 -6
  3. package/.turbo/turbo-type-check.log +1 -1
  4. package/CHANGELOG.md +135 -0
  5. package/dist/src/builder/define-v2.d.ts +101 -0
  6. package/dist/src/builder/define-v2.d.ts.map +1 -0
  7. package/dist/src/builder/define-v2.js +209 -0
  8. package/dist/src/builder/define-v2.js.map +1 -0
  9. package/dist/src/engine/command-dispatcher.d.ts +31 -0
  10. package/dist/src/engine/command-dispatcher.d.ts.map +1 -0
  11. package/dist/src/engine/command-dispatcher.js +26 -0
  12. package/dist/src/engine/command-dispatcher.js.map +1 -0
  13. package/dist/src/engine/event-router.d.ts +21 -0
  14. package/dist/src/engine/event-router.d.ts.map +1 -0
  15. package/dist/src/engine/event-router.js +22 -0
  16. package/dist/src/engine/event-router.js.map +1 -0
  17. package/dist/src/engine/index.d.ts +15 -0
  18. package/dist/src/engine/index.d.ts.map +1 -0
  19. package/dist/src/engine/index.js +15 -0
  20. package/dist/src/engine/index.js.map +1 -0
  21. package/dist/src/engine/pipeline-engine.d.ts +37 -0
  22. package/dist/src/engine/pipeline-engine.d.ts.map +1 -0
  23. package/dist/src/engine/pipeline-engine.js +53 -0
  24. package/dist/src/engine/pipeline-engine.js.map +1 -0
  25. package/dist/src/engine/projections/item-status.d.ts +9 -0
  26. package/dist/src/engine/projections/item-status.d.ts.map +1 -0
  27. package/dist/src/engine/projections/item-status.js +9 -0
  28. package/dist/src/engine/projections/item-status.js.map +1 -0
  29. package/dist/src/engine/projections/latest-run.d.ts +9 -0
  30. package/dist/src/engine/projections/latest-run.d.ts.map +1 -0
  31. package/dist/src/engine/projections/latest-run.js +9 -0
  32. package/dist/src/engine/projections/latest-run.js.map +1 -0
  33. package/dist/src/engine/projections/message-log.d.ts +9 -0
  34. package/dist/src/engine/projections/message-log.d.ts.map +1 -0
  35. package/dist/src/engine/projections/message-log.js +10 -0
  36. package/dist/src/engine/projections/message-log.js.map +1 -0
  37. package/dist/src/engine/projections/node-status.d.ts +9 -0
  38. package/dist/src/engine/projections/node-status.d.ts.map +1 -0
  39. package/dist/src/engine/projections/node-status.js +9 -0
  40. package/dist/src/engine/projections/node-status.js.map +1 -0
  41. package/dist/src/engine/projections/stats.d.ts +9 -0
  42. package/dist/src/engine/projections/stats.d.ts.map +1 -0
  43. package/dist/src/engine/projections/stats.js +9 -0
  44. package/dist/src/engine/projections/stats.js.map +1 -0
  45. package/dist/src/engine/sqlite-consumer.d.ts +11 -0
  46. package/dist/src/engine/sqlite-consumer.d.ts.map +1 -0
  47. package/dist/src/engine/sqlite-consumer.js +27 -0
  48. package/dist/src/engine/sqlite-consumer.js.map +1 -0
  49. package/dist/src/engine/sqlite-store.d.ts +10 -0
  50. package/dist/src/engine/sqlite-store.d.ts.map +1 -0
  51. package/dist/src/engine/sqlite-store.js +14 -0
  52. package/dist/src/engine/sqlite-store.js.map +1 -0
  53. package/dist/src/engine/workflow-processor.d.ts +20 -0
  54. package/dist/src/engine/workflow-processor.d.ts.map +1 -0
  55. package/dist/src/engine/workflow-processor.js +36 -0
  56. package/dist/src/engine/workflow-processor.js.map +1 -0
  57. package/dist/src/engine/workflows/await-workflow.d.ts +33 -0
  58. package/dist/src/engine/workflows/await-workflow.d.ts.map +1 -0
  59. package/dist/src/engine/workflows/await-workflow.js +45 -0
  60. package/dist/src/engine/workflows/await-workflow.js.map +1 -0
  61. package/dist/src/engine/workflows/phased-workflow.d.ts +64 -0
  62. package/dist/src/engine/workflows/phased-workflow.d.ts.map +1 -0
  63. package/dist/src/engine/workflows/phased-workflow.js +103 -0
  64. package/dist/src/engine/workflows/phased-workflow.js.map +1 -0
  65. package/dist/src/engine/workflows/settled-workflow.d.ts +62 -0
  66. package/dist/src/engine/workflows/settled-workflow.d.ts.map +1 -0
  67. package/dist/src/engine/workflows/settled-workflow.js +92 -0
  68. package/dist/src/engine/workflows/settled-workflow.js.map +1 -0
  69. package/dist/src/graph/types.d.ts +1 -1
  70. package/dist/src/graph/types.d.ts.map +1 -1
  71. package/dist/src/index.d.ts +2 -0
  72. package/dist/src/index.d.ts.map +1 -1
  73. package/dist/src/index.js +2 -0
  74. package/dist/src/index.js.map +1 -1
  75. package/dist/src/server/pipeline-server-v2.d.ts +48 -0
  76. package/dist/src/server/pipeline-server-v2.d.ts.map +1 -0
  77. package/dist/src/server/pipeline-server-v2.js +61 -0
  78. package/dist/src/server/pipeline-server-v2.js.map +1 -0
  79. package/dist/src/server/pipeline-server.d.ts +5 -1
  80. package/dist/src/server/pipeline-server.d.ts.map +1 -1
  81. package/dist/src/server/pipeline-server.js +71 -10
  82. package/dist/src/server/pipeline-server.js.map +1 -1
  83. package/dist/tsconfig.tsbuildinfo +1 -1
  84. package/ketchup-plan.md +13 -0
  85. package/package.json +3 -3
  86. package/src/builder/define-v2.specs.ts +236 -0
  87. package/src/builder/define-v2.ts +351 -0
  88. package/src/engine/command-dispatcher.specs.ts +62 -0
  89. package/src/engine/command-dispatcher.ts +46 -0
  90. package/src/engine/event-router.specs.ts +75 -0
  91. package/src/engine/event-router.ts +36 -0
  92. package/src/engine/index.ts +39 -0
  93. package/src/engine/pipeline-engine-e2e.specs.ts +776 -0
  94. package/src/engine/pipeline-engine.integration.specs.ts +126 -0
  95. package/src/engine/pipeline-engine.specs.ts +70 -0
  96. package/src/engine/pipeline-engine.ts +82 -0
  97. package/src/engine/projections/item-status.ts +11 -0
  98. package/src/engine/projections/latest-run.ts +10 -0
  99. package/src/engine/projections/message-log.ts +11 -0
  100. package/src/engine/projections/node-status.ts +10 -0
  101. package/src/engine/projections/projections.specs.ts +176 -0
  102. package/src/engine/projections/stats.ts +10 -0
  103. package/src/engine/sqlite-consumer.specs.ts +42 -0
  104. package/src/engine/sqlite-consumer.ts +34 -0
  105. package/src/engine/sqlite-store.specs.ts +46 -0
  106. package/src/engine/sqlite-store.ts +21 -0
  107. package/src/engine/workflow-processor.specs.ts +37 -0
  108. package/src/engine/workflow-processor.ts +57 -0
  109. package/src/engine/workflows/await-workflow.specs.ts +104 -0
  110. package/src/engine/workflows/await-workflow.ts +66 -0
  111. package/src/engine/workflows/phased-workflow.specs.ts +383 -0
  112. package/src/engine/workflows/phased-workflow.ts +153 -0
  113. package/src/engine/workflows/settled-workflow.specs.ts +364 -0
  114. package/src/engine/workflows/settled-workflow.ts +139 -0
  115. package/src/graph/types.ts +1 -1
  116. package/src/index.ts +2 -0
  117. package/src/server/pipeline-server-v2.specs.ts +91 -0
  118. package/src/server/pipeline-server-v2.ts +70 -0
  119. package/src/server/pipeline-server.specs.ts +327 -134
  120. 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 when correlationId provided', async () => {
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
- const commandResponse = await fetchAs<{ correlationId: string }>(`http://localhost:${server.port}/command`, {
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
- const commandResponse = await fetchAs<{ correlationId: string }>(`http://localhost:${server.port}/command`, {
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
- const commandResponse = await fetchAs<{ correlationId: string }>(`http://localhost:${server.port}/command`, {
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
- const commandResponse = await fetchAs<{ correlationId: string }>(`http://localhost:${server.port}/command`, {
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 when new correlationId is first seen', async () => {
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
- const commandResponse = await fetchAs<{ correlationId: string }>(`http://localhost:${server.port}/command`, {
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
- expect((pipelineRunStarted?.message as { correlationId?: string }).correlationId).toBe(
582
- commandResponse.correlationId,
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
- const commandResponse = await fetchAs<{ correlationId: string }>(`http://localhost:${server.port}/command`, {
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).toBe(commandResponse.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
- const commandResponse = await fetchAs<{ correlationId: string }>(`http://localhost:${server.port}/command`, {
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).toBe(commandResponse.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
- const commandResponse = await fetchAs<{ correlationId: string }>(`http://localhost:${server.port}/command`, {
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 status independently for different correlationIds', async () => {
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
- const run1 = await fetchAs<{ correlationId: string }>(`http://localhost:${server.port}/command`, {
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
- const run2 = await fetchAs<{ correlationId: string }>(`http://localhost:${server.port}/command`, {
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
- expect(run1.correlationId).not.toBe(run2.correlationId);
751
-
752
- const pipeline1 = await fetchAs<PipelineResponse>(
753
- `http://localhost:${server.port}/pipeline?correlationId=${run1.correlationId}`,
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 idle status for all command nodes when no correlationId provided', async () => {
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 fetchAs<{ correlationId: string }>(`http://localhost:${server.port}/command`, {
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 pipelineWithoutCorrelation = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
786
- const cmdNode = pipelineWithoutCorrelation.nodes.find((n) => n.id === 'cmd:IdleCmd');
787
- expect(cmdNode?.status).toBe('idle');
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 with the most recent correlationId', async () => {
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
- const run1 = await fetchAs<{ correlationId: string }>(`http://localhost:${server.port}/command`, {
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
- const run2 = await fetchAs<{ correlationId: string }>(`http://localhost:${server.port}/command`, {
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).toBe(run2.correlationId);
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
- const commandResponse = await fetchAs<{ correlationId: string }>(`http://localhost:${server.port}/command`, {
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' }, correlationId }),
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' }, correlationId }),
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' }, correlationId }),
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' }, correlationId }),
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 }, correlationId }),
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' }, correlationId }),
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 }, correlationId }),
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 }, correlationId }),
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' }, correlationId }),
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: {}, correlationId }),
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 idle status with zero counts when no correlationId provided', async () => {
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' }, correlationId }),
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' }, correlationId }),
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
  });