@auto-engineer/pipeline 0.15.0 → 0.17.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 (205) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +24 -0
  3. package/dist/src/runtime/phased-executor.d.ts +0 -1
  4. package/dist/src/runtime/phased-executor.d.ts.map +1 -1
  5. package/dist/src/runtime/phased-executor.js +4 -18
  6. package/dist/src/runtime/phased-executor.js.map +1 -1
  7. package/dist/src/runtime/settled-tracker.d.ts +0 -1
  8. package/dist/src/runtime/settled-tracker.d.ts.map +1 -1
  9. package/dist/src/runtime/settled-tracker.js +0 -6
  10. package/dist/src/runtime/settled-tracker.js.map +1 -1
  11. package/dist/src/server/pipeline-server.d.ts.map +1 -1
  12. package/dist/src/server/pipeline-server.js +26 -10
  13. package/dist/src/server/pipeline-server.js.map +1 -1
  14. package/dist/src/server/sse-manager.d.ts +0 -1
  15. package/dist/src/server/sse-manager.d.ts.map +1 -1
  16. package/dist/src/server/sse-manager.js +0 -3
  17. package/dist/src/server/sse-manager.js.map +1 -1
  18. package/dist/src/store/pipeline-read-model.d.ts.map +1 -1
  19. package/dist/src/store/pipeline-read-model.js +3 -4
  20. package/dist/src/store/pipeline-read-model.js.map +1 -1
  21. package/dist/src/store/sqlite-pipeline-event-store.d.ts +14 -0
  22. package/dist/src/store/sqlite-pipeline-event-store.d.ts.map +1 -0
  23. package/dist/src/store/sqlite-pipeline-event-store.js +20 -0
  24. package/dist/src/store/sqlite-pipeline-event-store.js.map +1 -0
  25. package/dist/src/testing/fixtures/kanban.pipeline.d.ts.map +1 -1
  26. package/dist/src/testing/fixtures/kanban.pipeline.js +7 -1
  27. package/dist/src/testing/fixtures/kanban.pipeline.js.map +1 -1
  28. package/dist/tsconfig.tsbuildinfo +1 -1
  29. package/ketchup-plan.md +256 -0
  30. package/package.json +4 -3
  31. package/src/builder/define.specs.ts +2 -1
  32. package/src/config/pipeline-config.specs.ts +32 -0
  33. package/src/graph/filter-graph.specs.ts +27 -1
  34. package/src/graph/types.specs.ts +0 -14
  35. package/src/projections/await-tracker-projection.specs.ts +24 -0
  36. package/src/projections/settled-instance-projection.specs.ts +47 -0
  37. package/src/runtime/phased-executor.ts +5 -15
  38. package/src/runtime/pipeline-runtime.specs.ts +23 -0
  39. package/src/runtime/settled-tracker.ts +1 -9
  40. package/src/server/pipeline-server.specs.ts +315 -0
  41. package/src/server/pipeline-server.ts +36 -13
  42. package/src/server/sse-manager.ts +0 -4
  43. package/src/store/pipeline-event-store.specs.ts +48 -0
  44. package/src/store/pipeline-read-model.specs.ts +203 -0
  45. package/src/store/pipeline-read-model.ts +3 -3
  46. package/src/store/sqlite-pipeline-event-store.specs.ts +13 -0
  47. package/src/store/sqlite-pipeline-event-store.ts +36 -0
  48. package/src/testing/fixtures/kanban.pipeline.ts +9 -2
  49. package/tsconfig.json +1 -1
  50. package/vitest.config.ts +1 -8
  51. package/dist/src/builder/define.specs.d.ts +0 -2
  52. package/dist/src/builder/define.specs.d.ts.map +0 -1
  53. package/dist/src/builder/define.specs.js +0 -435
  54. package/dist/src/builder/define.specs.js.map +0 -1
  55. package/dist/src/core/descriptors.specs.d.ts +0 -2
  56. package/dist/src/core/descriptors.specs.d.ts.map +0 -1
  57. package/dist/src/core/descriptors.specs.js +0 -24
  58. package/dist/src/core/descriptors.specs.js.map +0 -1
  59. package/dist/src/core/types.specs.d.ts +0 -2
  60. package/dist/src/core/types.specs.d.ts.map +0 -1
  61. package/dist/src/core/types.specs.js +0 -40
  62. package/dist/src/core/types.specs.js.map +0 -1
  63. package/dist/src/graph/filter-graph.specs.d.ts +0 -2
  64. package/dist/src/graph/filter-graph.specs.d.ts.map +0 -1
  65. package/dist/src/graph/filter-graph.specs.js +0 -204
  66. package/dist/src/graph/filter-graph.specs.js.map +0 -1
  67. package/dist/src/graph/types.specs.d.ts +0 -2
  68. package/dist/src/graph/types.specs.d.ts.map +0 -1
  69. package/dist/src/graph/types.specs.js +0 -148
  70. package/dist/src/graph/types.specs.js.map +0 -1
  71. package/dist/src/logging/event-logger.specs.d.ts +0 -2
  72. package/dist/src/logging/event-logger.specs.d.ts.map +0 -1
  73. package/dist/src/logging/event-logger.specs.js +0 -81
  74. package/dist/src/logging/event-logger.specs.js.map +0 -1
  75. package/dist/src/plugins/handler-adapter.specs.d.ts +0 -2
  76. package/dist/src/plugins/handler-adapter.specs.d.ts.map +0 -1
  77. package/dist/src/plugins/handler-adapter.specs.js +0 -129
  78. package/dist/src/plugins/handler-adapter.specs.js.map +0 -1
  79. package/dist/src/plugins/plugin-loader.specs.d.ts +0 -2
  80. package/dist/src/plugins/plugin-loader.specs.d.ts.map +0 -1
  81. package/dist/src/plugins/plugin-loader.specs.js +0 -246
  82. package/dist/src/plugins/plugin-loader.specs.js.map +0 -1
  83. package/dist/src/projections/item-status-projection.specs.d.ts +0 -2
  84. package/dist/src/projections/item-status-projection.specs.d.ts.map +0 -1
  85. package/dist/src/projections/item-status-projection.specs.js +0 -119
  86. package/dist/src/projections/item-status-projection.specs.js.map +0 -1
  87. package/dist/src/projections/latest-run-projection.specs.d.ts +0 -2
  88. package/dist/src/projections/latest-run-projection.specs.d.ts.map +0 -1
  89. package/dist/src/projections/latest-run-projection.specs.js +0 -33
  90. package/dist/src/projections/latest-run-projection.specs.js.map +0 -1
  91. package/dist/src/projections/message-log-projection.specs.d.ts +0 -2
  92. package/dist/src/projections/message-log-projection.specs.d.ts.map +0 -1
  93. package/dist/src/projections/message-log-projection.specs.js +0 -101
  94. package/dist/src/projections/message-log-projection.specs.js.map +0 -1
  95. package/dist/src/projections/node-status-projection.specs.d.ts +0 -2
  96. package/dist/src/projections/node-status-projection.specs.d.ts.map +0 -1
  97. package/dist/src/projections/node-status-projection.specs.js +0 -116
  98. package/dist/src/projections/node-status-projection.specs.js.map +0 -1
  99. package/dist/src/projections/phased-execution-projection.specs.d.ts +0 -2
  100. package/dist/src/projections/phased-execution-projection.specs.d.ts.map +0 -1
  101. package/dist/src/projections/phased-execution-projection.specs.js +0 -171
  102. package/dist/src/projections/phased-execution-projection.specs.js.map +0 -1
  103. package/dist/src/projections/settled-instance-projection.specs.d.ts +0 -2
  104. package/dist/src/projections/settled-instance-projection.specs.d.ts.map +0 -1
  105. package/dist/src/projections/settled-instance-projection.specs.js +0 -217
  106. package/dist/src/projections/settled-instance-projection.specs.js.map +0 -1
  107. package/dist/src/projections/stats-projection.specs.d.ts +0 -2
  108. package/dist/src/projections/stats-projection.specs.d.ts.map +0 -1
  109. package/dist/src/projections/stats-projection.specs.js +0 -91
  110. package/dist/src/projections/stats-projection.specs.js.map +0 -1
  111. package/dist/src/runtime/await-tracker.specs.d.ts +0 -2
  112. package/dist/src/runtime/await-tracker.specs.d.ts.map +0 -1
  113. package/dist/src/runtime/await-tracker.specs.js +0 -64
  114. package/dist/src/runtime/await-tracker.specs.js.map +0 -1
  115. package/dist/src/runtime/context.specs.d.ts +0 -2
  116. package/dist/src/runtime/context.specs.d.ts.map +0 -1
  117. package/dist/src/runtime/context.specs.js +0 -26
  118. package/dist/src/runtime/context.specs.js.map +0 -1
  119. package/dist/src/runtime/event-command-map.specs.d.ts +0 -2
  120. package/dist/src/runtime/event-command-map.specs.d.ts.map +0 -1
  121. package/dist/src/runtime/event-command-map.specs.js +0 -108
  122. package/dist/src/runtime/event-command-map.specs.js.map +0 -1
  123. package/dist/src/runtime/phased-executor.specs.d.ts +0 -2
  124. package/dist/src/runtime/phased-executor.specs.d.ts.map +0 -1
  125. package/dist/src/runtime/phased-executor.specs.js +0 -418
  126. package/dist/src/runtime/phased-executor.specs.js.map +0 -1
  127. package/dist/src/runtime/pipeline-runtime.specs.d.ts +0 -2
  128. package/dist/src/runtime/pipeline-runtime.specs.d.ts.map +0 -1
  129. package/dist/src/runtime/pipeline-runtime.specs.js +0 -227
  130. package/dist/src/runtime/pipeline-runtime.specs.js.map +0 -1
  131. package/dist/src/runtime/settled-tracker.specs.d.ts +0 -2
  132. package/dist/src/runtime/settled-tracker.specs.d.ts.map +0 -1
  133. package/dist/src/runtime/settled-tracker.specs.js +0 -811
  134. package/dist/src/runtime/settled-tracker.specs.js.map +0 -1
  135. package/dist/src/server/full-orchestration.e2e.specs.d.ts +0 -2
  136. package/dist/src/server/full-orchestration.e2e.specs.d.ts.map +0 -1
  137. package/dist/src/server/full-orchestration.e2e.specs.js +0 -561
  138. package/dist/src/server/full-orchestration.e2e.specs.js.map +0 -1
  139. package/dist/src/server/pipeline-server.e2e.specs.d.ts +0 -2
  140. package/dist/src/server/pipeline-server.e2e.specs.d.ts.map +0 -1
  141. package/dist/src/server/pipeline-server.e2e.specs.js +0 -373
  142. package/dist/src/server/pipeline-server.e2e.specs.js.map +0 -1
  143. package/dist/src/server/pipeline-server.specs.d.ts +0 -2
  144. package/dist/src/server/pipeline-server.specs.d.ts.map +0 -1
  145. package/dist/src/server/pipeline-server.specs.js +0 -1407
  146. package/dist/src/server/pipeline-server.specs.js.map +0 -1
  147. package/dist/src/server/sse-manager.specs.d.ts +0 -2
  148. package/dist/src/server/sse-manager.specs.d.ts.map +0 -1
  149. package/dist/src/server/sse-manager.specs.js +0 -178
  150. package/dist/src/server/sse-manager.specs.js.map +0 -1
  151. package/dist/src/store/pipeline-event-store.specs.d.ts +0 -2
  152. package/dist/src/store/pipeline-event-store.specs.d.ts.map +0 -1
  153. package/dist/src/store/pipeline-event-store.specs.js +0 -287
  154. package/dist/src/store/pipeline-event-store.specs.js.map +0 -1
  155. package/dist/src/store/pipeline-read-model.specs.d.ts +0 -2
  156. package/dist/src/store/pipeline-read-model.specs.d.ts.map +0 -1
  157. package/dist/src/store/pipeline-read-model.specs.js +0 -830
  158. package/dist/src/store/pipeline-read-model.specs.js.map +0 -1
  159. package/dist/src/testing/event-capture.specs.d.ts +0 -2
  160. package/dist/src/testing/event-capture.specs.d.ts.map +0 -1
  161. package/dist/src/testing/event-capture.specs.js +0 -114
  162. package/dist/src/testing/event-capture.specs.js.map +0 -1
  163. package/dist/src/testing/fixtures/kanban-full.pipeline.specs.d.ts +0 -2
  164. package/dist/src/testing/fixtures/kanban-full.pipeline.specs.d.ts.map +0 -1
  165. package/dist/src/testing/fixtures/kanban-full.pipeline.specs.js +0 -263
  166. package/dist/src/testing/fixtures/kanban-full.pipeline.specs.js.map +0 -1
  167. package/dist/src/testing/fixtures/kanban.pipeline.specs.d.ts +0 -2
  168. package/dist/src/testing/fixtures/kanban.pipeline.specs.d.ts.map +0 -1
  169. package/dist/src/testing/fixtures/kanban.pipeline.specs.js +0 -29
  170. package/dist/src/testing/fixtures/kanban.pipeline.specs.js.map +0 -1
  171. package/dist/src/testing/kanban-todo.e2e.specs.d.ts +0 -2
  172. package/dist/src/testing/kanban-todo.e2e.specs.d.ts.map +0 -1
  173. package/dist/src/testing/kanban-todo.e2e.specs.js +0 -160
  174. package/dist/src/testing/kanban-todo.e2e.specs.js.map +0 -1
  175. package/dist/src/testing/mock-handlers.specs.d.ts +0 -2
  176. package/dist/src/testing/mock-handlers.specs.d.ts.map +0 -1
  177. package/dist/src/testing/mock-handlers.specs.js +0 -193
  178. package/dist/src/testing/mock-handlers.specs.js.map +0 -1
  179. package/dist/src/testing/real-execution.e2e.specs.d.ts +0 -2
  180. package/dist/src/testing/real-execution.e2e.specs.d.ts.map +0 -1
  181. package/dist/src/testing/real-execution.e2e.specs.js +0 -140
  182. package/dist/src/testing/real-execution.e2e.specs.js.map +0 -1
  183. package/dist/src/testing/real-plugin.e2e.specs.d.ts +0 -2
  184. package/dist/src/testing/real-plugin.e2e.specs.d.ts.map +0 -1
  185. package/dist/src/testing/real-plugin.e2e.specs.js +0 -65
  186. package/dist/src/testing/real-plugin.e2e.specs.js.map +0 -1
  187. package/dist/src/testing/server-startup.e2e.specs.d.ts +0 -2
  188. package/dist/src/testing/server-startup.e2e.specs.d.ts.map +0 -1
  189. package/dist/src/testing/server-startup.e2e.specs.js +0 -104
  190. package/dist/src/testing/server-startup.e2e.specs.js.map +0 -1
  191. package/dist/src/testing/snapshot-compare.specs.d.ts +0 -2
  192. package/dist/src/testing/snapshot-compare.specs.d.ts.map +0 -1
  193. package/dist/src/testing/snapshot-compare.specs.js +0 -112
  194. package/dist/src/testing/snapshot-compare.specs.js.map +0 -1
  195. package/dist/src/testing/snapshot-sanitize.specs.d.ts +0 -2
  196. package/dist/src/testing/snapshot-sanitize.specs.d.ts.map +0 -1
  197. package/dist/src/testing/snapshot-sanitize.specs.js +0 -104
  198. package/dist/src/testing/snapshot-sanitize.specs.js.map +0 -1
  199. package/src/core/descriptors.specs.ts +0 -28
  200. package/src/core/types.specs.ts +0 -44
  201. package/src/projections/latest-run-projection.specs.ts +0 -38
  202. package/src/projections/message-log-projection.specs.ts +0 -118
  203. package/src/projections/node-status-projection.specs.ts +0 -127
  204. package/src/projections/stats-projection.specs.ts +0 -105
  205. package/src/runtime/context.specs.ts +0 -28
@@ -157,6 +157,28 @@ describe('PipelineServer', () => {
157
157
  expect(data.folds).toEqual([]);
158
158
  await server.stop();
159
159
  });
160
+
161
+ it('should exclude settled handlers from eventHandlers list', async () => {
162
+ const handler = {
163
+ name: 'CheckTests',
164
+ events: ['TestsPassed'],
165
+ handle: async () => ({ type: 'TestsPassed', data: {} }),
166
+ };
167
+ const pipeline = define('test')
168
+ .on('Start')
169
+ .emit('CheckTests', {})
170
+ .settled(['CheckTests'])
171
+ .dispatch({ dispatches: [] }, () => {})
172
+ .build();
173
+ const server = new PipelineServer({ port: 0 });
174
+ server.registerCommandHandlers([handler]);
175
+ server.registerPipeline(pipeline);
176
+ await server.start();
177
+ const data = await fetchAs<RegistryResponse>(`http://localhost:${server.port}/registry`);
178
+ expect(data.eventHandlers).toContain('Start');
179
+ expect(data.eventHandlers).not.toContain('settled:CheckTests');
180
+ await server.stop();
181
+ });
160
182
  });
161
183
 
162
184
  describe('GET /pipeline', () => {
@@ -319,6 +341,41 @@ describe('PipelineServer', () => {
319
341
  await server.stop();
320
342
  });
321
343
 
344
+ it('should have status from computeSettledStats on settled nodes when correlationId provided', async () => {
345
+ const handler = {
346
+ name: 'CheckTests',
347
+ events: ['TestsPassed'],
348
+ handle: async () => ({ type: 'TestsPassed', data: {} }),
349
+ };
350
+ const pipeline = define('test')
351
+ .on('Start')
352
+ .emit('CheckTests', {})
353
+ .settled(['CheckTests'])
354
+ .dispatch({ dispatches: [] }, () => {})
355
+ .build();
356
+ const server = new PipelineServer({ port: 0 });
357
+ server.registerCommandHandlers([handler]);
358
+ server.registerPipeline(pipeline);
359
+ await server.start();
360
+
361
+ const commandResponse = await fetchAs<{ correlationId: string }>(`http://localhost:${server.port}/command`, {
362
+ method: 'POST',
363
+ headers: { 'Content-Type': 'application/json' },
364
+ body: JSON.stringify({ type: 'CheckTests', data: {} }),
365
+ });
366
+
367
+ await new Promise((r) => setTimeout(r, 100));
368
+
369
+ const data = await fetchAs<PipelineResponse>(
370
+ `http://localhost:${server.port}/pipeline?correlationId=${commandResponse.correlationId}`,
371
+ );
372
+ const settledNode = data.nodes.find((n) => n.id === 'settled:CheckTests');
373
+ expect(settledNode?.status).toBeDefined();
374
+ expect(settledNode?.pendingCount).toBeDefined();
375
+ expect(settledNode?.endedCount).toBeDefined();
376
+ await server.stop();
377
+ });
378
+
322
379
  it('should show running status for command being executed', async () => {
323
380
  let resolveHandler: () => void = () => {};
324
381
  const handlerPromise = new Promise<void>((resolve) => {
@@ -1196,6 +1253,52 @@ describe('PipelineServer', () => {
1196
1253
  await server.stop();
1197
1254
  });
1198
1255
 
1256
+ it('should handle diamond graph patterns when detecting backlinks', async () => {
1257
+ const cmdAHandler = {
1258
+ name: 'CmdA',
1259
+ events: ['EventA'],
1260
+ handle: async () => ({ type: 'EventA', data: {} }),
1261
+ };
1262
+ const cmdBHandler = {
1263
+ name: 'CmdB',
1264
+ events: ['EventB'],
1265
+ handle: async () => ({ type: 'EventB', data: {} }),
1266
+ };
1267
+ const cmdCHandler = {
1268
+ name: 'CmdC',
1269
+ events: ['EventC'],
1270
+ handle: async () => ({ type: 'EventC', data: {} }),
1271
+ };
1272
+ const cmdDHandler = {
1273
+ name: 'CmdD',
1274
+ events: ['EventD'],
1275
+ handle: async () => ({ type: 'EventD', data: {} }),
1276
+ };
1277
+ const pipeline = define('test')
1278
+ .on('Start')
1279
+ .emit('CmdA', {})
1280
+ .on('EventA')
1281
+ .emit('CmdB', {})
1282
+ .on('EventA')
1283
+ .emit('CmdC', {})
1284
+ .on('EventB')
1285
+ .emit('CmdD', {})
1286
+ .on('EventC')
1287
+ .emit('CmdD', {})
1288
+ .on('EventD')
1289
+ .emit('CmdA', {})
1290
+ .build();
1291
+ const server = new PipelineServer({ port: 0 });
1292
+ server.registerCommandHandlers([cmdAHandler, cmdBHandler, cmdCHandler, cmdDHandler]);
1293
+ server.registerPipeline(pipeline);
1294
+ await server.start();
1295
+ const data = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
1296
+ const backLinkEdge = data.edges.find((e) => e.from === 'evt:EventD' && e.to === 'cmd:CmdA');
1297
+ expect(backLinkEdge).toBeDefined();
1298
+ expect(backLinkEdge?.backLink).toBe(true);
1299
+ await server.stop();
1300
+ });
1301
+
1199
1302
  it('should add event nodes from settled handler commandToEvents when not already added', async () => {
1200
1303
  const checkAHandler = {
1201
1304
  name: 'CheckA',
@@ -1641,6 +1744,56 @@ describe('PipelineServer', () => {
1641
1744
 
1642
1745
  await server.stop();
1643
1746
  });
1747
+
1748
+ it('documents behavior: status remains error after retry without itemKey extractor (fix: register extractor)', async () => {
1749
+ let callCount = 0;
1750
+ const handler = {
1751
+ name: 'RetryNoExtractor',
1752
+ events: ['RetryNoExtractorDone', 'RetryNoExtractorFailed'],
1753
+ handle: async () => {
1754
+ callCount++;
1755
+ if (callCount === 1) {
1756
+ return { type: 'RetryNoExtractorFailed', data: {} };
1757
+ }
1758
+ return { type: 'RetryNoExtractorDone', data: {} };
1759
+ },
1760
+ };
1761
+ const pipeline = define('test').on('Trigger').emit('RetryNoExtractor', {}).build();
1762
+ const server = new PipelineServer({ port: 0 });
1763
+ server.registerCommandHandlers([handler]);
1764
+ server.registerPipeline(pipeline);
1765
+ await server.start();
1766
+
1767
+ const correlationId = `corr-retry-no-extractor-bug`;
1768
+
1769
+ await fetch(`http://localhost:${server.port}/command`, {
1770
+ method: 'POST',
1771
+ headers: { 'Content-Type': 'application/json' },
1772
+ body: JSON.stringify({ type: 'RetryNoExtractor', data: { targetDir: '/slice1' }, correlationId }),
1773
+ });
1774
+ await new Promise((r) => setTimeout(r, 50));
1775
+
1776
+ const afterFailure = await fetchAs<PipelineResponse>(
1777
+ `http://localhost:${server.port}/pipeline?correlationId=${correlationId}`,
1778
+ );
1779
+ expect(afterFailure.nodes.find((n) => n.id === 'cmd:RetryNoExtractor')?.status).toBe('error');
1780
+
1781
+ await fetch(`http://localhost:${server.port}/command`, {
1782
+ method: 'POST',
1783
+ headers: { 'Content-Type': 'application/json' },
1784
+ body: JSON.stringify({ type: 'RetryNoExtractor', data: { targetDir: '/slice1' }, correlationId }),
1785
+ });
1786
+ await new Promise((r) => setTimeout(r, 50));
1787
+
1788
+ const afterRetry = await fetchAs<PipelineResponse>(
1789
+ `http://localhost:${server.port}/pipeline?correlationId=${correlationId}`,
1790
+ );
1791
+ const node = afterRetry.nodes.find((n) => n.id === 'cmd:RetryNoExtractor');
1792
+ expect(node?.status).toBe('error');
1793
+ expect(node?.endedCount).toBe(2);
1794
+
1795
+ await server.stop();
1796
+ });
1644
1797
  });
1645
1798
 
1646
1799
  describe('integration', () => {
@@ -1673,4 +1826,166 @@ describe('PipelineServer', () => {
1673
1826
  await server.stop();
1674
1827
  });
1675
1828
  });
1829
+
1830
+ describe('GET /events', () => {
1831
+ it('should accept SSE connections', async () => {
1832
+ const server = new PipelineServer({ port: 0 });
1833
+ await server.start();
1834
+
1835
+ const controller = new AbortController();
1836
+ const responsePromise = fetch(`http://localhost:${server.port}/events`, {
1837
+ signal: controller.signal,
1838
+ });
1839
+
1840
+ await new Promise((r) => setTimeout(r, 50));
1841
+ controller.abort();
1842
+
1843
+ try {
1844
+ await responsePromise;
1845
+ } catch {
1846
+ // AbortError expected
1847
+ }
1848
+
1849
+ await server.stop();
1850
+ });
1851
+
1852
+ it('should accept SSE connections with correlationId filter', async () => {
1853
+ const server = new PipelineServer({ port: 0 });
1854
+ await server.start();
1855
+
1856
+ const controller = new AbortController();
1857
+ const responsePromise = fetch(`http://localhost:${server.port}/events?correlationId=test-123`, {
1858
+ signal: controller.signal,
1859
+ });
1860
+
1861
+ await new Promise((r) => setTimeout(r, 50));
1862
+ controller.abort();
1863
+
1864
+ try {
1865
+ await responsePromise;
1866
+ } catch {
1867
+ // AbortError expected
1868
+ }
1869
+
1870
+ await server.stop();
1871
+ });
1872
+ });
1873
+
1874
+ describe('phased execution', () => {
1875
+ it('should emit phased execution events when foreach-phased handler runs', async () => {
1876
+ type Component = { path: string; priority: 'high' | 'medium' | 'low' };
1877
+ type ComponentEvent = { data: { components: Component[] } };
1878
+ type ResultEvent = { data: { componentPath: string } };
1879
+
1880
+ const generateHandler = {
1881
+ name: 'GenerateComponents',
1882
+ events: ['ComponentsGenerated'],
1883
+ handle: async () => ({
1884
+ type: 'ComponentsGenerated',
1885
+ data: { components: [{ path: '/comp/a.tsx', priority: 'high' }] },
1886
+ }),
1887
+ };
1888
+
1889
+ const implementHandler = {
1890
+ name: 'ImplementComponent',
1891
+ events: ['ComponentImplemented'],
1892
+ handle: async (cmd: { data: { componentPath: string } }) => ({
1893
+ type: 'ComponentImplemented',
1894
+ data: { componentPath: cmd.data.componentPath },
1895
+ }),
1896
+ };
1897
+
1898
+ const pipeline = define('test')
1899
+ .on('ComponentsGenerated')
1900
+ .forEach((e: ComponentEvent) => e.data.components)
1901
+ .groupInto(['high', 'medium', 'low'], (c: Component) => c.priority)
1902
+ .process('ImplementComponent', (c: Component) => ({ componentPath: c.path }))
1903
+ .onComplete({
1904
+ success: 'AllComponentsImplemented',
1905
+ failure: 'ComponentImplementationFailed',
1906
+ itemKey: (e: ResultEvent) => e.data.componentPath,
1907
+ })
1908
+ .build();
1909
+
1910
+ const server = new PipelineServer({ port: 0 });
1911
+ server.registerCommandHandlers([generateHandler, implementHandler]);
1912
+ server.registerPipeline(pipeline);
1913
+ await server.start();
1914
+
1915
+ await fetch(`http://localhost:${server.port}/command`, {
1916
+ method: 'POST',
1917
+ headers: { 'Content-Type': 'application/json' },
1918
+ body: JSON.stringify({ type: 'GenerateComponents', data: {} }),
1919
+ });
1920
+
1921
+ await new Promise((r) => setTimeout(r, 300));
1922
+ await server.stop();
1923
+ });
1924
+ });
1925
+
1926
+ describe('POST /execute', () => {
1927
+ it('should call handler and return event directly', async () => {
1928
+ const handler = {
1929
+ name: 'TestCmd',
1930
+ handle: async () => ({ type: 'TestDone', data: { result: 'success' } }),
1931
+ };
1932
+ const server = new PipelineServer({ port: 0 });
1933
+ server.registerCommandHandlers([handler]);
1934
+ await server.start();
1935
+
1936
+ const response = await fetch(`http://localhost:${server.port}/execute`, {
1937
+ method: 'POST',
1938
+ headers: { 'Content-Type': 'application/json' },
1939
+ body: JSON.stringify({ command: 'TestCmd', payload: { input: 'test' } }),
1940
+ });
1941
+
1942
+ const data = (await response.json()) as { event: string; data: Record<string, unknown> };
1943
+ expect(response.status).toBe(200);
1944
+ expect(data).toEqual({ event: 'TestDone', data: { result: 'success' } });
1945
+
1946
+ await server.stop();
1947
+ });
1948
+
1949
+ it('should return 400 for unknown command', async () => {
1950
+ const server = new PipelineServer({ port: 0 });
1951
+ await server.start();
1952
+
1953
+ const response = await fetch(`http://localhost:${server.port}/execute`, {
1954
+ method: 'POST',
1955
+ headers: { 'Content-Type': 'application/json' },
1956
+ body: JSON.stringify({ command: 'NonExistentCmd', payload: {} }),
1957
+ });
1958
+
1959
+ const data = (await response.json()) as { error: string };
1960
+ expect(response.status).toBe(400);
1961
+ expect(data).toEqual({ error: 'Unknown command: NonExistentCmd' });
1962
+
1963
+ await server.stop();
1964
+ });
1965
+
1966
+ it('should return first event when handler returns array', async () => {
1967
+ const handler = {
1968
+ name: 'MultiEventCmd',
1969
+ handle: async () => [
1970
+ { type: 'FirstEvent', data: { order: 1 } },
1971
+ { type: 'SecondEvent', data: { order: 2 } },
1972
+ ],
1973
+ };
1974
+ const server = new PipelineServer({ port: 0 });
1975
+ server.registerCommandHandlers([handler]);
1976
+ await server.start();
1977
+
1978
+ const response = await fetch(`http://localhost:${server.port}/execute`, {
1979
+ method: 'POST',
1980
+ headers: { 'Content-Type': 'application/json' },
1981
+ body: JSON.stringify({ command: 'MultiEventCmd', payload: {} }),
1982
+ });
1983
+
1984
+ const data = (await response.json()) as { event: string; data: Record<string, unknown> };
1985
+ expect(response.status).toBe(200);
1986
+ expect(data).toEqual({ event: 'FirstEvent', data: { order: 1 } });
1987
+
1988
+ await server.stop();
1989
+ });
1990
+ });
1676
1991
  });
@@ -69,9 +69,11 @@ export class PipelineServer {
69
69
  this.eventCommandMapper = new EventCommandMapper([]);
70
70
  this.settledTracker = new SettledTracker({
71
71
  readModel: this.eventStoreContext.readModel,
72
+ /* v8 ignore next 3 - integration callback tested via settled-tracker.specs.ts */
72
73
  onDispatch: (commandType, data, correlationId) => {
73
74
  void this.dispatchFromSettled(commandType, data, correlationId);
74
75
  },
76
+ /* v8 ignore next 4 - integration callback tested via settled-tracker.specs.ts */
75
77
  onEventEmit: async (event) => {
76
78
  const correlationId = event.data.correlationId;
77
79
  await this.eventStoreContext.eventStore.appendToStream(`pipeline-${correlationId}`, [event]);
@@ -79,12 +81,15 @@ export class PipelineServer {
79
81
  });
80
82
  this.phasedExecutor = new PhasedExecutor({
81
83
  readModel: this.eventStoreContext.readModel,
84
+ /* v8 ignore next 3 - integration callback tested via phased-executor.specs.ts */
82
85
  onDispatch: (commandType, data, correlationId) => {
83
86
  void this.dispatchFromSettled(commandType, data, correlationId);
84
87
  },
88
+ /* v8 ignore next 3 - integration callback tested via phased-executor.specs.ts */
85
89
  onComplete: (event, correlationId) => {
86
90
  void this.handlePhasedComplete(event, correlationId);
87
91
  },
92
+ /* v8 ignore next 5 - integration callback tested via phased-executor.specs.ts */
88
93
  onEventEmit: async (event) => {
89
94
  const data = event.data as Record<string, unknown>;
90
95
  const correlationId =
@@ -289,6 +294,27 @@ export class PipelineServer {
289
294
  const correlationIdFilter = req.query.correlationId as string | undefined;
290
295
  this.sseManager.addClient(clientId, res, correlationIdFilter);
291
296
  });
297
+
298
+ this.app.post('/execute', (req, res) => {
299
+ void (async () => {
300
+ const { command, payload } = req.body as {
301
+ command: string;
302
+ payload: Record<string, unknown>;
303
+ };
304
+
305
+ const handler = this.commandHandlers.get(command);
306
+ if (!handler) {
307
+ res.status(400).json({ error: `Unknown command: ${command}` });
308
+ return;
309
+ }
310
+
311
+ const resultEvent = await handler.handle({ type: command, data: payload });
312
+ const events = Array.isArray(resultEvent) ? resultEvent : [resultEvent];
313
+ const firstEvent = events[0];
314
+
315
+ res.json({ event: firstEvent.type, data: firstEvent.data });
316
+ })();
317
+ });
292
318
  }
293
319
 
294
320
  private buildCombinedGraph(): GraphIR {
@@ -622,14 +648,14 @@ export class PipelineServer {
622
648
  return { excludeTypes, maintainEdges };
623
649
  }
624
650
 
625
- private buildMermaidDiagram(filterOptions?: FilterOptions): string {
651
+ private buildMermaidDiagram(filterOptions: FilterOptions): string {
626
652
  const commandToEvents = this.buildCommandToEvents();
627
653
  const rawGraph = this.buildCombinedGraph();
628
654
  const pipelineEvents = this.extractPipelineEvents(rawGraph, commandToEvents);
629
655
  const graphWithEvents = this.addCommandEventEdgesToGraph(rawGraph, commandToEvents, pipelineEvents);
630
656
  const graphWithEnrichedEvents = this.enrichEventLabels(graphWithEvents);
631
657
  const completeGraph = this.markBackLinks(graphWithEnrichedEvents);
632
- const graph = filterOptions ? filterGraph(completeGraph, filterOptions) : completeGraph;
658
+ const graph = filterGraph(completeGraph, filterOptions);
633
659
  const lines: string[] = ['flowchart LR'];
634
660
 
635
661
  const eventNodes = new Set<string>();
@@ -707,10 +733,7 @@ export class PipelineServer {
707
733
  const queue = [from];
708
734
 
709
735
  while (queue.length > 0) {
710
- const current = queue.shift();
711
- if (current === undefined) {
712
- break;
713
- }
736
+ const current = queue.shift()!;
714
737
  if (current === target) {
715
738
  return true;
716
739
  }
@@ -721,10 +744,7 @@ export class PipelineServer {
721
744
 
722
745
  const neighbors = outgoingEdges.get(current) ?? [];
723
746
  for (const neighbor of neighbors) {
724
- if (neighbor.isBackLink) {
725
- continue;
726
- }
727
- if (!visited.has(neighbor.to)) {
747
+ if (!neighbor.isBackLink && !visited.has(neighbor.to)) {
728
748
  queue.push(neighbor.to);
729
749
  }
730
750
  }
@@ -863,8 +883,7 @@ export class PipelineServer {
863
883
  const events = Array.isArray(resultEvent) ? resultEvent : [resultEvent];
864
884
 
865
885
  const finalStatus = this.getStatusFromEvents(events);
866
- const itemFinalStatus = finalStatus === 'idle' ? 'success' : finalStatus;
867
- await this.updateItemStatus(command.correlationId, command.type, itemKey, itemFinalStatus);
886
+ await this.updateItemStatus(command.correlationId, command.type, itemKey, finalStatus);
868
887
  await this.updateNodeStatus(command.correlationId, command.type, finalStatus);
869
888
 
870
889
  const eventsWithIds: EventWithCorrelation[] = events.map((event) => ({
@@ -893,7 +912,7 @@ export class PipelineServer {
893
912
  await Promise.all(eventsWithIds.map((e) => this.routeEventToPipelines(e)));
894
913
  }
895
914
 
896
- private getStatusFromEvents(events: Event[]): NodeStatus {
915
+ private getStatusFromEvents(events: Event[]): 'success' | 'error' {
897
916
  for (const event of events) {
898
917
  if (event.type.includes('Failed')) {
899
918
  return 'error';
@@ -902,6 +921,7 @@ export class PipelineServer {
902
921
  return 'success';
903
922
  }
904
923
 
924
+ /* v8 ignore next 11 - integration path tested via settled-tracker.specs.ts */
905
925
  private async dispatchFromSettled(commandType: string, data: unknown, correlationId: string): Promise<void> {
906
926
  const requestId = `req-${nanoid()}`;
907
927
  const command: Command & { correlationId: string; requestId: string } = {
@@ -914,6 +934,7 @@ export class PipelineServer {
914
934
  await this.processCommand(command);
915
935
  }
916
936
 
937
+ /* v8 ignore next 10 - integration path tested via phased-executor.specs.ts */
917
938
  private async handlePhasedComplete(event: Event, correlationId: string): Promise<void> {
918
939
  const requestId = `req-${nanoid()}`;
919
940
  const eventWithIds: EventWithCorrelation = {
@@ -956,12 +977,14 @@ export class PipelineServer {
956
977
  await this.emitCommandDispatched(correlationId, requestId, type, data as Record<string, unknown>);
957
978
  await this.processCommand(command);
958
979
  },
980
+ /* v8 ignore next 3 - integration path tested via pipeline-runtime.specs.ts */
959
981
  startPhased: async (handler, event) => {
960
982
  await this.phasedExecutor.startPhased(handler, event, correlationId);
961
983
  },
962
984
  };
963
985
  }
964
986
 
987
+ /* v8 ignore next 10 - integration path tested via phased-executor.specs.ts */
965
988
  private async routeEventToPhasedExecutor(event: EventWithCorrelation): Promise<void> {
966
989
  for (const pipeline of this.pipelines.values()) {
967
990
  for (const handler of pipeline.descriptor.handlers) {
@@ -10,10 +10,6 @@ interface SSEClient {
10
10
  export class SSEManager {
11
11
  private clients = new Map<string, SSEClient>();
12
12
 
13
- get clientCount(): number {
14
- return this.clients.size;
15
- }
16
-
17
13
  addClient(id: string, response: Response, correlationIdFilter?: string): void {
18
14
  response.writeHead(200, {
19
15
  'Content-Type': 'text/event-stream',
@@ -170,6 +170,54 @@ describe('PipelineEventStore', () => {
170
170
  await close();
171
171
  }
172
172
  });
173
+
174
+ it('should track message stats through CommandDispatched and DomainEventEmitted', async () => {
175
+ const { eventStore, readModel, close } = createPipelineEventStore();
176
+ try {
177
+ await eventStore.appendToStream('pipeline-c1', [
178
+ {
179
+ type: 'CommandDispatched',
180
+ data: {
181
+ correlationId: 'c1',
182
+ requestId: 'r1',
183
+ commandType: 'CreateUser',
184
+ commandData: { name: 'Alice' },
185
+ timestamp: new Date(),
186
+ },
187
+ },
188
+ {
189
+ type: 'CommandDispatched',
190
+ data: {
191
+ correlationId: 'c1',
192
+ requestId: 'r2',
193
+ commandType: 'UpdateUser',
194
+ commandData: { name: 'Bob' },
195
+ timestamp: new Date(),
196
+ },
197
+ },
198
+ {
199
+ type: 'DomainEventEmitted',
200
+ data: {
201
+ correlationId: 'c1',
202
+ requestId: 'r1',
203
+ eventType: 'UserCreated',
204
+ eventData: { userId: '123' },
205
+ timestamp: new Date(),
206
+ },
207
+ },
208
+ ]);
209
+
210
+ const stats = await readModel.getStats();
211
+
212
+ expect(stats).toEqual({
213
+ totalMessages: 3,
214
+ totalCommands: 2,
215
+ totalEvents: 1,
216
+ });
217
+ } finally {
218
+ await close();
219
+ }
220
+ });
173
221
  });
174
222
 
175
223
  describe('settled instance projection', () => {