@auto-engineer/pipeline 0.0.1

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 (270) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/LICENSE +10 -0
  3. package/claude.md +160 -0
  4. package/dist/src/builder/define.d.ts +90 -0
  5. package/dist/src/builder/define.d.ts.map +1 -0
  6. package/dist/src/builder/define.js +425 -0
  7. package/dist/src/builder/define.js.map +1 -0
  8. package/dist/src/builder/define.specs.d.ts +2 -0
  9. package/dist/src/builder/define.specs.d.ts.map +1 -0
  10. package/dist/src/builder/define.specs.js +435 -0
  11. package/dist/src/builder/define.specs.js.map +1 -0
  12. package/dist/src/config/pipeline-config.d.ts +13 -0
  13. package/dist/src/config/pipeline-config.d.ts.map +1 -0
  14. package/dist/src/config/pipeline-config.js +15 -0
  15. package/dist/src/config/pipeline-config.js.map +1 -0
  16. package/dist/src/core/descriptors.d.ts +84 -0
  17. package/dist/src/core/descriptors.d.ts.map +1 -0
  18. package/dist/src/core/descriptors.js +2 -0
  19. package/dist/src/core/descriptors.js.map +1 -0
  20. package/dist/src/core/descriptors.specs.d.ts +2 -0
  21. package/dist/src/core/descriptors.specs.d.ts.map +1 -0
  22. package/dist/src/core/descriptors.specs.js +24 -0
  23. package/dist/src/core/descriptors.specs.js.map +1 -0
  24. package/dist/src/core/types.d.ts +10 -0
  25. package/dist/src/core/types.d.ts.map +1 -0
  26. package/dist/src/core/types.js +4 -0
  27. package/dist/src/core/types.js.map +1 -0
  28. package/dist/src/core/types.specs.d.ts +2 -0
  29. package/dist/src/core/types.specs.d.ts.map +1 -0
  30. package/dist/src/core/types.specs.js +40 -0
  31. package/dist/src/core/types.specs.js.map +1 -0
  32. package/dist/src/graph/types.d.ts +17 -0
  33. package/dist/src/graph/types.d.ts.map +1 -0
  34. package/dist/src/graph/types.js +2 -0
  35. package/dist/src/graph/types.js.map +1 -0
  36. package/dist/src/graph/types.specs.d.ts +2 -0
  37. package/dist/src/graph/types.specs.d.ts.map +1 -0
  38. package/dist/src/graph/types.specs.js +148 -0
  39. package/dist/src/graph/types.specs.js.map +1 -0
  40. package/dist/src/index.d.ts +20 -0
  41. package/dist/src/index.d.ts.map +1 -0
  42. package/dist/src/index.js +12 -0
  43. package/dist/src/index.js.map +1 -0
  44. package/dist/src/logging/event-logger.d.ts +21 -0
  45. package/dist/src/logging/event-logger.d.ts.map +1 -0
  46. package/dist/src/logging/event-logger.js +31 -0
  47. package/dist/src/logging/event-logger.js.map +1 -0
  48. package/dist/src/logging/event-logger.specs.d.ts +2 -0
  49. package/dist/src/logging/event-logger.specs.d.ts.map +1 -0
  50. package/dist/src/logging/event-logger.specs.js +81 -0
  51. package/dist/src/logging/event-logger.specs.js.map +1 -0
  52. package/dist/src/plugins/handler-adapter.d.ts +5 -0
  53. package/dist/src/plugins/handler-adapter.d.ts.map +1 -0
  54. package/dist/src/plugins/handler-adapter.js +17 -0
  55. package/dist/src/plugins/handler-adapter.js.map +1 -0
  56. package/dist/src/plugins/handler-adapter.specs.d.ts +2 -0
  57. package/dist/src/plugins/handler-adapter.specs.d.ts.map +1 -0
  58. package/dist/src/plugins/handler-adapter.specs.js +129 -0
  59. package/dist/src/plugins/handler-adapter.specs.js.map +1 -0
  60. package/dist/src/plugins/plugin-loader.d.ts +25 -0
  61. package/dist/src/plugins/plugin-loader.d.ts.map +1 -0
  62. package/dist/src/plugins/plugin-loader.js +150 -0
  63. package/dist/src/plugins/plugin-loader.js.map +1 -0
  64. package/dist/src/plugins/plugin-loader.specs.d.ts +2 -0
  65. package/dist/src/plugins/plugin-loader.specs.d.ts.map +1 -0
  66. package/dist/src/plugins/plugin-loader.specs.js +246 -0
  67. package/dist/src/plugins/plugin-loader.specs.js.map +1 -0
  68. package/dist/src/runtime/await-tracker.d.ts +10 -0
  69. package/dist/src/runtime/await-tracker.d.ts.map +1 -0
  70. package/dist/src/runtime/await-tracker.js +42 -0
  71. package/dist/src/runtime/await-tracker.js.map +1 -0
  72. package/dist/src/runtime/await-tracker.specs.d.ts +2 -0
  73. package/dist/src/runtime/await-tracker.specs.d.ts.map +1 -0
  74. package/dist/src/runtime/await-tracker.specs.js +46 -0
  75. package/dist/src/runtime/await-tracker.specs.js.map +1 -0
  76. package/dist/src/runtime/context.d.ts +12 -0
  77. package/dist/src/runtime/context.d.ts.map +1 -0
  78. package/dist/src/runtime/context.js +2 -0
  79. package/dist/src/runtime/context.js.map +1 -0
  80. package/dist/src/runtime/context.specs.d.ts +2 -0
  81. package/dist/src/runtime/context.specs.d.ts.map +1 -0
  82. package/dist/src/runtime/context.specs.js +26 -0
  83. package/dist/src/runtime/context.specs.js.map +1 -0
  84. package/dist/src/runtime/event-command-map.d.ts +15 -0
  85. package/dist/src/runtime/event-command-map.d.ts.map +1 -0
  86. package/dist/src/runtime/event-command-map.js +26 -0
  87. package/dist/src/runtime/event-command-map.js.map +1 -0
  88. package/dist/src/runtime/event-command-map.specs.d.ts +2 -0
  89. package/dist/src/runtime/event-command-map.specs.d.ts.map +1 -0
  90. package/dist/src/runtime/event-command-map.specs.js +108 -0
  91. package/dist/src/runtime/event-command-map.specs.js.map +1 -0
  92. package/dist/src/runtime/phased-executor.d.ts +29 -0
  93. package/dist/src/runtime/phased-executor.d.ts.map +1 -0
  94. package/dist/src/runtime/phased-executor.js +164 -0
  95. package/dist/src/runtime/phased-executor.js.map +1 -0
  96. package/dist/src/runtime/phased-executor.specs.d.ts +2 -0
  97. package/dist/src/runtime/phased-executor.specs.d.ts.map +1 -0
  98. package/dist/src/runtime/phased-executor.specs.js +256 -0
  99. package/dist/src/runtime/phased-executor.specs.js.map +1 -0
  100. package/dist/src/runtime/pipeline-runtime.d.ts +17 -0
  101. package/dist/src/runtime/pipeline-runtime.d.ts.map +1 -0
  102. package/dist/src/runtime/pipeline-runtime.js +87 -0
  103. package/dist/src/runtime/pipeline-runtime.js.map +1 -0
  104. package/dist/src/runtime/pipeline-runtime.specs.d.ts +2 -0
  105. package/dist/src/runtime/pipeline-runtime.specs.d.ts.map +1 -0
  106. package/dist/src/runtime/pipeline-runtime.specs.js +192 -0
  107. package/dist/src/runtime/pipeline-runtime.specs.js.map +1 -0
  108. package/dist/src/runtime/settled-tracker.d.ts +42 -0
  109. package/dist/src/runtime/settled-tracker.d.ts.map +1 -0
  110. package/dist/src/runtime/settled-tracker.js +161 -0
  111. package/dist/src/runtime/settled-tracker.js.map +1 -0
  112. package/dist/src/runtime/settled-tracker.specs.d.ts +2 -0
  113. package/dist/src/runtime/settled-tracker.specs.d.ts.map +1 -0
  114. package/dist/src/runtime/settled-tracker.specs.js +361 -0
  115. package/dist/src/runtime/settled-tracker.specs.js.map +1 -0
  116. package/dist/src/server/full-orchestration.e2e.specs.d.ts +2 -0
  117. package/dist/src/server/full-orchestration.e2e.specs.d.ts.map +1 -0
  118. package/dist/src/server/full-orchestration.e2e.specs.js +561 -0
  119. package/dist/src/server/full-orchestration.e2e.specs.js.map +1 -0
  120. package/dist/src/server/pipeline-server.d.ts +59 -0
  121. package/dist/src/server/pipeline-server.d.ts.map +1 -0
  122. package/dist/src/server/pipeline-server.e2e.specs.d.ts +2 -0
  123. package/dist/src/server/pipeline-server.e2e.specs.d.ts.map +1 -0
  124. package/dist/src/server/pipeline-server.e2e.specs.js +381 -0
  125. package/dist/src/server/pipeline-server.e2e.specs.js.map +1 -0
  126. package/dist/src/server/pipeline-server.js +527 -0
  127. package/dist/src/server/pipeline-server.js.map +1 -0
  128. package/dist/src/server/pipeline-server.specs.d.ts +2 -0
  129. package/dist/src/server/pipeline-server.specs.d.ts.map +1 -0
  130. package/dist/src/server/pipeline-server.specs.js +662 -0
  131. package/dist/src/server/pipeline-server.specs.js.map +1 -0
  132. package/dist/src/server/sse-manager.d.ts +12 -0
  133. package/dist/src/server/sse-manager.d.ts.map +1 -0
  134. package/dist/src/server/sse-manager.js +63 -0
  135. package/dist/src/server/sse-manager.js.map +1 -0
  136. package/dist/src/server/sse-manager.specs.d.ts +2 -0
  137. package/dist/src/server/sse-manager.specs.d.ts.map +1 -0
  138. package/dist/src/server/sse-manager.specs.js +158 -0
  139. package/dist/src/server/sse-manager.specs.js.map +1 -0
  140. package/dist/src/testing/event-capture.d.ts +14 -0
  141. package/dist/src/testing/event-capture.d.ts.map +1 -0
  142. package/dist/src/testing/event-capture.js +55 -0
  143. package/dist/src/testing/event-capture.js.map +1 -0
  144. package/dist/src/testing/event-capture.specs.d.ts +2 -0
  145. package/dist/src/testing/event-capture.specs.d.ts.map +1 -0
  146. package/dist/src/testing/event-capture.specs.js +114 -0
  147. package/dist/src/testing/event-capture.specs.js.map +1 -0
  148. package/dist/src/testing/fixtures/kanban-full.pipeline.d.ts +7 -0
  149. package/dist/src/testing/fixtures/kanban-full.pipeline.d.ts.map +1 -0
  150. package/dist/src/testing/fixtures/kanban-full.pipeline.js +168 -0
  151. package/dist/src/testing/fixtures/kanban-full.pipeline.js.map +1 -0
  152. package/dist/src/testing/fixtures/kanban-full.pipeline.specs.d.ts +2 -0
  153. package/dist/src/testing/fixtures/kanban-full.pipeline.specs.d.ts.map +1 -0
  154. package/dist/src/testing/fixtures/kanban-full.pipeline.specs.js +263 -0
  155. package/dist/src/testing/fixtures/kanban-full.pipeline.specs.js.map +1 -0
  156. package/dist/src/testing/fixtures/kanban-todo.config.d.ts +3 -0
  157. package/dist/src/testing/fixtures/kanban-todo.config.d.ts.map +1 -0
  158. package/dist/src/testing/fixtures/kanban-todo.config.js +19 -0
  159. package/dist/src/testing/fixtures/kanban-todo.config.js.map +1 -0
  160. package/dist/src/testing/fixtures/kanban.pipeline.d.ts +5 -0
  161. package/dist/src/testing/fixtures/kanban.pipeline.d.ts.map +1 -0
  162. package/dist/src/testing/fixtures/kanban.pipeline.js +76 -0
  163. package/dist/src/testing/fixtures/kanban.pipeline.js.map +1 -0
  164. package/dist/src/testing/fixtures/kanban.pipeline.specs.d.ts +2 -0
  165. package/dist/src/testing/fixtures/kanban.pipeline.specs.d.ts.map +1 -0
  166. package/dist/src/testing/fixtures/kanban.pipeline.specs.js +29 -0
  167. package/dist/src/testing/fixtures/kanban.pipeline.specs.js.map +1 -0
  168. package/dist/src/testing/kanban-todo.e2e.specs.d.ts +2 -0
  169. package/dist/src/testing/kanban-todo.e2e.specs.d.ts.map +1 -0
  170. package/dist/src/testing/kanban-todo.e2e.specs.js +160 -0
  171. package/dist/src/testing/kanban-todo.e2e.specs.js.map +1 -0
  172. package/dist/src/testing/mock-handlers.d.ts +21 -0
  173. package/dist/src/testing/mock-handlers.d.ts.map +1 -0
  174. package/dist/src/testing/mock-handlers.js +34 -0
  175. package/dist/src/testing/mock-handlers.js.map +1 -0
  176. package/dist/src/testing/mock-handlers.specs.d.ts +2 -0
  177. package/dist/src/testing/mock-handlers.specs.d.ts.map +1 -0
  178. package/dist/src/testing/mock-handlers.specs.js +193 -0
  179. package/dist/src/testing/mock-handlers.specs.js.map +1 -0
  180. package/dist/src/testing/real-execution.e2e.specs.d.ts +2 -0
  181. package/dist/src/testing/real-execution.e2e.specs.d.ts.map +1 -0
  182. package/dist/src/testing/real-execution.e2e.specs.js +140 -0
  183. package/dist/src/testing/real-execution.e2e.specs.js.map +1 -0
  184. package/dist/src/testing/real-plugin.e2e.specs.d.ts +2 -0
  185. package/dist/src/testing/real-plugin.e2e.specs.d.ts.map +1 -0
  186. package/dist/src/testing/real-plugin.e2e.specs.js +65 -0
  187. package/dist/src/testing/real-plugin.e2e.specs.js.map +1 -0
  188. package/dist/src/testing/server-startup.e2e.specs.d.ts +2 -0
  189. package/dist/src/testing/server-startup.e2e.specs.d.ts.map +1 -0
  190. package/dist/src/testing/server-startup.e2e.specs.js +104 -0
  191. package/dist/src/testing/server-startup.e2e.specs.js.map +1 -0
  192. package/dist/src/testing/snapshot-compare.d.ts +18 -0
  193. package/dist/src/testing/snapshot-compare.d.ts.map +1 -0
  194. package/dist/src/testing/snapshot-compare.js +86 -0
  195. package/dist/src/testing/snapshot-compare.js.map +1 -0
  196. package/dist/src/testing/snapshot-compare.specs.d.ts +2 -0
  197. package/dist/src/testing/snapshot-compare.specs.d.ts.map +1 -0
  198. package/dist/src/testing/snapshot-compare.specs.js +112 -0
  199. package/dist/src/testing/snapshot-compare.specs.js.map +1 -0
  200. package/dist/src/testing/snapshot-sanitize.d.ts +8 -0
  201. package/dist/src/testing/snapshot-sanitize.d.ts.map +1 -0
  202. package/dist/src/testing/snapshot-sanitize.js +10 -0
  203. package/dist/src/testing/snapshot-sanitize.js.map +1 -0
  204. package/dist/src/testing/snapshot-sanitize.specs.d.ts +2 -0
  205. package/dist/src/testing/snapshot-sanitize.specs.d.ts.map +1 -0
  206. package/dist/src/testing/snapshot-sanitize.specs.js +104 -0
  207. package/dist/src/testing/snapshot-sanitize.specs.js.map +1 -0
  208. package/dist/tsconfig.tsbuildinfo +1 -0
  209. package/docs/testing-analysis.md +395 -0
  210. package/package.json +31 -0
  211. package/pipeline-api-new.md +1078 -0
  212. package/pomodoro-plan.md +651 -0
  213. package/scripts/run-kanban-e2e.ts +219 -0
  214. package/scripts/start-server.ts +64 -0
  215. package/snapshots/e2e-run-2025-12-22T15-52-03.json +613 -0
  216. package/snapshots/e2e-run-2025-12-22T16-51-30.json +699 -0
  217. package/src/builder/define.specs.ts +531 -0
  218. package/src/builder/define.ts +700 -0
  219. package/src/config/pipeline-config.ts +32 -0
  220. package/src/core/descriptors.specs.ts +28 -0
  221. package/src/core/descriptors.ts +99 -0
  222. package/src/core/types.specs.ts +44 -0
  223. package/src/core/types.ts +16 -0
  224. package/src/graph/types.specs.ts +176 -0
  225. package/src/graph/types.ts +19 -0
  226. package/src/index.ts +54 -0
  227. package/src/logging/event-logger.specs.ts +100 -0
  228. package/src/logging/event-logger.ts +50 -0
  229. package/src/plugins/handler-adapter.specs.ts +164 -0
  230. package/src/plugins/handler-adapter.ts +21 -0
  231. package/src/plugins/plugin-loader.specs.ts +295 -0
  232. package/src/plugins/plugin-loader.ts +202 -0
  233. package/src/runtime/await-tracker.specs.ts +52 -0
  234. package/src/runtime/await-tracker.ts +50 -0
  235. package/src/runtime/context.specs.ts +28 -0
  236. package/src/runtime/context.ts +13 -0
  237. package/src/runtime/event-command-map.specs.ts +136 -0
  238. package/src/runtime/event-command-map.ts +38 -0
  239. package/src/runtime/phased-executor.specs.ts +358 -0
  240. package/src/runtime/phased-executor.ts +224 -0
  241. package/src/runtime/pipeline-runtime.specs.ts +214 -0
  242. package/src/runtime/pipeline-runtime.ts +119 -0
  243. package/src/runtime/settled-tracker.specs.ts +448 -0
  244. package/src/runtime/settled-tracker.ts +237 -0
  245. package/src/server/full-orchestration.e2e.specs.ts +672 -0
  246. package/src/server/pipeline-server.e2e.specs.ts +505 -0
  247. package/src/server/pipeline-server.specs.ts +761 -0
  248. package/src/server/pipeline-server.ts +656 -0
  249. package/src/server/sse-manager.specs.ts +208 -0
  250. package/src/server/sse-manager.ts +79 -0
  251. package/src/testing/event-capture.specs.ts +143 -0
  252. package/src/testing/event-capture.ts +65 -0
  253. package/src/testing/fixtures/kanban-full.pipeline.specs.ts +337 -0
  254. package/src/testing/fixtures/kanban-full.pipeline.ts +225 -0
  255. package/src/testing/fixtures/kanban-todo.config.ts +19 -0
  256. package/src/testing/fixtures/kanban.pipeline.specs.ts +33 -0
  257. package/src/testing/fixtures/kanban.pipeline.ts +124 -0
  258. package/src/testing/kanban-todo.e2e.specs.ts +209 -0
  259. package/src/testing/mock-handlers.specs.ts +229 -0
  260. package/src/testing/mock-handlers.ts +58 -0
  261. package/src/testing/real-execution.e2e.specs.ts +193 -0
  262. package/src/testing/real-plugin.e2e.specs.ts +94 -0
  263. package/src/testing/server-startup.e2e.specs.ts +162 -0
  264. package/src/testing/snapshot-compare.specs.ts +136 -0
  265. package/src/testing/snapshot-compare.ts +106 -0
  266. package/src/testing/snapshot-sanitize.specs.ts +131 -0
  267. package/src/testing/snapshot-sanitize.ts +17 -0
  268. package/tsconfig.json +11 -0
  269. package/tsconfig.test.json +9 -0
  270. package/vitest.config.ts +29 -0
@@ -0,0 +1,672 @@
1
+ import type { Command, Event } from '@auto-engineer/message-bus';
2
+ import { define } from '../builder/define';
3
+ import { createKanbanPipeline, resetRetryState } from '../testing/fixtures/kanban.pipeline';
4
+ import {
5
+ createMockHandlers,
6
+ createStatefulHandler,
7
+ getHandlerCallCount,
8
+ resetCallCounts,
9
+ } from '../testing/mock-handlers';
10
+ import { containsSubsequence, findMissingEvents } from '../testing/snapshot-compare';
11
+ import { type CommandHandlerWithMetadata, PipelineServer } from './pipeline-server';
12
+
13
+ interface StoredMessage {
14
+ message: { type: string; data?: Record<string, unknown> };
15
+ messageType: string;
16
+ }
17
+
18
+ async function fetchJson<T>(url: string, options?: RequestInit): Promise<T> {
19
+ const res = await fetch(url, options);
20
+ return res.json() as Promise<T>;
21
+ }
22
+
23
+ function delay(ms: number): Promise<void> {
24
+ return new Promise((resolve) => setTimeout(resolve, ms));
25
+ }
26
+
27
+ describe('Full Orchestration E2E', () => {
28
+ beforeEach(() => {
29
+ resetRetryState();
30
+ resetCallCounts();
31
+ });
32
+
33
+ describe('Kanban pipeline definition', () => {
34
+ it('should build kanban pipeline with all handler types', () => {
35
+ const pipeline = createKanbanPipeline();
36
+
37
+ expect(pipeline.descriptor.name).toBe('kanban');
38
+ expect(pipeline.descriptor.handlers.length).toBeGreaterThan(0);
39
+
40
+ const handlerTypes = pipeline.descriptor.handlers.map((h) => h.type);
41
+ expect(handlerTypes).toContain('emit');
42
+ expect(handlerTypes).toContain('settled');
43
+ expect(handlerTypes).toContain('foreach-phased');
44
+ });
45
+
46
+ it('should generate graph with nodes and edges', () => {
47
+ const pipeline = createKanbanPipeline();
48
+ const graph = pipeline.toGraph();
49
+
50
+ expect(graph.nodes.length).toBeGreaterThan(0);
51
+ expect(graph.edges.length).toBeGreaterThan(0);
52
+
53
+ const nodeIds = graph.nodes.map((n) => n.id);
54
+ expect(nodeIds).toContain('evt:SchemaExported');
55
+ expect(nodeIds).toContain('cmd:GenerateServer');
56
+ expect(nodeIds).toContain('evt:SliceImplemented');
57
+ expect(nodeIds).toContain('evt:AllComponentsImplemented');
58
+ });
59
+ });
60
+
61
+ describe('scatter-gather workflow', () => {
62
+ it('should execute scatter-gather with settled handler', async () => {
63
+ let settledCalled = false;
64
+ let checkEvents: Record<string, Event[]> = {};
65
+
66
+ const handlers = createMockHandlers([
67
+ {
68
+ name: 'Start',
69
+ events: ['Started'],
70
+ fn: () => ({ type: 'Started', data: {} }),
71
+ },
72
+ {
73
+ name: 'CheckA',
74
+ events: ['AChecked'],
75
+ fn: () => ({ type: 'AChecked', data: { result: 'pass' } }),
76
+ },
77
+ {
78
+ name: 'CheckB',
79
+ events: ['BChecked'],
80
+ fn: () => ({ type: 'BChecked', data: { result: 'pass' } }),
81
+ },
82
+ {
83
+ name: 'CheckC',
84
+ events: ['CChecked'],
85
+ fn: () => ({ type: 'CChecked', data: { result: 'pass' } }),
86
+ },
87
+ ]);
88
+
89
+ const pipeline = define('scatter-gather')
90
+ .on('Started')
91
+ .emit('CheckA', {})
92
+ .emit('CheckB', {})
93
+ .emit('CheckC', {})
94
+ .settled(['CheckA', 'CheckB', 'CheckC'])
95
+ .dispatch({ dispatches: [] }, (events) => {
96
+ settledCalled = true;
97
+ checkEvents = events;
98
+ })
99
+ .build();
100
+
101
+ const server = new PipelineServer({ port: 0 });
102
+ server.registerCommandHandlers(handlers);
103
+ server.registerPipeline(pipeline);
104
+ await server.start();
105
+
106
+ await fetchJson(`http://localhost:${server.port}/command`, {
107
+ method: 'POST',
108
+ headers: { 'Content-Type': 'application/json' },
109
+ body: JSON.stringify({ type: 'Start', data: {} }),
110
+ });
111
+
112
+ await delay(300);
113
+
114
+ expect(settledCalled).toBe(true);
115
+ expect(checkEvents.CheckA).toHaveLength(1);
116
+ expect(checkEvents.CheckB).toHaveLength(1);
117
+ expect(checkEvents.CheckC).toHaveLength(1);
118
+
119
+ await server.stop();
120
+ });
121
+ });
122
+
123
+ describe('retry with persist pattern', () => {
124
+ it('should retry failed checks with persist: true', async () => {
125
+ const checkHandler = createStatefulHandler({
126
+ name: 'RunCheck',
127
+ events: ['CheckPassed', 'CheckFailed'],
128
+ initialFails: 2,
129
+ failEvent: (cmd: Command) => ({
130
+ type: 'CheckFailed',
131
+ data: { target: (cmd.data as { target?: string }).target ?? 'unknown', error: 'something went wrong' },
132
+ }),
133
+ successEvent: (cmd: Command) => ({
134
+ type: 'CheckPassed',
135
+ data: { target: (cmd.data as { target?: string }).target ?? 'unknown' },
136
+ }),
137
+ });
138
+
139
+ const startHandler: CommandHandlerWithMetadata = {
140
+ name: 'Start',
141
+ events: ['Started'],
142
+ handle: async () => ({ type: 'Started', data: { target: './src' } }),
143
+ };
144
+
145
+ let settledCallCount = 0;
146
+
147
+ const pipeline = define('retry-persist')
148
+ .on('Started')
149
+ .emit('RunCheck', (e: { data: { target: string } }) => ({ target: e.data.target }))
150
+ .settled(['RunCheck'])
151
+ .dispatch({ dispatches: ['RunCheck'] }, (events, send) => {
152
+ settledCallCount++;
153
+ const hasFailure = (events.RunCheck ?? []).some((e) => e.type === 'CheckFailed');
154
+
155
+ if (hasFailure && settledCallCount < 3) {
156
+ const target = (events.RunCheck[0]?.data as { target?: string })?.target ?? './src';
157
+ send('RunCheck', { target, retryAttempt: settledCallCount });
158
+ return { persist: true };
159
+ }
160
+ })
161
+ .build();
162
+
163
+ const server = new PipelineServer({ port: 0 });
164
+ server.registerCommandHandlers([startHandler, checkHandler]);
165
+ server.registerPipeline(pipeline);
166
+ await server.start();
167
+
168
+ await fetchJson(`http://localhost:${server.port}/command`, {
169
+ method: 'POST',
170
+ headers: { 'Content-Type': 'application/json' },
171
+ body: JSON.stringify({ type: 'Start', data: {} }),
172
+ });
173
+
174
+ await delay(500);
175
+
176
+ const messages = await fetchJson<StoredMessage[]>(`http://localhost:${server.port}/messages`);
177
+ const eventTypes = messages.filter((m) => m.messageType === 'event').map((m) => m.message.type);
178
+
179
+ expect(eventTypes.filter((t) => t === 'CheckFailed')).toHaveLength(2);
180
+ expect(eventTypes).toContain('CheckPassed');
181
+ expect(settledCallCount).toBe(3);
182
+
183
+ await server.stop();
184
+ });
185
+ });
186
+
187
+ describe('phased execution workflow', () => {
188
+ it('should execute components in phase order', async () => {
189
+ const executionOrder: string[] = [];
190
+
191
+ interface Component {
192
+ id: string;
193
+ type: 'molecule' | 'organism' | 'page';
194
+ }
195
+
196
+ const handlers: CommandHandlerWithMetadata[] = [
197
+ {
198
+ name: 'GenerateClient',
199
+ events: ['ClientGenerated'],
200
+ handle: async () => ({
201
+ type: 'ClientGenerated',
202
+ data: {
203
+ components: [
204
+ { id: 'page1', type: 'page' },
205
+ { id: 'mol1', type: 'molecule' },
206
+ { id: 'org1', type: 'organism' },
207
+ { id: 'mol2', type: 'molecule' },
208
+ { id: 'org2', type: 'organism' },
209
+ ],
210
+ },
211
+ }),
212
+ },
213
+ {
214
+ name: 'ImplementComponent',
215
+ events: ['ComponentImplemented'],
216
+ handle: async (cmd) => {
217
+ const filePath = (cmd.data as { filePath: string }).filePath;
218
+ executionOrder.push(filePath);
219
+ return { type: 'ComponentImplemented', data: { filePath } };
220
+ },
221
+ },
222
+ ];
223
+
224
+ const pipeline = define('phased')
225
+ .on('ClientGenerated')
226
+ .forEach((e: { data: { components: Component[] } }) => e.data.components)
227
+ .groupInto(['molecule', 'organism', 'page'], (c: Component) => c.type)
228
+ .process('ImplementComponent', (c: Component) => ({ filePath: c.id }))
229
+ .onComplete({
230
+ success: 'AllComponentsImplemented',
231
+ failure: 'ComponentsFailed',
232
+ itemKey: (e) => (e.data as { filePath?: string; id?: string }).filePath ?? (e.data as { id: string }).id,
233
+ })
234
+ .build();
235
+
236
+ const server = new PipelineServer({ port: 0 });
237
+ server.registerCommandHandlers(handlers);
238
+ server.registerPipeline(pipeline);
239
+ await server.start();
240
+
241
+ await fetchJson(`http://localhost:${server.port}/command`, {
242
+ method: 'POST',
243
+ headers: { 'Content-Type': 'application/json' },
244
+ body: JSON.stringify({ type: 'GenerateClient', data: {} }),
245
+ });
246
+
247
+ await delay(600);
248
+
249
+ expect(executionOrder).toEqual(['mol1', 'mol2', 'org1', 'org2', 'page1']);
250
+
251
+ const messages = await fetchJson<StoredMessage[]>(`http://localhost:${server.port}/messages`);
252
+ const eventTypes = messages.filter((m) => m.messageType === 'event').map((m) => m.message.type);
253
+
254
+ expect(eventTypes).toContain('AllComponentsImplemented');
255
+
256
+ await server.stop();
257
+ });
258
+ });
259
+
260
+ describe('complete kanban workflow', () => {
261
+ it('should execute full kanban workflow with mock handlers', async () => {
262
+ const handlers = createMockHandlers([
263
+ {
264
+ name: 'ExportSchema',
265
+ events: ['SchemaExported'],
266
+ fn: () => ({ type: 'SchemaExported', data: { outputPath: './schema.json' } }),
267
+ },
268
+ {
269
+ name: 'GenerateServer',
270
+ events: ['ServerGenerated', 'SliceGenerated'],
271
+ fn: () => [
272
+ { type: 'SliceGenerated', data: { slicePath: './adds-todo' } },
273
+ { type: 'ServerGenerated', data: { modelPath: './schema.json' } },
274
+ ],
275
+ },
276
+ {
277
+ name: 'ImplementSlice',
278
+ events: ['SliceImplemented'],
279
+ fn: (cmd) => ({
280
+ type: 'SliceImplemented',
281
+ data: { slicePath: (cmd.data as { slicePath: string }).slicePath },
282
+ }),
283
+ },
284
+ {
285
+ name: 'CheckTests',
286
+ events: ['TestsCheckPassed', 'TestsCheckFailed'],
287
+ fn: () => ({ type: 'TestsCheckPassed', data: {} }),
288
+ },
289
+ {
290
+ name: 'CheckTypes',
291
+ events: ['TypeCheckPassed', 'TypeCheckFailed'],
292
+ fn: () => ({ type: 'TypeCheckPassed', data: {} }),
293
+ },
294
+ {
295
+ name: 'CheckLint',
296
+ events: ['LintCheckPassed', 'LintCheckFailed'],
297
+ fn: () => ({ type: 'LintCheckPassed', data: {} }),
298
+ },
299
+ {
300
+ name: 'GenerateIA',
301
+ events: ['IAGenerated'],
302
+ fn: () => ({ type: 'IAGenerated', data: {} }),
303
+ },
304
+ {
305
+ name: 'StartServer',
306
+ events: ['ServerStarted'],
307
+ fn: () => ({ type: 'ServerStarted', data: {} }),
308
+ },
309
+ {
310
+ name: 'GenerateClient',
311
+ events: ['ClientGenerated'],
312
+ fn: () => ({
313
+ type: 'ClientGenerated',
314
+ data: {
315
+ components: [
316
+ { id: 'm1', type: 'molecule', filePath: 'm1.tsx' },
317
+ { id: 'o1', type: 'organism', filePath: 'o1.tsx' },
318
+ { id: 'p1', type: 'page', filePath: 'p1.tsx' },
319
+ ],
320
+ },
321
+ }),
322
+ },
323
+ {
324
+ name: 'ImplementComponent',
325
+ events: ['ComponentImplemented'],
326
+ fn: (cmd) => ({
327
+ type: 'ComponentImplemented',
328
+ data: { filePath: (cmd.data as { filePath: string }).filePath },
329
+ }),
330
+ },
331
+ ]);
332
+
333
+ const pipeline = createKanbanPipeline();
334
+
335
+ const server = new PipelineServer({ port: 0 });
336
+ server.registerCommandHandlers(handlers);
337
+ server.registerPipeline(pipeline);
338
+ await server.start();
339
+
340
+ await fetchJson(`http://localhost:${server.port}/command`, {
341
+ method: 'POST',
342
+ headers: { 'Content-Type': 'application/json' },
343
+ body: JSON.stringify({ type: 'ExportSchema', data: {} }),
344
+ });
345
+
346
+ await delay(800);
347
+
348
+ const messages = await fetchJson<StoredMessage[]>(`http://localhost:${server.port}/messages`);
349
+ const eventTypes = messages.filter((m) => m.messageType === 'event').map((m) => m.message.type);
350
+
351
+ const expectedSubsequence = [
352
+ 'SchemaExported',
353
+ 'SliceGenerated',
354
+ 'SliceImplemented',
355
+ 'TestsCheckPassed',
356
+ 'TypeCheckPassed',
357
+ 'LintCheckPassed',
358
+ ];
359
+
360
+ expect(containsSubsequence(eventTypes, expectedSubsequence)).toBe(true);
361
+ expect(eventTypes).toContain('IAGenerated');
362
+ expect(eventTypes).toContain('ClientGenerated');
363
+ expect(eventTypes).toContain('AllComponentsImplemented');
364
+
365
+ const missingEvents = findMissingEvents(eventTypes, [
366
+ 'SchemaExported',
367
+ 'SliceGenerated',
368
+ 'SliceImplemented',
369
+ 'ServerGenerated',
370
+ 'IAGenerated',
371
+ 'ClientGenerated',
372
+ 'AllComponentsImplemented',
373
+ ]);
374
+
375
+ expect(missingEvents).toHaveLength(0);
376
+
377
+ await server.stop();
378
+ });
379
+
380
+ it('should handle retry scenario in kanban workflow', async () => {
381
+ let typeCheckCallCount = 0;
382
+
383
+ const handlers = createMockHandlers([
384
+ {
385
+ name: 'ExportSchema',
386
+ events: ['SchemaExported'],
387
+ fn: () => ({ type: 'SchemaExported', data: { outputPath: './schema.json' } }),
388
+ },
389
+ {
390
+ name: 'GenerateServer',
391
+ events: ['ServerGenerated', 'SliceGenerated'],
392
+ fn: () => [
393
+ { type: 'SliceGenerated', data: { slicePath: './adds-todo' } },
394
+ { type: 'ServerGenerated', data: { modelPath: './schema.json' } },
395
+ ],
396
+ },
397
+ {
398
+ name: 'ImplementSlice',
399
+ events: ['SliceImplemented'],
400
+ fn: (cmd) => ({
401
+ type: 'SliceImplemented',
402
+ data: { slicePath: (cmd.data as { slicePath: string }).slicePath },
403
+ }),
404
+ },
405
+ {
406
+ name: 'CheckTests',
407
+ events: ['TestsCheckPassed', 'TestsCheckFailed'],
408
+ fn: () => ({ type: 'TestsCheckPassed', data: { target: './adds-todo' } }),
409
+ },
410
+ {
411
+ name: 'CheckTypes',
412
+ events: ['TypeCheckPassed', 'TypeCheckFailed'],
413
+ fn: () => {
414
+ typeCheckCallCount++;
415
+ if (typeCheckCallCount < 3) {
416
+ return { type: 'TypeCheckFailed', data: { target: './adds-todo', error: 'TS2322' } };
417
+ }
418
+ return { type: 'TypeCheckPassed', data: { target: './adds-todo' } };
419
+ },
420
+ },
421
+ {
422
+ name: 'CheckLint',
423
+ events: ['LintCheckPassed', 'LintCheckFailed'],
424
+ fn: () => ({ type: 'LintCheckPassed', data: { target: './adds-todo' } }),
425
+ },
426
+ {
427
+ name: 'GenerateIA',
428
+ events: ['IAGenerated'],
429
+ fn: () => ({ type: 'IAGenerated', data: {} }),
430
+ },
431
+ {
432
+ name: 'StartServer',
433
+ events: ['ServerStarted'],
434
+ fn: () => ({ type: 'ServerStarted', data: {} }),
435
+ },
436
+ {
437
+ name: 'GenerateClient',
438
+ events: ['ClientGenerated'],
439
+ fn: () => ({
440
+ type: 'ClientGenerated',
441
+ data: { components: [{ id: 'm1', type: 'molecule', filePath: 'm1.tsx' }] },
442
+ }),
443
+ },
444
+ {
445
+ name: 'ImplementComponent',
446
+ events: ['ComponentImplemented'],
447
+ fn: (cmd) => ({
448
+ type: 'ComponentImplemented',
449
+ data: { filePath: (cmd.data as { filePath: string }).filePath },
450
+ }),
451
+ },
452
+ ]);
453
+
454
+ const pipeline = createKanbanPipeline();
455
+
456
+ const server = new PipelineServer({ port: 0 });
457
+ server.registerCommandHandlers(handlers);
458
+ server.registerPipeline(pipeline);
459
+ await server.start();
460
+
461
+ await fetchJson(`http://localhost:${server.port}/command`, {
462
+ method: 'POST',
463
+ headers: { 'Content-Type': 'application/json' },
464
+ body: JSON.stringify({ type: 'ExportSchema', data: {} }),
465
+ });
466
+
467
+ await delay(1000);
468
+
469
+ const messages = await fetchJson<StoredMessage[]>(`http://localhost:${server.port}/messages`);
470
+ const eventTypes = messages.filter((m) => m.messageType === 'event').map((m) => m.message.type);
471
+
472
+ const typeCheckFailCount = eventTypes.filter((t) => t === 'TypeCheckFailed').length;
473
+ expect(typeCheckFailCount).toBe(2);
474
+ expect(eventTypes).toContain('TypeCheckPassed');
475
+
476
+ expect(getHandlerCallCount('ImplementSlice')).toBe(3);
477
+
478
+ await server.stop();
479
+ });
480
+ });
481
+
482
+ describe('SSE streaming verification', () => {
483
+ it('should receive events via SSE endpoint', async () => {
484
+ const handlers = createMockHandlers([
485
+ {
486
+ name: 'Start',
487
+ events: ['Started'],
488
+ fn: () => ({ type: 'Started', data: { message: 'workflow started' } }),
489
+ },
490
+ {
491
+ name: 'Process',
492
+ events: ['Processed'],
493
+ fn: () => ({ type: 'Processed', data: { message: 'work done' } }),
494
+ },
495
+ ]);
496
+
497
+ const pipeline = define('sse-test').on('Started').emit('Process', {}).build();
498
+
499
+ const server = new PipelineServer({ port: 0 });
500
+ server.registerCommandHandlers(handlers);
501
+ server.registerPipeline(pipeline);
502
+ await server.start();
503
+
504
+ const sseEvents: Event[] = [];
505
+
506
+ const controller = new AbortController();
507
+ const ssePromise = fetch(`http://localhost:${server.port}/events`, {
508
+ signal: controller.signal,
509
+ }).then(async (response) => {
510
+ const reader = response.body?.getReader();
511
+ if (reader === undefined) return;
512
+
513
+ const decoder = new TextDecoder();
514
+ let buffer = '';
515
+
516
+ while (true) {
517
+ const { done, value } = await reader.read();
518
+ if (done) break;
519
+
520
+ buffer += decoder.decode(value, { stream: true });
521
+
522
+ const lines = buffer.split('\n\n');
523
+ buffer = lines.pop() ?? '';
524
+
525
+ for (const line of lines) {
526
+ if (line.startsWith('data: ')) {
527
+ const jsonStr = line.slice(6);
528
+ const event = JSON.parse(jsonStr) as Event;
529
+ sseEvents.push(event);
530
+ }
531
+ }
532
+ }
533
+ });
534
+
535
+ await delay(100);
536
+
537
+ await fetchJson(`http://localhost:${server.port}/command`, {
538
+ method: 'POST',
539
+ headers: { 'Content-Type': 'application/json' },
540
+ body: JSON.stringify({ type: 'Start', data: {} }),
541
+ });
542
+
543
+ await delay(300);
544
+
545
+ controller.abort();
546
+ await ssePromise.catch(() => {});
547
+
548
+ const sseEventTypes = sseEvents.map((e) => e.type);
549
+ expect(sseEventTypes).toContain('Started');
550
+ expect(sseEventTypes).toContain('Processed');
551
+
552
+ await server.stop();
553
+ });
554
+
555
+ it('should filter SSE events by correlationId', async () => {
556
+ const handlers = createMockHandlers([
557
+ {
558
+ name: 'Start',
559
+ events: ['Started'],
560
+ fn: () => ({ type: 'Started', data: {} }),
561
+ },
562
+ ]);
563
+
564
+ const server = new PipelineServer({ port: 0 });
565
+ server.registerCommandHandlers(handlers);
566
+ await server.start();
567
+
568
+ const filteredEvents: Event[] = [];
569
+ const targetCorrelationId = 'workflow-123';
570
+
571
+ const controller = new AbortController();
572
+ const ssePromise = fetch(`http://localhost:${server.port}/events?correlationId=${targetCorrelationId}`, {
573
+ signal: controller.signal,
574
+ }).then(async (response) => {
575
+ const reader = response.body?.getReader();
576
+ if (reader === undefined) return;
577
+
578
+ const decoder = new TextDecoder();
579
+ let buffer = '';
580
+
581
+ while (true) {
582
+ const { done, value } = await reader.read();
583
+ if (done) break;
584
+
585
+ buffer += decoder.decode(value, { stream: true });
586
+
587
+ const lines = buffer.split('\n\n');
588
+ buffer = lines.pop() ?? '';
589
+
590
+ for (const line of lines) {
591
+ if (line.startsWith('data: ')) {
592
+ const jsonStr = line.slice(6);
593
+ const event = JSON.parse(jsonStr) as Event;
594
+ filteredEvents.push(event);
595
+ }
596
+ }
597
+ }
598
+ });
599
+
600
+ await delay(100);
601
+
602
+ await fetchJson(`http://localhost:${server.port}/command`, {
603
+ method: 'POST',
604
+ headers: { 'Content-Type': 'application/json' },
605
+ body: JSON.stringify({ type: 'Start', correlationId: targetCorrelationId, data: {} }),
606
+ });
607
+
608
+ await fetchJson(`http://localhost:${server.port}/command`, {
609
+ method: 'POST',
610
+ headers: { 'Content-Type': 'application/json' },
611
+ body: JSON.stringify({ type: 'Start', correlationId: 'other-workflow', data: {} }),
612
+ });
613
+
614
+ await delay(300);
615
+
616
+ controller.abort();
617
+ await ssePromise.catch(() => {});
618
+
619
+ expect(filteredEvents.every((e) => e.correlationId === targetCorrelationId)).toBe(true);
620
+
621
+ await server.stop();
622
+ });
623
+ });
624
+
625
+ describe('event sequence snapshot verification', () => {
626
+ it('should produce expected event sequence for simple workflow', async () => {
627
+ const handlers = createMockHandlers([
628
+ {
629
+ name: 'A',
630
+ events: ['ADone'],
631
+ fn: () => ({ type: 'ADone', data: {} }),
632
+ },
633
+ {
634
+ name: 'B',
635
+ events: ['BDone'],
636
+ fn: () => ({ type: 'BDone', data: {} }),
637
+ },
638
+ {
639
+ name: 'C',
640
+ events: ['CDone'],
641
+ fn: () => ({ type: 'CDone', data: {} }),
642
+ },
643
+ ]);
644
+
645
+ const pipeline = define('sequence').on('ADone').emit('B', {}).on('BDone').emit('C', {}).build();
646
+
647
+ const server = new PipelineServer({ port: 0 });
648
+ server.registerCommandHandlers(handlers);
649
+ server.registerPipeline(pipeline);
650
+ await server.start();
651
+
652
+ await fetchJson(`http://localhost:${server.port}/command`, {
653
+ method: 'POST',
654
+ headers: { 'Content-Type': 'application/json' },
655
+ body: JSON.stringify({ type: 'A', data: {} }),
656
+ });
657
+
658
+ await delay(300);
659
+
660
+ const messages = await fetchJson<StoredMessage[]>(`http://localhost:${server.port}/messages`);
661
+ const eventTypes = messages.filter((m) => m.messageType === 'event').map((m) => m.message.type);
662
+
663
+ const expectedSequence = ['ADone', 'BDone', 'CDone'];
664
+ expect(containsSubsequence(eventTypes, expectedSequence)).toBe(true);
665
+
666
+ const missing = findMissingEvents(eventTypes, expectedSequence);
667
+ expect(missing).toHaveLength(0);
668
+
669
+ await server.stop();
670
+ });
671
+ });
672
+ });