@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,448 @@
1
+ import type { Command, Event } from '@auto-engineer/message-bus';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { SettledTracker } from './settled-tracker';
4
+
5
+ describe('SettledTracker', () => {
6
+ let tracker: SettledTracker;
7
+
8
+ beforeEach(() => {
9
+ tracker = new SettledTracker();
10
+ });
11
+
12
+ describe('handler registration', () => {
13
+ it('should register a handler for multiple command types', () => {
14
+ tracker.registerHandler({
15
+ commandTypes: ['CheckTests', 'CheckTypes', 'CheckLint'],
16
+ handler: () => {},
17
+ });
18
+
19
+ expect(tracker.getRegisteredHandlerCount()).toBe(1);
20
+ });
21
+
22
+ it('should register multiple handlers', () => {
23
+ tracker.registerHandler({
24
+ commandTypes: ['A', 'B'],
25
+ handler: () => {},
26
+ });
27
+ tracker.registerHandler({
28
+ commandTypes: ['C', 'D'],
29
+ handler: () => {},
30
+ });
31
+
32
+ expect(tracker.getRegisteredHandlerCount()).toBe(2);
33
+ });
34
+ });
35
+
36
+ describe('command tracking', () => {
37
+ it('should track multiple commands by correlationId', () => {
38
+ tracker.registerHandler({
39
+ commandTypes: ['CheckTests', 'CheckTypes', 'CheckLint'],
40
+ handler: () => {},
41
+ });
42
+
43
+ const command: Command = {
44
+ type: 'CheckTests',
45
+ correlationId: 'c1',
46
+ requestId: 'r1',
47
+ data: {},
48
+ };
49
+
50
+ tracker.onCommandStarted(command);
51
+
52
+ expect(tracker.isWaitingFor('c1', 'CheckTests')).toBe(true);
53
+ expect(tracker.isWaitingFor('c1', 'CheckTypes')).toBe(false);
54
+ });
55
+
56
+ it('should instantiate handler instance when first tracked command arrives', () => {
57
+ tracker.registerHandler({
58
+ commandTypes: ['A', 'B'],
59
+ handler: () => {},
60
+ });
61
+
62
+ tracker.onCommandStarted({
63
+ type: 'A',
64
+ correlationId: 'c1',
65
+ requestId: 'r1',
66
+ data: {},
67
+ });
68
+
69
+ expect(tracker.getActiveInstanceCount()).toBe(1);
70
+ });
71
+
72
+ it('should not create duplicate instances for same correlationId', () => {
73
+ tracker.registerHandler({
74
+ commandTypes: ['A', 'B'],
75
+ handler: () => {},
76
+ });
77
+
78
+ tracker.onCommandStarted({
79
+ type: 'A',
80
+ correlationId: 'c1',
81
+ requestId: 'r1',
82
+ data: {},
83
+ });
84
+ tracker.onCommandStarted({
85
+ type: 'B',
86
+ correlationId: 'c1',
87
+ requestId: 'r2',
88
+ data: {},
89
+ });
90
+
91
+ expect(tracker.getActiveInstanceCount()).toBe(1);
92
+ });
93
+
94
+ it('should ignore commands without correlationId', () => {
95
+ tracker.registerHandler({
96
+ commandTypes: ['A'],
97
+ handler: () => {},
98
+ });
99
+
100
+ tracker.onCommandStarted({
101
+ type: 'A',
102
+ requestId: 'r1',
103
+ data: {},
104
+ } as Command);
105
+
106
+ expect(tracker.getActiveInstanceCount()).toBe(0);
107
+ });
108
+
109
+ it('should ignore commands without requestId', () => {
110
+ tracker.registerHandler({
111
+ commandTypes: ['A'],
112
+ handler: () => {},
113
+ });
114
+
115
+ tracker.onCommandStarted({
116
+ type: 'A',
117
+ correlationId: 'c1',
118
+ data: {},
119
+ } as Command);
120
+
121
+ expect(tracker.getActiveInstanceCount()).toBe(0);
122
+ });
123
+ });
124
+
125
+ describe('event routing', () => {
126
+ it('should mark command as complete when event received', () => {
127
+ tracker.registerHandler({
128
+ commandTypes: ['A'],
129
+ handler: () => {},
130
+ });
131
+
132
+ tracker.onCommandStarted({
133
+ type: 'A',
134
+ correlationId: 'c1',
135
+ requestId: 'r1',
136
+ data: {},
137
+ });
138
+
139
+ const event: Event = {
140
+ type: 'ADone',
141
+ correlationId: 'c1',
142
+ data: {},
143
+ };
144
+
145
+ tracker.onEventReceived(event, 'A');
146
+
147
+ expect(tracker.isWaitingFor('c1', 'A')).toBe(false);
148
+ });
149
+
150
+ it('should collect events for each command type', () => {
151
+ let receivedEvents: Record<string, Event[]> = {};
152
+
153
+ tracker.registerHandler({
154
+ commandTypes: ['A', 'B'],
155
+ handler: (events) => {
156
+ receivedEvents = events;
157
+ },
158
+ });
159
+
160
+ tracker.onCommandStarted({
161
+ type: 'A',
162
+ correlationId: 'c1',
163
+ requestId: 'r1',
164
+ data: {},
165
+ });
166
+ tracker.onCommandStarted({
167
+ type: 'B',
168
+ correlationId: 'c1',
169
+ requestId: 'r2',
170
+ data: {},
171
+ });
172
+
173
+ tracker.onEventReceived({ type: 'ADone', correlationId: 'c1', data: { foo: 1 } }, 'A');
174
+ tracker.onEventReceived({ type: 'BDone', correlationId: 'c1', data: { bar: 2 } }, 'B');
175
+
176
+ expect(receivedEvents.A).toHaveLength(1);
177
+ expect(receivedEvents.A[0].type).toBe('ADone');
178
+ expect(receivedEvents.B).toHaveLength(1);
179
+ expect(receivedEvents.B[0].type).toBe('BDone');
180
+ });
181
+
182
+ it('should ignore events without correlationId', () => {
183
+ let handlerCalled = false;
184
+
185
+ tracker.registerHandler({
186
+ commandTypes: ['A'],
187
+ handler: () => {
188
+ handlerCalled = true;
189
+ },
190
+ });
191
+
192
+ tracker.onCommandStarted({
193
+ type: 'A',
194
+ correlationId: 'c1',
195
+ requestId: 'r1',
196
+ data: {},
197
+ });
198
+
199
+ tracker.onEventReceived({ type: 'ADone', data: {} } as Event, 'A');
200
+
201
+ expect(handlerCalled).toBe(false);
202
+ });
203
+ });
204
+
205
+ describe('handler execution', () => {
206
+ it('should fire handler when all commands complete', () => {
207
+ let fired = false;
208
+
209
+ tracker.registerHandler({
210
+ commandTypes: ['A', 'B'],
211
+ handler: () => {
212
+ fired = true;
213
+ },
214
+ });
215
+
216
+ tracker.onCommandStarted({
217
+ type: 'A',
218
+ correlationId: 'c1',
219
+ requestId: 'r1',
220
+ data: {},
221
+ });
222
+ tracker.onCommandStarted({
223
+ type: 'B',
224
+ correlationId: 'c1',
225
+ requestId: 'r2',
226
+ data: {},
227
+ });
228
+
229
+ tracker.onEventReceived({ type: 'ADone', correlationId: 'c1', data: {} }, 'A');
230
+ expect(fired).toBe(false);
231
+
232
+ tracker.onEventReceived({ type: 'BDone', correlationId: 'c1', data: {} }, 'B');
233
+ expect(fired).toBe(true);
234
+ });
235
+
236
+ it('should not fire handler until all tracked commands have events', () => {
237
+ let fireCount = 0;
238
+
239
+ tracker.registerHandler({
240
+ commandTypes: ['CheckTests', 'CheckTypes', 'CheckLint'],
241
+ handler: () => {
242
+ fireCount++;
243
+ },
244
+ });
245
+
246
+ tracker.onCommandStarted({
247
+ type: 'CheckTests',
248
+ correlationId: 'c1',
249
+ requestId: 'r1',
250
+ data: {},
251
+ });
252
+ tracker.onCommandStarted({
253
+ type: 'CheckTypes',
254
+ correlationId: 'c1',
255
+ requestId: 'r2',
256
+ data: {},
257
+ });
258
+ tracker.onCommandStarted({
259
+ type: 'CheckLint',
260
+ correlationId: 'c1',
261
+ requestId: 'r3',
262
+ data: {},
263
+ });
264
+
265
+ tracker.onEventReceived({ type: 'TestsCheckPassed', correlationId: 'c1', data: {} }, 'CheckTests');
266
+ expect(fireCount).toBe(0);
267
+
268
+ tracker.onEventReceived({ type: 'TypeCheckPassed', correlationId: 'c1', data: {} }, 'CheckTypes');
269
+ expect(fireCount).toBe(0);
270
+
271
+ tracker.onEventReceived({ type: 'LintCheckPassed', correlationId: 'c1', data: {} }, 'CheckLint');
272
+ expect(fireCount).toBe(1);
273
+ });
274
+
275
+ it('should cleanup instance after handler fires', () => {
276
+ tracker.registerHandler({
277
+ commandTypes: ['A'],
278
+ handler: () => {},
279
+ });
280
+
281
+ tracker.onCommandStarted({
282
+ type: 'A',
283
+ correlationId: 'c1',
284
+ requestId: 'r1',
285
+ data: {},
286
+ });
287
+
288
+ expect(tracker.getActiveInstanceCount()).toBe(1);
289
+
290
+ tracker.onEventReceived({ type: 'ADone', correlationId: 'c1', data: {} }, 'A');
291
+
292
+ expect(tracker.getActiveInstanceCount()).toBe(0);
293
+ });
294
+
295
+ it('should handle separate correlationIds independently', () => {
296
+ const firedFor: string[] = [];
297
+
298
+ tracker.registerHandler({
299
+ commandTypes: ['A', 'B'],
300
+ handler: (events) => {
301
+ firedFor.push(events.A[0].correlationId ?? 'unknown');
302
+ },
303
+ });
304
+
305
+ tracker.onCommandStarted({ type: 'A', correlationId: 'c1', requestId: 'r1', data: {} });
306
+ tracker.onCommandStarted({ type: 'B', correlationId: 'c1', requestId: 'r2', data: {} });
307
+ tracker.onCommandStarted({ type: 'A', correlationId: 'c2', requestId: 'r3', data: {} });
308
+ tracker.onCommandStarted({ type: 'B', correlationId: 'c2', requestId: 'r4', data: {} });
309
+
310
+ tracker.onEventReceived({ type: 'ADone', correlationId: 'c1', data: {} }, 'A');
311
+ tracker.onEventReceived({ type: 'BDone', correlationId: 'c2', data: {} }, 'B');
312
+
313
+ expect(firedFor).toHaveLength(0);
314
+
315
+ tracker.onEventReceived({ type: 'BDone', correlationId: 'c1', data: {} }, 'B');
316
+ expect(firedFor).toEqual(['c1']);
317
+
318
+ tracker.onEventReceived({ type: 'ADone', correlationId: 'c2', data: {} }, 'A');
319
+ expect(firedFor).toEqual(['c1', 'c2']);
320
+ });
321
+ });
322
+
323
+ describe('persist for retry', () => {
324
+ it('should reset trackers when handler returns persist: true', () => {
325
+ let callCount = 0;
326
+
327
+ tracker.registerHandler({
328
+ commandTypes: ['A'],
329
+ handler: () => {
330
+ callCount++;
331
+ return callCount < 3 ? { persist: true } : undefined;
332
+ },
333
+ });
334
+
335
+ tracker.onCommandStarted({ type: 'A', correlationId: 'c1', requestId: 'r1', data: {} });
336
+ tracker.onEventReceived({ type: 'ADone', correlationId: 'c1', data: {} }, 'A');
337
+ expect(callCount).toBe(1);
338
+ expect(tracker.getActiveInstanceCount()).toBe(1);
339
+
340
+ tracker.onCommandStarted({ type: 'A', correlationId: 'c1', requestId: 'r2', data: {} });
341
+ tracker.onEventReceived({ type: 'ADone', correlationId: 'c1', data: {} }, 'A');
342
+ expect(callCount).toBe(2);
343
+ expect(tracker.getActiveInstanceCount()).toBe(1);
344
+
345
+ tracker.onCommandStarted({ type: 'A', correlationId: 'c1', requestId: 'r3', data: {} });
346
+ tracker.onEventReceived({ type: 'ADone', correlationId: 'c1', data: {} }, 'A');
347
+ expect(callCount).toBe(3);
348
+ expect(tracker.getActiveInstanceCount()).toBe(0);
349
+ });
350
+
351
+ it('should cleanup on handler error', () => {
352
+ tracker.registerHandler({
353
+ commandTypes: ['A'],
354
+ handler: () => {
355
+ throw new Error('Handler error');
356
+ },
357
+ });
358
+
359
+ tracker.onCommandStarted({ type: 'A', correlationId: 'c1', requestId: 'r1', data: {} });
360
+
361
+ expect(() => {
362
+ tracker.onEventReceived({ type: 'ADone', correlationId: 'c1', data: {} }, 'A');
363
+ }).not.toThrow();
364
+
365
+ expect(tracker.getActiveInstanceCount()).toBe(0);
366
+ });
367
+ });
368
+
369
+ describe('error callback', () => {
370
+ it('should accept onError callback in options', () => {
371
+ const onError = vi.fn();
372
+
373
+ tracker = new SettledTracker({ onError });
374
+
375
+ expect(tracker).toBeInstanceOf(SettledTracker);
376
+ });
377
+
378
+ it('should call onError when handler throws', () => {
379
+ const onError = vi.fn();
380
+ const thrownError = new Error('Handler failed');
381
+
382
+ tracker = new SettledTracker({ onError });
383
+
384
+ tracker.registerHandler({
385
+ commandTypes: ['A'],
386
+ handler: () => {
387
+ throw thrownError;
388
+ },
389
+ });
390
+
391
+ tracker.onCommandStarted({ type: 'A', correlationId: 'c1', requestId: 'r1', data: {} });
392
+ tracker.onEventReceived({ type: 'ADone', correlationId: 'c1', data: {} }, 'A');
393
+
394
+ expect(onError).toHaveBeenCalledTimes(1);
395
+ expect(onError).toHaveBeenCalledWith(thrownError, {
396
+ commandTypes: ['A'],
397
+ correlationId: 'c1',
398
+ });
399
+ });
400
+ });
401
+
402
+ describe('dispatch callback', () => {
403
+ it('should call onDispatch when provided', () => {
404
+ const dispatched: Array<{ type: string; data: unknown }> = [];
405
+
406
+ tracker = new SettledTracker({
407
+ onDispatch: (commandType, data, _correlationId) => {
408
+ dispatched.push({ type: commandType, data });
409
+ },
410
+ });
411
+
412
+ tracker.registerHandler({
413
+ commandTypes: ['A'],
414
+ handler: (_events, send) => {
415
+ send('FollowUp', { foo: 'bar' });
416
+ },
417
+ });
418
+
419
+ tracker.onCommandStarted({ type: 'A', correlationId: 'c1', requestId: 'r1', data: {} });
420
+ tracker.onEventReceived({ type: 'ADone', correlationId: 'c1', data: {} }, 'A');
421
+
422
+ expect(dispatched).toHaveLength(1);
423
+ expect(dispatched[0]).toEqual({ type: 'FollowUp', data: { foo: 'bar' } });
424
+ });
425
+
426
+ it('should pass correlationId to onDispatch', () => {
427
+ let receivedCorrelationId: string | undefined;
428
+
429
+ tracker = new SettledTracker({
430
+ onDispatch: (_commandType, _data, correlationId) => {
431
+ receivedCorrelationId = correlationId;
432
+ },
433
+ });
434
+
435
+ tracker.registerHandler({
436
+ commandTypes: ['A'],
437
+ handler: (_events, send) => {
438
+ send('FollowUp', {});
439
+ },
440
+ });
441
+
442
+ tracker.onCommandStarted({ type: 'A', correlationId: 'c1', requestId: 'r1', data: {} });
443
+ tracker.onEventReceived({ type: 'ADone', correlationId: 'c1', data: {} }, 'A');
444
+
445
+ expect(receivedCorrelationId).toBe('c1');
446
+ });
447
+ });
448
+ });
@@ -0,0 +1,237 @@
1
+ import type { Command, Event } from '@auto-engineer/message-bus';
2
+
3
+ type SendFunction = (commandType: string, data: unknown) => void;
4
+
5
+ type SettledHandler = (events: Record<string, Event[]>, send: SendFunction) => void | { persist: boolean };
6
+
7
+ interface SettledHandlerRegistration {
8
+ commandTypes: readonly string[];
9
+ handler: SettledHandler;
10
+ }
11
+
12
+ interface CommandTracker {
13
+ commandType: string;
14
+ hasStarted: boolean;
15
+ hasCompleted: boolean;
16
+ events: Event[];
17
+ }
18
+
19
+ interface HandlerTemplate {
20
+ id: string;
21
+ registration: SettledHandlerRegistration;
22
+ }
23
+
24
+ interface HandlerInstance {
25
+ templateId: string;
26
+ correlationId: string;
27
+ registration: SettledHandlerRegistration;
28
+ commandTrackers: Map<string, CommandTracker>;
29
+ }
30
+
31
+ interface SettledErrorContext {
32
+ commandTypes: readonly string[];
33
+ correlationId: string;
34
+ }
35
+
36
+ interface SettledTrackerOptions {
37
+ onDispatch?: (commandType: string, data: unknown, correlationId: string) => void;
38
+ onError?: (error: unknown, context: SettledErrorContext) => void;
39
+ }
40
+
41
+ export class SettledTracker {
42
+ private handlerTemplates = new Map<string, HandlerTemplate>();
43
+ private handlerInstances = new Map<string, HandlerInstance>();
44
+ private commandToTemplateIds = new Map<string, Set<string>>();
45
+ private readonly onDispatch?: (commandType: string, data: unknown, correlationId: string) => void;
46
+ private readonly onError?: (error: unknown, context: SettledErrorContext) => void;
47
+
48
+ constructor(options?: SettledTrackerOptions) {
49
+ this.onDispatch = options?.onDispatch;
50
+ this.onError = options?.onError;
51
+ }
52
+
53
+ registerHandler(registration: SettledHandlerRegistration): void {
54
+ const templateId = this.generateTemplateId(registration);
55
+
56
+ this.handlerTemplates.set(templateId, {
57
+ id: templateId,
58
+ registration,
59
+ });
60
+
61
+ for (const commandType of registration.commandTypes) {
62
+ const existing = this.commandToTemplateIds.get(commandType) ?? new Set<string>();
63
+ existing.add(templateId);
64
+ this.commandToTemplateIds.set(commandType, existing);
65
+ }
66
+ }
67
+
68
+ getRegisteredHandlerCount(): number {
69
+ return this.handlerTemplates.size;
70
+ }
71
+
72
+ getActiveInstanceCount(): number {
73
+ return this.handlerInstances.size;
74
+ }
75
+
76
+ onCommandStarted(command: Command): void {
77
+ const { type: commandType, correlationId, requestId } = command;
78
+
79
+ if (!this.isValidId(correlationId) || !this.isValidId(requestId)) {
80
+ return;
81
+ }
82
+
83
+ const templateIds = this.commandToTemplateIds.get(commandType);
84
+ if (!templateIds) {
85
+ return;
86
+ }
87
+
88
+ for (const templateId of templateIds) {
89
+ const template = this.handlerTemplates.get(templateId);
90
+ if (template) {
91
+ this.ensureInstanceExists(template, correlationId);
92
+ this.markCommandStarted(template.id, correlationId, commandType);
93
+ }
94
+ }
95
+ }
96
+
97
+ isWaitingFor(correlationId: string, commandType: string): boolean {
98
+ for (const instance of this.handlerInstances.values()) {
99
+ if (instance.correlationId === correlationId) {
100
+ const tracker = instance.commandTrackers.get(commandType);
101
+ if (tracker?.hasStarted === true && tracker.hasCompleted === false) {
102
+ return true;
103
+ }
104
+ }
105
+ }
106
+ return false;
107
+ }
108
+
109
+ onEventReceived(event: Event, sourceCommandType: string): void {
110
+ const correlationId = event.correlationId;
111
+
112
+ if (!this.isValidId(correlationId)) {
113
+ return;
114
+ }
115
+
116
+ for (const [instanceId, instance] of this.handlerInstances) {
117
+ if (instance.correlationId !== correlationId) {
118
+ continue;
119
+ }
120
+
121
+ const tracker = instance.commandTrackers.get(sourceCommandType);
122
+ if (tracker && !tracker.hasCompleted) {
123
+ tracker.events.push(event);
124
+ tracker.hasCompleted = true;
125
+ this.checkAndFireHandler(instanceId, instance);
126
+ }
127
+ }
128
+ }
129
+
130
+ private isValidId(id: string | undefined): id is string {
131
+ return id !== undefined && id !== null && id !== '';
132
+ }
133
+
134
+ private generateTemplateId(registration: SettledHandlerRegistration): string {
135
+ return `template-${registration.commandTypes.join(',')}`;
136
+ }
137
+
138
+ private generateInstanceId(templateId: string, correlationId: string): string {
139
+ return `${templateId}-${correlationId}`;
140
+ }
141
+
142
+ private ensureInstanceExists(template: HandlerTemplate, correlationId: string): void {
143
+ const instanceId = this.generateInstanceId(template.id, correlationId);
144
+
145
+ if (this.handlerInstances.has(instanceId)) {
146
+ return;
147
+ }
148
+
149
+ const commandTrackers = new Map<string, CommandTracker>();
150
+ for (const commandType of template.registration.commandTypes) {
151
+ commandTrackers.set(commandType, {
152
+ commandType,
153
+ hasStarted: false,
154
+ hasCompleted: false,
155
+ events: [],
156
+ });
157
+ }
158
+
159
+ this.handlerInstances.set(instanceId, {
160
+ templateId: template.id,
161
+ correlationId,
162
+ registration: template.registration,
163
+ commandTrackers,
164
+ });
165
+ }
166
+
167
+ private markCommandStarted(templateId: string, correlationId: string, commandType: string): void {
168
+ const instanceId = this.generateInstanceId(templateId, correlationId);
169
+ const instance = this.handlerInstances.get(instanceId);
170
+
171
+ const tracker = instance?.commandTrackers.get(commandType);
172
+ if (tracker) {
173
+ tracker.hasStarted = true;
174
+ tracker.hasCompleted = false;
175
+ }
176
+ }
177
+
178
+ private checkAndFireHandler(instanceId: string, instance: HandlerInstance): void {
179
+ const allComplete = Array.from(instance.commandTrackers.values()).every(
180
+ (tracker) => tracker.hasStarted && tracker.hasCompleted,
181
+ );
182
+
183
+ if (!allComplete) {
184
+ return;
185
+ }
186
+
187
+ const eventsByCommandType = this.collectEvents(instance);
188
+
189
+ try {
190
+ const send: SendFunction = (commandType, data) => {
191
+ if (this.onDispatch) {
192
+ this.onDispatch(commandType, data, instance.correlationId);
193
+ }
194
+ };
195
+
196
+ const result = instance.registration.handler(eventsByCommandType, send);
197
+
198
+ if (this.shouldPersist(result)) {
199
+ this.resetTrackers(instance);
200
+ } else {
201
+ this.handlerInstances.delete(instanceId);
202
+ }
203
+ } catch (error) {
204
+ this.onError?.(error, {
205
+ commandTypes: instance.registration.commandTypes,
206
+ correlationId: instance.correlationId,
207
+ });
208
+ this.handlerInstances.delete(instanceId);
209
+ }
210
+ }
211
+
212
+ private collectEvents(instance: HandlerInstance): Record<string, Event[]> {
213
+ const events: Record<string, Event[]> = {};
214
+ for (const [commandType, tracker] of instance.commandTrackers) {
215
+ events[commandType] = [...tracker.events];
216
+ }
217
+ return events;
218
+ }
219
+
220
+ private shouldPersist(result: void | { persist: boolean }): boolean {
221
+ return (
222
+ result !== null &&
223
+ result !== undefined &&
224
+ typeof result === 'object' &&
225
+ 'persist' in result &&
226
+ result.persist === true
227
+ );
228
+ }
229
+
230
+ private resetTrackers(instance: HandlerInstance): void {
231
+ for (const tracker of instance.commandTrackers.values()) {
232
+ tracker.hasStarted = false;
233
+ tracker.hasCompleted = false;
234
+ tracker.events = [];
235
+ }
236
+ }
237
+ }