@auto-engineer/pipeline 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (316) hide show
  1. package/.turbo/turbo-build.log +5 -6
  2. package/CHANGELOG.md +24 -0
  3. package/README.md +279 -0
  4. package/dist/src/builder/define.d.ts +6 -2
  5. package/dist/src/builder/define.d.ts.map +1 -1
  6. package/dist/src/builder/define.js +17 -7
  7. package/dist/src/builder/define.js.map +1 -1
  8. package/dist/src/core/descriptors.d.ts +6 -2
  9. package/dist/src/core/descriptors.d.ts.map +1 -1
  10. package/dist/src/graph/filter-graph.d.ts +3 -0
  11. package/dist/src/graph/filter-graph.d.ts.map +1 -0
  12. package/dist/src/graph/filter-graph.js +80 -0
  13. package/dist/src/graph/filter-graph.js.map +1 -0
  14. package/dist/src/graph/types.d.ts +8 -0
  15. package/dist/src/graph/types.d.ts.map +1 -1
  16. package/dist/src/index.d.ts +1 -0
  17. package/dist/src/index.d.ts.map +1 -1
  18. package/dist/src/index.js.map +1 -1
  19. package/dist/src/projections/await-tracker-projection.d.ts +31 -0
  20. package/dist/src/projections/await-tracker-projection.d.ts.map +1 -0
  21. package/dist/src/projections/await-tracker-projection.js +35 -0
  22. package/dist/src/projections/await-tracker-projection.js.map +1 -0
  23. package/dist/src/projections/index.d.ts +4 -0
  24. package/dist/src/projections/index.d.ts.map +1 -0
  25. package/dist/src/projections/index.js +4 -0
  26. package/dist/src/projections/index.js.map +1 -0
  27. package/dist/src/projections/item-status-projection.d.ts +22 -0
  28. package/dist/src/projections/item-status-projection.d.ts.map +1 -0
  29. package/dist/src/projections/item-status-projection.js +11 -0
  30. package/dist/src/projections/item-status-projection.js.map +1 -0
  31. package/dist/src/projections/latest-run-projection.d.ts +15 -0
  32. package/dist/src/projections/latest-run-projection.d.ts.map +1 -0
  33. package/dist/src/projections/latest-run-projection.js +7 -0
  34. package/dist/src/projections/latest-run-projection.js.map +1 -0
  35. package/dist/src/projections/message-log-projection.d.ts +51 -0
  36. package/dist/src/projections/message-log-projection.d.ts.map +1 -0
  37. package/dist/src/projections/message-log-projection.js +51 -0
  38. package/dist/src/projections/message-log-projection.js.map +1 -0
  39. package/dist/src/projections/node-status-projection.d.ts +23 -0
  40. package/dist/src/projections/node-status-projection.d.ts.map +1 -0
  41. package/dist/src/projections/node-status-projection.js +10 -0
  42. package/dist/src/projections/node-status-projection.js.map +1 -0
  43. package/dist/src/projections/phased-execution-projection.d.ts +77 -0
  44. package/dist/src/projections/phased-execution-projection.d.ts.map +1 -0
  45. package/dist/src/projections/phased-execution-projection.js +54 -0
  46. package/dist/src/projections/phased-execution-projection.js.map +1 -0
  47. package/dist/src/projections/settled-instance-projection.d.ts +67 -0
  48. package/dist/src/projections/settled-instance-projection.d.ts.map +1 -0
  49. package/dist/src/projections/settled-instance-projection.js +66 -0
  50. package/dist/src/projections/settled-instance-projection.js.map +1 -0
  51. package/dist/src/projections/stats-projection.d.ts +9 -0
  52. package/dist/src/projections/stats-projection.d.ts.map +1 -0
  53. package/dist/src/projections/stats-projection.js +16 -0
  54. package/dist/src/projections/stats-projection.js.map +1 -0
  55. package/dist/src/runtime/await-tracker.d.ts +17 -7
  56. package/dist/src/runtime/await-tracker.d.ts.map +1 -1
  57. package/dist/src/runtime/await-tracker.js +32 -29
  58. package/dist/src/runtime/await-tracker.js.map +1 -1
  59. package/dist/src/runtime/context.d.ts +1 -1
  60. package/dist/src/runtime/context.d.ts.map +1 -1
  61. package/dist/src/runtime/event-command-map.d.ts +3 -3
  62. package/dist/src/runtime/event-command-map.d.ts.map +1 -1
  63. package/dist/src/runtime/event-command-map.js +6 -2
  64. package/dist/src/runtime/event-command-map.js.map +1 -1
  65. package/dist/src/runtime/phased-executor.d.ts +14 -9
  66. package/dist/src/runtime/phased-executor.d.ts.map +1 -1
  67. package/dist/src/runtime/phased-executor.js +113 -105
  68. package/dist/src/runtime/phased-executor.js.map +1 -1
  69. package/dist/src/runtime/pipeline-runtime.d.ts.map +1 -1
  70. package/dist/src/runtime/pipeline-runtime.js +2 -2
  71. package/dist/src/runtime/pipeline-runtime.js.map +1 -1
  72. package/dist/src/runtime/settled-tracker.d.ts +12 -10
  73. package/dist/src/runtime/settled-tracker.d.ts.map +1 -1
  74. package/dist/src/runtime/settled-tracker.js +89 -80
  75. package/dist/src/runtime/settled-tracker.js.map +1 -1
  76. package/dist/src/server/pipeline-server.d.ts +31 -9
  77. package/dist/src/server/pipeline-server.d.ts.map +1 -1
  78. package/dist/src/server/pipeline-server.js +424 -123
  79. package/dist/src/server/pipeline-server.js.map +1 -1
  80. package/dist/src/server/sse-manager.d.ts +0 -1
  81. package/dist/src/server/sse-manager.d.ts.map +1 -1
  82. package/dist/src/server/sse-manager.js +0 -3
  83. package/dist/src/server/sse-manager.js.map +1 -1
  84. package/dist/src/store/index.d.ts +3 -0
  85. package/dist/src/store/index.d.ts.map +1 -0
  86. package/dist/src/store/index.js +3 -0
  87. package/dist/src/store/index.js.map +1 -0
  88. package/dist/src/store/pipeline-event-store.d.ts +10 -0
  89. package/dist/src/store/pipeline-event-store.d.ts.map +1 -0
  90. package/dist/src/store/pipeline-event-store.js +112 -0
  91. package/dist/src/store/pipeline-event-store.js.map +1 -0
  92. package/dist/src/store/pipeline-read-model.d.ts +49 -0
  93. package/dist/src/store/pipeline-read-model.d.ts.map +1 -0
  94. package/dist/src/store/pipeline-read-model.js +156 -0
  95. package/dist/src/store/pipeline-read-model.js.map +1 -0
  96. package/dist/src/store/sqlite-pipeline-event-store.d.ts +14 -0
  97. package/dist/src/store/sqlite-pipeline-event-store.d.ts.map +1 -0
  98. package/dist/src/store/sqlite-pipeline-event-store.js +20 -0
  99. package/dist/src/store/sqlite-pipeline-event-store.js.map +1 -0
  100. package/dist/src/testing/fixtures/kanban-full.pipeline.js +2 -2
  101. package/dist/src/testing/fixtures/kanban-full.pipeline.js.map +1 -1
  102. package/dist/src/testing/fixtures/kanban.pipeline.js +2 -2
  103. package/dist/src/testing/fixtures/kanban.pipeline.js.map +1 -1
  104. package/dist/tsconfig.tsbuildinfo +1 -1
  105. package/ketchup-plan.md +1216 -0
  106. package/package.json +6 -4
  107. package/src/builder/define.specs.ts +5 -4
  108. package/src/builder/define.ts +24 -11
  109. package/src/config/pipeline-config.specs.ts +32 -0
  110. package/src/core/descriptors.ts +7 -2
  111. package/src/graph/filter-graph.specs.ts +267 -0
  112. package/src/graph/filter-graph.ts +111 -0
  113. package/src/graph/types.specs.ts +0 -14
  114. package/src/graph/types.ts +10 -0
  115. package/src/index.ts +1 -0
  116. package/src/projections/await-tracker-projection.specs.ts +24 -0
  117. package/src/projections/await-tracker-projection.ts +68 -0
  118. package/src/projections/index.ts +11 -0
  119. package/src/projections/item-status-projection.specs.ts +130 -0
  120. package/src/projections/item-status-projection.ts +32 -0
  121. package/src/projections/latest-run-projection.ts +20 -0
  122. package/src/projections/message-log-projection.ts +113 -0
  123. package/src/projections/node-status-projection.ts +33 -0
  124. package/src/projections/phased-execution-projection.specs.ts +202 -0
  125. package/src/projections/phased-execution-projection.ts +146 -0
  126. package/src/projections/settled-instance-projection.specs.ts +296 -0
  127. package/src/projections/settled-instance-projection.ts +160 -0
  128. package/src/projections/stats-projection.ts +26 -0
  129. package/src/runtime/await-tracker.specs.ts +57 -34
  130. package/src/runtime/await-tracker.ts +43 -31
  131. package/src/runtime/context.ts +1 -1
  132. package/src/runtime/event-command-map.ts +11 -4
  133. package/src/runtime/phased-executor.specs.ts +357 -81
  134. package/src/runtime/phased-executor.ts +134 -128
  135. package/src/runtime/pipeline-runtime.specs.ts +65 -0
  136. package/src/runtime/pipeline-runtime.ts +6 -4
  137. package/src/runtime/settled-tracker.specs.ts +716 -120
  138. package/src/runtime/settled-tracker.ts +100 -102
  139. package/src/server/pipeline-server.e2e.specs.ts +10 -16
  140. package/src/server/pipeline-server.specs.ts +1441 -211
  141. package/src/server/pipeline-server.ts +535 -144
  142. package/src/server/sse-manager.specs.ts +67 -36
  143. package/src/server/sse-manager.ts +0 -4
  144. package/src/store/index.ts +2 -0
  145. package/src/store/pipeline-event-store.specs.ts +357 -0
  146. package/src/store/pipeline-event-store.ts +156 -0
  147. package/src/store/pipeline-read-model.specs.ts +1170 -0
  148. package/src/store/pipeline-read-model.ts +223 -0
  149. package/src/store/sqlite-pipeline-event-store.specs.ts +13 -0
  150. package/src/store/sqlite-pipeline-event-store.ts +36 -0
  151. package/src/testing/fixtures/kanban-full.pipeline.ts +2 -2
  152. package/src/testing/fixtures/kanban.pipeline.ts +2 -2
  153. package/tsconfig.json +1 -1
  154. package/vitest.config.ts +1 -8
  155. package/claude.md +0 -160
  156. package/dist/src/__tests__/e2e/helpers.d.ts +0 -48
  157. package/dist/src/__tests__/e2e/helpers.d.ts.map +0 -1
  158. package/dist/src/__tests__/e2e/helpers.js +0 -253
  159. package/dist/src/__tests__/e2e/helpers.js.map +0 -1
  160. package/dist/src/__tests__/e2e/kanban-migration.e2e.specs.d.ts +0 -2
  161. package/dist/src/__tests__/e2e/kanban-migration.e2e.specs.d.ts.map +0 -1
  162. package/dist/src/__tests__/e2e/kanban-migration.e2e.specs.js +0 -195
  163. package/dist/src/__tests__/e2e/kanban-migration.e2e.specs.js.map +0 -1
  164. package/dist/src/__tests__/e2e/types.d.ts +0 -107
  165. package/dist/src/__tests__/e2e/types.d.ts.map +0 -1
  166. package/dist/src/__tests__/e2e/types.js +0 -2
  167. package/dist/src/__tests__/e2e/types.js.map +0 -1
  168. package/dist/src/builder/define.specs.d.ts +0 -2
  169. package/dist/src/builder/define.specs.d.ts.map +0 -1
  170. package/dist/src/builder/define.specs.js +0 -435
  171. package/dist/src/builder/define.specs.js.map +0 -1
  172. package/dist/src/core/descriptors.specs.d.ts +0 -2
  173. package/dist/src/core/descriptors.specs.d.ts.map +0 -1
  174. package/dist/src/core/descriptors.specs.js +0 -24
  175. package/dist/src/core/descriptors.specs.js.map +0 -1
  176. package/dist/src/core/types.specs.d.ts +0 -2
  177. package/dist/src/core/types.specs.d.ts.map +0 -1
  178. package/dist/src/core/types.specs.js +0 -40
  179. package/dist/src/core/types.specs.js.map +0 -1
  180. package/dist/src/file-syncer/crypto/jwe-encryptor.d.ts +0 -15
  181. package/dist/src/file-syncer/crypto/jwe-encryptor.d.ts.map +0 -1
  182. package/dist/src/file-syncer/crypto/jwe-encryptor.js +0 -64
  183. package/dist/src/file-syncer/crypto/jwe-encryptor.js.map +0 -1
  184. package/dist/src/file-syncer/crypto/provider-resolver.d.ts +0 -24
  185. package/dist/src/file-syncer/crypto/provider-resolver.d.ts.map +0 -1
  186. package/dist/src/file-syncer/crypto/provider-resolver.js +0 -71
  187. package/dist/src/file-syncer/crypto/provider-resolver.js.map +0 -1
  188. package/dist/src/file-syncer/discovery/bareImports.d.ts +0 -3
  189. package/dist/src/file-syncer/discovery/bareImports.d.ts.map +0 -1
  190. package/dist/src/file-syncer/discovery/bareImports.js +0 -36
  191. package/dist/src/file-syncer/discovery/bareImports.js.map +0 -1
  192. package/dist/src/file-syncer/discovery/dts.d.ts +0 -8
  193. package/dist/src/file-syncer/discovery/dts.d.ts.map +0 -1
  194. package/dist/src/file-syncer/discovery/dts.js +0 -99
  195. package/dist/src/file-syncer/discovery/dts.js.map +0 -1
  196. package/dist/src/file-syncer/index.d.ts +0 -46
  197. package/dist/src/file-syncer/index.d.ts.map +0 -1
  198. package/dist/src/file-syncer/index.js +0 -392
  199. package/dist/src/file-syncer/index.js.map +0 -1
  200. package/dist/src/file-syncer/sync/resolveSyncFileSet.d.ts +0 -7
  201. package/dist/src/file-syncer/sync/resolveSyncFileSet.d.ts.map +0 -1
  202. package/dist/src/file-syncer/sync/resolveSyncFileSet.js +0 -86
  203. package/dist/src/file-syncer/sync/resolveSyncFileSet.js.map +0 -1
  204. package/dist/src/file-syncer/types/wire.d.ts +0 -14
  205. package/dist/src/file-syncer/types/wire.d.ts.map +0 -1
  206. package/dist/src/file-syncer/types/wire.js +0 -2
  207. package/dist/src/file-syncer/types/wire.js.map +0 -1
  208. package/dist/src/file-syncer/utils/hash.d.ts +0 -5
  209. package/dist/src/file-syncer/utils/hash.d.ts.map +0 -1
  210. package/dist/src/file-syncer/utils/hash.js +0 -19
  211. package/dist/src/file-syncer/utils/hash.js.map +0 -1
  212. package/dist/src/file-syncer/utils/path.d.ts +0 -13
  213. package/dist/src/file-syncer/utils/path.d.ts.map +0 -1
  214. package/dist/src/file-syncer/utils/path.js +0 -74
  215. package/dist/src/file-syncer/utils/path.js.map +0 -1
  216. package/dist/src/graph/types.specs.d.ts +0 -2
  217. package/dist/src/graph/types.specs.d.ts.map +0 -1
  218. package/dist/src/graph/types.specs.js +0 -148
  219. package/dist/src/graph/types.specs.js.map +0 -1
  220. package/dist/src/logging/event-logger.specs.d.ts +0 -2
  221. package/dist/src/logging/event-logger.specs.d.ts.map +0 -1
  222. package/dist/src/logging/event-logger.specs.js +0 -81
  223. package/dist/src/logging/event-logger.specs.js.map +0 -1
  224. package/dist/src/plugins/handler-adapter.specs.d.ts +0 -2
  225. package/dist/src/plugins/handler-adapter.specs.d.ts.map +0 -1
  226. package/dist/src/plugins/handler-adapter.specs.js +0 -129
  227. package/dist/src/plugins/handler-adapter.specs.js.map +0 -1
  228. package/dist/src/plugins/plugin-loader.specs.d.ts +0 -2
  229. package/dist/src/plugins/plugin-loader.specs.d.ts.map +0 -1
  230. package/dist/src/plugins/plugin-loader.specs.js +0 -246
  231. package/dist/src/plugins/plugin-loader.specs.js.map +0 -1
  232. package/dist/src/runtime/await-tracker.specs.d.ts +0 -2
  233. package/dist/src/runtime/await-tracker.specs.d.ts.map +0 -1
  234. package/dist/src/runtime/await-tracker.specs.js +0 -46
  235. package/dist/src/runtime/await-tracker.specs.js.map +0 -1
  236. package/dist/src/runtime/context.specs.d.ts +0 -2
  237. package/dist/src/runtime/context.specs.d.ts.map +0 -1
  238. package/dist/src/runtime/context.specs.js +0 -26
  239. package/dist/src/runtime/context.specs.js.map +0 -1
  240. package/dist/src/runtime/event-command-map.specs.d.ts +0 -2
  241. package/dist/src/runtime/event-command-map.specs.d.ts.map +0 -1
  242. package/dist/src/runtime/event-command-map.specs.js +0 -108
  243. package/dist/src/runtime/event-command-map.specs.js.map +0 -1
  244. package/dist/src/runtime/phased-executor.specs.d.ts +0 -2
  245. package/dist/src/runtime/phased-executor.specs.d.ts.map +0 -1
  246. package/dist/src/runtime/phased-executor.specs.js +0 -256
  247. package/dist/src/runtime/phased-executor.specs.js.map +0 -1
  248. package/dist/src/runtime/pipeline-runtime.specs.d.ts +0 -2
  249. package/dist/src/runtime/pipeline-runtime.specs.d.ts.map +0 -1
  250. package/dist/src/runtime/pipeline-runtime.specs.js +0 -192
  251. package/dist/src/runtime/pipeline-runtime.specs.js.map +0 -1
  252. package/dist/src/runtime/settled-tracker.specs.d.ts +0 -2
  253. package/dist/src/runtime/settled-tracker.specs.d.ts.map +0 -1
  254. package/dist/src/runtime/settled-tracker.specs.js +0 -361
  255. package/dist/src/runtime/settled-tracker.specs.js.map +0 -1
  256. package/dist/src/server/full-orchestration.e2e.specs.d.ts +0 -2
  257. package/dist/src/server/full-orchestration.e2e.specs.d.ts.map +0 -1
  258. package/dist/src/server/full-orchestration.e2e.specs.js +0 -561
  259. package/dist/src/server/full-orchestration.e2e.specs.js.map +0 -1
  260. package/dist/src/server/pipeline-server.e2e.specs.d.ts +0 -2
  261. package/dist/src/server/pipeline-server.e2e.specs.d.ts.map +0 -1
  262. package/dist/src/server/pipeline-server.e2e.specs.js +0 -381
  263. package/dist/src/server/pipeline-server.e2e.specs.js.map +0 -1
  264. package/dist/src/server/pipeline-server.specs.d.ts +0 -2
  265. package/dist/src/server/pipeline-server.specs.d.ts.map +0 -1
  266. package/dist/src/server/pipeline-server.specs.js +0 -662
  267. package/dist/src/server/pipeline-server.specs.js.map +0 -1
  268. package/dist/src/server/sse-manager.specs.d.ts +0 -2
  269. package/dist/src/server/sse-manager.specs.d.ts.map +0 -1
  270. package/dist/src/server/sse-manager.specs.js +0 -158
  271. package/dist/src/server/sse-manager.specs.js.map +0 -1
  272. package/dist/src/testing/event-capture.specs.d.ts +0 -2
  273. package/dist/src/testing/event-capture.specs.d.ts.map +0 -1
  274. package/dist/src/testing/event-capture.specs.js +0 -114
  275. package/dist/src/testing/event-capture.specs.js.map +0 -1
  276. package/dist/src/testing/fixtures/kanban-full.pipeline.specs.d.ts +0 -2
  277. package/dist/src/testing/fixtures/kanban-full.pipeline.specs.d.ts.map +0 -1
  278. package/dist/src/testing/fixtures/kanban-full.pipeline.specs.js +0 -263
  279. package/dist/src/testing/fixtures/kanban-full.pipeline.specs.js.map +0 -1
  280. package/dist/src/testing/fixtures/kanban.pipeline.specs.d.ts +0 -2
  281. package/dist/src/testing/fixtures/kanban.pipeline.specs.d.ts.map +0 -1
  282. package/dist/src/testing/fixtures/kanban.pipeline.specs.js +0 -29
  283. package/dist/src/testing/fixtures/kanban.pipeline.specs.js.map +0 -1
  284. package/dist/src/testing/kanban-todo.e2e.specs.d.ts +0 -2
  285. package/dist/src/testing/kanban-todo.e2e.specs.d.ts.map +0 -1
  286. package/dist/src/testing/kanban-todo.e2e.specs.js +0 -160
  287. package/dist/src/testing/kanban-todo.e2e.specs.js.map +0 -1
  288. package/dist/src/testing/mock-handlers.specs.d.ts +0 -2
  289. package/dist/src/testing/mock-handlers.specs.d.ts.map +0 -1
  290. package/dist/src/testing/mock-handlers.specs.js +0 -193
  291. package/dist/src/testing/mock-handlers.specs.js.map +0 -1
  292. package/dist/src/testing/real-execution.e2e.specs.d.ts +0 -2
  293. package/dist/src/testing/real-execution.e2e.specs.d.ts.map +0 -1
  294. package/dist/src/testing/real-execution.e2e.specs.js +0 -140
  295. package/dist/src/testing/real-execution.e2e.specs.js.map +0 -1
  296. package/dist/src/testing/real-plugin.e2e.specs.d.ts +0 -2
  297. package/dist/src/testing/real-plugin.e2e.specs.d.ts.map +0 -1
  298. package/dist/src/testing/real-plugin.e2e.specs.js +0 -65
  299. package/dist/src/testing/real-plugin.e2e.specs.js.map +0 -1
  300. package/dist/src/testing/server-startup.e2e.specs.d.ts +0 -2
  301. package/dist/src/testing/server-startup.e2e.specs.d.ts.map +0 -1
  302. package/dist/src/testing/server-startup.e2e.specs.js +0 -104
  303. package/dist/src/testing/server-startup.e2e.specs.js.map +0 -1
  304. package/dist/src/testing/snapshot-compare.specs.d.ts +0 -2
  305. package/dist/src/testing/snapshot-compare.specs.d.ts.map +0 -1
  306. package/dist/src/testing/snapshot-compare.specs.js +0 -112
  307. package/dist/src/testing/snapshot-compare.specs.js.map +0 -1
  308. package/dist/src/testing/snapshot-sanitize.specs.d.ts +0 -2
  309. package/dist/src/testing/snapshot-sanitize.specs.d.ts.map +0 -1
  310. package/dist/src/testing/snapshot-sanitize.specs.js +0 -104
  311. package/dist/src/testing/snapshot-sanitize.specs.js.map +0 -1
  312. package/docs/testing-analysis.md +0 -395
  313. package/pomodoro-plan.md +0 -651
  314. package/src/core/descriptors.specs.ts +0 -28
  315. package/src/core/types.specs.ts +0 -44
  316. package/src/runtime/context.specs.ts +0 -28
@@ -1,6 +1,9 @@
1
1
  import type { Event } from '@auto-engineer/message-bus';
2
- import { beforeEach, describe, expect, it } from 'vitest';
2
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
3
3
  import type { ForEachPhasedDescriptor } from '../core/descriptors';
4
+ import type { PhasedExecutionEvent } from '../projections/phased-execution-projection';
5
+ import type { PipelineEventStoreContext } from '../store/pipeline-event-store';
6
+ import { createPipelineEventStore } from '../store/pipeline-event-store';
4
7
  import { PhasedExecutor } from './phased-executor';
5
8
 
6
9
  interface TestItem {
@@ -21,33 +24,59 @@ function createHandler(_items: TestItem[]): ForEachPhasedDescriptor {
21
24
  data: { filePath: (item as TestItem).id },
22
25
  }),
23
26
  completion: {
24
- successEvent: 'AllComponentsImplemented',
25
- failureEvent: 'ComponentsFailed',
27
+ successEvent: { name: 'AllComponentsImplemented' },
28
+ failureEvent: { name: 'ComponentsFailed' },
26
29
  itemKey: (e: Event) => (e.data as { filePath?: string; id?: string }).filePath ?? (e.data as TestItem).id,
27
30
  },
28
31
  };
29
32
  }
30
33
 
34
+ interface ESExecutorOptions {
35
+ onEventEmit?: (event: PhasedExecutionEvent) => void;
36
+ }
37
+
38
+ function createESExecutor(
39
+ ctx: PipelineEventStoreContext,
40
+ dispatched: Array<{ commandType: string; data: unknown; correlationId: string }>,
41
+ completed: Event[],
42
+ options: ESExecutorOptions = {},
43
+ ): PhasedExecutor {
44
+ return new PhasedExecutor({
45
+ readModel: ctx.readModel,
46
+ onDispatch: (commandType, data, correlationId) => {
47
+ dispatched.push({ commandType, data, correlationId });
48
+ },
49
+ onComplete: (event) => {
50
+ completed.push(event);
51
+ },
52
+ onEventEmit: async (event) => {
53
+ const data = event.data as Record<string, unknown>;
54
+ const correlationId = (data.correlationId as string) ?? (data.executionId as string)?.split('-')[1] ?? 'default';
55
+ await ctx.eventStore.appendToStream(`phased-${correlationId}`, [{ type: event.type, data: event.data }]);
56
+ options.onEventEmit?.(event);
57
+ },
58
+ });
59
+ }
60
+
31
61
  describe('PhasedExecutor', () => {
32
62
  let executor: PhasedExecutor;
33
63
  let dispatched: Array<{ commandType: string; data: unknown; correlationId: string }>;
34
64
  let completed: Event[];
65
+ let ctx: PipelineEventStoreContext;
35
66
 
36
67
  beforeEach(() => {
37
68
  dispatched = [];
38
69
  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
- });
70
+ ctx = createPipelineEventStore();
71
+ executor = createESExecutor(ctx, dispatched, completed);
72
+ });
73
+
74
+ afterEach(async () => {
75
+ await ctx.close();
47
76
  });
48
77
 
49
78
  describe('phase gating', () => {
50
- it('should dispatch only first phase items initially', () => {
79
+ it('should dispatch only first phase items initially', async () => {
51
80
  const items: TestItem[] = [
52
81
  { id: 'm1', type: 'molecule' },
53
82
  { id: 'm2', type: 'molecule' },
@@ -57,49 +86,60 @@ describe('PhasedExecutor', () => {
57
86
  const handler = createHandler(items);
58
87
  const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
59
88
 
60
- executor.startPhased(handler, event, 'c1');
89
+ await executor.startPhased(handler, event, 'c1');
61
90
 
62
91
  expect(dispatched).toHaveLength(2);
63
92
  expect(dispatched.map((d) => (d.data as { filePath: string }).filePath)).toEqual(['m1', 'm2']);
64
93
  });
65
94
 
66
- it('should wait for all items in phase to complete before next phase', () => {
95
+ it('should wait for all items in phase to complete before next phase', async () => {
67
96
  const items: TestItem[] = [
68
97
  { id: 'm1', type: 'molecule' },
69
98
  { id: 'm2', type: 'molecule' },
70
99
  { id: 'o1', type: 'organism' },
71
100
  ];
72
101
  const handler = createHandler(items);
102
+ executor.registerHandler(handler);
73
103
  const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
74
104
 
75
- executor.startPhased(handler, event, 'c1');
105
+ await executor.startPhased(handler, event, 'c1');
76
106
 
77
107
  expect(dispatched).toHaveLength(2);
78
108
 
79
- executor.onEventReceived({ type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm1' } }, 'm1');
109
+ await executor.onEventReceived(
110
+ { type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm1' } },
111
+ 'm1',
112
+ );
80
113
 
81
114
  expect(dispatched).toHaveLength(2);
82
115
 
83
- executor.onEventReceived({ type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm2' } }, 'm2');
116
+ await executor.onEventReceived(
117
+ { type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm2' } },
118
+ 'm2',
119
+ );
84
120
 
85
121
  expect(dispatched).toHaveLength(3);
86
122
  expect((dispatched[2].data as { filePath: string }).filePath).toBe('o1');
87
123
  });
88
124
 
89
- it('should skip empty phases', () => {
125
+ it('should skip empty phases', async () => {
90
126
  const items: TestItem[] = [
91
127
  { id: 'm1', type: 'molecule' },
92
128
  { id: 'p1', type: 'page' },
93
129
  ];
94
130
  const handler = createHandler(items);
131
+ executor.registerHandler(handler);
95
132
  const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
96
133
 
97
- executor.startPhased(handler, event, 'c1');
134
+ await executor.startPhased(handler, event, 'c1');
98
135
 
99
136
  expect(dispatched).toHaveLength(1);
100
137
  expect((dispatched[0].data as { filePath: string }).filePath).toBe('m1');
101
138
 
102
- executor.onEventReceived({ type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm1' } }, 'm1');
139
+ await executor.onEventReceived(
140
+ { type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm1' } },
141
+ 'm1',
142
+ );
103
143
 
104
144
  expect(dispatched).toHaveLength(2);
105
145
  expect((dispatched[1].data as { filePath: string }).filePath).toBe('p1');
@@ -107,73 +147,93 @@ describe('PhasedExecutor', () => {
107
147
  });
108
148
 
109
149
  describe('completion tracking', () => {
110
- it('should emit success event when all phases complete', () => {
150
+ it('should emit success event when all phases complete', async () => {
111
151
  const items: TestItem[] = [
112
152
  { id: 'm1', type: 'molecule' },
113
153
  { id: 'o1', type: 'organism' },
114
154
  ];
115
155
  const handler = createHandler(items);
156
+ executor.registerHandler(handler);
116
157
  const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
117
158
 
118
- executor.startPhased(handler, event, 'c1');
159
+ await executor.startPhased(handler, event, 'c1');
119
160
 
120
- executor.onEventReceived({ type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm1' } }, 'm1');
121
- executor.onEventReceived({ type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'o1' } }, 'o1');
161
+ await executor.onEventReceived(
162
+ { type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm1' } },
163
+ 'm1',
164
+ );
165
+ await executor.onEventReceived(
166
+ { type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'o1' } },
167
+ 'o1',
168
+ );
122
169
 
123
170
  expect(completed).toHaveLength(1);
124
171
  expect(completed[0].type).toBe('AllComponentsImplemented');
125
172
  expect(completed[0].correlationId).toBe('c1');
126
173
  });
127
174
 
128
- it('should cleanup session after completion', () => {
175
+ it('should cleanup session after completion allowing new session with same correlationId', async () => {
129
176
  const items: TestItem[] = [{ id: 'm1', type: 'molecule' }];
130
177
  const handler = createHandler(items);
178
+ executor.registerHandler(handler);
131
179
  const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
132
180
 
133
- executor.startPhased(handler, event, 'c1');
134
- expect(executor.getActiveSessionCount()).toBe(1);
181
+ await executor.startPhased(handler, event, 'c1');
182
+ await executor.onEventReceived(
183
+ { type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm1' } },
184
+ 'm1',
185
+ );
186
+
187
+ expect(completed).toHaveLength(1);
135
188
 
136
- executor.onEventReceived({ type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm1' } }, 'm1');
189
+ dispatched.length = 0;
190
+ completed.length = 0;
137
191
 
138
- expect(executor.getActiveSessionCount()).toBe(0);
192
+ await executor.startPhased(handler, event, 'c1');
193
+ expect(dispatched).toHaveLength(1);
194
+ expect((dispatched[0].data as { filePath: string }).filePath).toBe('m1');
139
195
  });
140
196
  });
141
197
 
142
198
  describe('state queries', () => {
143
- it('should report phase completion status', () => {
199
+ it('should report phase completion status', async () => {
144
200
  const items: TestItem[] = [
145
201
  { id: 'm1', type: 'molecule' },
146
202
  { id: 'o1', type: 'organism' },
147
203
  ];
148
204
  const handler = createHandler(items);
205
+ executor.registerHandler(handler);
149
206
  const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
150
207
 
151
- executor.startPhased(handler, event, 'c1');
208
+ await executor.startPhased(handler, event, 'c1');
152
209
 
153
- expect(executor.isPhaseComplete('c1', 'molecule')).toBe(false);
154
- expect(executor.isPhaseComplete('c1', 'organism')).toBe(false);
210
+ expect(await executor.isPhaseComplete('c1', 'molecule')).toBe(false);
211
+ expect(await executor.isPhaseComplete('c1', 'organism')).toBe(false);
155
212
 
156
- executor.onEventReceived({ type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm1' } }, 'm1');
213
+ await executor.onEventReceived(
214
+ { type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm1' } },
215
+ 'm1',
216
+ );
157
217
 
158
- expect(executor.isPhaseComplete('c1', 'molecule')).toBe(true);
159
- expect(executor.isPhaseComplete('c1', 'organism')).toBe(false);
218
+ expect(await executor.isPhaseComplete('c1', 'molecule')).toBe(true);
219
+ expect(await executor.isPhaseComplete('c1', 'organism')).toBe(false);
160
220
  });
161
221
 
162
- it('should return false for unknown correlationId', () => {
163
- expect(executor.isPhaseComplete('unknown', 'molecule')).toBe(false);
222
+ it('should return false for unknown correlationId', async () => {
223
+ expect(await executor.isPhaseComplete('unknown', 'molecule')).toBe(false);
164
224
  });
165
225
 
166
- it('should return false for unknown phase name', () => {
226
+ it('should return false for unknown phase name', async () => {
167
227
  const items: TestItem[] = [{ id: 'm1', type: 'molecule' }];
168
228
  const handler = createHandler(items);
169
229
  const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
170
230
 
171
- executor.startPhased(handler, event, 'c1');
231
+ await executor.startPhased(handler, event, 'c1');
172
232
 
173
- expect(executor.isPhaseComplete('c1', 'nonexistent-phase')).toBe(false);
233
+ expect(await executor.isPhaseComplete('c1', 'nonexistent-phase')).toBe(false);
174
234
  });
175
235
 
176
- it('should return false for future phase when current phase is earlier', () => {
236
+ it('should return false for future phase when current phase is earlier', async () => {
177
237
  const items: TestItem[] = [
178
238
  { id: 'm1', type: 'molecule' },
179
239
  { id: 'p1', type: 'page' },
@@ -181,12 +241,12 @@ describe('PhasedExecutor', () => {
181
241
  const handler = createHandler(items);
182
242
  const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
183
243
 
184
- executor.startPhased(handler, event, 'c1');
244
+ await executor.startPhased(handler, event, 'c1');
185
245
 
186
- expect(executor.isPhaseComplete('c1', 'page')).toBe(false);
246
+ expect(await executor.isPhaseComplete('c1', 'page')).toBe(false);
187
247
  });
188
248
 
189
- it('should check correct session when multiple sessions exist with different correlationIds', () => {
249
+ it('should check correct session when multiple sessions exist with different correlationIds', async () => {
190
250
  const items1: TestItem[] = [
191
251
  { id: 'm1', type: 'molecule' },
192
252
  { id: 'o1', type: 'organism' },
@@ -197,27 +257,32 @@ describe('PhasedExecutor', () => {
197
257
  ];
198
258
  const handler1 = createHandler(items1);
199
259
  const handler2 = createHandler(items2);
260
+ executor.registerHandler(handler1);
261
+ executor.registerHandler(handler2);
200
262
 
201
- executor.startPhased(
263
+ await executor.startPhased(
202
264
  handler1,
203
265
  { type: 'ClientGenerated', correlationId: 'c1', data: { components: items1 } },
204
266
  'c1',
205
267
  );
206
- executor.startPhased(
268
+ await executor.startPhased(
207
269
  handler2,
208
270
  { type: 'ClientGenerated', correlationId: 'c2', data: { components: items2 } },
209
271
  'c2',
210
272
  );
211
273
 
212
- executor.onEventReceived({ type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm1' } }, 'm1');
274
+ await executor.onEventReceived(
275
+ { type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm1' } },
276
+ 'm1',
277
+ );
213
278
 
214
- expect(executor.isPhaseComplete('c1', 'molecule')).toBe(true);
215
- expect(executor.isPhaseComplete('c2', 'molecule')).toBe(false);
279
+ expect(await executor.isPhaseComplete('c1', 'molecule')).toBe(true);
280
+ expect(await executor.isPhaseComplete('c2', 'molecule')).toBe(false);
216
281
  });
217
282
  });
218
283
 
219
284
  describe('failure handling', () => {
220
- it('should stop on failure when stopOnFailure is true', () => {
285
+ it('should stop on failure when stopOnFailure is true and emit failure event', async () => {
221
286
  const items: TestItem[] = [
222
287
  { id: 'm1', type: 'molecule' },
223
288
  { id: 'm2', type: 'molecule' },
@@ -227,132 +292,343 @@ describe('PhasedExecutor', () => {
227
292
  ...createHandler(items),
228
293
  stopOnFailure: true,
229
294
  };
295
+ executor.registerHandler(handler);
230
296
  const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
231
297
 
232
- executor.startPhased(handler, event, 'c1');
298
+ await executor.startPhased(handler, event, 'c1');
233
299
 
234
- executor.onEventReceived({ type: 'ComponentsFailed', correlationId: 'c1', data: { filePath: 'm1' } }, 'm1');
300
+ await executor.onEventReceived({ type: 'ComponentsFailed', correlationId: 'c1', data: { filePath: 'm1' } }, 'm1');
235
301
 
236
302
  expect(completed).toHaveLength(1);
237
303
  expect(completed[0].type).toBe('ComponentsFailed');
238
- expect(executor.getActiveSessionCount()).toBe(0);
239
304
  });
240
305
 
241
- it('should continue on failure when stopOnFailure is false', () => {
306
+ it('should continue on failure when stopOnFailure is false', async () => {
242
307
  const items: TestItem[] = [
243
308
  { id: 'm1', type: 'molecule' },
244
309
  { id: 'm2', type: 'molecule' },
245
310
  { id: 'o1', type: 'organism' },
246
311
  ];
247
312
  const handler = createHandler(items);
313
+ executor.registerHandler(handler);
248
314
  const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
249
315
 
250
- executor.startPhased(handler, event, 'c1');
316
+ await executor.startPhased(handler, event, 'c1');
251
317
 
252
- executor.onEventReceived({ type: 'ComponentsFailed', correlationId: 'c1', data: { filePath: 'm1' } }, 'm1');
318
+ await executor.onEventReceived({ type: 'ComponentsFailed', correlationId: 'c1', data: { filePath: 'm1' } }, 'm1');
253
319
 
254
320
  expect(completed).toHaveLength(0);
255
- expect(executor.getActiveSessionCount()).toBe(1);
256
321
 
257
- executor.onEventReceived({ type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm2' } }, 'm2');
322
+ await executor.onEventReceived(
323
+ { type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm2' } },
324
+ 'm2',
325
+ );
258
326
 
259
327
  expect(dispatched).toHaveLength(3);
260
328
  });
329
+
330
+ it('should cleanup session after stopOnFailure allowing new session with same correlationId', async () => {
331
+ const items: TestItem[] = [
332
+ { id: 'm1', type: 'molecule' },
333
+ { id: 'o1', type: 'organism' },
334
+ ];
335
+ const handler: ForEachPhasedDescriptor = {
336
+ ...createHandler(items),
337
+ stopOnFailure: true,
338
+ };
339
+ executor.registerHandler(handler);
340
+ const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
341
+
342
+ await executor.startPhased(handler, event, 'c1');
343
+ await executor.onEventReceived({ type: 'ComponentsFailed', correlationId: 'c1', data: { filePath: 'm1' } }, 'm1');
344
+
345
+ expect(completed).toHaveLength(1);
346
+ expect(completed[0].type).toBe('ComponentsFailed');
347
+
348
+ dispatched.length = 0;
349
+ completed.length = 0;
350
+
351
+ await executor.startPhased(handler, event, 'c1');
352
+ expect(dispatched).toHaveLength(1);
353
+ });
261
354
  });
262
355
 
263
356
  describe('concurrent sessions', () => {
264
- it('should track sessions independently by correlationId', () => {
357
+ it('should track sessions independently by correlationId', async () => {
265
358
  const items: TestItem[] = [{ id: 'm1', type: 'molecule' }];
266
359
  const handler = createHandler(items);
360
+ executor.registerHandler(handler);
267
361
 
268
- executor.startPhased(
362
+ await executor.startPhased(
269
363
  handler,
270
364
  { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } },
271
365
  'c1',
272
366
  );
273
- executor.startPhased(
367
+ await executor.startPhased(
274
368
  handler,
275
369
  { type: 'ClientGenerated', correlationId: 'c2', data: { components: items } },
276
370
  'c2',
277
371
  );
278
372
 
279
- expect(executor.getActiveSessionCount()).toBe(2);
373
+ expect(dispatched).toHaveLength(2);
374
+ expect(dispatched[0].correlationId).toBe('c1');
375
+ expect(dispatched[1].correlationId).toBe('c2');
280
376
 
281
- executor.onEventReceived({ type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm1' } }, 'm1');
377
+ await executor.onEventReceived(
378
+ { type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm1' } },
379
+ 'm1',
380
+ );
282
381
 
283
- expect(executor.getActiveSessionCount()).toBe(1);
284
382
  expect(completed).toHaveLength(1);
285
383
  expect(completed[0].correlationId).toBe('c1');
384
+
385
+ await executor.onEventReceived(
386
+ { type: 'ComponentImplemented', correlationId: 'c2', data: { filePath: 'm1' } },
387
+ 'm1',
388
+ );
389
+
390
+ expect(completed).toHaveLength(2);
391
+ expect(completed[1].correlationId).toBe('c2');
392
+ });
393
+
394
+ it('should not interfere between concurrent sessions with different items', async () => {
395
+ const items1: TestItem[] = [
396
+ { id: 'a1', type: 'molecule' },
397
+ { id: 'a2', type: 'organism' },
398
+ ];
399
+ const items2: TestItem[] = [
400
+ { id: 'b1', type: 'molecule' },
401
+ { id: 'b2', type: 'page' },
402
+ ];
403
+ const handler1 = createHandler(items1);
404
+ const handler2 = createHandler(items2);
405
+ executor.registerHandler(handler1);
406
+ executor.registerHandler(handler2);
407
+
408
+ await executor.startPhased(
409
+ handler1,
410
+ { type: 'ClientGenerated', correlationId: 'c1', data: { components: items1 } },
411
+ 'c1',
412
+ );
413
+ await executor.startPhased(
414
+ handler2,
415
+ { type: 'ClientGenerated', correlationId: 'c2', data: { components: items2 } },
416
+ 'c2',
417
+ );
418
+
419
+ expect(dispatched).toHaveLength(2);
420
+
421
+ await executor.onEventReceived(
422
+ { type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'a1' } },
423
+ 'a1',
424
+ );
425
+
426
+ expect(dispatched).toHaveLength(3);
427
+ expect((dispatched[2].data as { filePath: string }).filePath).toBe('a2');
428
+ expect(dispatched[2].correlationId).toBe('c1');
429
+
430
+ await executor.onEventReceived(
431
+ { type: 'ComponentImplemented', correlationId: 'c2', data: { filePath: 'b1' } },
432
+ 'b1',
433
+ );
434
+
435
+ expect(dispatched).toHaveLength(4);
436
+ expect((dispatched[3].data as { filePath: string }).filePath).toBe('b2');
437
+ expect(dispatched[3].correlationId).toBe('c2');
286
438
  });
287
439
  });
288
440
 
289
441
  describe('event deduplication', () => {
290
- it('should ignore duplicate events for already completed items', () => {
442
+ it('should ignore duplicate events for already completed items', async () => {
291
443
  const items: TestItem[] = [
292
444
  { id: 'm1', type: 'molecule' },
293
445
  { id: 'm2', type: 'molecule' },
294
446
  { id: 'o1', type: 'organism' },
295
447
  ];
296
448
  const handler = createHandler(items);
449
+ executor.registerHandler(handler);
297
450
  const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
298
451
 
299
- executor.startPhased(handler, event, 'c1');
452
+ await executor.startPhased(handler, event, 'c1');
300
453
 
301
- executor.onEventReceived({ type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm1' } }, 'm1');
454
+ await executor.onEventReceived(
455
+ { type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm1' } },
456
+ 'm1',
457
+ );
302
458
 
303
459
  expect(dispatched).toHaveLength(2);
304
460
 
305
- executor.onEventReceived({ type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm1' } }, 'm1');
461
+ await executor.onEventReceived(
462
+ { type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm1' } },
463
+ 'm1',
464
+ );
306
465
 
307
466
  expect(dispatched).toHaveLength(2);
308
467
  });
309
468
  });
310
469
 
311
470
  describe('event edge cases', () => {
312
- it('should ignore events with undefined correlationId', () => {
471
+ it('should ignore events with undefined correlationId', async () => {
313
472
  const items: TestItem[] = [{ id: 'm1', type: 'molecule' }];
314
473
  const handler = createHandler(items);
315
474
  const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
316
475
 
317
- executor.startPhased(handler, event, 'c1');
476
+ await executor.startPhased(handler, event, 'c1');
318
477
 
319
478
  expect(dispatched).toHaveLength(1);
320
479
 
321
- executor.onEventReceived({ type: 'ComponentImplemented', data: { filePath: 'm1' } }, 'm1');
480
+ await executor.onEventReceived({ type: 'ComponentImplemented', data: { filePath: 'm1' } }, 'm1');
322
481
 
323
482
  expect(dispatched).toHaveLength(1);
324
- expect(executor.getActiveSessionCount()).toBe(1);
483
+ expect(completed).toHaveLength(0);
325
484
  });
326
485
 
327
- it('should ignore events with empty correlationId', () => {
486
+ it('should ignore events with empty correlationId', async () => {
328
487
  const items: TestItem[] = [{ id: 'm1', type: 'molecule' }];
329
488
  const handler = createHandler(items);
330
489
  const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
331
490
 
332
- executor.startPhased(handler, event, 'c1');
491
+ await executor.startPhased(handler, event, 'c1');
333
492
 
334
493
  expect(dispatched).toHaveLength(1);
335
494
 
336
- executor.onEventReceived({ type: 'ComponentImplemented', correlationId: '', data: { filePath: 'm1' } }, 'm1');
495
+ await executor.onEventReceived(
496
+ { type: 'ComponentImplemented', correlationId: '', data: { filePath: 'm1' } },
497
+ 'm1',
498
+ );
337
499
 
338
500
  expect(dispatched).toHaveLength(1);
339
- expect(executor.getActiveSessionCount()).toBe(1);
501
+ expect(completed).toHaveLength(0);
340
502
  });
341
503
 
342
- it('should ignore events with unknown itemKey', () => {
504
+ it('should ignore events with unknown itemKey', async () => {
343
505
  const items: TestItem[] = [{ id: 'm1', type: 'molecule' }];
344
506
  const handler = createHandler(items);
345
507
  const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
346
508
 
347
- executor.startPhased(handler, event, 'c1');
509
+ await executor.startPhased(handler, event, 'c1');
348
510
 
349
- executor.onEventReceived(
511
+ await executor.onEventReceived(
350
512
  { type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'unknown' } },
351
513
  'unknown',
352
514
  );
353
515
 
354
516
  expect(dispatched).toHaveLength(1);
355
- expect(executor.getActiveSessionCount()).toBe(1);
517
+ expect(completed).toHaveLength(0);
518
+ });
519
+ });
520
+
521
+ describe('event emission', () => {
522
+ let emittedEvents: PhasedExecutionEvent[];
523
+
524
+ beforeEach(() => {
525
+ emittedEvents = [];
526
+ executor = createESExecutor(ctx, dispatched, completed, {
527
+ onEventEmit: (event) => {
528
+ emittedEvents.push(event);
529
+ },
530
+ });
531
+ });
532
+
533
+ it('should emit PhasedExecutionStarted when starting', async () => {
534
+ const items: TestItem[] = [{ id: 'm1', type: 'molecule' }];
535
+ const handler = createHandler(items);
536
+ const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
537
+
538
+ await executor.startPhased(handler, event, 'c1');
539
+
540
+ const startEvent = emittedEvents.find((e) => e.type === 'PhasedExecutionStarted');
541
+ expect(startEvent).toBeDefined();
542
+ expect(startEvent?.data.correlationId).toBe('c1');
543
+ expect(startEvent?.data.items).toHaveLength(1);
544
+ });
545
+
546
+ it('should emit PhasedItemDispatched when dispatching items', async () => {
547
+ const items: TestItem[] = [{ id: 'm1', type: 'molecule' }];
548
+ const handler = createHandler(items);
549
+ const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
550
+
551
+ await executor.startPhased(handler, event, 'c1');
552
+
553
+ const dispatchEvents = emittedEvents.filter((e) => e.type === 'PhasedItemDispatched');
554
+ expect(dispatchEvents).toHaveLength(1);
555
+ expect(dispatchEvents[0].data.itemKey).toBe('m1');
556
+ });
557
+
558
+ it('should emit PhasedItemCompleted when item completes', async () => {
559
+ const items: TestItem[] = [{ id: 'm1', type: 'molecule' }];
560
+ const handler = createHandler(items);
561
+ executor.registerHandler(handler);
562
+ const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
563
+
564
+ await executor.startPhased(handler, event, 'c1');
565
+ await executor.onEventReceived(
566
+ { type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm1' } },
567
+ 'm1',
568
+ );
569
+
570
+ const completeEvents = emittedEvents.filter((e) => e.type === 'PhasedItemCompleted');
571
+ expect(completeEvents).toHaveLength(1);
572
+ expect(completeEvents[0].data.itemKey).toBe('m1');
573
+ });
574
+
575
+ it('should emit PhasedPhaseAdvanced when advancing phases', async () => {
576
+ const items: TestItem[] = [
577
+ { id: 'm1', type: 'molecule' },
578
+ { id: 'o1', type: 'organism' },
579
+ ];
580
+ const handler = createHandler(items);
581
+ executor.registerHandler(handler);
582
+ const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
583
+
584
+ await executor.startPhased(handler, event, 'c1');
585
+ await executor.onEventReceived(
586
+ { type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm1' } },
587
+ 'm1',
588
+ );
589
+
590
+ const advanceEvents = emittedEvents.filter((e) => e.type === 'PhasedPhaseAdvanced');
591
+ expect(advanceEvents).toHaveLength(1);
592
+ expect(advanceEvents[0].data.fromPhase).toBe(0);
593
+ expect(advanceEvents[0].data.toPhase).toBe(1);
594
+ });
595
+
596
+ it('should emit PhasedExecutionCompleted on success', async () => {
597
+ const items: TestItem[] = [{ id: 'm1', type: 'molecule' }];
598
+ const handler = createHandler(items);
599
+ executor.registerHandler(handler);
600
+ const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
601
+
602
+ await executor.startPhased(handler, event, 'c1');
603
+ await executor.onEventReceived(
604
+ { type: 'ComponentImplemented', correlationId: 'c1', data: { filePath: 'm1' } },
605
+ 'm1',
606
+ );
607
+
608
+ const completedEvents = emittedEvents.filter((e) => e.type === 'PhasedExecutionCompleted');
609
+ expect(completedEvents).toHaveLength(1);
610
+ expect(completedEvents[0].data.success).toBe(true);
611
+ });
612
+
613
+ it('should emit PhasedItemFailed and PhasedExecutionCompleted on failure', async () => {
614
+ const items: TestItem[] = [{ id: 'm1', type: 'molecule' }];
615
+ const handler: ForEachPhasedDescriptor = {
616
+ ...createHandler(items),
617
+ stopOnFailure: true,
618
+ };
619
+ executor.registerHandler(handler);
620
+ const event: Event = { type: 'ClientGenerated', correlationId: 'c1', data: { components: items } };
621
+
622
+ await executor.startPhased(handler, event, 'c1');
623
+ await executor.onEventReceived({ type: 'ComponentsFailed', correlationId: 'c1', data: { filePath: 'm1' } }, 'm1');
624
+
625
+ const failedEvents = emittedEvents.filter((e) => e.type === 'PhasedItemFailed');
626
+ expect(failedEvents).toHaveLength(1);
627
+ expect(failedEvents[0].data.itemKey).toBe('m1');
628
+
629
+ const completedEvents = emittedEvents.filter((e) => e.type === 'PhasedExecutionCompleted');
630
+ expect(completedEvents).toHaveLength(1);
631
+ expect(completedEvents[0].data.success).toBe(false);
356
632
  });
357
633
  });
358
634
  });