@auto-engineer/pipeline 1.3.0 → 1.3.2

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 (155) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +7 -7
  3. package/.turbo/turbo-type-check.log +1 -1
  4. package/CHANGELOG.md +24 -0
  5. package/LICENSE +10 -0
  6. package/dist/tsconfig.tsbuildinfo +1 -1
  7. package/package.json +14 -15
  8. package/dist/src/builder/define.specs.d.ts +0 -2
  9. package/dist/src/builder/define.specs.d.ts.map +0 -1
  10. package/dist/src/builder/define.specs.js +0 -435
  11. package/dist/src/builder/define.specs.js.map +0 -1
  12. package/dist/src/core/descriptors.specs.d.ts +0 -2
  13. package/dist/src/core/descriptors.specs.d.ts.map +0 -1
  14. package/dist/src/core/descriptors.specs.js +0 -24
  15. package/dist/src/core/descriptors.specs.js.map +0 -1
  16. package/dist/src/core/types.specs.d.ts +0 -2
  17. package/dist/src/core/types.specs.d.ts.map +0 -1
  18. package/dist/src/core/types.specs.js +0 -40
  19. package/dist/src/core/types.specs.js.map +0 -1
  20. package/dist/src/graph/filter-graph.specs.d.ts +0 -2
  21. package/dist/src/graph/filter-graph.specs.d.ts.map +0 -1
  22. package/dist/src/graph/filter-graph.specs.js +0 -204
  23. package/dist/src/graph/filter-graph.specs.js.map +0 -1
  24. package/dist/src/graph/types.specs.d.ts +0 -2
  25. package/dist/src/graph/types.specs.d.ts.map +0 -1
  26. package/dist/src/graph/types.specs.js +0 -148
  27. package/dist/src/graph/types.specs.js.map +0 -1
  28. package/dist/src/logging/event-logger.specs.d.ts +0 -2
  29. package/dist/src/logging/event-logger.specs.d.ts.map +0 -1
  30. package/dist/src/logging/event-logger.specs.js +0 -81
  31. package/dist/src/logging/event-logger.specs.js.map +0 -1
  32. package/dist/src/plugins/handler-adapter.specs.d.ts +0 -2
  33. package/dist/src/plugins/handler-adapter.specs.d.ts.map +0 -1
  34. package/dist/src/plugins/handler-adapter.specs.js +0 -129
  35. package/dist/src/plugins/handler-adapter.specs.js.map +0 -1
  36. package/dist/src/plugins/plugin-loader.specs.d.ts +0 -2
  37. package/dist/src/plugins/plugin-loader.specs.d.ts.map +0 -1
  38. package/dist/src/plugins/plugin-loader.specs.js +0 -246
  39. package/dist/src/plugins/plugin-loader.specs.js.map +0 -1
  40. package/dist/src/projections/item-status-projection.specs.d.ts +0 -2
  41. package/dist/src/projections/item-status-projection.specs.d.ts.map +0 -1
  42. package/dist/src/projections/item-status-projection.specs.js +0 -119
  43. package/dist/src/projections/item-status-projection.specs.js.map +0 -1
  44. package/dist/src/projections/latest-run-projection.specs.d.ts +0 -2
  45. package/dist/src/projections/latest-run-projection.specs.d.ts.map +0 -1
  46. package/dist/src/projections/latest-run-projection.specs.js +0 -33
  47. package/dist/src/projections/latest-run-projection.specs.js.map +0 -1
  48. package/dist/src/projections/message-log-projection.specs.d.ts +0 -2
  49. package/dist/src/projections/message-log-projection.specs.d.ts.map +0 -1
  50. package/dist/src/projections/message-log-projection.specs.js +0 -101
  51. package/dist/src/projections/message-log-projection.specs.js.map +0 -1
  52. package/dist/src/projections/node-status-projection.specs.d.ts +0 -2
  53. package/dist/src/projections/node-status-projection.specs.d.ts.map +0 -1
  54. package/dist/src/projections/node-status-projection.specs.js +0 -116
  55. package/dist/src/projections/node-status-projection.specs.js.map +0 -1
  56. package/dist/src/projections/phased-execution-projection.specs.d.ts +0 -2
  57. package/dist/src/projections/phased-execution-projection.specs.d.ts.map +0 -1
  58. package/dist/src/projections/phased-execution-projection.specs.js +0 -171
  59. package/dist/src/projections/phased-execution-projection.specs.js.map +0 -1
  60. package/dist/src/projections/settled-instance-projection.specs.d.ts +0 -2
  61. package/dist/src/projections/settled-instance-projection.specs.d.ts.map +0 -1
  62. package/dist/src/projections/settled-instance-projection.specs.js +0 -217
  63. package/dist/src/projections/settled-instance-projection.specs.js.map +0 -1
  64. package/dist/src/projections/stats-projection.specs.d.ts +0 -2
  65. package/dist/src/projections/stats-projection.specs.d.ts.map +0 -1
  66. package/dist/src/projections/stats-projection.specs.js +0 -91
  67. package/dist/src/projections/stats-projection.specs.js.map +0 -1
  68. package/dist/src/runtime/await-tracker.specs.d.ts +0 -2
  69. package/dist/src/runtime/await-tracker.specs.d.ts.map +0 -1
  70. package/dist/src/runtime/await-tracker.specs.js +0 -64
  71. package/dist/src/runtime/await-tracker.specs.js.map +0 -1
  72. package/dist/src/runtime/context.specs.d.ts +0 -2
  73. package/dist/src/runtime/context.specs.d.ts.map +0 -1
  74. package/dist/src/runtime/context.specs.js +0 -26
  75. package/dist/src/runtime/context.specs.js.map +0 -1
  76. package/dist/src/runtime/event-command-map.specs.d.ts +0 -2
  77. package/dist/src/runtime/event-command-map.specs.d.ts.map +0 -1
  78. package/dist/src/runtime/event-command-map.specs.js +0 -108
  79. package/dist/src/runtime/event-command-map.specs.js.map +0 -1
  80. package/dist/src/runtime/phased-executor.specs.d.ts +0 -2
  81. package/dist/src/runtime/phased-executor.specs.d.ts.map +0 -1
  82. package/dist/src/runtime/phased-executor.specs.js +0 -418
  83. package/dist/src/runtime/phased-executor.specs.js.map +0 -1
  84. package/dist/src/runtime/pipeline-runtime.specs.d.ts +0 -2
  85. package/dist/src/runtime/pipeline-runtime.specs.d.ts.map +0 -1
  86. package/dist/src/runtime/pipeline-runtime.specs.js +0 -227
  87. package/dist/src/runtime/pipeline-runtime.specs.js.map +0 -1
  88. package/dist/src/runtime/settled-tracker.specs.d.ts +0 -2
  89. package/dist/src/runtime/settled-tracker.specs.d.ts.map +0 -1
  90. package/dist/src/runtime/settled-tracker.specs.js +0 -811
  91. package/dist/src/runtime/settled-tracker.specs.js.map +0 -1
  92. package/dist/src/server/full-orchestration.e2e.specs.d.ts +0 -2
  93. package/dist/src/server/full-orchestration.e2e.specs.d.ts.map +0 -1
  94. package/dist/src/server/full-orchestration.e2e.specs.js +0 -561
  95. package/dist/src/server/full-orchestration.e2e.specs.js.map +0 -1
  96. package/dist/src/server/pipeline-server.e2e.specs.d.ts +0 -2
  97. package/dist/src/server/pipeline-server.e2e.specs.d.ts.map +0 -1
  98. package/dist/src/server/pipeline-server.e2e.specs.js +0 -373
  99. package/dist/src/server/pipeline-server.e2e.specs.js.map +0 -1
  100. package/dist/src/server/pipeline-server.specs.d.ts +0 -2
  101. package/dist/src/server/pipeline-server.specs.d.ts.map +0 -1
  102. package/dist/src/server/pipeline-server.specs.js +0 -1407
  103. package/dist/src/server/pipeline-server.specs.js.map +0 -1
  104. package/dist/src/server/sse-manager.specs.d.ts +0 -2
  105. package/dist/src/server/sse-manager.specs.d.ts.map +0 -1
  106. package/dist/src/server/sse-manager.specs.js +0 -178
  107. package/dist/src/server/sse-manager.specs.js.map +0 -1
  108. package/dist/src/store/pipeline-event-store.specs.d.ts +0 -2
  109. package/dist/src/store/pipeline-event-store.specs.d.ts.map +0 -1
  110. package/dist/src/store/pipeline-event-store.specs.js +0 -287
  111. package/dist/src/store/pipeline-event-store.specs.js.map +0 -1
  112. package/dist/src/store/pipeline-read-model.specs.d.ts +0 -2
  113. package/dist/src/store/pipeline-read-model.specs.d.ts.map +0 -1
  114. package/dist/src/store/pipeline-read-model.specs.js +0 -830
  115. package/dist/src/store/pipeline-read-model.specs.js.map +0 -1
  116. package/dist/src/testing/event-capture.specs.d.ts +0 -2
  117. package/dist/src/testing/event-capture.specs.d.ts.map +0 -1
  118. package/dist/src/testing/event-capture.specs.js +0 -114
  119. package/dist/src/testing/event-capture.specs.js.map +0 -1
  120. package/dist/src/testing/fixtures/kanban-full.pipeline.specs.d.ts +0 -2
  121. package/dist/src/testing/fixtures/kanban-full.pipeline.specs.d.ts.map +0 -1
  122. package/dist/src/testing/fixtures/kanban-full.pipeline.specs.js +0 -263
  123. package/dist/src/testing/fixtures/kanban-full.pipeline.specs.js.map +0 -1
  124. package/dist/src/testing/fixtures/kanban.pipeline.specs.d.ts +0 -2
  125. package/dist/src/testing/fixtures/kanban.pipeline.specs.d.ts.map +0 -1
  126. package/dist/src/testing/fixtures/kanban.pipeline.specs.js +0 -29
  127. package/dist/src/testing/fixtures/kanban.pipeline.specs.js.map +0 -1
  128. package/dist/src/testing/kanban-todo.e2e.specs.d.ts +0 -2
  129. package/dist/src/testing/kanban-todo.e2e.specs.d.ts.map +0 -1
  130. package/dist/src/testing/kanban-todo.e2e.specs.js +0 -160
  131. package/dist/src/testing/kanban-todo.e2e.specs.js.map +0 -1
  132. package/dist/src/testing/mock-handlers.specs.d.ts +0 -2
  133. package/dist/src/testing/mock-handlers.specs.d.ts.map +0 -1
  134. package/dist/src/testing/mock-handlers.specs.js +0 -193
  135. package/dist/src/testing/mock-handlers.specs.js.map +0 -1
  136. package/dist/src/testing/real-execution.e2e.specs.d.ts +0 -2
  137. package/dist/src/testing/real-execution.e2e.specs.d.ts.map +0 -1
  138. package/dist/src/testing/real-execution.e2e.specs.js +0 -140
  139. package/dist/src/testing/real-execution.e2e.specs.js.map +0 -1
  140. package/dist/src/testing/real-plugin.e2e.specs.d.ts +0 -2
  141. package/dist/src/testing/real-plugin.e2e.specs.d.ts.map +0 -1
  142. package/dist/src/testing/real-plugin.e2e.specs.js +0 -65
  143. package/dist/src/testing/real-plugin.e2e.specs.js.map +0 -1
  144. package/dist/src/testing/server-startup.e2e.specs.d.ts +0 -2
  145. package/dist/src/testing/server-startup.e2e.specs.d.ts.map +0 -1
  146. package/dist/src/testing/server-startup.e2e.specs.js +0 -104
  147. package/dist/src/testing/server-startup.e2e.specs.js.map +0 -1
  148. package/dist/src/testing/snapshot-compare.specs.d.ts +0 -2
  149. package/dist/src/testing/snapshot-compare.specs.d.ts.map +0 -1
  150. package/dist/src/testing/snapshot-compare.specs.js +0 -112
  151. package/dist/src/testing/snapshot-compare.specs.js.map +0 -1
  152. package/dist/src/testing/snapshot-sanitize.specs.d.ts +0 -2
  153. package/dist/src/testing/snapshot-sanitize.specs.d.ts.map +0 -1
  154. package/dist/src/testing/snapshot-sanitize.specs.js +0 -104
  155. package/dist/src/testing/snapshot-sanitize.specs.js.map +0 -1
@@ -1,1407 +0,0 @@
1
- import { define } from '../builder/define.js';
2
- import { PipelineServer } from './pipeline-server.js';
3
- async function fetchAs(url, options) {
4
- const res = await fetch(url, options);
5
- return res.json();
6
- }
7
- async function fetchWithStatus(url, options) {
8
- const res = await fetch(url, options);
9
- return {
10
- status: res.status,
11
- json: () => res.json(),
12
- };
13
- }
14
- describe('PipelineServer', () => {
15
- describe('health endpoint', () => {
16
- it('should respond to /health', async () => {
17
- const server = new PipelineServer({ port: 0 });
18
- await server.start();
19
- const data = await fetchAs(`http://localhost:${server.port}/health`);
20
- expect(data.status).toBe('healthy');
21
- await server.stop();
22
- });
23
- });
24
- describe('command handlers', () => {
25
- it('should register command handlers', () => {
26
- const handler = {
27
- name: 'Cmd',
28
- handle: async () => ({ type: 'Done', data: {} }),
29
- };
30
- const server = new PipelineServer({ port: 0 });
31
- server.registerCommandHandlers([handler]);
32
- expect(server.getRegisteredCommands()).toContain('Cmd');
33
- });
34
- });
35
- describe('pipeline registration', () => {
36
- it('should register pipeline', () => {
37
- const pipeline = define('test').on('A').emit('B', {}).build();
38
- const server = new PipelineServer({ port: 0 });
39
- server.registerPipeline(pipeline);
40
- expect(server.getPipelineNames()).toContain('test');
41
- });
42
- });
43
- describe('GET /registry', () => {
44
- it('should return registry with event handlers', async () => {
45
- const pipeline = define('test').on('Start').emit('Cmd', {}).build();
46
- const server = new PipelineServer({ port: 0 });
47
- server.registerPipeline(pipeline);
48
- await server.start();
49
- const data = await fetchAs(`http://localhost:${server.port}/registry`);
50
- expect(data.eventHandlers).toContain('Start');
51
- await server.stop();
52
- });
53
- it('should return registry with command metadata defaults', async () => {
54
- const handler = {
55
- name: 'MinimalCmd',
56
- handle: async () => ({ type: 'Done', data: {} }),
57
- };
58
- const server = new PipelineServer({ port: 0 });
59
- server.registerCommandHandlers([handler]);
60
- await server.start();
61
- const data = await fetchAs(`http://localhost:${server.port}/registry`);
62
- const metadata = data.commandsWithMetadata[0];
63
- expect(metadata.alias).toBe('MinimalCmd');
64
- expect(metadata.description).toBe('');
65
- expect(metadata.fields).toEqual({});
66
- expect(metadata.examples).toEqual([]);
67
- await server.stop();
68
- });
69
- it('should return registry with command metadata', async () => {
70
- const handler = {
71
- name: 'Cmd',
72
- alias: 'cmd',
73
- description: 'Test',
74
- fields: { x: 1 },
75
- examples: ['ex'],
76
- handle: async () => ({ type: 'Done', data: {} }),
77
- };
78
- const server = new PipelineServer({ port: 0 });
79
- server.registerCommandHandlers([handler]);
80
- await server.start();
81
- const data = await fetchAs(`http://localhost:${server.port}/registry`);
82
- const metadata = data.commandsWithMetadata[0];
83
- expect(metadata.alias).toBe('cmd');
84
- expect(metadata.description).toBe('Test');
85
- expect(metadata.fields).toEqual({ x: 1 });
86
- expect(metadata.examples).toEqual(['ex']);
87
- await server.stop();
88
- });
89
- it('should return registry with folds array', async () => {
90
- const server = new PipelineServer({ port: 0 });
91
- await server.start();
92
- const data = await fetchAs(`http://localhost:${server.port}/registry`);
93
- expect(data.folds).toEqual([]);
94
- await server.stop();
95
- });
96
- });
97
- describe('GET /pipeline', () => {
98
- it('should return pipeline graph', async () => {
99
- const pipeline = define('test').on('Start').emit('Cmd', {}).build();
100
- const server = new PipelineServer({ port: 0 });
101
- server.registerPipeline(pipeline);
102
- await server.start();
103
- const data = await fetchAs(`http://localhost:${server.port}/pipeline`);
104
- expect(data.nodes.some((n) => n.id === 'evt:Start')).toBe(true);
105
- await server.stop();
106
- });
107
- it('should use displayName as label for command graph nodes', async () => {
108
- const handler = {
109
- name: 'Cmd',
110
- displayName: 'My Command',
111
- handle: async () => ({ type: 'Done', data: {} }),
112
- };
113
- const pipeline = define('test').on('Start').emit('Cmd', {}).build();
114
- const server = new PipelineServer({ port: 0 });
115
- server.registerCommandHandlers([handler]);
116
- server.registerPipeline(pipeline);
117
- await server.start();
118
- const data = await fetchAs(`http://localhost:${server.port}/pipeline`);
119
- const cmdNode = data.nodes.find((n) => n.id === 'cmd:Cmd');
120
- expect(cmdNode?.label).toBe('My Command');
121
- await server.stop();
122
- });
123
- it('should use command name as graph node label when displayName not provided', async () => {
124
- const handler = {
125
- name: 'SimpleCmd',
126
- handle: async () => ({ type: 'Done', data: {} }),
127
- };
128
- const pipeline = define('test').on('Start').emit('SimpleCmd', {}).build();
129
- const server = new PipelineServer({ port: 0 });
130
- server.registerCommandHandlers([handler]);
131
- server.registerPipeline(pipeline);
132
- await server.start();
133
- const data = await fetchAs(`http://localhost:${server.port}/pipeline`);
134
- const cmdNode = data.nodes.find((n) => n.id === 'cmd:SimpleCmd');
135
- expect(cmdNode?.label).toBe('SimpleCmd');
136
- await server.stop();
137
- });
138
- it('should filter out event nodes when excludeTypes=event', async () => {
139
- const pipeline = define('test').on('Start').emit('Cmd', {}).build();
140
- const server = new PipelineServer({ port: 0 });
141
- server.registerPipeline(pipeline);
142
- await server.start();
143
- const data = await fetchAs(`http://localhost:${server.port}/pipeline?excludeTypes=event`);
144
- expect(data.nodes.every((n) => n.type !== 'event')).toBe(true);
145
- await server.stop();
146
- });
147
- it('should reconnect edges when maintainEdges=true and filter commands', async () => {
148
- const pipeline = define('test').on('Start').emit('Process', {}).build();
149
- const server = new PipelineServer({ port: 0 });
150
- server.registerPipeline(pipeline);
151
- await server.start();
152
- const data = await fetchAs(`http://localhost:${server.port}/pipeline?excludeTypes=command&maintainEdges=true`);
153
- expect(data.nodes.every((n) => n.type !== 'command')).toBe(true);
154
- expect(data.edges).toHaveLength(0);
155
- await server.stop();
156
- });
157
- it('should filter multiple node types', async () => {
158
- const pipeline = define('test')
159
- .on('Start')
160
- .emit('CheckA', {})
161
- .emit('CheckB', {})
162
- .settled(['CheckA', 'CheckB'])
163
- .dispatch({ dispatches: [] }, () => { })
164
- .build();
165
- const server = new PipelineServer({ port: 0 });
166
- server.registerPipeline(pipeline);
167
- await server.start();
168
- const data = await fetchAs(`http://localhost:${server.port}/pipeline?excludeTypes=event,settled`);
169
- expect(data.nodes.every((n) => n.type !== 'event' && n.type !== 'settled')).toBe(true);
170
- await server.stop();
171
- });
172
- it('should reconnect commands through events when filtering events with maintainEdges=true', async () => {
173
- const generateHandler = {
174
- name: 'Generate',
175
- events: ['Generated'],
176
- handle: async () => ({ type: 'Generated', data: {} }),
177
- };
178
- const pipeline = define('test').on('Start').emit('Generate', {}).on('Generated').emit('Process', {}).build();
179
- const server = new PipelineServer({ port: 0 });
180
- server.registerCommandHandlers([generateHandler]);
181
- server.registerPipeline(pipeline);
182
- await server.start();
183
- const data = await fetchAs(`http://localhost:${server.port}/pipeline?excludeTypes=event&maintainEdges=true`);
184
- expect(data.nodes.every((n) => n.type !== 'event')).toBe(true);
185
- expect(data.edges.some((e) => e.from === 'cmd:Generate' && e.to === 'cmd:Process')).toBe(true);
186
- await server.stop();
187
- });
188
- it('should have status idle on command nodes by default', async () => {
189
- const handler = {
190
- name: 'Cmd',
191
- events: ['Done'],
192
- handle: async () => ({ type: 'Done', data: {} }),
193
- };
194
- const pipeline = define('test').on('Start').emit('Cmd', {}).build();
195
- const server = new PipelineServer({ port: 0 });
196
- server.registerCommandHandlers([handler]);
197
- server.registerPipeline(pipeline);
198
- await server.start();
199
- const data = await fetchAs(`http://localhost:${server.port}/pipeline`);
200
- const cmdNode = data.nodes.find((n) => n.id === 'cmd:Cmd');
201
- expect(cmdNode?.status).toBe('idle');
202
- await server.stop();
203
- });
204
- it('should not have status on event nodes', async () => {
205
- const handler = {
206
- name: 'Cmd',
207
- events: ['Done'],
208
- handle: async () => ({ type: 'Done', data: {} }),
209
- };
210
- const pipeline = define('test').on('Start').emit('Cmd', {}).build();
211
- const server = new PipelineServer({ port: 0 });
212
- server.registerCommandHandlers([handler]);
213
- server.registerPipeline(pipeline);
214
- await server.start();
215
- const data = await fetchAs(`http://localhost:${server.port}/pipeline`);
216
- const eventNode = data.nodes.find((n) => n.id === 'evt:Start');
217
- expect(eventNode?.status).toBeUndefined();
218
- await server.stop();
219
- });
220
- it('should have idle status on settled nodes when no correlationId provided', async () => {
221
- const handler = {
222
- name: 'CheckTests',
223
- events: ['TestsPassed'],
224
- handle: async () => ({ type: 'TestsPassed', data: {} }),
225
- };
226
- const pipeline = define('test')
227
- .on('Start')
228
- .emit('CheckTests', {})
229
- .settled(['CheckTests'])
230
- .dispatch({ dispatches: [] }, () => { })
231
- .build();
232
- const server = new PipelineServer({ port: 0 });
233
- server.registerCommandHandlers([handler]);
234
- server.registerPipeline(pipeline);
235
- await server.start();
236
- const data = await fetchAs(`http://localhost:${server.port}/pipeline`);
237
- const settledNode = data.nodes.find((n) => n.id === 'settled:CheckTests');
238
- expect(settledNode?.status).toBe('idle');
239
- expect(settledNode?.pendingCount).toBe(0);
240
- expect(settledNode?.endedCount).toBe(0);
241
- await server.stop();
242
- });
243
- it('should show running status for command being executed', async () => {
244
- let resolveHandler = () => { };
245
- const handlerPromise = new Promise((resolve) => {
246
- resolveHandler = resolve;
247
- });
248
- const handler = {
249
- name: 'SlowCmd',
250
- events: ['Done'],
251
- handle: async () => {
252
- await handlerPromise;
253
- return { type: 'Done', data: {} };
254
- },
255
- };
256
- const pipeline = define('test').on('Start').emit('SlowCmd', {}).build();
257
- const server = new PipelineServer({ port: 0 });
258
- server.registerCommandHandlers([handler]);
259
- server.registerPipeline(pipeline);
260
- await server.start();
261
- const commandResponse = await fetchAs(`http://localhost:${server.port}/command`, {
262
- method: 'POST',
263
- headers: { 'Content-Type': 'application/json' },
264
- body: JSON.stringify({ type: 'SlowCmd', data: {} }),
265
- });
266
- await new Promise((r) => setTimeout(r, 50));
267
- const data = await fetchAs(`http://localhost:${server.port}/pipeline?correlationId=${commandResponse.correlationId}`);
268
- const cmdNode = data.nodes.find((n) => n.id === 'cmd:SlowCmd');
269
- expect(cmdNode?.status).toBe('running');
270
- resolveHandler();
271
- await server.stop();
272
- });
273
- it('should show success status after command completes with success event', async () => {
274
- const handler = {
275
- name: 'SuccessCmd',
276
- events: ['CmdCompleted'],
277
- handle: async () => ({ type: 'CmdCompleted', data: {} }),
278
- };
279
- const pipeline = define('test').on('Start').emit('SuccessCmd', {}).build();
280
- const server = new PipelineServer({ port: 0 });
281
- server.registerCommandHandlers([handler]);
282
- server.registerPipeline(pipeline);
283
- await server.start();
284
- const commandResponse = await fetchAs(`http://localhost:${server.port}/command`, {
285
- method: 'POST',
286
- headers: { 'Content-Type': 'application/json' },
287
- body: JSON.stringify({ type: 'SuccessCmd', data: {} }),
288
- });
289
- await new Promise((r) => setTimeout(r, 100));
290
- const data = await fetchAs(`http://localhost:${server.port}/pipeline?correlationId=${commandResponse.correlationId}`);
291
- const cmdNode = data.nodes.find((n) => n.id === 'cmd:SuccessCmd');
292
- expect(cmdNode?.status).toBe('success');
293
- await server.stop();
294
- });
295
- it('should show error status after command completes with failed event', async () => {
296
- const handler = {
297
- name: 'FailCmd',
298
- events: ['CmdFailed'],
299
- handle: async () => ({ type: 'CmdFailed', data: { error: 'Something went wrong' } }),
300
- };
301
- const pipeline = define('test').on('Start').emit('FailCmd', {}).build();
302
- const server = new PipelineServer({ port: 0 });
303
- server.registerCommandHandlers([handler]);
304
- server.registerPipeline(pipeline);
305
- await server.start();
306
- const commandResponse = await fetchAs(`http://localhost:${server.port}/command`, {
307
- method: 'POST',
308
- headers: { 'Content-Type': 'application/json' },
309
- body: JSON.stringify({ type: 'FailCmd', data: {} }),
310
- });
311
- await new Promise((r) => setTimeout(r, 100));
312
- const data = await fetchAs(`http://localhost:${server.port}/pipeline?correlationId=${commandResponse.correlationId}`);
313
- const cmdNode = data.nodes.find((n) => n.id === 'cmd:FailCmd');
314
- expect(cmdNode?.status).toBe('error');
315
- await server.stop();
316
- });
317
- it('should broadcast PipelineRunStarted event when new correlationId is first seen', async () => {
318
- const handler = {
319
- name: 'StartCmd',
320
- events: ['Started'],
321
- handle: async () => ({ type: 'Started', data: {} }),
322
- };
323
- const pipeline = define('test').on('Trigger').emit('StartCmd', {}).build();
324
- const server = new PipelineServer({ port: 0 });
325
- server.registerCommandHandlers([handler]);
326
- server.registerPipeline(pipeline);
327
- await server.start();
328
- const commandResponse = await fetchAs(`http://localhost:${server.port}/command`, {
329
- method: 'POST',
330
- headers: { 'Content-Type': 'application/json' },
331
- body: JSON.stringify({ type: 'StartCmd', data: {} }),
332
- });
333
- await new Promise((r) => setTimeout(r, 100));
334
- const msgs = await fetchAs(`http://localhost:${server.port}/messages`);
335
- const pipelineRunStarted = msgs.find((m) => m.message.type === 'PipelineRunStarted');
336
- expect(pipelineRunStarted).toBeDefined();
337
- expect((pipelineRunStarted?.message).correlationId).toBe(commandResponse.correlationId);
338
- expect((pipelineRunStarted?.message).data?.triggerCommand).toBe('StartCmd');
339
- await server.stop();
340
- });
341
- it('should broadcast NodeStatusChanged event when command starts running', async () => {
342
- const handler = {
343
- name: 'RunCmd',
344
- events: ['RunDone'],
345
- handle: async () => ({ type: 'RunDone', data: {} }),
346
- };
347
- const pipeline = define('test').on('Trigger').emit('RunCmd', {}).build();
348
- const server = new PipelineServer({ port: 0 });
349
- server.registerCommandHandlers([handler]);
350
- server.registerPipeline(pipeline);
351
- await server.start();
352
- const commandResponse = await fetchAs(`http://localhost:${server.port}/command`, {
353
- method: 'POST',
354
- headers: { 'Content-Type': 'application/json' },
355
- body: JSON.stringify({ type: 'RunCmd', data: {} }),
356
- });
357
- await new Promise((r) => setTimeout(r, 100));
358
- const msgs = await fetchAs(`http://localhost:${server.port}/messages`);
359
- const nodeStatusChanged = msgs.filter((m) => m.message.type === 'NodeStatusChanged');
360
- const runningEvent = nodeStatusChanged.find((m) => m.message.data?.status === 'running');
361
- expect(runningEvent).toBeDefined();
362
- expect((runningEvent?.message).data?.nodeId).toBe('cmd:RunCmd');
363
- expect((runningEvent?.message).data?.previousStatus).toBe('idle');
364
- expect((runningEvent?.message).correlationId).toBe(commandResponse.correlationId);
365
- await server.stop();
366
- });
367
- it('should broadcast NodeStatusChanged event when command completes', async () => {
368
- const handler = {
369
- name: 'CompleteCmd',
370
- events: ['CompleteDone'],
371
- handle: async () => ({ type: 'CompleteDone', data: {} }),
372
- };
373
- const pipeline = define('test').on('Trigger').emit('CompleteCmd', {}).build();
374
- const server = new PipelineServer({ port: 0 });
375
- server.registerCommandHandlers([handler]);
376
- server.registerPipeline(pipeline);
377
- await server.start();
378
- const commandResponse = await fetchAs(`http://localhost:${server.port}/command`, {
379
- method: 'POST',
380
- headers: { 'Content-Type': 'application/json' },
381
- body: JSON.stringify({ type: 'CompleteCmd', data: {} }),
382
- });
383
- await new Promise((r) => setTimeout(r, 100));
384
- const msgs = await fetchAs(`http://localhost:${server.port}/messages`);
385
- const nodeStatusChanged = msgs.filter((m) => m.message.type === 'NodeStatusChanged');
386
- const successEvent = nodeStatusChanged.find((m) => m.message.data?.status === 'success');
387
- expect(successEvent).toBeDefined();
388
- expect((successEvent?.message).data?.nodeId).toBe('cmd:CompleteCmd');
389
- expect((successEvent?.message).data?.previousStatus).toBe('running');
390
- expect((successEvent?.message).correlationId).toBe(commandResponse.correlationId);
391
- await server.stop();
392
- });
393
- it('should persist status across multiple /pipeline calls', async () => {
394
- const handler = {
395
- name: 'PersistCmd',
396
- events: ['PersistDone'],
397
- handle: async () => ({ type: 'PersistDone', data: {} }),
398
- };
399
- const pipeline = define('test').on('Trigger').emit('PersistCmd', {}).build();
400
- const server = new PipelineServer({ port: 0 });
401
- server.registerCommandHandlers([handler]);
402
- server.registerPipeline(pipeline);
403
- await server.start();
404
- const commandResponse = await fetchAs(`http://localhost:${server.port}/command`, {
405
- method: 'POST',
406
- headers: { 'Content-Type': 'application/json' },
407
- body: JSON.stringify({ type: 'PersistCmd', data: {} }),
408
- });
409
- await new Promise((r) => setTimeout(r, 100));
410
- const firstCall = await fetchAs(`http://localhost:${server.port}/pipeline?correlationId=${commandResponse.correlationId}`);
411
- expect(firstCall.nodes.find((n) => n.id === 'cmd:PersistCmd')?.status).toBe('success');
412
- const secondCall = await fetchAs(`http://localhost:${server.port}/pipeline?correlationId=${commandResponse.correlationId}`);
413
- expect(secondCall.nodes.find((n) => n.id === 'cmd:PersistCmd')?.status).toBe('success');
414
- await server.stop();
415
- });
416
- it('should track status independently for different correlationIds', async () => {
417
- const handler = {
418
- name: 'IndependentCmd',
419
- events: ['IndependentDone'],
420
- handle: async () => ({ type: 'IndependentDone', data: {} }),
421
- };
422
- const pipeline = define('test').on('Trigger').emit('IndependentCmd', {}).build();
423
- const server = new PipelineServer({ port: 0 });
424
- server.registerCommandHandlers([handler]);
425
- server.registerPipeline(pipeline);
426
- await server.start();
427
- const run1 = await fetchAs(`http://localhost:${server.port}/command`, {
428
- method: 'POST',
429
- headers: { 'Content-Type': 'application/json' },
430
- body: JSON.stringify({ type: 'IndependentCmd', data: {} }),
431
- });
432
- const run2 = await fetchAs(`http://localhost:${server.port}/command`, {
433
- method: 'POST',
434
- headers: { 'Content-Type': 'application/json' },
435
- body: JSON.stringify({ type: 'IndependentCmd', data: {} }),
436
- });
437
- await new Promise((r) => setTimeout(r, 100));
438
- expect(run1.correlationId).not.toBe(run2.correlationId);
439
- const pipeline1 = await fetchAs(`http://localhost:${server.port}/pipeline?correlationId=${run1.correlationId}`);
440
- const pipeline2 = await fetchAs(`http://localhost:${server.port}/pipeline?correlationId=${run2.correlationId}`);
441
- expect(pipeline1.nodes.find((n) => n.id === 'cmd:IndependentCmd')?.status).toBe('success');
442
- expect(pipeline2.nodes.find((n) => n.id === 'cmd:IndependentCmd')?.status).toBe('success');
443
- await server.stop();
444
- });
445
- it('should show idle status for all command nodes when no correlationId provided', async () => {
446
- const handler = {
447
- name: 'IdleCmd',
448
- events: ['IdleDone'],
449
- handle: async () => ({ type: 'IdleDone', data: {} }),
450
- };
451
- const pipeline = define('test').on('Trigger').emit('IdleCmd', {}).build();
452
- const server = new PipelineServer({ port: 0 });
453
- server.registerCommandHandlers([handler]);
454
- server.registerPipeline(pipeline);
455
- await server.start();
456
- await fetchAs(`http://localhost:${server.port}/command`, {
457
- method: 'POST',
458
- headers: { 'Content-Type': 'application/json' },
459
- body: JSON.stringify({ type: 'IdleCmd', data: {} }),
460
- });
461
- await new Promise((r) => setTimeout(r, 100));
462
- const pipelineWithoutCorrelation = await fetchAs(`http://localhost:${server.port}/pipeline`);
463
- const cmdNode = pipelineWithoutCorrelation.nodes.find((n) => n.id === 'cmd:IdleCmd');
464
- expect(cmdNode?.status).toBe('idle');
465
- await server.stop();
466
- });
467
- it('should return latestRun with the most recent correlationId', async () => {
468
- const handler = {
469
- name: 'LatestCmd',
470
- events: ['LatestDone'],
471
- handle: async () => ({ type: 'LatestDone', data: {} }),
472
- };
473
- const pipeline = define('test').on('Trigger').emit('LatestCmd', {}).build();
474
- const server = new PipelineServer({ port: 0 });
475
- server.registerCommandHandlers([handler]);
476
- server.registerPipeline(pipeline);
477
- await server.start();
478
- const run1 = await fetchAs(`http://localhost:${server.port}/command`, {
479
- method: 'POST',
480
- headers: { 'Content-Type': 'application/json' },
481
- body: JSON.stringify({ type: 'LatestCmd', data: {} }),
482
- });
483
- await new Promise((r) => setTimeout(r, 50));
484
- const run2 = await fetchAs(`http://localhost:${server.port}/command`, {
485
- method: 'POST',
486
- headers: { 'Content-Type': 'application/json' },
487
- body: JSON.stringify({ type: 'LatestCmd', data: {} }),
488
- });
489
- await new Promise((r) => setTimeout(r, 50));
490
- const data = await fetchAs(`http://localhost:${server.port}/pipeline`);
491
- expect(data.latestRun).toBe(run2.correlationId);
492
- expect(data.latestRun).not.toBe(run1.correlationId);
493
- await server.stop();
494
- });
495
- });
496
- describe('POST /command', () => {
497
- it('should accept command', async () => {
498
- const handler = {
499
- name: 'Cmd',
500
- handle: async () => ({ type: 'Done', data: {} }),
501
- };
502
- const server = new PipelineServer({ port: 0 });
503
- server.registerCommandHandlers([handler]);
504
- await server.start();
505
- const data = await fetchAs(`http://localhost:${server.port}/command`, {
506
- method: 'POST',
507
- headers: { 'Content-Type': 'application/json' },
508
- body: JSON.stringify({ type: 'Cmd', data: {} }),
509
- });
510
- expect(data.status).toBe('ack');
511
- await server.stop();
512
- });
513
- it('should return 404 for unknown command', async () => {
514
- const server = new PipelineServer({ port: 0 });
515
- await server.start();
516
- const res = await fetchWithStatus(`http://localhost:${server.port}/command`, {
517
- method: 'POST',
518
- headers: { 'Content-Type': 'application/json' },
519
- body: JSON.stringify({ type: 'UnknownCmd', data: {} }),
520
- });
521
- expect(res.status).toBe(404);
522
- const data = await res.json();
523
- expect(data.status).toBe('nack');
524
- await server.stop();
525
- });
526
- it('should handle command that returns multiple events', async () => {
527
- const handler = {
528
- name: 'Multi',
529
- handle: async () => [
530
- { type: 'EventA', data: { a: 1 } },
531
- { type: 'EventB', data: { b: 2 } },
532
- ],
533
- };
534
- const server = new PipelineServer({ port: 0 });
535
- server.registerCommandHandlers([handler]);
536
- await server.start();
537
- await fetch(`http://localhost:${server.port}/command`, {
538
- method: 'POST',
539
- headers: { 'Content-Type': 'application/json' },
540
- body: JSON.stringify({ type: 'Multi', data: {} }),
541
- });
542
- await new Promise((r) => setTimeout(r, 100));
543
- const msgs = await fetchAs(`http://localhost:${server.port}/messages`);
544
- expect(msgs.some((m) => m.message.type === 'EventA')).toBe(true);
545
- expect(msgs.some((m) => m.message.type === 'EventB')).toBe(true);
546
- await server.stop();
547
- });
548
- });
549
- describe('GET /messages', () => {
550
- it('should return messages', async () => {
551
- const server = new PipelineServer({ port: 0 });
552
- await server.start();
553
- const data = await fetchAs(`http://localhost:${server.port}/messages`);
554
- expect(Array.isArray(data)).toBe(true);
555
- await server.stop();
556
- });
557
- });
558
- describe('GET /stats', () => {
559
- it('should return stats', async () => {
560
- const server = new PipelineServer({ port: 0 });
561
- await server.start();
562
- const data = await fetchAs(`http://localhost:${server.port}/stats`);
563
- expect(data.totalMessages).toBeDefined();
564
- await server.stop();
565
- });
566
- });
567
- describe('event routing', () => {
568
- it('should route events through pipeline', async () => {
569
- const handler = {
570
- name: 'Init',
571
- handle: async () => ({ type: 'Ready', data: {} }),
572
- };
573
- const pipeline = define('test').on('Ready').emit('Next', {}).build();
574
- const server = new PipelineServer({ port: 0 });
575
- server.registerCommandHandlers([handler]);
576
- server.registerPipeline(pipeline);
577
- await server.start();
578
- await fetch(`http://localhost:${server.port}/command`, {
579
- method: 'POST',
580
- headers: { 'Content-Type': 'application/json' },
581
- body: JSON.stringify({ type: 'Init', data: {} }),
582
- });
583
- await new Promise((r) => setTimeout(r, 100));
584
- const msgs = await fetchAs(`http://localhost:${server.port}/messages`);
585
- expect(msgs.some((m) => m.message.type === 'Next')).toBe(true);
586
- await server.stop();
587
- });
588
- it('should handle custom handler that emits events', async () => {
589
- const handler = {
590
- name: 'Start',
591
- handle: async () => ({ type: 'Started', data: {} }),
592
- };
593
- const pipeline = define('test')
594
- .on('Started')
595
- .handle(async (_e, ctx) => {
596
- await ctx.emit('CustomEvent', { emitted: true });
597
- })
598
- .build();
599
- const server = new PipelineServer({ port: 0 });
600
- server.registerCommandHandlers([handler]);
601
- server.registerPipeline(pipeline);
602
- await server.start();
603
- await fetch(`http://localhost:${server.port}/command`, {
604
- method: 'POST',
605
- headers: { 'Content-Type': 'application/json' },
606
- body: JSON.stringify({ type: 'Start', data: {} }),
607
- });
608
- await new Promise((r) => setTimeout(r, 100));
609
- const msgs = await fetchAs(`http://localhost:${server.port}/messages`);
610
- expect(msgs.some((m) => m.message.type === 'CustomEvent')).toBe(true);
611
- await server.stop();
612
- });
613
- });
614
- describe('GET /pipeline/mermaid', () => {
615
- it('should return mermaid diagram as text', async () => {
616
- const pipeline = define('test').on('Start').emit('Process', {}).build();
617
- const server = new PipelineServer({ port: 0 });
618
- server.registerPipeline(pipeline);
619
- await server.start();
620
- const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
621
- expect(res.headers.get('content-type')).toContain('text/plain');
622
- const mermaid = await res.text();
623
- expect(mermaid).toContain('flowchart LR');
624
- await server.stop();
625
- });
626
- it('should filter out event nodes when excludeTypes=event', async () => {
627
- const pipeline = define('test').on('Start').emit('Process', {}).build();
628
- const server = new PipelineServer({ port: 0 });
629
- server.registerPipeline(pipeline);
630
- await server.start();
631
- const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid?excludeTypes=event`);
632
- const mermaid = await res.text();
633
- expect(mermaid).not.toContain('evt_Start');
634
- expect(mermaid).toContain('Process[Process]');
635
- await server.stop();
636
- });
637
- it('should filter out settled nodes when excludeTypes=settled', async () => {
638
- const checkAHandler = {
639
- name: 'CheckA',
640
- events: ['CheckAPassed', 'CheckAFailed'],
641
- handle: async () => ({ type: 'CheckAPassed', data: {} }),
642
- };
643
- const pipeline = define('test')
644
- .on('Start')
645
- .emit('CheckA', {})
646
- .settled(['CheckA'])
647
- .dispatch({ dispatches: [] }, () => { })
648
- .build();
649
- const server = new PipelineServer({ port: 0 });
650
- server.registerCommandHandlers([checkAHandler]);
651
- server.registerPipeline(pipeline);
652
- await server.start();
653
- const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid?excludeTypes=settled`);
654
- const mermaid = await res.text();
655
- expect(mermaid).not.toContain('settled_');
656
- expect(mermaid).toContain('CheckA');
657
- await server.stop();
658
- });
659
- it('should use displayName as label for command nodes in mermaid diagram', async () => {
660
- const handler = {
661
- name: 'Cmd',
662
- displayName: 'My Command',
663
- handle: async () => ({ type: 'Done', data: {} }),
664
- };
665
- const pipeline = define('test').on('Start').emit('Cmd', {}).build();
666
- const server = new PipelineServer({ port: 0 });
667
- server.registerCommandHandlers([handler]);
668
- server.registerPipeline(pipeline);
669
- await server.start();
670
- const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
671
- const mermaid = await res.text();
672
- expect(mermaid).toContain('Cmd[My Command]');
673
- await server.stop();
674
- });
675
- it('should include event nodes in mermaid diagram', async () => {
676
- const pipeline = define('test').on('Start').emit('Process', {}).build();
677
- const server = new PipelineServer({ port: 0 });
678
- server.registerPipeline(pipeline);
679
- await server.start();
680
- const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
681
- const mermaid = await res.text();
682
- expect(mermaid).toContain('evt_Start');
683
- await server.stop();
684
- });
685
- it('should use displayName as label for event nodes in mermaid diagram', async () => {
686
- const handler = {
687
- name: 'Cmd',
688
- events: [{ name: 'CmdCompleted', displayName: 'Command Completed' }],
689
- handle: async () => ({ type: 'CmdCompleted', data: {} }),
690
- };
691
- const nextHandler = {
692
- name: 'NextCmd',
693
- handle: async () => ({ type: 'Done', data: {} }),
694
- };
695
- const pipeline = define('test').on('Start').emit('Cmd', {}).on('CmdCompleted').emit('NextCmd', {}).build();
696
- const server = new PipelineServer({ port: 0 });
697
- server.registerCommandHandlers([handler, nextHandler]);
698
- server.registerPipeline(pipeline);
699
- await server.start();
700
- const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
701
- const mermaid = await res.text();
702
- expect(mermaid).toContain('evt_CmdCompleted([Command Completed])');
703
- await server.stop();
704
- });
705
- it('should include command nodes in mermaid diagram', async () => {
706
- const pipeline = define('test').on('Start').emit('Process', {}).build();
707
- const server = new PipelineServer({ port: 0 });
708
- server.registerPipeline(pipeline);
709
- await server.start();
710
- const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
711
- const mermaid = await res.text();
712
- expect(mermaid).toContain('Process[Process]');
713
- await server.stop();
714
- });
715
- it('should include edges in mermaid diagram', async () => {
716
- const pipeline = define('test').on('Start').emit('Process', {}).build();
717
- const server = new PipelineServer({ port: 0 });
718
- server.registerPipeline(pipeline);
719
- await server.start();
720
- const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
721
- const mermaid = await res.text();
722
- expect(mermaid).toContain('-->');
723
- await server.stop();
724
- });
725
- it('should style commands as blue and events as orange', async () => {
726
- const pipeline = define('test').on('Start').emit('Process', {}).build();
727
- const server = new PipelineServer({ port: 0 });
728
- server.registerPipeline(pipeline);
729
- await server.start();
730
- const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
731
- const mermaid = await res.text();
732
- expect(mermaid).toContain('classDef event fill:#fff3e0,stroke:#e65100');
733
- expect(mermaid).toContain('classDef command fill:#e3f2fd,stroke:#1565c0');
734
- await server.stop();
735
- });
736
- it('should style failed events with red text', async () => {
737
- const handler = {
738
- name: 'Gen',
739
- events: ['GenDone', 'GenFailed'],
740
- handle: async () => ({ type: 'GenDone', data: {} }),
741
- };
742
- const retryHandler = {
743
- name: 'Retry',
744
- events: ['RetryDone'],
745
- handle: async () => ({ type: 'RetryDone', data: {} }),
746
- };
747
- const pipeline = define('test').on('Start').emit('Gen', {}).on('GenFailed').emit('Retry', {}).build();
748
- const server = new PipelineServer({ port: 0 });
749
- server.registerCommandHandlers([handler, retryHandler]);
750
- server.registerPipeline(pipeline);
751
- await server.start();
752
- const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
753
- const mermaid = await res.text();
754
- expect(mermaid).toContain('classDef eventFailed fill:#fff3e0,stroke:#e65100,color:#d32f2f');
755
- expect(mermaid).toContain('class evt_GenFailed eventFailed');
756
- await server.stop();
757
- });
758
- it('should include edges from commands to their pipeline events only', async () => {
759
- const handler = {
760
- name: 'Gen',
761
- events: ['GenDone', 'GenFailed'],
762
- handle: async () => ({ type: 'GenDone', data: {} }),
763
- };
764
- const nextHandler = {
765
- name: 'Next',
766
- events: ['NextDone'],
767
- handle: async () => ({ type: 'NextDone', data: {} }),
768
- };
769
- const pipeline = define('test').on('Start').emit('Gen', {}).on('GenDone').emit('Next', {}).build();
770
- const server = new PipelineServer({ port: 0 });
771
- server.registerCommandHandlers([handler, nextHandler]);
772
- server.registerPipeline(pipeline);
773
- await server.start();
774
- const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
775
- const mermaid = await res.text();
776
- expect(mermaid).toContain('Gen --> evt_GenDone');
777
- expect(mermaid).not.toContain('GenFailed');
778
- await server.stop();
779
- });
780
- it('should show complete flow from event to command with command events', async () => {
781
- const genHandler = {
782
- name: 'GenerateServer',
783
- events: ['ServerGenerated', 'SliceGenerated'],
784
- handle: async () => ({ type: 'ServerGenerated', data: {} }),
785
- };
786
- const iaHandler = {
787
- name: 'GenerateIA',
788
- events: ['IAGenerated'],
789
- handle: async () => ({ type: 'IAGenerated', data: {} }),
790
- };
791
- const implHandler = {
792
- name: 'ImplementSlice',
793
- events: ['SliceImplemented'],
794
- handle: async () => ({ type: 'SliceImplemented', data: {} }),
795
- };
796
- const pipeline = define('test')
797
- .on('SchemaExported')
798
- .emit('GenerateServer', {})
799
- .on('ServerGenerated')
800
- .emit('GenerateIA', {})
801
- .on('SliceGenerated')
802
- .emit('ImplementSlice', {})
803
- .build();
804
- const server = new PipelineServer({ port: 0 });
805
- server.registerCommandHandlers([genHandler, iaHandler, implHandler]);
806
- server.registerPipeline(pipeline);
807
- await server.start();
808
- const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
809
- const mermaid = await res.text();
810
- expect(mermaid).toContain('evt_SchemaExported --> GenerateServer');
811
- expect(mermaid).toContain('GenerateServer --> evt_ServerGenerated');
812
- expect(mermaid).toContain('GenerateServer --> evt_SliceGenerated');
813
- await server.stop();
814
- });
815
- it('should only show commands and events that are used in the pipeline', async () => {
816
- const usedHandler = {
817
- name: 'UsedCommand',
818
- events: ['UsedEvent'],
819
- handle: async () => ({ type: 'UsedEvent', data: {} }),
820
- };
821
- const unusedHandler = {
822
- name: 'UnusedCommand',
823
- events: ['UnusedEvent', 'AnotherUnusedEvent'],
824
- handle: async () => ({ type: 'UnusedEvent', data: {} }),
825
- };
826
- const nextHandler = {
827
- name: 'NextCommand',
828
- events: ['NextDone'],
829
- handle: async () => ({ type: 'NextDone', data: {} }),
830
- };
831
- const pipeline = define('test')
832
- .on('TriggerEvent')
833
- .emit('UsedCommand', {})
834
- .on('UsedEvent')
835
- .emit('NextCommand', {})
836
- .build();
837
- const server = new PipelineServer({ port: 0 });
838
- server.registerCommandHandlers([usedHandler, unusedHandler, nextHandler]);
839
- server.registerPipeline(pipeline);
840
- await server.start();
841
- const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
842
- const mermaid = await res.text();
843
- expect(mermaid).toContain('evt_TriggerEvent');
844
- expect(mermaid).toContain('UsedCommand');
845
- expect(mermaid).toContain('evt_UsedEvent');
846
- expect(mermaid).not.toContain('UnusedCommand');
847
- expect(mermaid).not.toContain('UnusedEvent');
848
- expect(mermaid).not.toContain('AnotherUnusedEvent');
849
- await server.stop();
850
- });
851
- it('should only show events that have handlers in the pipeline, not unhandled command events', async () => {
852
- const startHandler = {
853
- name: 'StartServer',
854
- events: ['ServerStarted', 'ServerStartFailed'],
855
- handle: async () => ({ type: 'ServerStarted', data: {} }),
856
- };
857
- const processHandler = {
858
- name: 'ProcessRequest',
859
- events: ['RequestProcessed'],
860
- handle: async () => ({ type: 'RequestProcessed', data: {} }),
861
- };
862
- const pipeline = define('test')
863
- .on('TriggerEvent')
864
- .emit('StartServer', {})
865
- .on('ServerStarted')
866
- .emit('ProcessRequest', {})
867
- .build();
868
- const server = new PipelineServer({ port: 0 });
869
- server.registerCommandHandlers([startHandler, processHandler]);
870
- server.registerPipeline(pipeline);
871
- await server.start();
872
- const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
873
- const mermaid = await res.text();
874
- expect(mermaid).toContain('evt_ServerStarted');
875
- expect(mermaid).toContain('StartServer --> evt_ServerStarted');
876
- expect(mermaid).not.toContain('ServerStartFailed');
877
- await server.stop();
878
- });
879
- it('should show edges from commands to settled node', async () => {
880
- const checkAHandler = {
881
- name: 'CheckA',
882
- events: ['CheckAPassed', 'CheckAFailed'],
883
- handle: async () => ({ type: 'CheckAPassed', data: {} }),
884
- };
885
- const checkBHandler = {
886
- name: 'CheckB',
887
- events: ['CheckBPassed', 'CheckBFailed'],
888
- handle: async () => ({ type: 'CheckBPassed', data: {} }),
889
- };
890
- const pipeline = define('test')
891
- .on('Start')
892
- .emit('CheckA', {})
893
- .emit('CheckB', {})
894
- .settled(['CheckA', 'CheckB'])
895
- .dispatch({ dispatches: [] }, () => { })
896
- .build();
897
- const server = new PipelineServer({ port: 0 });
898
- server.registerCommandHandlers([checkAHandler, checkBHandler]);
899
- server.registerPipeline(pipeline);
900
- await server.start();
901
- const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
902
- const mermaid = await res.text();
903
- expect(mermaid).toContain('CheckA --> settled_CheckA_CheckB');
904
- expect(mermaid).toContain('CheckB --> settled_CheckA_CheckB');
905
- await server.stop();
906
- });
907
- it('should show edges from settled node to dispatched commands', async () => {
908
- const checkHandler = {
909
- name: 'CheckA',
910
- events: ['CheckAPassed', 'CheckAFailed'],
911
- handle: async () => ({ type: 'CheckAPassed', data: {} }),
912
- };
913
- const retryHandler = {
914
- name: 'RetryCommand',
915
- events: ['RetryDone'],
916
- handle: async () => ({ type: 'RetryDone', data: {} }),
917
- };
918
- const pipeline = define('test')
919
- .on('Start')
920
- .emit('CheckA', {})
921
- .settled(['CheckA'])
922
- .dispatch({ dispatches: ['RetryCommand'] }, () => { })
923
- .build();
924
- const server = new PipelineServer({ port: 0 });
925
- server.registerCommandHandlers([checkHandler, retryHandler]);
926
- server.registerPipeline(pipeline);
927
- await server.start();
928
- const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
929
- const mermaid = await res.text();
930
- expect(mermaid).toContain('settled_CheckA -.->|retry| RetryCommand');
931
- await server.stop();
932
- });
933
- it('should style backLink edges in red', async () => {
934
- const checkHandler = {
935
- name: 'CheckA',
936
- events: ['CheckAPassed', 'CheckAFailed'],
937
- handle: async () => ({ type: 'CheckAPassed', data: {} }),
938
- };
939
- const retryHandler = {
940
- name: 'RetryCommand',
941
- events: ['RetryDone'],
942
- handle: async () => ({ type: 'RetryDone', data: {} }),
943
- };
944
- const pipeline = define('test')
945
- .on('Start')
946
- .emit('CheckA', {})
947
- .settled(['CheckA'])
948
- .dispatch({ dispatches: ['RetryCommand'] }, () => { })
949
- .build();
950
- const server = new PipelineServer({ port: 0 });
951
- server.registerCommandHandlers([checkHandler, retryHandler]);
952
- server.registerPipeline(pipeline);
953
- await server.start();
954
- const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
955
- const mermaid = await res.text();
956
- expect(mermaid).toContain('linkStyle');
957
- expect(mermaid).toMatch(/stroke:#[a-fA-F0-9]{6}|stroke:red/);
958
- await server.stop();
959
- });
960
- it('should mark event-to-command edges as backLink when they create cycles', async () => {
961
- const generateHandler = {
962
- name: 'GenerateIA',
963
- events: ['IAGenerated', 'IAValidationFailed'],
964
- handle: async () => ({ type: 'IAGenerated', data: {} }),
965
- };
966
- const pipeline = define('test')
967
- .on('Start')
968
- .emit('GenerateIA', {})
969
- .on('IAValidationFailed')
970
- .emit('GenerateIA', {})
971
- .build();
972
- const server = new PipelineServer({ port: 0 });
973
- server.registerCommandHandlers([generateHandler]);
974
- server.registerPipeline(pipeline);
975
- await server.start();
976
- const data = await fetchAs(`http://localhost:${server.port}/pipeline`);
977
- const backLinkEdge = data.edges.find((e) => e.from === 'evt:IAValidationFailed' && e.to === 'cmd:GenerateIA');
978
- expect(backLinkEdge).toBeDefined();
979
- expect(backLinkEdge?.backLink).toBe(true);
980
- await server.stop();
981
- });
982
- it('should NOT mark forward edges as backLink when cycle is broken by settled dispatch', async () => {
983
- const implHandler = {
984
- name: 'ImplementSlice',
985
- events: ['SliceImplemented'],
986
- handle: async () => ({ type: 'SliceImplemented', data: {} }),
987
- };
988
- const checkHandler = {
989
- name: 'CheckTests',
990
- events: ['TestsCheckPassed', 'TestsCheckFailed'],
991
- handle: async () => ({ type: 'TestsCheckPassed', data: {} }),
992
- };
993
- const pipeline = define('test')
994
- .on('Start')
995
- .emit('ImplementSlice', {})
996
- .on('SliceImplemented')
997
- .emit('CheckTests', {})
998
- .settled(['CheckTests'])
999
- .dispatch({ dispatches: ['ImplementSlice'] }, () => { })
1000
- .build();
1001
- const server = new PipelineServer({ port: 0 });
1002
- server.registerCommandHandlers([implHandler, checkHandler]);
1003
- server.registerPipeline(pipeline);
1004
- await server.start();
1005
- const data = await fetchAs(`http://localhost:${server.port}/pipeline`);
1006
- const forwardEdge = data.edges.find((e) => e.from === 'evt:SliceImplemented' && e.to === 'cmd:CheckTests');
1007
- expect(forwardEdge).toBeDefined();
1008
- expect(forwardEdge?.backLink).toBeUndefined();
1009
- await server.stop();
1010
- });
1011
- it('should add event nodes from settled handler commandToEvents when not already added', async () => {
1012
- const checkAHandler = {
1013
- name: 'CheckA',
1014
- events: ['CheckAPassed', 'CheckAFailed'],
1015
- handle: async () => ({ type: 'CheckAPassed', data: {} }),
1016
- };
1017
- const checkBHandler = {
1018
- name: 'CheckB',
1019
- events: ['CheckBPassed', 'CheckBFailed'],
1020
- handle: async () => ({ type: 'CheckBPassed', data: {} }),
1021
- };
1022
- const pipeline = define('test')
1023
- .on('Start')
1024
- .emit('CheckA', {})
1025
- .emit('CheckB', {})
1026
- .settled(['CheckA', 'CheckB'])
1027
- .dispatch({ dispatches: [] }, () => { })
1028
- .build();
1029
- const server = new PipelineServer({ port: 0 });
1030
- server.registerCommandHandlers([checkAHandler, checkBHandler]);
1031
- server.registerPipeline(pipeline);
1032
- await server.start();
1033
- const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
1034
- const mermaid = await res.text();
1035
- expect(mermaid).toContain('evt_CheckAPassed');
1036
- expect(mermaid).toContain('evt_CheckAFailed');
1037
- expect(mermaid).toContain('evt_CheckBPassed');
1038
- expect(mermaid).toContain('evt_CheckBFailed');
1039
- await server.stop();
1040
- });
1041
- });
1042
- describe('GET /pipeline/diagram', () => {
1043
- it('should return HTML content type', async () => {
1044
- const pipeline = define('test').on('Start').emit('Process', {}).build();
1045
- const server = new PipelineServer({ port: 0 });
1046
- server.registerPipeline(pipeline);
1047
- await server.start();
1048
- const res = await fetch(`http://localhost:${server.port}/pipeline/diagram`);
1049
- expect(res.headers.get('content-type')).toContain('text/html');
1050
- await server.stop();
1051
- });
1052
- it('should filter nodes when excludeTypes is provided', async () => {
1053
- const pipeline = define('test').on('Start').emit('Process', {}).build();
1054
- const server = new PipelineServer({ port: 0 });
1055
- server.registerPipeline(pipeline);
1056
- await server.start();
1057
- const res = await fetch(`http://localhost:${server.port}/pipeline/diagram?excludeTypes=event`);
1058
- const html = await res.text();
1059
- expect(html).not.toContain('evt_Start');
1060
- expect(html).toContain('Process');
1061
- await server.stop();
1062
- });
1063
- it('should include mermaid.js script', async () => {
1064
- const pipeline = define('test').on('Start').emit('Process', {}).build();
1065
- const server = new PipelineServer({ port: 0 });
1066
- server.registerPipeline(pipeline);
1067
- await server.start();
1068
- const res = await fetch(`http://localhost:${server.port}/pipeline/diagram`);
1069
- const html = await res.text();
1070
- expect(html).toContain('mermaid');
1071
- await server.stop();
1072
- });
1073
- it('should include the pipeline mermaid definition', async () => {
1074
- const pipeline = define('test').on('Start').emit('Process', {}).build();
1075
- const server = new PipelineServer({ port: 0 });
1076
- server.registerPipeline(pipeline);
1077
- await server.start();
1078
- const res = await fetch(`http://localhost:${server.port}/pipeline/diagram`);
1079
- const html = await res.text();
1080
- expect(html).toContain('flowchart LR');
1081
- expect(html).toContain('evt_Start');
1082
- await server.stop();
1083
- });
1084
- it('should have a valid HTML structure', async () => {
1085
- const pipeline = define('test').on('Start').emit('Process', {}).build();
1086
- const server = new PipelineServer({ port: 0 });
1087
- server.registerPipeline(pipeline);
1088
- await server.start();
1089
- const res = await fetch(`http://localhost:${server.port}/pipeline/diagram`);
1090
- const html = await res.text();
1091
- expect(html).toContain('<!DOCTYPE html>');
1092
- expect(html).toContain('<html');
1093
- expect(html).toContain('</html>');
1094
- await server.stop();
1095
- });
1096
- });
1097
- describe('item-level tracking', () => {
1098
- it('should extract itemKey from command data using registered extractor', async () => {
1099
- const handler = {
1100
- name: 'ImplementSlice',
1101
- events: ['SliceImplemented'],
1102
- handle: async () => ({ type: 'SliceImplemented', data: {} }),
1103
- };
1104
- const pipeline = define('test').on('Trigger').emit('ImplementSlice', {}).build();
1105
- const server = new PipelineServer({ port: 0 });
1106
- server.registerCommandHandlers([handler]);
1107
- server.registerPipeline(pipeline);
1108
- server.registerItemKeyExtractor('ImplementSlice', (d) => d.slicePath);
1109
- await server.start();
1110
- const commandResponse = await fetchAs(`http://localhost:${server.port}/command`, {
1111
- method: 'POST',
1112
- headers: { 'Content-Type': 'application/json' },
1113
- body: JSON.stringify({ type: 'ImplementSlice', data: { slicePath: '/server/slice-1' } }),
1114
- });
1115
- await new Promise((r) => setTimeout(r, 100));
1116
- const data = await fetchAs(`http://localhost:${server.port}/pipeline?correlationId=${commandResponse.correlationId}`);
1117
- const cmdNode = data.nodes.find((n) => n.id === 'cmd:ImplementSlice');
1118
- expect(cmdNode?.pendingCount).toBe(0);
1119
- expect(cmdNode?.endedCount).toBe(1);
1120
- await server.stop();
1121
- });
1122
- it('should count multiple parallel items correctly', async () => {
1123
- const handler = {
1124
- name: 'ImplementSlice',
1125
- events: ['SliceImplemented'],
1126
- handle: async () => ({ type: 'SliceImplemented', data: {} }),
1127
- };
1128
- const pipeline = define('test').on('Trigger').emit('ImplementSlice', {}).build();
1129
- const server = new PipelineServer({ port: 0 });
1130
- server.registerCommandHandlers([handler]);
1131
- server.registerPipeline(pipeline);
1132
- server.registerItemKeyExtractor('ImplementSlice', (d) => d.slicePath);
1133
- await server.start();
1134
- const correlationId = `corr-parallel-test`;
1135
- await Promise.all([
1136
- fetch(`http://localhost:${server.port}/command`, {
1137
- method: 'POST',
1138
- headers: { 'Content-Type': 'application/json' },
1139
- body: JSON.stringify({
1140
- type: 'ImplementSlice',
1141
- data: { slicePath: '/server/slice-1' },
1142
- correlationId,
1143
- }),
1144
- }),
1145
- fetch(`http://localhost:${server.port}/command`, {
1146
- method: 'POST',
1147
- headers: { 'Content-Type': 'application/json' },
1148
- body: JSON.stringify({
1149
- type: 'ImplementSlice',
1150
- data: { slicePath: '/server/slice-2' },
1151
- correlationId,
1152
- }),
1153
- }),
1154
- fetch(`http://localhost:${server.port}/command`, {
1155
- method: 'POST',
1156
- headers: { 'Content-Type': 'application/json' },
1157
- body: JSON.stringify({
1158
- type: 'ImplementSlice',
1159
- data: { slicePath: '/server/slice-3' },
1160
- correlationId,
1161
- }),
1162
- }),
1163
- ]);
1164
- await new Promise((r) => setTimeout(r, 100));
1165
- const data = await fetchAs(`http://localhost:${server.port}/pipeline?correlationId=${correlationId}`);
1166
- const cmdNode = data.nodes.find((n) => n.id === 'cmd:ImplementSlice');
1167
- expect(cmdNode?.pendingCount).toBe(0);
1168
- expect(cmdNode?.endedCount).toBe(3);
1169
- await server.stop();
1170
- });
1171
- it('should show pending count while commands are running', async () => {
1172
- const resolveHandlers = [];
1173
- const handler = {
1174
- name: 'SlowSlice',
1175
- events: ['SlowSliceDone'],
1176
- handle: async () => {
1177
- await new Promise((resolve) => {
1178
- resolveHandlers.push(resolve);
1179
- });
1180
- return { type: 'SlowSliceDone', data: {} };
1181
- },
1182
- };
1183
- const pipeline = define('test').on('Trigger').emit('SlowSlice', {}).build();
1184
- const server = new PipelineServer({ port: 0 });
1185
- server.registerCommandHandlers([handler]);
1186
- server.registerPipeline(pipeline);
1187
- server.registerItemKeyExtractor('SlowSlice', (d) => d.id);
1188
- await server.start();
1189
- const correlationId = `corr-slow-test`;
1190
- void fetch(`http://localhost:${server.port}/command`, {
1191
- method: 'POST',
1192
- headers: { 'Content-Type': 'application/json' },
1193
- body: JSON.stringify({ type: 'SlowSlice', data: { id: 'item-1' }, correlationId }),
1194
- });
1195
- void fetch(`http://localhost:${server.port}/command`, {
1196
- method: 'POST',
1197
- headers: { 'Content-Type': 'application/json' },
1198
- body: JSON.stringify({ type: 'SlowSlice', data: { id: 'item-2' }, correlationId }),
1199
- });
1200
- void fetch(`http://localhost:${server.port}/command`, {
1201
- method: 'POST',
1202
- headers: { 'Content-Type': 'application/json' },
1203
- body: JSON.stringify({ type: 'SlowSlice', data: { id: 'item-3' }, correlationId }),
1204
- });
1205
- await new Promise((r) => setTimeout(r, 50));
1206
- const midwayData = await fetchAs(`http://localhost:${server.port}/pipeline?correlationId=${correlationId}`);
1207
- const midwayNode = midwayData.nodes.find((n) => n.id === 'cmd:SlowSlice');
1208
- expect(midwayNode?.pendingCount).toBe(3);
1209
- expect(midwayNode?.endedCount).toBe(0);
1210
- expect(midwayNode?.status).toBe('running');
1211
- resolveHandlers.forEach((r) => r());
1212
- await new Promise((r) => setTimeout(r, 50));
1213
- const finalData = await fetchAs(`http://localhost:${server.port}/pipeline?correlationId=${correlationId}`);
1214
- const finalNode = finalData.nodes.find((n) => n.id === 'cmd:SlowSlice');
1215
- expect(finalNode?.pendingCount).toBe(0);
1216
- expect(finalNode?.endedCount).toBe(3);
1217
- expect(finalNode?.status).toBe('success');
1218
- await server.stop();
1219
- });
1220
- it('should show error status when any item fails', async () => {
1221
- const handler = {
1222
- name: 'MixedSlice',
1223
- events: ['MixedSliceDone', 'MixedSliceFailed'],
1224
- handle: async (cmd) => {
1225
- if (cmd.data.shouldFail === true) {
1226
- return { type: 'MixedSliceFailed', data: {} };
1227
- }
1228
- return { type: 'MixedSliceDone', data: {} };
1229
- },
1230
- };
1231
- const pipeline = define('test').on('Trigger').emit('MixedSlice', {}).build();
1232
- const server = new PipelineServer({ port: 0 });
1233
- server.registerCommandHandlers([handler]);
1234
- server.registerPipeline(pipeline);
1235
- server.registerItemKeyExtractor('MixedSlice', (d) => d.id);
1236
- await server.start();
1237
- const correlationId = `corr-mixed-test`;
1238
- await Promise.all([
1239
- fetch(`http://localhost:${server.port}/command`, {
1240
- method: 'POST',
1241
- headers: { 'Content-Type': 'application/json' },
1242
- body: JSON.stringify({ type: 'MixedSlice', data: { id: 'pass-1' }, correlationId }),
1243
- }),
1244
- fetch(`http://localhost:${server.port}/command`, {
1245
- method: 'POST',
1246
- headers: { 'Content-Type': 'application/json' },
1247
- body: JSON.stringify({ type: 'MixedSlice', data: { id: 'fail-1', shouldFail: true }, correlationId }),
1248
- }),
1249
- fetch(`http://localhost:${server.port}/command`, {
1250
- method: 'POST',
1251
- headers: { 'Content-Type': 'application/json' },
1252
- body: JSON.stringify({ type: 'MixedSlice', data: { id: 'pass-2' }, correlationId }),
1253
- }),
1254
- ]);
1255
- await new Promise((r) => setTimeout(r, 100));
1256
- const data = await fetchAs(`http://localhost:${server.port}/pipeline?correlationId=${correlationId}`);
1257
- const cmdNode = data.nodes.find((n) => n.id === 'cmd:MixedSlice');
1258
- expect(cmdNode?.pendingCount).toBe(0);
1259
- expect(cmdNode?.endedCount).toBe(3);
1260
- expect(cmdNode?.status).toBe('error');
1261
- await server.stop();
1262
- });
1263
- it('should reset item to running when retry command arrives for same itemKey', async () => {
1264
- let attemptCount = 0;
1265
- const handler = {
1266
- name: 'RetrySlice',
1267
- events: ['RetrySliceDone', 'RetrySliceFailed'],
1268
- handle: async () => {
1269
- attemptCount++;
1270
- if (attemptCount === 1) {
1271
- return { type: 'RetrySliceFailed', data: {} };
1272
- }
1273
- return { type: 'RetrySliceDone', data: {} };
1274
- },
1275
- };
1276
- const pipeline = define('test').on('Trigger').emit('RetrySlice', {}).build();
1277
- const server = new PipelineServer({ port: 0 });
1278
- server.registerCommandHandlers([handler]);
1279
- server.registerPipeline(pipeline);
1280
- server.registerItemKeyExtractor('RetrySlice', (d) => d.slicePath);
1281
- await server.start();
1282
- const correlationId = `corr-retry-test`;
1283
- const slicePath = '/server/retry-slice';
1284
- await fetch(`http://localhost:${server.port}/command`, {
1285
- method: 'POST',
1286
- headers: { 'Content-Type': 'application/json' },
1287
- body: JSON.stringify({ type: 'RetrySlice', data: { slicePath }, correlationId }),
1288
- });
1289
- await new Promise((r) => setTimeout(r, 50));
1290
- const afterFailure = await fetchAs(`http://localhost:${server.port}/pipeline?correlationId=${correlationId}`);
1291
- expect(afterFailure.nodes.find((n) => n.id === 'cmd:RetrySlice')?.status).toBe('error');
1292
- await fetch(`http://localhost:${server.port}/command`, {
1293
- method: 'POST',
1294
- headers: { 'Content-Type': 'application/json' },
1295
- body: JSON.stringify({ type: 'RetrySlice', data: { slicePath }, correlationId }),
1296
- });
1297
- await new Promise((r) => setTimeout(r, 50));
1298
- const afterRetry = await fetchAs(`http://localhost:${server.port}/pipeline?correlationId=${correlationId}`);
1299
- const node = afterRetry.nodes.find((n) => n.id === 'cmd:RetrySlice');
1300
- expect(node?.status).toBe('success');
1301
- expect(node?.pendingCount).toBe(0);
1302
- expect(node?.endedCount).toBe(1);
1303
- await server.stop();
1304
- });
1305
- it('should include pendingCount and endedCount in NodeStatusChanged events', async () => {
1306
- const handler = {
1307
- name: 'CountSlice',
1308
- events: ['CountSliceDone'],
1309
- handle: async () => ({ type: 'CountSliceDone', data: {} }),
1310
- };
1311
- const pipeline = define('test').on('Trigger').emit('CountSlice', {}).build();
1312
- const server = new PipelineServer({ port: 0 });
1313
- server.registerCommandHandlers([handler]);
1314
- server.registerPipeline(pipeline);
1315
- server.registerItemKeyExtractor('CountSlice', (d) => d.id);
1316
- await server.start();
1317
- const correlationId = `corr-counts-event-test`;
1318
- await fetch(`http://localhost:${server.port}/command`, {
1319
- method: 'POST',
1320
- headers: { 'Content-Type': 'application/json' },
1321
- body: JSON.stringify({ type: 'CountSlice', data: { id: 'item-1' }, correlationId }),
1322
- });
1323
- await new Promise((r) => setTimeout(r, 100));
1324
- const msgs = await fetchAs(`http://localhost:${server.port}/messages`);
1325
- const nodeStatusChanged = msgs.filter((m) => m.message.type === 'NodeStatusChanged');
1326
- const successEvent = nodeStatusChanged.find((m) => m.message.data?.status === 'success');
1327
- expect(successEvent).toBeDefined();
1328
- expect((successEvent?.message).data?.pendingCount).toBe(0);
1329
- expect((successEvent?.message).data?.endedCount).toBe(1);
1330
- await server.stop();
1331
- });
1332
- it('should use requestId as fallback when no itemKey extractor is registered', async () => {
1333
- const handler = {
1334
- name: 'NoExtractorCmd',
1335
- events: ['NoExtractorDone'],
1336
- handle: async () => ({ type: 'NoExtractorDone', data: {} }),
1337
- };
1338
- const pipeline = define('test').on('Trigger').emit('NoExtractorCmd', {}).build();
1339
- const server = new PipelineServer({ port: 0 });
1340
- server.registerCommandHandlers([handler]);
1341
- server.registerPipeline(pipeline);
1342
- await server.start();
1343
- const correlationId = `corr-no-extractor-test`;
1344
- await fetch(`http://localhost:${server.port}/command`, {
1345
- method: 'POST',
1346
- headers: { 'Content-Type': 'application/json' },
1347
- body: JSON.stringify({ type: 'NoExtractorCmd', data: {}, correlationId }),
1348
- });
1349
- await new Promise((r) => setTimeout(r, 100));
1350
- const data = await fetchAs(`http://localhost:${server.port}/pipeline?correlationId=${correlationId}`);
1351
- const cmdNode = data.nodes.find((n) => n.id === 'cmd:NoExtractorCmd');
1352
- expect(cmdNode?.pendingCount).toBe(0);
1353
- expect(cmdNode?.endedCount).toBe(1);
1354
- expect(cmdNode?.status).toBe('success');
1355
- await server.stop();
1356
- });
1357
- it('should show idle status with zero counts when no correlationId provided', async () => {
1358
- const handler = {
1359
- name: 'IdleCountCmd',
1360
- events: ['IdleCountDone'],
1361
- handle: async () => ({ type: 'IdleCountDone', data: {} }),
1362
- };
1363
- const pipeline = define('test').on('Trigger').emit('IdleCountCmd', {}).build();
1364
- const server = new PipelineServer({ port: 0 });
1365
- server.registerCommandHandlers([handler]);
1366
- server.registerPipeline(pipeline);
1367
- await server.start();
1368
- const data = await fetchAs(`http://localhost:${server.port}/pipeline`);
1369
- const cmdNode = data.nodes.find((n) => n.id === 'cmd:IdleCountCmd');
1370
- expect(cmdNode?.status).toBe('idle');
1371
- expect(cmdNode?.pendingCount).toBe(0);
1372
- expect(cmdNode?.endedCount).toBe(0);
1373
- await server.stop();
1374
- });
1375
- });
1376
- describe('integration', () => {
1377
- it('should execute complete workflow', async () => {
1378
- const handler = {
1379
- name: 'Gen',
1380
- alias: 'gen',
1381
- description: '',
1382
- fields: {},
1383
- examples: [],
1384
- events: ['Done'],
1385
- handle: async () => ({ type: 'Done', data: { id: '1' } }),
1386
- };
1387
- const pipeline = define('wf')
1388
- .on('Done')
1389
- .emit('Process', (e) => ({ x: e.data.id }))
1390
- .build();
1391
- const server = new PipelineServer({ port: 0 });
1392
- server.registerCommandHandlers([handler]);
1393
- server.registerPipeline(pipeline);
1394
- await server.start();
1395
- await fetch(`http://localhost:${server.port}/command`, {
1396
- method: 'POST',
1397
- headers: { 'Content-Type': 'application/json' },
1398
- body: JSON.stringify({ type: 'Gen', data: {} }),
1399
- });
1400
- await new Promise((r) => setTimeout(r, 200));
1401
- const msgs = await fetchAs(`http://localhost:${server.port}/messages`);
1402
- expect(msgs.some((m) => m.message.type === 'Process')).toBe(true);
1403
- await server.stop();
1404
- });
1405
- });
1406
- });
1407
- //# sourceMappingURL=pipeline-server.specs.js.map