@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,52 @@
1
+ import { AwaitTracker } from './await-tracker';
2
+
3
+ describe('AwaitTracker', () => {
4
+ it('should track pending keys', () => {
5
+ const tracker = new AwaitTracker();
6
+ tracker.startAwaiting('corr-1', ['a', 'b']);
7
+ expect(tracker.isPending('corr-1')).toBe(true);
8
+ expect(tracker.getPendingKeys('corr-1')).toEqual(['a', 'b']);
9
+ });
10
+
11
+ it('should detect completion', () => {
12
+ const tracker = new AwaitTracker();
13
+ tracker.startAwaiting('c', ['a', 'b']);
14
+ tracker.markComplete('c', 'a', { result: 1 });
15
+ expect(tracker.isComplete('c')).toBe(false);
16
+ tracker.markComplete('c', 'b', { result: 2 });
17
+ expect(tracker.isComplete('c')).toBe(true);
18
+ });
19
+
20
+ it('should return false for unknown correlationId', () => {
21
+ const tracker = new AwaitTracker();
22
+ expect(tracker.isPending('unknown')).toBe(false);
23
+ expect(tracker.isComplete('unknown')).toBe(false);
24
+ });
25
+
26
+ it('should return empty array for unknown correlationId keys', () => {
27
+ const tracker = new AwaitTracker();
28
+ expect(tracker.getPendingKeys('unknown')).toEqual([]);
29
+ });
30
+
31
+ it('should collect results when all keys complete', () => {
32
+ const tracker = new AwaitTracker();
33
+ tracker.startAwaiting('c', ['x', 'y']);
34
+ tracker.markComplete('c', 'x', { val: 1 });
35
+ tracker.markComplete('c', 'y', { val: 2 });
36
+ const results = tracker.getResults('c');
37
+ expect(results).toEqual({ x: { val: 1 }, y: { val: 2 } });
38
+ });
39
+
40
+ it('should clear tracking after getting results', () => {
41
+ const tracker = new AwaitTracker();
42
+ tracker.startAwaiting('c', ['a']);
43
+ tracker.markComplete('c', 'a', {});
44
+ tracker.getResults('c');
45
+ expect(tracker.isPending('c')).toBe(false);
46
+ });
47
+
48
+ it('should return empty object for unknown correlationId results', () => {
49
+ const tracker = new AwaitTracker();
50
+ expect(tracker.getResults('unknown')).toEqual({});
51
+ });
52
+ });
@@ -0,0 +1,50 @@
1
+ interface AwaitState {
2
+ pendingKeys: Set<string>;
3
+ results: Map<string, unknown>;
4
+ }
5
+
6
+ export class AwaitTracker {
7
+ private readonly state = new Map<string, AwaitState>();
8
+
9
+ startAwaiting(correlationId: string, keys: string[]): void {
10
+ this.state.set(correlationId, {
11
+ pendingKeys: new Set(keys),
12
+ results: new Map(),
13
+ });
14
+ }
15
+
16
+ isPending(correlationId: string): boolean {
17
+ return this.state.has(correlationId);
18
+ }
19
+
20
+ getPendingKeys(correlationId: string): string[] {
21
+ const awaitState = this.state.get(correlationId);
22
+ return awaitState ? Array.from(awaitState.pendingKeys) : [];
23
+ }
24
+
25
+ markComplete(correlationId: string, key: string, result: unknown): void {
26
+ const awaitState = this.state.get(correlationId);
27
+ if (awaitState) {
28
+ awaitState.pendingKeys.delete(key);
29
+ awaitState.results.set(key, result);
30
+ }
31
+ }
32
+
33
+ isComplete(correlationId: string): boolean {
34
+ const awaitState = this.state.get(correlationId);
35
+ return awaitState ? awaitState.pendingKeys.size === 0 : false;
36
+ }
37
+
38
+ getResults(correlationId: string): Record<string, unknown> {
39
+ const awaitState = this.state.get(correlationId);
40
+ if (!awaitState) {
41
+ return {};
42
+ }
43
+ const results: Record<string, unknown> = {};
44
+ for (const [key, value] of awaitState.results) {
45
+ results[key] = value;
46
+ }
47
+ this.state.delete(correlationId);
48
+ return results;
49
+ }
50
+ }
@@ -0,0 +1,28 @@
1
+ import type { PipelineContext, RuntimeConfig } from './context';
2
+
3
+ describe('PipelineContext', () => {
4
+ it('should define PipelineContext interface', () => {
5
+ const ctx: PipelineContext = {
6
+ emit: async () => {},
7
+ sendCommand: async () => {},
8
+ correlationId: 'test-id',
9
+ };
10
+ expect(typeof ctx.emit).toBe('function');
11
+ expect(typeof ctx.sendCommand).toBe('function');
12
+ expect(ctx.correlationId).toBe('test-id');
13
+ });
14
+ });
15
+
16
+ describe('RuntimeConfig', () => {
17
+ it('should define RuntimeConfig interface', () => {
18
+ const config: RuntimeConfig = {
19
+ defaultTimeout: 30000,
20
+ };
21
+ expect(config.defaultTimeout).toBe(30000);
22
+ });
23
+
24
+ it('should allow optional defaultTimeout', () => {
25
+ const config: RuntimeConfig = {};
26
+ expect(config.defaultTimeout).toBeUndefined();
27
+ });
28
+ });
@@ -0,0 +1,13 @@
1
+ import type { Event } from '@auto-engineer/message-bus';
2
+ import type { ForEachPhasedDescriptor } from '../core/descriptors';
3
+
4
+ export interface PipelineContext {
5
+ emit: (type: string, data: unknown) => Promise<void>;
6
+ sendCommand: (type: string, data: unknown) => Promise<void>;
7
+ correlationId: string;
8
+ startPhased?: (handler: ForEachPhasedDescriptor, event: Event) => void;
9
+ }
10
+
11
+ export interface RuntimeConfig {
12
+ defaultTimeout?: number;
13
+ }
@@ -0,0 +1,136 @@
1
+ import type { CommandHandler } from '@auto-engineer/message-bus';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { EventCommandMapper } from './event-command-map';
4
+
5
+ interface CommandHandlerWithEvents extends CommandHandler {
6
+ events?: readonly string[];
7
+ }
8
+
9
+ describe('EventCommandMapper', () => {
10
+ describe('basic mapping', () => {
11
+ it('should map events to source commands from handler metadata', () => {
12
+ const handlers: CommandHandlerWithEvents[] = [
13
+ {
14
+ name: 'CheckTests',
15
+ events: ['TestsCheckPassed', 'TestsCheckFailed'],
16
+ handle: async () => ({ type: 'TestsCheckPassed', data: {} }),
17
+ },
18
+ {
19
+ name: 'CheckTypes',
20
+ events: ['TypeCheckPassed', 'TypeCheckFailed'],
21
+ handle: async () => ({ type: 'TypeCheckPassed', data: {} }),
22
+ },
23
+ ];
24
+
25
+ const mapper = new EventCommandMapper(handlers);
26
+
27
+ expect(mapper.getSourceCommand('TestsCheckPassed')).toBe('CheckTests');
28
+ expect(mapper.getSourceCommand('TestsCheckFailed')).toBe('CheckTests');
29
+ expect(mapper.getSourceCommand('TypeCheckPassed')).toBe('CheckTypes');
30
+ expect(mapper.getSourceCommand('TypeCheckFailed')).toBe('CheckTypes');
31
+ });
32
+
33
+ it('should return undefined for unknown events', () => {
34
+ const handlers: CommandHandlerWithEvents[] = [
35
+ {
36
+ name: 'CheckTests',
37
+ events: ['TestsCheckPassed'],
38
+ handle: async () => ({ type: 'TestsCheckPassed', data: {} }),
39
+ },
40
+ ];
41
+
42
+ const mapper = new EventCommandMapper(handlers);
43
+
44
+ expect(mapper.getSourceCommand('UnknownEvent')).toBeUndefined();
45
+ });
46
+
47
+ it('should handle handlers without events array', () => {
48
+ const handlers: CommandHandlerWithEvents[] = [
49
+ {
50
+ name: 'SomeCommand',
51
+ handle: async () => ({ type: 'SomeEvent', data: {} }),
52
+ },
53
+ ];
54
+
55
+ const mapper = new EventCommandMapper(handlers);
56
+
57
+ expect(mapper.getSourceCommand('SomeEvent')).toBeUndefined();
58
+ });
59
+ });
60
+
61
+ describe('incremental updates', () => {
62
+ it('should allow adding handlers after construction', () => {
63
+ const mapper = new EventCommandMapper([]);
64
+
65
+ mapper.addHandler({
66
+ name: 'CheckLint',
67
+ events: ['LintCheckPassed', 'LintCheckFailed'],
68
+ handle: async () => ({ type: 'LintCheckPassed', data: {} }),
69
+ });
70
+
71
+ expect(mapper.getSourceCommand('LintCheckPassed')).toBe('CheckLint');
72
+ expect(mapper.getSourceCommand('LintCheckFailed')).toBe('CheckLint');
73
+ });
74
+
75
+ it('should handle duplicate event registrations gracefully', () => {
76
+ const handlers: CommandHandlerWithEvents[] = [
77
+ {
78
+ name: 'Handler1',
79
+ events: ['SharedEvent'],
80
+ handle: async () => ({ type: 'SharedEvent', data: {} }),
81
+ },
82
+ ];
83
+
84
+ const mapper = new EventCommandMapper(handlers);
85
+
86
+ mapper.addHandler({
87
+ name: 'Handler2',
88
+ events: ['SharedEvent'],
89
+ handle: async () => ({ type: 'SharedEvent', data: {} }),
90
+ });
91
+
92
+ const result = mapper.getSourceCommand('SharedEvent');
93
+ expect(['Handler1', 'Handler2']).toContain(result);
94
+ });
95
+ });
96
+
97
+ describe('query methods', () => {
98
+ it('should return all events for a command', () => {
99
+ const handlers: CommandHandlerWithEvents[] = [
100
+ {
101
+ name: 'CheckTests',
102
+ events: ['TestsCheckPassed', 'TestsCheckFailed', 'TestsCheckSkipped'],
103
+ handle: async () => ({ type: 'TestsCheckPassed', data: {} }),
104
+ },
105
+ ];
106
+
107
+ const mapper = new EventCommandMapper(handlers);
108
+ const events = mapper.getEventsForCommand('CheckTests');
109
+
110
+ expect(events).toEqual(['TestsCheckPassed', 'TestsCheckFailed', 'TestsCheckSkipped']);
111
+ });
112
+
113
+ it('should return empty array for unknown command', () => {
114
+ const mapper = new EventCommandMapper([]);
115
+ const events = mapper.getEventsForCommand('UnknownCommand');
116
+
117
+ expect(events).toEqual([]);
118
+ });
119
+
120
+ it('should check if event is from command', () => {
121
+ const handlers: CommandHandlerWithEvents[] = [
122
+ {
123
+ name: 'CheckTests',
124
+ events: ['TestsCheckPassed', 'TestsCheckFailed'],
125
+ handle: async () => ({ type: 'TestsCheckPassed', data: {} }),
126
+ },
127
+ ];
128
+
129
+ const mapper = new EventCommandMapper(handlers);
130
+
131
+ expect(mapper.isEventFromCommand('TestsCheckPassed', 'CheckTests')).toBe(true);
132
+ expect(mapper.isEventFromCommand('TestsCheckFailed', 'CheckTests')).toBe(true);
133
+ expect(mapper.isEventFromCommand('TypeCheckPassed', 'CheckTests')).toBe(false);
134
+ });
135
+ });
136
+ });
@@ -0,0 +1,38 @@
1
+ import type { CommandHandler } from '@auto-engineer/message-bus';
2
+
3
+ interface CommandHandlerWithEvents extends CommandHandler {
4
+ events?: readonly string[];
5
+ }
6
+
7
+ export class EventCommandMapper {
8
+ private eventToCommand = new Map<string, string>();
9
+ private commandToEvents = new Map<string, string[]>();
10
+
11
+ constructor(handlers: CommandHandlerWithEvents[]) {
12
+ for (const handler of handlers) {
13
+ this.addHandler(handler);
14
+ }
15
+ }
16
+
17
+ addHandler(handler: CommandHandlerWithEvents): void {
18
+ const events = handler.events ?? [];
19
+
20
+ for (const eventType of events) {
21
+ this.eventToCommand.set(eventType, handler.name);
22
+ }
23
+
24
+ this.commandToEvents.set(handler.name, [...events]);
25
+ }
26
+
27
+ getSourceCommand(eventType: string): string | undefined {
28
+ return this.eventToCommand.get(eventType);
29
+ }
30
+
31
+ getEventsForCommand(commandType: string): string[] {
32
+ return this.commandToEvents.get(commandType) ?? [];
33
+ }
34
+
35
+ isEventFromCommand(eventType: string, commandType: string): boolean {
36
+ return this.getSourceCommand(eventType) === commandType;
37
+ }
38
+ }
@@ -0,0 +1,358 @@
1
+ import type { Event } from '@auto-engineer/message-bus';
2
+ import { beforeEach, describe, expect, it } from 'vitest';
3
+ import type { ForEachPhasedDescriptor } from '../core/descriptors';
4
+ import { PhasedExecutor } from './phased-executor';
5
+
6
+ interface TestItem {
7
+ id: string;
8
+ type: 'molecule' | 'organism' | 'page';
9
+ }
10
+
11
+ function createHandler(_items: TestItem[]): ForEachPhasedDescriptor {
12
+ return {
13
+ type: 'foreach-phased',
14
+ eventType: 'ClientGenerated',
15
+ itemsSelector: (e: Event) => (e.data as { components: TestItem[] }).components,
16
+ phases: ['molecule', 'organism', 'page'],
17
+ classifier: (item: unknown) => (item as TestItem).type,
18
+ stopOnFailure: false,
19
+ emitFactory: (item: unknown, _phase: string, _event: Event) => ({
20
+ commandType: 'ImplementComponent',
21
+ data: { filePath: (item as TestItem).id },
22
+ }),
23
+ completion: {
24
+ successEvent: 'AllComponentsImplemented',
25
+ failureEvent: 'ComponentsFailed',
26
+ itemKey: (e: Event) => (e.data as { filePath?: string; id?: string }).filePath ?? (e.data as TestItem).id,
27
+ },
28
+ };
29
+ }
30
+
31
+ describe('PhasedExecutor', () => {
32
+ let executor: PhasedExecutor;
33
+ let dispatched: Array<{ commandType: string; data: unknown; correlationId: string }>;
34
+ let completed: Event[];
35
+
36
+ beforeEach(() => {
37
+ dispatched = [];
38
+ completed = [];
39
+ executor = new PhasedExecutor({
40
+ onDispatch: (commandType, data, correlationId) => {
41
+ dispatched.push({ commandType, data, correlationId });
42
+ },
43
+ onComplete: (event) => {
44
+ completed.push(event);
45
+ },
46
+ });
47
+ });
48
+
49
+ describe('phase gating', () => {
50
+ it('should dispatch only first phase items initially', () => {
51
+ const items: TestItem[] = [
52
+ { id: 'm1', type: 'molecule' },
53
+ { id: 'm2', type: 'molecule' },
54
+ { id: 'o1', type: 'organism' },
55
+ { id: 'p1', type: 'page' },
56
+ ];
57
+ const handler = createHandler(items);
58
+ const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
59
+
60
+ executor.startPhased(handler, event, 'c1');
61
+
62
+ expect(dispatched).toHaveLength(2);
63
+ expect(dispatched.map((d) => (d.data as { filePath: string }).filePath)).toEqual(['m1', 'm2']);
64
+ });
65
+
66
+ it('should wait for all items in phase to complete before next phase', () => {
67
+ const items: TestItem[] = [
68
+ { id: 'm1', type: 'molecule' },
69
+ { id: 'm2', type: 'molecule' },
70
+ { id: 'o1', type: 'organism' },
71
+ ];
72
+ const handler = createHandler(items);
73
+ const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
74
+
75
+ executor.startPhased(handler, event, 'c1');
76
+
77
+ expect(dispatched).toHaveLength(2);
78
+
79
+ executor.onEventReceived({ type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm1' } }, 'm1');
80
+
81
+ expect(dispatched).toHaveLength(2);
82
+
83
+ executor.onEventReceived({ type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm2' } }, 'm2');
84
+
85
+ expect(dispatched).toHaveLength(3);
86
+ expect((dispatched[2].data as { filePath: string }).filePath).toBe('o1');
87
+ });
88
+
89
+ it('should skip empty phases', () => {
90
+ const items: TestItem[] = [
91
+ { id: 'm1', type: 'molecule' },
92
+ { id: 'p1', type: 'page' },
93
+ ];
94
+ const handler = createHandler(items);
95
+ const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
96
+
97
+ executor.startPhased(handler, event, 'c1');
98
+
99
+ expect(dispatched).toHaveLength(1);
100
+ expect((dispatched[0].data as { filePath: string }).filePath).toBe('m1');
101
+
102
+ executor.onEventReceived({ type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm1' } }, 'm1');
103
+
104
+ expect(dispatched).toHaveLength(2);
105
+ expect((dispatched[1].data as { filePath: string }).filePath).toBe('p1');
106
+ });
107
+ });
108
+
109
+ describe('completion tracking', () => {
110
+ it('should emit success event when all phases complete', () => {
111
+ const items: TestItem[] = [
112
+ { id: 'm1', type: 'molecule' },
113
+ { id: 'o1', type: 'organism' },
114
+ ];
115
+ const handler = createHandler(items);
116
+ const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
117
+
118
+ executor.startPhased(handler, event, 'c1');
119
+
120
+ executor.onEventReceived({ type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm1' } }, 'm1');
121
+ executor.onEventReceived({ type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'o1' } }, 'o1');
122
+
123
+ expect(completed).toHaveLength(1);
124
+ expect(completed[0].type).toBe('AllComponentsImplemented');
125
+ expect(completed[0].correlationId).toBe('c1');
126
+ });
127
+
128
+ it('should cleanup session after completion', () => {
129
+ const items: TestItem[] = [{ id: 'm1', type: 'molecule' }];
130
+ const handler = createHandler(items);
131
+ const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
132
+
133
+ executor.startPhased(handler, event, 'c1');
134
+ expect(executor.getActiveSessionCount()).toBe(1);
135
+
136
+ executor.onEventReceived({ type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm1' } }, 'm1');
137
+
138
+ expect(executor.getActiveSessionCount()).toBe(0);
139
+ });
140
+ });
141
+
142
+ describe('state queries', () => {
143
+ it('should report phase completion status', () => {
144
+ const items: TestItem[] = [
145
+ { id: 'm1', type: 'molecule' },
146
+ { id: 'o1', type: 'organism' },
147
+ ];
148
+ const handler = createHandler(items);
149
+ const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
150
+
151
+ executor.startPhased(handler, event, 'c1');
152
+
153
+ expect(executor.isPhaseComplete('c1', 'molecule')).toBe(false);
154
+ expect(executor.isPhaseComplete('c1', 'organism')).toBe(false);
155
+
156
+ executor.onEventReceived({ type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm1' } }, 'm1');
157
+
158
+ expect(executor.isPhaseComplete('c1', 'molecule')).toBe(true);
159
+ expect(executor.isPhaseComplete('c1', 'organism')).toBe(false);
160
+ });
161
+
162
+ it('should return false for unknown correlationId', () => {
163
+ expect(executor.isPhaseComplete('unknown', 'molecule')).toBe(false);
164
+ });
165
+
166
+ it('should return false for unknown phase name', () => {
167
+ const items: TestItem[] = [{ id: 'm1', type: 'molecule' }];
168
+ const handler = createHandler(items);
169
+ const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
170
+
171
+ executor.startPhased(handler, event, 'c1');
172
+
173
+ expect(executor.isPhaseComplete('c1', 'nonexistent-phase')).toBe(false);
174
+ });
175
+
176
+ it('should return false for future phase when current phase is earlier', () => {
177
+ const items: TestItem[] = [
178
+ { id: 'm1', type: 'molecule' },
179
+ { id: 'p1', type: 'page' },
180
+ ];
181
+ const handler = createHandler(items);
182
+ const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
183
+
184
+ executor.startPhased(handler, event, 'c1');
185
+
186
+ expect(executor.isPhaseComplete('c1', 'page')).toBe(false);
187
+ });
188
+
189
+ it('should check correct session when multiple sessions exist with different correlationIds', () => {
190
+ const items1: TestItem[] = [
191
+ { id: 'm1', type: 'molecule' },
192
+ { id: 'o1', type: 'organism' },
193
+ ];
194
+ const items2: TestItem[] = [
195
+ { id: 'm2', type: 'molecule' },
196
+ { id: 'o2', type: 'organism' },
197
+ ];
198
+ const handler1 = createHandler(items1);
199
+ const handler2 = createHandler(items2);
200
+
201
+ executor.startPhased(
202
+ handler1,
203
+ { type: 'ClientGenerated', correlationId: 'c1', data: { components: items1 } },
204
+ 'c1',
205
+ );
206
+ executor.startPhased(
207
+ handler2,
208
+ { type: 'ClientGenerated', correlationId: 'c2', data: { components: items2 } },
209
+ 'c2',
210
+ );
211
+
212
+ executor.onEventReceived({ type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm1' } }, 'm1');
213
+
214
+ expect(executor.isPhaseComplete('c1', 'molecule')).toBe(true);
215
+ expect(executor.isPhaseComplete('c2', 'molecule')).toBe(false);
216
+ });
217
+ });
218
+
219
+ describe('failure handling', () => {
220
+ it('should stop on failure when stopOnFailure is true', () => {
221
+ const items: TestItem[] = [
222
+ { id: 'm1', type: 'molecule' },
223
+ { id: 'm2', type: 'molecule' },
224
+ { id: 'o1', type: 'organism' },
225
+ ];
226
+ const handler: ForEachPhasedDescriptor = {
227
+ ...createHandler(items),
228
+ stopOnFailure: true,
229
+ };
230
+ const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
231
+
232
+ executor.startPhased(handler, event, 'c1');
233
+
234
+ executor.onEventReceived({ type: 'ComponentsFailed', correlationId: 'c1', data: { filePath: 'm1' } }, 'm1');
235
+
236
+ expect(completed).toHaveLength(1);
237
+ expect(completed[0].type).toBe('ComponentsFailed');
238
+ expect(executor.getActiveSessionCount()).toBe(0);
239
+ });
240
+
241
+ it('should continue on failure when stopOnFailure is false', () => {
242
+ const items: TestItem[] = [
243
+ { id: 'm1', type: 'molecule' },
244
+ { id: 'm2', type: 'molecule' },
245
+ { id: 'o1', type: 'organism' },
246
+ ];
247
+ const handler = createHandler(items);
248
+ const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
249
+
250
+ executor.startPhased(handler, event, 'c1');
251
+
252
+ executor.onEventReceived({ type: 'ComponentsFailed', correlationId: 'c1', data: { filePath: 'm1' } }, 'm1');
253
+
254
+ expect(completed).toHaveLength(0);
255
+ expect(executor.getActiveSessionCount()).toBe(1);
256
+
257
+ executor.onEventReceived({ type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm2' } }, 'm2');
258
+
259
+ expect(dispatched).toHaveLength(3);
260
+ });
261
+ });
262
+
263
+ describe('concurrent sessions', () => {
264
+ it('should track sessions independently by correlationId', () => {
265
+ const items: TestItem[] = [{ id: 'm1', type: 'molecule' }];
266
+ const handler = createHandler(items);
267
+
268
+ executor.startPhased(
269
+ handler,
270
+ { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } },
271
+ 'c1',
272
+ );
273
+ executor.startPhased(
274
+ handler,
275
+ { type: 'ClientGenerated', correlationId: 'c2', data: { components: items } },
276
+ 'c2',
277
+ );
278
+
279
+ expect(executor.getActiveSessionCount()).toBe(2);
280
+
281
+ executor.onEventReceived({ type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm1' } }, 'm1');
282
+
283
+ expect(executor.getActiveSessionCount()).toBe(1);
284
+ expect(completed).toHaveLength(1);
285
+ expect(completed[0].correlationId).toBe('c1');
286
+ });
287
+ });
288
+
289
+ describe('event deduplication', () => {
290
+ it('should ignore duplicate events for already completed items', () => {
291
+ const items: TestItem[] = [
292
+ { id: 'm1', type: 'molecule' },
293
+ { id: 'm2', type: 'molecule' },
294
+ { id: 'o1', type: 'organism' },
295
+ ];
296
+ const handler = createHandler(items);
297
+ const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
298
+
299
+ executor.startPhased(handler, event, 'c1');
300
+
301
+ executor.onEventReceived({ type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm1' } }, 'm1');
302
+
303
+ expect(dispatched).toHaveLength(2);
304
+
305
+ executor.onEventReceived({ type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm1' } }, 'm1');
306
+
307
+ expect(dispatched).toHaveLength(2);
308
+ });
309
+ });
310
+
311
+ describe('event edge cases', () => {
312
+ it('should ignore events with undefined correlationId', () => {
313
+ const items: TestItem[] = [{ id: 'm1', type: 'molecule' }];
314
+ const handler = createHandler(items);
315
+ const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
316
+
317
+ executor.startPhased(handler, event, 'c1');
318
+
319
+ expect(dispatched).toHaveLength(1);
320
+
321
+ executor.onEventReceived({ type: 'ComponentImplemented', data: { filePath: 'm1' } }, 'm1');
322
+
323
+ expect(dispatched).toHaveLength(1);
324
+ expect(executor.getActiveSessionCount()).toBe(1);
325
+ });
326
+
327
+ it('should ignore events with empty correlationId', () => {
328
+ const items: TestItem[] = [{ id: 'm1', type: 'molecule' }];
329
+ const handler = createHandler(items);
330
+ const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
331
+
332
+ executor.startPhased(handler, event, 'c1');
333
+
334
+ expect(dispatched).toHaveLength(1);
335
+
336
+ executor.onEventReceived({ type: 'ComponentImplemented', correlationId: '', data: { filePath: 'm1' } }, 'm1');
337
+
338
+ expect(dispatched).toHaveLength(1);
339
+ expect(executor.getActiveSessionCount()).toBe(1);
340
+ });
341
+
342
+ it('should ignore events with unknown itemKey', () => {
343
+ const items: TestItem[] = [{ id: 'm1', type: 'molecule' }];
344
+ const handler = createHandler(items);
345
+ const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
346
+
347
+ executor.startPhased(handler, event, 'c1');
348
+
349
+ executor.onEventReceived(
350
+ { type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'unknown' } },
351
+ 'unknown',
352
+ );
353
+
354
+ expect(dispatched).toHaveLength(1);
355
+ expect(executor.getActiveSessionCount()).toBe(1);
356
+ });
357
+ });
358
+ });