@neurocode-ai/core 1.18.8

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 (490) hide show
  1. package/bin/neurocode +199 -0
  2. package/bunfig.toml +2 -0
  3. package/drizzle.config.ts +10 -0
  4. package/package.json +131 -0
  5. package/schema.json +2071 -0
  6. package/script/fix-node-pty.ts +28 -0
  7. package/script/migration.ts +196 -0
  8. package/src/account/sql.ts +39 -0
  9. package/src/account.ts +101 -0
  10. package/src/agent.ts +111 -0
  11. package/src/aisdk.ts +235 -0
  12. package/src/background-job.ts +365 -0
  13. package/src/catalog.ts +301 -0
  14. package/src/command.ts +64 -0
  15. package/src/config/agent.ts +25 -0
  16. package/src/config/attachments.ts +15 -0
  17. package/src/config/command.ts +12 -0
  18. package/src/config/compaction.ts +15 -0
  19. package/src/config/experimental.ts +18 -0
  20. package/src/config/formatter.ts +12 -0
  21. package/src/config/lsp.ts +18 -0
  22. package/src/config/markdown.ts +36 -0
  23. package/src/config/mcp.ts +48 -0
  24. package/src/config/plugin/agent.ts +179 -0
  25. package/src/config/plugin/command.ts +83 -0
  26. package/src/config/plugin/external.ts +91 -0
  27. package/src/config/plugin/provider.ts +113 -0
  28. package/src/config/plugin/reference.ts +70 -0
  29. package/src/config/plugin/skill.ts +50 -0
  30. package/src/config/plugin.ts +13 -0
  31. package/src/config/provider.ts +71 -0
  32. package/src/config/reference.ts +22 -0
  33. package/src/config/tool-output.ts +9 -0
  34. package/src/config/watcher.ts +7 -0
  35. package/src/config.ts +227 -0
  36. package/src/control-plane/move-session.ts +148 -0
  37. package/src/control-plane/workspace.sql.ts +20 -0
  38. package/src/credential/sql.ts +14 -0
  39. package/src/credential.ts +138 -0
  40. package/src/cross-spawn-spawner.ts +507 -0
  41. package/src/data-migration.sql.ts +6 -0
  42. package/src/database/database.ts +57 -0
  43. package/src/database/migration/20260127222353_familiar_lady_ursula.ts +107 -0
  44. package/src/database/migration/20260211171708_add_project_commands.ts +11 -0
  45. package/src/database/migration/20260213144116_wakeful_the_professor.ts +23 -0
  46. package/src/database/migration/20260225215848_workspace.ts +19 -0
  47. package/src/database/migration/20260227213759_add_session_workspace_id.ts +12 -0
  48. package/src/database/migration/20260228203230_blue_harpoon.ts +30 -0
  49. package/src/database/migration/20260303231226_add_workspace_fields.ts +15 -0
  50. package/src/database/migration/20260309230000_move_org_to_state.ts +15 -0
  51. package/src/database/migration/20260312043431_session_message_cursor.ts +16 -0
  52. package/src/database/migration/20260323234822_events.ts +26 -0
  53. package/src/database/migration/20260410174513_workspace-name.ts +29 -0
  54. package/src/database/migration/20260413175956_chief_energizer.ts +24 -0
  55. package/src/database/migration/20260423070820_add_icon_url_override.ts +14 -0
  56. package/src/database/migration/20260427172553_slow_nightmare.ts +30 -0
  57. package/src/database/migration/20260428004200_add_session_path.ts +11 -0
  58. package/src/database/migration/20260501142318_next_venus.ts +12 -0
  59. package/src/database/migration/20260504145000_add_sync_owner.ts +11 -0
  60. package/src/database/migration/20260507164347_add_workspace_time.ts +11 -0
  61. package/src/database/migration/20260510033149_session_usage.ts +56 -0
  62. package/src/database/migration/20260511000411_data_migration_state.ts +16 -0
  63. package/src/database/migration/20260511173437_session-metadata.ts +16 -0
  64. package/src/database/migration/20260601010001_normalize_storage_paths.ts +22 -0
  65. package/src/database/migration/20260601202201_amazing_prowler.ts +11 -0
  66. package/src/database/migration/20260602002951_lowly_union_jack.ts +24 -0
  67. package/src/database/migration/20260602182828_add_project_directories.ts +20 -0
  68. package/src/database/migration/20260603001617_session_message_projection_indexes.ts +19 -0
  69. package/src/database/migration/20260603040000_session_message_projection_order.ts +19 -0
  70. package/src/database/migration/20260603141458_session_input_inbox.ts +25 -0
  71. package/src/database/migration/20260603160727_jittery_ezekiel_stane.ts +20 -0
  72. package/src/database/migration/20260604172448_event_sourced_session_input.ts +47 -0
  73. package/src/database/migration/20260605003541_add_session_context_snapshot.ts +21 -0
  74. package/src/database/migration/20260605042240_add_context_epoch_agent.ts +11 -0
  75. package/src/database/migration/20260611035744_credential.ts +25 -0
  76. package/src/database/migration/20260611192811_lush_chimera.ts +25 -0
  77. package/src/database/migration/20260612174303_project_dir_strategy.ts +29 -0
  78. package/src/database/migration/20260622142730_simplify_session_context_epoch.ts +13 -0
  79. package/src/database/migration/20260622170816_reset_v2_session_state.ts +17 -0
  80. package/src/database/migration/20260622202450_simplify_session_input.ts +17 -0
  81. package/src/database/migration.gen.ts +44 -0
  82. package/src/database/migration.ts +81 -0
  83. package/src/database/path.ts +91 -0
  84. package/src/database/schema.gen.ts +274 -0
  85. package/src/database/schema.sql.ts +10 -0
  86. package/src/database/sqlite.bun.ts +183 -0
  87. package/src/database/sqlite.node.ts +178 -0
  88. package/src/database/sqlite.ts +8 -0
  89. package/src/effect/app-node-builder.ts +23 -0
  90. package/src/effect/app-node-platform.ts +18 -0
  91. package/src/effect/app-node.ts +14 -0
  92. package/src/effect/dfdf +1 -0
  93. package/src/effect/keyed-mutex.ts +45 -0
  94. package/src/effect/layer-node.ts +333 -0
  95. package/src/effect/memo-map.ts +3 -0
  96. package/src/effect/runtime.ts +21 -0
  97. package/src/effect/service-use.ts +43 -0
  98. package/src/event/sql.ts +25 -0
  99. package/src/event.ts +638 -0
  100. package/src/file-mutation.ts +207 -0
  101. package/src/file.ts +6 -0
  102. package/src/filesystem/fff.bun.ts +140 -0
  103. package/src/filesystem/fff.node.ts +138 -0
  104. package/src/filesystem/ignore.ts +67 -0
  105. package/src/filesystem/protected.ts +53 -0
  106. package/src/filesystem/search.ts +239 -0
  107. package/src/filesystem/watcher.ts +140 -0
  108. package/src/filesystem.ts +118 -0
  109. package/src/flag/flag.ts +78 -0
  110. package/src/fs-util.ts +274 -0
  111. package/src/git.ts +987 -0
  112. package/src/github-copilot/README.md +5 -0
  113. package/src/github-copilot/chat/convert-to-openai-compatible-chat-messages.ts +170 -0
  114. package/src/github-copilot/chat/get-response-metadata.ts +15 -0
  115. package/src/github-copilot/chat/map-openai-compatible-finish-reason.ts +19 -0
  116. package/src/github-copilot/chat/openai-compatible-api-types.ts +64 -0
  117. package/src/github-copilot/chat/openai-compatible-chat-language-model.ts +815 -0
  118. package/src/github-copilot/chat/openai-compatible-chat-options.ts +28 -0
  119. package/src/github-copilot/chat/openai-compatible-metadata-extractor.ts +44 -0
  120. package/src/github-copilot/chat/openai-compatible-prepare-tools.ts +83 -0
  121. package/src/github-copilot/copilot-provider.ts +100 -0
  122. package/src/github-copilot/openai-compatible-error.ts +27 -0
  123. package/src/github-copilot/responses/convert-to-openai-responses-input.ts +335 -0
  124. package/src/github-copilot/responses/map-openai-responses-finish-reason.ts +22 -0
  125. package/src/github-copilot/responses/openai-config.ts +18 -0
  126. package/src/github-copilot/responses/openai-error.ts +22 -0
  127. package/src/github-copilot/responses/openai-responses-api-types.ts +214 -0
  128. package/src/github-copilot/responses/openai-responses-language-model.ts +1770 -0
  129. package/src/github-copilot/responses/openai-responses-prepare-tools.ts +173 -0
  130. package/src/github-copilot/responses/openai-responses-settings.ts +1 -0
  131. package/src/github-copilot/responses/tool/code-interpreter.ts +87 -0
  132. package/src/github-copilot/responses/tool/file-search.ts +127 -0
  133. package/src/github-copilot/responses/tool/image-generation.ts +114 -0
  134. package/src/github-copilot/responses/tool/local-shell.ts +64 -0
  135. package/src/github-copilot/responses/tool/web-search-preview.ts +103 -0
  136. package/src/github-copilot/responses/tool/web-search.ts +102 -0
  137. package/src/global.ts +87 -0
  138. package/src/id/id.ts +47 -0
  139. package/src/image/photon.ts +94 -0
  140. package/src/image.ts +81 -0
  141. package/src/installation/version.ts +8 -0
  142. package/src/instruction-context.ts +101 -0
  143. package/src/integration/connection.ts +12 -0
  144. package/src/integration.ts +520 -0
  145. package/src/location-mutation.ts +162 -0
  146. package/src/location-service-map.ts +18 -0
  147. package/src/location-services.ts +115 -0
  148. package/src/location.ts +39 -0
  149. package/src/markdown.d.ts +4 -0
  150. package/src/model.ts +41 -0
  151. package/src/models-dev.ts +260 -0
  152. package/src/npm-config.ts +40 -0
  153. package/src/npm.ts +269 -0
  154. package/src/oauth/page.ts +276 -0
  155. package/src/observability/logging.ts +71 -0
  156. package/src/observability/otlp.ts +79 -0
  157. package/src/observability/shared.ts +1 -0
  158. package/src/observability.ts +24 -0
  159. package/src/patch.ts +197 -0
  160. package/src/permission/saved.ts +79 -0
  161. package/src/permission/sql.ts +20 -0
  162. package/src/permission.ts +310 -0
  163. package/src/plugin/agent.ts +206 -0
  164. package/src/plugin/command/initialize.txt +65 -0
  165. package/src/plugin/command/review.txt +100 -0
  166. package/src/plugin/command.ts +25 -0
  167. package/src/plugin/host.ts +219 -0
  168. package/src/plugin/internal.ts +153 -0
  169. package/src/plugin/layer-map.example.ts +94 -0
  170. package/src/plugin/models-dev.ts +183 -0
  171. package/src/plugin/promise.ts +93 -0
  172. package/src/plugin/provider/alibaba.ts +15 -0
  173. package/src/plugin/provider/amazon-bedrock.ts +126 -0
  174. package/src/plugin/provider/anthropic.ts +27 -0
  175. package/src/plugin/provider/azure.ts +82 -0
  176. package/src/plugin/provider/cerebras.ts +26 -0
  177. package/src/plugin/provider/cloudflare-ai-gateway.ts +81 -0
  178. package/src/plugin/provider/cloudflare-workers-ai.ts +81 -0
  179. package/src/plugin/provider/cohere.ts +15 -0
  180. package/src/plugin/provider/deepinfra.ts +15 -0
  181. package/src/plugin/provider/dynamic.ts +31 -0
  182. package/src/plugin/provider/gateway.ts +15 -0
  183. package/src/plugin/provider/github-copilot.ts +52 -0
  184. package/src/plugin/provider/gitlab.ts +65 -0
  185. package/src/plugin/provider/google-vertex.ts +173 -0
  186. package/src/plugin/provider/google.ts +15 -0
  187. package/src/plugin/provider/groq.ts +15 -0
  188. package/src/plugin/provider/kilo.ts +21 -0
  189. package/src/plugin/provider/llmgateway.ts +26 -0
  190. package/src/plugin/provider/mistral.ts +15 -0
  191. package/src/plugin/provider/nvidia.ts +22 -0
  192. package/src/plugin/provider/openai-compatible.ts +17 -0
  193. package/src/plugin/provider/openai.ts +292 -0
  194. package/src/plugin/provider/opencode.ts +311 -0
  195. package/src/plugin/provider/openrouter.ts +36 -0
  196. package/src/plugin/provider/perplexity.ts +15 -0
  197. package/src/plugin/provider/sap-ai-core.ts +46 -0
  198. package/src/plugin/provider/snowflake-cortex.ts +89 -0
  199. package/src/plugin/provider/togetherai.ts +15 -0
  200. package/src/plugin/provider/venice.ts +15 -0
  201. package/src/plugin/provider/vercel.ts +27 -0
  202. package/src/plugin/provider/xai.ts +22 -0
  203. package/src/plugin/provider/zenmux.ts +21 -0
  204. package/src/plugin/provider.ts +71 -0
  205. package/src/plugin/skill/customize-neurocode.md +452 -0
  206. package/src/plugin/skill.ts +31 -0
  207. package/src/plugin/variant.ts +39 -0
  208. package/src/plugin.ts +167 -0
  209. package/src/policy.ts +49 -0
  210. package/src/process.ts +261 -0
  211. package/src/project/copy-strategies.ts +35 -0
  212. package/src/project/copy.ts +292 -0
  213. package/src/project/directories.ts +158 -0
  214. package/src/project/schema.ts +16 -0
  215. package/src/project/sql.ts +35 -0
  216. package/src/project.ts +136 -0
  217. package/src/provider.ts +25 -0
  218. package/src/pty/protocol.ts +37 -0
  219. package/src/pty/pty.bun.ts +26 -0
  220. package/src/pty/pty.node.ts +29 -0
  221. package/src/pty/pty.ts +25 -0
  222. package/src/pty/schema.ts +1 -0
  223. package/src/pty/ticket.ts +56 -0
  224. package/src/pty.ts +318 -0
  225. package/src/public-event-manifest.ts +7 -0
  226. package/src/question.ts +153 -0
  227. package/src/reference/guidance.ts +69 -0
  228. package/src/reference.ts +125 -0
  229. package/src/repository-cache.ts +259 -0
  230. package/src/repository.ts +214 -0
  231. package/src/ripgrep/binary.ts +132 -0
  232. package/src/ripgrep.ts +281 -0
  233. package/src/schema.ts +79 -0
  234. package/src/session/compaction.ts +241 -0
  235. package/src/session/context-epoch.ts +174 -0
  236. package/src/session/error.ts +24 -0
  237. package/src/session/event.ts +2 -0
  238. package/src/session/execution/local.ts +46 -0
  239. package/src/session/execution.ts +34 -0
  240. package/src/session/history.ts +101 -0
  241. package/src/session/info.ts +50 -0
  242. package/src/session/input.ts +288 -0
  243. package/src/session/message-updater.ts +397 -0
  244. package/src/session/message.ts +2 -0
  245. package/src/session/projector.ts +458 -0
  246. package/src/session/prompt.ts +1 -0
  247. package/src/session/revert.ts +121 -0
  248. package/src/session/run-coordinator.ts +104 -0
  249. package/src/session/runner/index.ts +28 -0
  250. package/src/session/runner/llm.ts +432 -0
  251. package/src/session/runner/max-steps.ts +16 -0
  252. package/src/session/runner/model.ts +218 -0
  253. package/src/session/runner/publish-llm-event.ts +423 -0
  254. package/src/session/runner/to-llm-message.ts +171 -0
  255. package/src/session/schema.ts +9 -0
  256. package/src/session/sql.ts +176 -0
  257. package/src/session/store.ts +63 -0
  258. package/src/session/todo.ts +78 -0
  259. package/src/session.ts +486 -0
  260. package/src/share/sql.ts +13 -0
  261. package/src/shell.ts +226 -0
  262. package/src/skill/discovery.ts +213 -0
  263. package/src/skill/guidance.ts +76 -0
  264. package/src/skill.ts +132 -0
  265. package/src/snapshot.ts +266 -0
  266. package/src/state.ts +128 -0
  267. package/src/system-context/builtins.ts +50 -0
  268. package/src/system-context/index.ts +320 -0
  269. package/src/system-context/registry.ts +49 -0
  270. package/src/tool/AGENTS.md +59 -0
  271. package/src/tool/application-tools.ts +57 -0
  272. package/src/tool/apply-patch.ts +219 -0
  273. package/src/tool/bash.ts +207 -0
  274. package/src/tool/builtins.ts +48 -0
  275. package/src/tool/edit.ts +223 -0
  276. package/src/tool/glob.ts +105 -0
  277. package/src/tool/grep.ts +137 -0
  278. package/src/tool/http-body.ts +30 -0
  279. package/src/tool/question.ts +94 -0
  280. package/src/tool/read-filesystem.ts +366 -0
  281. package/src/tool/read.ts +117 -0
  282. package/src/tool/registry.ts +147 -0
  283. package/src/tool/skill.ts +109 -0
  284. package/src/tool/todowrite.ts +62 -0
  285. package/src/tool/tool.ts +162 -0
  286. package/src/tool/tools.ts +13 -0
  287. package/src/tool/webfetch.ts +218 -0
  288. package/src/tool/websearch.ts +260 -0
  289. package/src/tool/write.ts +101 -0
  290. package/src/tool-output-store.ts +211 -0
  291. package/src/util/array.ts +10 -0
  292. package/src/util/binary.ts +41 -0
  293. package/src/util/effect-flock.ts +284 -0
  294. package/src/util/encode.ts +51 -0
  295. package/src/util/error.ts +70 -0
  296. package/src/util/flock.ts +358 -0
  297. package/src/util/glob.ts +34 -0
  298. package/src/util/hash.ts +11 -0
  299. package/src/util/identifier.ts +1 -0
  300. package/src/util/iife.ts +3 -0
  301. package/src/util/lazy.ts +11 -0
  302. package/src/util/module.ts +10 -0
  303. package/src/util/path.ts +37 -0
  304. package/src/util/retry.ts +42 -0
  305. package/src/util/slug.ts +74 -0
  306. package/src/util/token.ts +5 -0
  307. package/src/util/which.ts +14 -0
  308. package/src/util/wildcard.ts +14 -0
  309. package/src/v1/config/agent.ts +89 -0
  310. package/src/v1/config/attachment.ts +25 -0
  311. package/src/v1/config/command.ts +13 -0
  312. package/src/v1/config/config.ts +192 -0
  313. package/src/v1/config/console-state.ts +16 -0
  314. package/src/v1/config/error.ts +39 -0
  315. package/src/v1/config/formatter.ts +13 -0
  316. package/src/v1/config/layout.ts +6 -0
  317. package/src/v1/config/lsp.ts +80 -0
  318. package/src/v1/config/mcp.ts +63 -0
  319. package/src/v1/config/migrate.ts +254 -0
  320. package/src/v1/config/permission.ts +50 -0
  321. package/src/v1/config/plugin.ts +9 -0
  322. package/src/v1/config/provider-options.ts +227 -0
  323. package/src/v1/config/provider.ts +121 -0
  324. package/src/v1/config/server.ts +19 -0
  325. package/src/v1/config/skills.ts +13 -0
  326. package/src/v1/permission.ts +33 -0
  327. package/src/v1/session.ts +68 -0
  328. package/src/v2-schema.ts +3 -0
  329. package/src/workspace.ts +6 -0
  330. package/sst-env.d.ts +10 -0
  331. package/test/agent.test.ts +131 -0
  332. package/test/application-tools.test.ts +287 -0
  333. package/test/background-job.test.ts +106 -0
  334. package/test/catalog.test.ts +353 -0
  335. package/test/command.test.ts +56 -0
  336. package/test/config/agent.test.ts +349 -0
  337. package/test/config/command.test.ts +83 -0
  338. package/test/config/config.test.ts +798 -0
  339. package/test/config/fixtures/plugin/directory-plugin.ts +13 -0
  340. package/test/config/plugin.test.ts +248 -0
  341. package/test/config/provider-options.test.ts +224 -0
  342. package/test/config/provider.test.ts +269 -0
  343. package/test/config/skill.test.ts +80 -0
  344. package/test/credential.test.ts +36 -0
  345. package/test/database-migration.test.ts +644 -0
  346. package/test/effect/cross-spawn-spawner.test.ts +426 -0
  347. package/test/effect/keyed-mutex.test.ts +73 -0
  348. package/test/effect/layer-node/layer-node-types.test.ts +143 -0
  349. package/test/effect/layer-node/layer-node.test.ts +261 -0
  350. package/test/effect/layer-node/node-build.test.ts +125 -0
  351. package/test/effect/observability.test.ts +109 -0
  352. package/test/event.test.ts +1124 -0
  353. package/test/file-mutation.test.ts +368 -0
  354. package/test/filesystem/filesystem.test.ts +387 -0
  355. package/test/filesystem/ignore.test.ts +10 -0
  356. package/test/filesystem/search.test.ts +44 -0
  357. package/test/filesystem/watcher.test.ts +266 -0
  358. package/test/fixture/effect-flock-worker.ts +60 -0
  359. package/test/fixture/flock-worker.ts +72 -0
  360. package/test/fixture/git.ts +49 -0
  361. package/test/fixture/location.ts +26 -0
  362. package/test/fixture/tmpdir.ts +25 -0
  363. package/test/fixtures/recordings/session-runner/openai-chat-streams-text.json +27 -0
  364. package/test/git.test.ts +164 -0
  365. package/test/github-copilot/convert-to-copilot-messages.test.ts +523 -0
  366. package/test/github-copilot/copilot-chat-model.test.ts +592 -0
  367. package/test/github-copilot/openai-responses-language-model.test.ts +206 -0
  368. package/test/global.test.ts +16 -0
  369. package/test/instruction-context.test.ts +323 -0
  370. package/test/integration.test.ts +349 -0
  371. package/test/legacy-event-schema.test.ts +16 -0
  372. package/test/lib/effect.ts +53 -0
  373. package/test/lib/tool.ts +20 -0
  374. package/test/location-filesystem.test.ts +69 -0
  375. package/test/location-layer.test.ts +226 -0
  376. package/test/location-mutation.test.ts +178 -0
  377. package/test/location.test.ts +42 -0
  378. package/test/model.test.ts +23 -0
  379. package/test/models.test.ts +290 -0
  380. package/test/move-session.test.ts +235 -0
  381. package/test/npm-config.test.ts +51 -0
  382. package/test/npm.test.ts +84 -0
  383. package/test/oauth-page.test.ts +15 -0
  384. package/test/patch.test.ts +68 -0
  385. package/test/permission.test.ts +315 -0
  386. package/test/plugin/command.test.ts +47 -0
  387. package/test/plugin/fixture.ts +52 -0
  388. package/test/plugin/fixtures/config-effect-plugin.ts +15 -0
  389. package/test/plugin/fixtures/config-promise-plugin.ts +13 -0
  390. package/test/plugin/fixtures/invalid-plugin.ts +1 -0
  391. package/test/plugin/fixtures/models-dev.json +14 -0
  392. package/test/plugin/fixtures/provider-factory.ts +9 -0
  393. package/test/plugin/host.ts +303 -0
  394. package/test/plugin/models-dev.test.ts +170 -0
  395. package/test/plugin/promise.test.ts +67 -0
  396. package/test/plugin/provider-alibaba.test.ts +92 -0
  397. package/test/plugin/provider-amazon-bedrock.test.ts +622 -0
  398. package/test/plugin/provider-anthropic.test.ts +94 -0
  399. package/test/plugin/provider-azure-cognitive-services.test.ts +199 -0
  400. package/test/plugin/provider-azure.test.ts +293 -0
  401. package/test/plugin/provider-cerebras.test.ts +136 -0
  402. package/test/plugin/provider-cloudflare-ai-gateway.test.ts +426 -0
  403. package/test/plugin/provider-cloudflare-workers-ai.test.ts +279 -0
  404. package/test/plugin/provider-cohere.test.ts +123 -0
  405. package/test/plugin/provider-deepinfra.test.ts +159 -0
  406. package/test/plugin/provider-dynamic.test.ts +184 -0
  407. package/test/plugin/provider-gateway.test.ts +115 -0
  408. package/test/plugin/provider-github-copilot.test.ts +278 -0
  409. package/test/plugin/provider-gitlab.test.ts +308 -0
  410. package/test/plugin/provider-google-vertex-anthropic.test.ts +264 -0
  411. package/test/plugin/provider-google-vertex.test.ts +387 -0
  412. package/test/plugin/provider-google.test.ts +80 -0
  413. package/test/plugin/provider-groq.test.ts +121 -0
  414. package/test/plugin/provider-kilo.test.ts +99 -0
  415. package/test/plugin/provider-llmgateway.test.ts +80 -0
  416. package/test/plugin/provider-mistral.test.ts +129 -0
  417. package/test/plugin/provider-nvidia.test.ts +97 -0
  418. package/test/plugin/provider-openai-compatible.test.ts +107 -0
  419. package/test/plugin/provider-openai.test.ts +176 -0
  420. package/test/plugin/provider-opencode.test.ts +423 -0
  421. package/test/plugin/provider-openrouter.test.ts +113 -0
  422. package/test/plugin/provider-perplexity.test.ts +122 -0
  423. package/test/plugin/provider-sap-ai-core.test.ts +159 -0
  424. package/test/plugin/provider-snowflake-cortex.test.ts +261 -0
  425. package/test/plugin/provider-togetherai.test.ts +129 -0
  426. package/test/plugin/provider-venice.test.ts +115 -0
  427. package/test/plugin/provider-vercel.test.ts +83 -0
  428. package/test/plugin/provider-xai.test.ts +125 -0
  429. package/test/plugin/provider-zenmux.test.ts +110 -0
  430. package/test/plugin/skill.test.ts +25 -0
  431. package/test/plugin/variant.test.ts +67 -0
  432. package/test/plugin.test.ts +71 -0
  433. package/test/policy.test.ts +85 -0
  434. package/test/preload.ts +5 -0
  435. package/test/process/process.test.ts +368 -0
  436. package/test/project-copy.test.ts +400 -0
  437. package/test/project-directories.test.ts +61 -0
  438. package/test/project.test.ts +221 -0
  439. package/test/provider-mistral.test.ts +282 -0
  440. package/test/provider-xai-responses.test.ts +32 -0
  441. package/test/pty/info-schema.test.ts +32 -0
  442. package/test/pty/protocol.test.ts +27 -0
  443. package/test/pty/pty-session.test.ts +240 -0
  444. package/test/pty/ticket.test.ts +62 -0
  445. package/test/question.test.ts +114 -0
  446. package/test/reference-guidance.test.ts +76 -0
  447. package/test/reference.test.ts +78 -0
  448. package/test/repository-cache.test.ts +153 -0
  449. package/test/repository.test.ts +71 -0
  450. package/test/ripgrep.test.ts +65 -0
  451. package/test/session-compaction.test.ts +27 -0
  452. package/test/session-create.test.ts +425 -0
  453. package/test/session-history.test.ts +165 -0
  454. package/test/session-projector.test.ts +533 -0
  455. package/test/session-prompt.test.ts +584 -0
  456. package/test/session-run-coordinator.test.ts +418 -0
  457. package/test/session-runner-message.test.ts +501 -0
  458. package/test/session-runner-model.test.ts +347 -0
  459. package/test/session-runner-recorded.test.ts +193 -0
  460. package/test/session-runner-tool-events.test.ts +136 -0
  461. package/test/session-runner-tool-registry.test.ts +452 -0
  462. package/test/session-runner.test.ts +3365 -0
  463. package/test/session-todo.test.ts +94 -0
  464. package/test/session-tool-progress.test.ts +158 -0
  465. package/test/shared-schema.test.ts +206 -0
  466. package/test/shell.test.ts +108 -0
  467. package/test/skill/guidance.test.ts +144 -0
  468. package/test/skill-discovery.test.ts +165 -0
  469. package/test/skill.test.ts +125 -0
  470. package/test/snapshot.test.ts +178 -0
  471. package/test/state.test.ts +115 -0
  472. package/test/system-context/builtins.test.ts +129 -0
  473. package/test/system-context/index.test.ts +307 -0
  474. package/test/system-context/registry.test.ts +114 -0
  475. package/test/tool-apply-patch.test.ts +436 -0
  476. package/test/tool-bash.test.ts +437 -0
  477. package/test/tool-edit.test.ts +434 -0
  478. package/test/tool-output-store.test.ts +247 -0
  479. package/test/tool-question.test.ts +162 -0
  480. package/test/tool-read-filesystem.test.ts +118 -0
  481. package/test/tool-read.test.ts +674 -0
  482. package/test/tool-skill.test.ts +149 -0
  483. package/test/tool-todowrite.test.ts +125 -0
  484. package/test/tool-webfetch.test.ts +281 -0
  485. package/test/tool-websearch.test.ts +316 -0
  486. package/test/tool-write.test.ts +305 -0
  487. package/test/util/effect-flock.test.ts +392 -0
  488. package/test/util/flock.test.ts +428 -0
  489. package/test/util/which.test.ts +100 -0
  490. package/tsconfig.json +7 -0
@@ -0,0 +1,1124 @@
1
+ import { describe, expect } from "bun:test"
2
+ import { Cause, DateTime, Deferred, Effect, Exit, Fiber, Layer, Option, Schema, Stream } from "effect"
3
+ import { EventV2 } from "@neurocode-ai/core/event"
4
+ import { Event } from "@neurocode-ai/schema/event"
5
+ import { Session } from "@neurocode-ai/schema/session"
6
+ import { SessionEvent } from "@neurocode-ai/schema/session-event"
7
+ import { SessionV1 } from "@neurocode-ai/schema/session-v1"
8
+ import { Database } from "@neurocode-ai/core/database/database"
9
+ import { AppNodeBuilder } from "@neurocode-ai/core/effect/app-node-builder"
10
+ import { LayerNode } from "@neurocode-ai/core/effect/layer-node"
11
+ import { EventSequenceTable, EventTable } from "@neurocode-ai/core/event/sql"
12
+ import { Location } from "@neurocode-ai/core/location"
13
+ import { AbsolutePath } from "@neurocode-ai/core/schema"
14
+ import { WorkspaceV2 } from "@neurocode-ai/core/workspace"
15
+ import { eq } from "drizzle-orm"
16
+ import { location } from "./fixture/location"
17
+ import { testEffect } from "./lib/effect"
18
+
19
+ const locationLayer = Layer.succeed(
20
+ Location.Service,
21
+ Location.Service.of(
22
+ location({ directory: AbsolutePath.make("project"), workspaceID: WorkspaceV2.ID.make("wrk_test") }),
23
+ ),
24
+ )
25
+ const Message = EventV2.define({
26
+ type: "test.message",
27
+ schema: {
28
+ text: Schema.String,
29
+ },
30
+ })
31
+
32
+ const SyncMessage = EventV2.define({
33
+ type: "test.sync",
34
+ durable: {
35
+ version: 1,
36
+ aggregate: "id",
37
+ },
38
+ schema: {
39
+ id: Schema.String,
40
+ text: Schema.String,
41
+ },
42
+ })
43
+
44
+ const SyncSent = EventV2.define({
45
+ type: "test.sent",
46
+ durable: {
47
+ version: 1,
48
+ aggregate: "messageID",
49
+ },
50
+ schema: {
51
+ messageID: Schema.String,
52
+ text: Schema.String,
53
+ },
54
+ })
55
+
56
+ const GlobalMessage = EventV2.define({
57
+ type: "test.global",
58
+ schema: {
59
+ text: Schema.String,
60
+ },
61
+ })
62
+
63
+ const VersionedMessage = EventV2.define({
64
+ type: "test.versioned",
65
+ durable: {
66
+ version: 2,
67
+ aggregate: "id",
68
+ },
69
+ schema: {
70
+ id: Schema.String,
71
+ text: Schema.String,
72
+ },
73
+ })
74
+
75
+ const DurableMessage = SessionV1.Event.MessageRemoved
76
+ const durableData = (sessionID: Session.ID, text: string) => ({
77
+ sessionID,
78
+ messageID: SessionV1.MessageID.ascending(`msg_${text}`),
79
+ })
80
+
81
+ const it = testEffect(
82
+ AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, Location.node]), [[Location.node, locationLayer]]),
83
+ )
84
+ const itWithoutLocation = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node])))
85
+
86
+ describe("EventV2", () => {
87
+ it.effect("publishes events with the current location", () =>
88
+ Effect.gen(function* () {
89
+ const events = yield* EventV2.Service
90
+ const fiber = yield* events.subscribe(Message).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
91
+ yield* Effect.yieldNow
92
+ const event = yield* events.publish(Message, { text: "hello" })
93
+ const received = Array.from(yield* Fiber.join(fiber))
94
+
95
+ expect(received).toEqual([event])
96
+ expect(event.type).toBe("test.message")
97
+ expect(event).not.toHaveProperty("version")
98
+ expect(event.data).toEqual({ text: "hello" })
99
+ expect(event.location).toEqual({
100
+ directory: AbsolutePath.make("project"),
101
+ workspaceID: WorkspaceV2.ID.make("wrk_test"),
102
+ })
103
+ }),
104
+ )
105
+
106
+ itWithoutLocation.effect("omits location when no location is available", () =>
107
+ Effect.gen(function* () {
108
+ const events = yield* EventV2.Service
109
+ const event = yield* events.publish(GlobalMessage, { text: "hello" })
110
+
111
+ expect(event).not.toHaveProperty("location")
112
+ expect(event.type).toBe("test.global")
113
+ }),
114
+ )
115
+
116
+ it.effect("publishes definition version", () =>
117
+ Effect.gen(function* () {
118
+ const events = yield* EventV2.Service
119
+ const event = yield* events.publish(VersionedMessage, { id: "one", text: "hello" })
120
+
121
+ expect(event.type).toBe("test.versioned")
122
+ expect(event.durable?.version).toBe(2)
123
+ }),
124
+ )
125
+
126
+ it.effect("selects the latest durable definition independent of declaration order", () =>
127
+ Effect.sync(() => {
128
+ const latest = EventV2.define({
129
+ type: "test.out-of-order",
130
+ durable: { version: 2, aggregate: "id" },
131
+ schema: { id: Schema.String },
132
+ })
133
+ const historical = EventV2.define({
134
+ type: "test.out-of-order",
135
+ durable: { version: 1, aggregate: "id" },
136
+ schema: { id: Schema.String },
137
+ })
138
+
139
+ expect(Event.latest([latest, historical]).get("test.out-of-order")).toBe(latest)
140
+ expect(Event.latest([historical, latest]).get("test.out-of-order")).toBe(latest)
141
+ }),
142
+ )
143
+
144
+ it.effect("publishes to typed and wildcard subscriptions", () =>
145
+ Effect.gen(function* () {
146
+ const events = yield* EventV2.Service
147
+ const typed = yield* events.subscribe(Message).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
148
+ const wildcard = yield* events.all().pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
149
+ yield* Effect.yieldNow
150
+ const event = yield* events.publish(Message, { text: "hello" })
151
+
152
+ expect(Array.from(yield* Fiber.join(typed))).toEqual([event])
153
+ expect(Array.from(yield* Fiber.join(wildcard))).toEqual([event])
154
+ }),
155
+ )
156
+
157
+ it.effect("runs projectors inline", () =>
158
+ Effect.gen(function* () {
159
+ const events = yield* EventV2.Service
160
+ const received = new Array<EventV2.Payload>()
161
+ yield* events.project(SyncMessage, (event) =>
162
+ Effect.sync(() => {
163
+ received.push(event)
164
+ }),
165
+ )
166
+
167
+ const event = yield* events.publish(SyncMessage, { id: "one", text: "hello" })
168
+ yield* events.publish(SyncMessage, { id: "one", text: "after unsubscribe" })
169
+
170
+ expect(received[0]).toEqual(event)
171
+ expect(received[1]?.data).toEqual({ id: "one", text: "after unsubscribe" })
172
+ }),
173
+ )
174
+
175
+ it.effect("commits local operational state inside a new durable event transaction", () =>
176
+ Effect.gen(function* () {
177
+ const events = yield* EventV2.Service
178
+ const received = new Array<string>()
179
+ const aggregateID = EventV2.ID.create()
180
+ yield* events.project(SyncMessage, () => Effect.sync(() => received.push("projector")))
181
+
182
+ yield* events.publish(
183
+ SyncMessage,
184
+ { id: aggregateID, text: "hello" },
185
+ { commit: (seq) => Effect.sync(() => received.push(`commit:${seq}`)) },
186
+ )
187
+
188
+ expect(received).toEqual(["projector", "commit:0"])
189
+ }),
190
+ )
191
+
192
+ it.effect("rolls back the durable event and projector when the local commit fails", () =>
193
+ Effect.gen(function* () {
194
+ const events = yield* EventV2.Service
195
+ const { db } = yield* Database.Service
196
+ const aggregateID = EventV2.ID.create()
197
+ yield* db.run("CREATE TABLE IF NOT EXISTS event_commit_probe (value text NOT NULL)")
198
+ yield* db.run("DELETE FROM event_commit_probe")
199
+ yield* events.project(SyncMessage, () =>
200
+ db.run("INSERT INTO event_commit_probe (value) VALUES ('projected')").pipe(Effect.orDie, Effect.asVoid),
201
+ )
202
+
203
+ const exit = yield* events
204
+ .publish(SyncMessage, { id: aggregateID, text: "hello" }, { commit: () => Effect.die("commit failed") })
205
+ .pipe(Effect.exit)
206
+
207
+ expect(String(exit)).toContain("commit failed")
208
+ expect(yield* db.all("SELECT value FROM event_commit_probe")).toEqual([])
209
+ expect(yield* db.select().from(EventTable).where(eq(EventTable.aggregate_id, aggregateID)).all()).toEqual([])
210
+ expect(
211
+ yield* db.select().from(EventSequenceTable).where(eq(EventSequenceTable.aggregate_id, aggregateID)).all(),
212
+ ).toEqual([])
213
+ }),
214
+ )
215
+
216
+ it.effect("rejects local commit hooks on live-only events", () =>
217
+ Effect.gen(function* () {
218
+ const events = yield* EventV2.Service
219
+ const exit = yield* events.publish(Message, { text: "hello" }, { commit: () => Effect.void }).pipe(Effect.exit)
220
+
221
+ expect(String(exit)).toContain("Local commit hooks require a durable event")
222
+ }),
223
+ )
224
+
225
+ it.effect("runs projectors before publishing to streams", () =>
226
+ Effect.gen(function* () {
227
+ const events = yield* EventV2.Service
228
+ const received = new Array<string>()
229
+ const fiber = yield* events.all().pipe(
230
+ Stream.take(1),
231
+ Stream.runForEach(() => Effect.sync(() => received.push("stream"))),
232
+ Effect.forkScoped,
233
+ )
234
+ yield* events.project(SyncMessage, (event) =>
235
+ Effect.sync(() => {
236
+ received.push(event.type)
237
+ }),
238
+ )
239
+
240
+ yield* Effect.yieldNow
241
+ yield* events.publish(SyncMessage, { id: "one", text: "hello" })
242
+ yield* Fiber.join(fiber)
243
+
244
+ expect(received).toEqual([SyncMessage.type, "stream"])
245
+ }),
246
+ )
247
+
248
+ it.effect("runs listeners inline after projectors", () =>
249
+ Effect.gen(function* () {
250
+ const events = yield* EventV2.Service
251
+ const received = new Array<string>()
252
+ yield* events.project(SyncMessage, () =>
253
+ Effect.sync(() => {
254
+ received.push("projector")
255
+ }),
256
+ )
257
+ const unsubscribe = yield* events.listen(() =>
258
+ Effect.sync(() => {
259
+ received.push("listener")
260
+ }),
261
+ )
262
+
263
+ yield* events.publish(SyncMessage, { id: "one", text: "hello" })
264
+ yield* unsubscribe
265
+ yield* events.publish(SyncMessage, { id: "one", text: "after unsubscribe" })
266
+
267
+ expect(received).toEqual(["projector", "listener", "projector"])
268
+ }),
269
+ )
270
+
271
+ it.effect("isolates observer defects after durable events commit", () =>
272
+ Effect.gen(function* () {
273
+ const events = yield* EventV2.Service
274
+ const received = new Array<string>()
275
+ yield* events.listen(() => {
276
+ throw new Error("listener defect")
277
+ })
278
+ yield* events.listen((event) =>
279
+ Effect.sync(() => {
280
+ received.push(event.type)
281
+ }),
282
+ )
283
+
284
+ const event = yield* events.publish(SyncMessage, { id: "one", text: "hello" })
285
+
286
+ expect(received).toEqual([SyncMessage.type])
287
+ expect(event.durable?.seq).toBeNumber()
288
+ }),
289
+ )
290
+
291
+ it.effect("notifies global listeners only after a durable event is committed", () =>
292
+ Effect.gen(function* () {
293
+ const events = yield* EventV2.Service
294
+ const { db } = yield* Database.Service
295
+ const aggregateID = EventV2.ID.create()
296
+ const observed = new Array<{ id: string; seq: number }>()
297
+ yield* events.listen((event) =>
298
+ event.type !== SyncMessage.type
299
+ ? Effect.void
300
+ : db
301
+ .select({ id: EventTable.id, seq: EventTable.seq })
302
+ .from(EventTable)
303
+ .where(eq(EventTable.id, event.id))
304
+ .get()
305
+ .pipe(
306
+ Effect.orDie,
307
+ Effect.tap((row) =>
308
+ Effect.sync(() => {
309
+ if (row) observed.push(row)
310
+ }),
311
+ ),
312
+ Effect.asVoid,
313
+ ),
314
+ )
315
+
316
+ const event = yield* events.publish(SyncMessage, { id: aggregateID, text: "committed" })
317
+ if (!event.durable) throw new Error("Expected durable event metadata")
318
+
319
+ expect(observed).toEqual([{ id: event.id, seq: event.durable.seq }])
320
+ }),
321
+ )
322
+
323
+ it.effect("ends only an overflowing bounded subscriber without blocking other listeners", () =>
324
+ Effect.gen(function* () {
325
+ const events = yield* EventV2.Service
326
+ const consuming = yield* Deferred.make<void>()
327
+ const release = yield* Deferred.make<void>()
328
+ const slowStream = yield* EventV2.allBounded(events, 1)
329
+ const fastStream = yield* EventV2.allBounded(events, 8)
330
+ const slow = yield* slowStream.pipe(
331
+ Stream.runForEach(() => Deferred.succeed(consuming, undefined).pipe(Effect.andThen(Deferred.await(release)))),
332
+ Effect.forkScoped,
333
+ )
334
+ const fast = yield* fastStream.pipe(Stream.take(4), Stream.runCollect, Effect.forkScoped)
335
+
336
+ yield* events.publish(Message, { text: "one" })
337
+ yield* Deferred.await(consuming)
338
+ yield* events.publish(Message, { text: "two" })
339
+ yield* events.publish(Message, { text: "overflow" })
340
+ const last = yield* events.publish(Message, { text: "still delivered" })
341
+ yield* Deferred.succeed(release, undefined)
342
+
343
+ const slowExit = yield* Fiber.await(slow)
344
+ expect(Exit.findErrorOption(slowExit).pipe(Option.getOrUndefined)).toBeInstanceOf(EventV2.SubscriberOverflowError)
345
+ expect(Array.from(yield* Fiber.join(fast))).toEqual([
346
+ expect.objectContaining({ data: { text: "one" } }),
347
+ expect.objectContaining({ data: { text: "two" } }),
348
+ expect.objectContaining({ data: { text: "overflow" } }),
349
+ last,
350
+ ])
351
+ }),
352
+ )
353
+
354
+ it.effect("preserves observer interruption", () =>
355
+ Effect.gen(function* () {
356
+ const events = yield* EventV2.Service
357
+ const { db } = yield* Database.Service
358
+ yield* events.listen(() => Effect.interrupt)
359
+
360
+ const exit = yield* events.publish(SyncMessage, { id: "interrupted", text: "hello" }).pipe(Effect.exit)
361
+ const committed = yield* db
362
+ .select({ id: EventTable.id })
363
+ .from(EventTable)
364
+ .where(eq(EventTable.aggregate_id, "interrupted"))
365
+ .get()
366
+ .pipe(Effect.orDie)
367
+
368
+ expect(Exit.isFailure(exit) && Cause.hasInterrupts(exit.cause)).toBeTrue()
369
+ expect(committed).toBeDefined()
370
+ }),
371
+ )
372
+
373
+ it.effect("keeps live-only listener defects fail-fast", () =>
374
+ Effect.gen(function* () {
375
+ const events = yield* EventV2.Service
376
+ const defect = new Error("listener defect")
377
+ yield* events.listen(() => Effect.die(defect))
378
+
379
+ expect(yield* events.publish(Message, { text: "hello" }).pipe(Effect.catchDefect(Effect.succeed))).toBe(defect)
380
+ }),
381
+ )
382
+
383
+ it.effect("inserts durable event rows on publish", () =>
384
+ Effect.gen(function* () {
385
+ const events = yield* EventV2.Service
386
+ const { db } = yield* Database.Service
387
+ const aggregateID = EventV2.ID.create()
388
+
389
+ yield* events.publish(SyncMessage, { id: aggregateID, text: "first" })
390
+ const rows = yield* db
391
+ .select()
392
+ .from(EventTable)
393
+ .where(eq(EventTable.aggregate_id, aggregateID))
394
+ .all()
395
+ .pipe(Effect.orDie)
396
+
397
+ expect(rows).toHaveLength(1)
398
+ expect(rows[0]?.type).toBe(EventV2.versionedType(SyncMessage.type, 1))
399
+ expect(rows[0]?.aggregate_id).toBe(aggregateID)
400
+ }),
401
+ )
402
+
403
+ it.effect("increments durable event seq per aggregate", () =>
404
+ Effect.gen(function* () {
405
+ const events = yield* EventV2.Service
406
+ const { db } = yield* Database.Service
407
+ const aggregateID = EventV2.ID.create()
408
+
409
+ yield* events.publish(SyncMessage, { id: aggregateID, text: "first" })
410
+ yield* events.publish(SyncMessage, { id: aggregateID, text: "second" })
411
+ const rows = yield* db
412
+ .select()
413
+ .from(EventTable)
414
+ .where(eq(EventTable.aggregate_id, aggregateID))
415
+ .all()
416
+ .pipe(Effect.orDie)
417
+
418
+ expect(rows.map((row) => row.seq)).toEqual([0, 1])
419
+ }),
420
+ )
421
+
422
+ it.effect("replays durable aggregate events after a sequence and tails new events", () =>
423
+ Effect.gen(function* () {
424
+ const events = yield* EventV2.Service
425
+ const aggregateID = Session.ID.create()
426
+ yield* events.publish(DurableMessage, durableData(aggregateID, "zero"))
427
+ yield* events.publish(DurableMessage, durableData(aggregateID, "one"))
428
+ const fiber = yield* events
429
+ .durable({ aggregateID, after: 0 })
430
+ .pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped)
431
+ yield* Effect.yieldNow
432
+
433
+ yield* events.publish(DurableMessage, durableData(aggregateID, "two"))
434
+
435
+ expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([
436
+ [1, durableData(aggregateID, "one")],
437
+ [2, durableData(aggregateID, "two")],
438
+ ])
439
+ }),
440
+ )
441
+
442
+ it.effect("catches durable aggregate events published during replay handoff", () =>
443
+ Effect.gen(function* () {
444
+ const events = yield* EventV2.Service
445
+ const aggregateID = Session.ID.create()
446
+ yield* events.publish(DurableMessage, durableData(aggregateID, "zero"))
447
+ const fiber = yield* events.durable({ aggregateID }).pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped)
448
+
449
+ yield* events.publish(DurableMessage, durableData(aggregateID, "one"))
450
+
451
+ expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([
452
+ [0, durableData(aggregateID, "zero")],
453
+ [1, durableData(aggregateID, "one")],
454
+ ])
455
+ }),
456
+ )
457
+
458
+ it.effect("retains a durable wake committed while historical replay is paused", () =>
459
+ Effect.gen(function* () {
460
+ const readStarted = yield* Deferred.make<void>()
461
+ const continueRead = yield* Deferred.make<void>()
462
+ let pause = true
463
+ const eventLayer = EventV2.layerWith({
464
+ beforeAggregateRead: () =>
465
+ pause
466
+ ? Deferred.succeed(readStarted, undefined).pipe(Effect.andThen(Deferred.await(continueRead)))
467
+ : Effect.void,
468
+ }).pipe(Layer.provide(LayerNode.compile(Database.node)))
469
+
470
+ yield* Effect.gen(function* () {
471
+ const events = yield* EventV2.Service
472
+ const aggregateID = Session.ID.create()
473
+ const fiber = yield* events.durable({ aggregateID }).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
474
+ yield* Deferred.await(readStarted)
475
+
476
+ pause = false
477
+ yield* events.publish(DurableMessage, durableData(aggregateID, "during handoff"))
478
+ yield* Deferred.succeed(continueRead, undefined)
479
+
480
+ expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([
481
+ [0, durableData(aggregateID, "during handoff")],
482
+ ])
483
+ }).pipe(Effect.provide(Layer.merge(LayerNode.compile(Database.node), eventLayer)))
484
+ }),
485
+ )
486
+
487
+ it.effect("coalesces durable aggregate wakes while draining every committed event", () =>
488
+ Effect.gen(function* () {
489
+ const events = yield* EventV2.Service
490
+ const aggregateID = Session.ID.create()
491
+ const count = 64
492
+ const fiber = yield* events
493
+ .durable({ aggregateID })
494
+ .pipe(Stream.take(count), Stream.runCollect, Effect.forkScoped)
495
+ yield* Effect.yieldNow
496
+
497
+ for (let index = 0; index < count; index++) {
498
+ yield* events.publish(DurableMessage, durableData(aggregateID, String(index)))
499
+ }
500
+
501
+ expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual(
502
+ Array.from({ length: count }, (_, index) => [index, durableData(aggregateID, String(index))]),
503
+ )
504
+ }),
505
+ )
506
+
507
+ it.effect("omits live-only events from durable aggregate streams", () =>
508
+ Effect.gen(function* () {
509
+ const events = yield* EventV2.Service
510
+ const aggregateID = Session.ID.create()
511
+ const fiber = yield* events.durable({ aggregateID }).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
512
+ yield* Effect.yieldNow
513
+
514
+ yield* events.publish(Message, { text: "live only" })
515
+ yield* events.publish(DurableMessage, durableData(aggregateID, "durable"))
516
+
517
+ expect(Array.from(yield* Fiber.join(fiber)).map((event) => event.type)).toEqual([DurableMessage.type])
518
+ }),
519
+ )
520
+
521
+ it.effect("uses custom sync aggregate field", () =>
522
+ Effect.gen(function* () {
523
+ const events = yield* EventV2.Service
524
+ const { db } = yield* Database.Service
525
+ const aggregateID = EventV2.ID.create()
526
+
527
+ yield* events.publish(SyncSent, { messageID: aggregateID, text: "sent" })
528
+ const rows = yield* db
529
+ .select()
530
+ .from(EventTable)
531
+ .where(eq(EventTable.aggregate_id, aggregateID))
532
+ .all()
533
+ .pipe(Effect.orDie)
534
+
535
+ expect(rows).toHaveLength(1)
536
+ expect(rows[0]?.aggregate_id).toBe(aggregateID)
537
+ }),
538
+ )
539
+
540
+ it.effect("replays durable events through projectors", () =>
541
+ Effect.gen(function* () {
542
+ const events = yield* EventV2.Service
543
+ const received = new Array<EventV2.Payload>()
544
+ yield* events.project(DurableMessage, (event) =>
545
+ Effect.sync(() => {
546
+ received.push(event)
547
+ }),
548
+ )
549
+ const aggregateID = Session.ID.create()
550
+
551
+ yield* events.replay({
552
+ id: EventV2.ID.create(),
553
+ type: EventV2.versionedType(DurableMessage.type, 1),
554
+ seq: 0,
555
+ aggregateID,
556
+ data: durableData(aggregateID, "hello"),
557
+ })
558
+
559
+ expect(received[0]?.type).toBe(DurableMessage.type)
560
+ expect(received[0]?.data).toEqual(durableData(aggregateID, "hello"))
561
+ }),
562
+ )
563
+
564
+ it.effect("replay inserts external event rows", () =>
565
+ Effect.gen(function* () {
566
+ const events = yield* EventV2.Service
567
+ const { db } = yield* Database.Service
568
+ const aggregateID = Session.ID.create()
569
+
570
+ yield* events.replay({
571
+ id: EventV2.ID.create(),
572
+ type: EventV2.versionedType(DurableMessage.type, 1),
573
+ seq: 0,
574
+ aggregateID,
575
+ data: durableData(aggregateID, "replayed"),
576
+ })
577
+ const rows = yield* db
578
+ .select()
579
+ .from(EventTable)
580
+ .where(eq(EventTable.aggregate_id, aggregateID))
581
+ .all()
582
+ .pipe(Effect.orDie)
583
+
584
+ expect(rows).toHaveLength(1)
585
+ expect(rows[0]?.aggregate_id).toBe(aggregateID)
586
+ }),
587
+ )
588
+
589
+ it.effect(
590
+ "replay rejects an envelope aggregate that differs from its payload without mutating the payload aggregate",
591
+ () =>
592
+ Effect.gen(function* () {
593
+ const events = yield* EventV2.Service
594
+ const { db } = yield* Database.Service
595
+ const envelopeAggregateID = Session.ID.create()
596
+ const payloadAggregateID = Session.ID.create()
597
+ const received = new Array<EventV2.Payload>()
598
+ yield* events.publish(DurableMessage, durableData(payloadAggregateID, "seed"))
599
+ yield* events.project(DurableMessage, (event) =>
600
+ Effect.sync(() => {
601
+ received.push(event)
602
+ }),
603
+ )
604
+
605
+ const exit = yield* events
606
+ .replay({
607
+ id: EventV2.ID.create(),
608
+ type: EventV2.versionedType(DurableMessage.type, 1),
609
+ seq: 1,
610
+ aggregateID: envelopeAggregateID,
611
+ data: durableData(payloadAggregateID, "replayed"),
612
+ })
613
+ .pipe(Effect.exit)
614
+ const rows = yield* db
615
+ .select()
616
+ .from(EventTable)
617
+ .where(eq(EventTable.aggregate_id, payloadAggregateID))
618
+ .all()
619
+ .pipe(Effect.orDie)
620
+ const sequence = yield* db
621
+ .select({ seq: EventSequenceTable.seq })
622
+ .from(EventSequenceTable)
623
+ .where(eq(EventSequenceTable.aggregate_id, payloadAggregateID))
624
+ .get()
625
+ .pipe(Effect.orDie)
626
+
627
+ expect(String(exit)).toContain("Aggregate mismatch")
628
+ expect(received).toHaveLength(0)
629
+ expect(rows).toHaveLength(1)
630
+ expect(sequence).toEqual({ seq: 0 })
631
+ }),
632
+ )
633
+
634
+ it.effect("replay defects on sequence mismatch", () =>
635
+ Effect.gen(function* () {
636
+ const events = yield* EventV2.Service
637
+ const aggregateID = Session.ID.create()
638
+
639
+ yield* events.replay({
640
+ id: EventV2.ID.create(),
641
+ type: EventV2.versionedType(DurableMessage.type, 1),
642
+ seq: 0,
643
+ aggregateID,
644
+ data: durableData(aggregateID, "first"),
645
+ })
646
+ const exit = yield* events
647
+ .replay({
648
+ id: EventV2.ID.create(),
649
+ type: EventV2.versionedType(DurableMessage.type, 1),
650
+ seq: 5,
651
+ aggregateID,
652
+ data: durableData(aggregateID, "bad"),
653
+ })
654
+ .pipe(Effect.exit)
655
+
656
+ expect(String(exit)).toContain("Sequence mismatch")
657
+ }),
658
+ )
659
+
660
+ it.effect("replay decodes synchronized transformed values before projection", () =>
661
+ Effect.gen(function* () {
662
+ const events = yield* EventV2.Service
663
+ const aggregateID = Session.ID.create()
664
+ const received = new Array<typeof SessionEvent.ContextUpdated.Type>()
665
+ yield* events.project(SessionEvent.ContextUpdated, (event) =>
666
+ Effect.sync(() => {
667
+ received.push(event)
668
+ }),
669
+ )
670
+
671
+ yield* events.replay({
672
+ id: EventV2.ID.create(),
673
+ type: EventV2.versionedType(SessionEvent.ContextUpdated.type, 1),
674
+ seq: 0,
675
+ aggregateID,
676
+ data: { sessionID: aggregateID, messageID: "msg_context", timestamp: 0, text: "context" },
677
+ })
678
+
679
+ expect(received[0]?.data.timestamp).toEqual(DateTime.makeUnsafe(0))
680
+ }),
681
+ )
682
+
683
+ it.effect("replay defects on unknown event type", () =>
684
+ Effect.gen(function* () {
685
+ const events = yield* EventV2.Service
686
+ const exit = yield* events
687
+ .replay({
688
+ id: EventV2.ID.create(),
689
+ type: "unknown.event.1",
690
+ seq: 0,
691
+ aggregateID: EventV2.ID.create(),
692
+ data: {},
693
+ })
694
+ .pipe(Effect.exit)
695
+
696
+ expect(String(exit)).toContain("Unknown durable event type")
697
+ }),
698
+ )
699
+
700
+ it.effect("replayAll validates contiguous aggregate events", () =>
701
+ Effect.gen(function* () {
702
+ const events = yield* EventV2.Service
703
+ const aggregateID = Session.ID.create()
704
+ const source = yield* events.replayAll([
705
+ {
706
+ id: EventV2.ID.create(),
707
+ type: EventV2.versionedType(DurableMessage.type, 1),
708
+ seq: 0,
709
+ aggregateID,
710
+ data: durableData(aggregateID, "one"),
711
+ },
712
+ {
713
+ id: EventV2.ID.create(),
714
+ type: EventV2.versionedType(DurableMessage.type, 1),
715
+ seq: 1,
716
+ aggregateID,
717
+ data: durableData(aggregateID, "two"),
718
+ },
719
+ ])
720
+
721
+ expect(source).toBe(aggregateID)
722
+ }),
723
+ )
724
+
725
+ it.effect("replayAll accepts later chunks after the first batch", () =>
726
+ Effect.gen(function* () {
727
+ const events = yield* EventV2.Service
728
+ const { db } = yield* Database.Service
729
+ const aggregateID = Session.ID.create()
730
+
731
+ const one = yield* events.replayAll([
732
+ {
733
+ id: EventV2.ID.create(),
734
+ type: EventV2.versionedType(DurableMessage.type, 1),
735
+ seq: 0,
736
+ aggregateID,
737
+ data: durableData(aggregateID, "one"),
738
+ },
739
+ {
740
+ id: EventV2.ID.create(),
741
+ type: EventV2.versionedType(DurableMessage.type, 1),
742
+ seq: 1,
743
+ aggregateID,
744
+ data: durableData(aggregateID, "two"),
745
+ },
746
+ ])
747
+ const two = yield* events.replayAll([
748
+ {
749
+ id: EventV2.ID.create(),
750
+ type: EventV2.versionedType(DurableMessage.type, 1),
751
+ seq: 2,
752
+ aggregateID,
753
+ data: durableData(aggregateID, "three"),
754
+ },
755
+ {
756
+ id: EventV2.ID.create(),
757
+ type: EventV2.versionedType(DurableMessage.type, 1),
758
+ seq: 3,
759
+ aggregateID,
760
+ data: durableData(aggregateID, "four"),
761
+ },
762
+ ])
763
+ const rows = yield* db
764
+ .select()
765
+ .from(EventTable)
766
+ .where(eq(EventTable.aggregate_id, aggregateID))
767
+ .all()
768
+ .pipe(Effect.orDie)
769
+
770
+ expect(one).toBe(aggregateID)
771
+ expect(two).toBe(aggregateID)
772
+ expect(rows.map((row) => row.seq)).toEqual([0, 1, 2, 3])
773
+ }),
774
+ )
775
+
776
+ it.effect("claim fences replay owners", () =>
777
+ Effect.gen(function* () {
778
+ const events = yield* EventV2.Service
779
+ const received = new Array<EventV2.Payload>()
780
+ const aggregateID = Session.ID.create()
781
+ yield* events.publish(DurableMessage, durableData(aggregateID, "seed"))
782
+ yield* events.claim(aggregateID, "owner-a")
783
+ yield* events.project(DurableMessage, (event) =>
784
+ Effect.sync(() => {
785
+ received.push(event)
786
+ }),
787
+ )
788
+
789
+ yield* events.replay(
790
+ {
791
+ id: EventV2.ID.create(),
792
+ type: EventV2.versionedType(DurableMessage.type, 1),
793
+ seq: 1,
794
+ aggregateID,
795
+ data: durableData(aggregateID, "ignored"),
796
+ },
797
+ { ownerID: "owner-b" },
798
+ )
799
+
800
+ expect(received).toHaveLength(0)
801
+ }),
802
+ )
803
+
804
+ it.effect("strict owner fences exact replay", () =>
805
+ Effect.gen(function* () {
806
+ const events = yield* EventV2.Service
807
+ const aggregateID = Session.ID.create()
808
+ const id = EventV2.ID.create()
809
+ const replayed = {
810
+ id,
811
+ type: EventV2.versionedType(DurableMessage.type, 1),
812
+ seq: 0,
813
+ aggregateID,
814
+ data: durableData(aggregateID, "owned"),
815
+ }
816
+ yield* events.replay(replayed, { ownerID: "owner-a" })
817
+
818
+ const exit = yield* events.replay(replayed, { ownerID: "owner-b", strictOwner: true }).pipe(Effect.exit)
819
+
820
+ expect(String(exit)).toContain("Replay owner mismatch")
821
+ }),
822
+ )
823
+
824
+ it.effect("exact replay claims an unowned aggregate", () =>
825
+ Effect.gen(function* () {
826
+ const events = yield* EventV2.Service
827
+ const { db } = yield* Database.Service
828
+ const aggregateID = Session.ID.create()
829
+ const published = yield* events.publish(DurableMessage, durableData(aggregateID, "owned"))
830
+ const replayed = {
831
+ id: published.id,
832
+ type: EventV2.versionedType(DurableMessage.type, 1),
833
+ seq: published.durable!.seq,
834
+ aggregateID,
835
+ data: published.data,
836
+ }
837
+
838
+ yield* events.replay(replayed, { ownerID: "owner-a", strictOwner: true })
839
+ const row = yield* db
840
+ .select({ ownerID: EventSequenceTable.owner_id })
841
+ .from(EventSequenceTable)
842
+ .where(eq(EventSequenceTable.aggregate_id, aggregateID))
843
+ .get()
844
+ .pipe(Effect.orDie)
845
+
846
+ expect(row?.ownerID).toBe("owner-a")
847
+ const exit = yield* events
848
+ .replay(
849
+ { ...replayed, id: EventV2.ID.create(), seq: 1, data: durableData(aggregateID, "conflict") },
850
+ { ownerID: "owner-b", strictOwner: true },
851
+ )
852
+ .pipe(Effect.exit)
853
+ expect(String(exit)).toContain("Replay owner mismatch")
854
+ }),
855
+ )
856
+
857
+ it.effect("replay with owner claims an unowned sequence", () =>
858
+ Effect.gen(function* () {
859
+ const events = yield* EventV2.Service
860
+ const { db } = yield* Database.Service
861
+ const aggregateID = Session.ID.create()
862
+
863
+ yield* events.replay(
864
+ {
865
+ id: EventV2.ID.create(),
866
+ type: EventV2.versionedType(DurableMessage.type, 1),
867
+ seq: 0,
868
+ aggregateID,
869
+ data: durableData(aggregateID, "owned"),
870
+ },
871
+ { ownerID: "owner-1" },
872
+ )
873
+ const row = yield* db
874
+ .select({ seq: EventSequenceTable.seq, ownerID: EventSequenceTable.owner_id })
875
+ .from(EventSequenceTable)
876
+ .where(eq(EventSequenceTable.aggregate_id, aggregateID))
877
+ .get()
878
+ .pipe(Effect.orDie)
879
+
880
+ expect(row).toEqual({ seq: 0, ownerID: "owner-1" })
881
+ }),
882
+ )
883
+
884
+ it.effect("replay claims an existing unowned sequence before fencing a different owner", () =>
885
+ Effect.gen(function* () {
886
+ const events = yield* EventV2.Service
887
+ const { db } = yield* Database.Service
888
+ const aggregateID = Session.ID.create()
889
+ yield* events.publish(DurableMessage, durableData(aggregateID, "local"))
890
+
891
+ yield* events.replay(
892
+ {
893
+ id: EventV2.ID.create(),
894
+ type: EventV2.versionedType(DurableMessage.type, 1),
895
+ seq: 1,
896
+ aggregateID,
897
+ data: durableData(aggregateID, "claimed"),
898
+ },
899
+ { ownerID: "owner-1" },
900
+ )
901
+ yield* events.replay(
902
+ {
903
+ id: EventV2.ID.create(),
904
+ type: EventV2.versionedType(DurableMessage.type, 1),
905
+ seq: 2,
906
+ aggregateID,
907
+ data: durableData(aggregateID, "fenced"),
908
+ },
909
+ { ownerID: "owner-2" },
910
+ )
911
+ const rows = yield* db
912
+ .select()
913
+ .from(EventTable)
914
+ .where(eq(EventTable.aggregate_id, aggregateID))
915
+ .all()
916
+ .pipe(Effect.orDie)
917
+ const sequence = yield* db
918
+ .select({ seq: EventSequenceTable.seq, ownerID: EventSequenceTable.owner_id })
919
+ .from(EventSequenceTable)
920
+ .where(eq(EventSequenceTable.aggregate_id, aggregateID))
921
+ .get()
922
+ .pipe(Effect.orDie)
923
+
924
+ expect(rows.map((row) => row.seq)).toEqual([0, 1])
925
+ expect(sequence).toEqual({ seq: 1, ownerID: "owner-1" })
926
+ }),
927
+ )
928
+
929
+ it.effect("strict replay rejects an owner conflict instead of silently skipping it", () =>
930
+ Effect.gen(function* () {
931
+ const events = yield* EventV2.Service
932
+ const aggregateID = Session.ID.create()
933
+ yield* events.replay(
934
+ {
935
+ id: EventV2.ID.create(),
936
+ type: EventV2.versionedType(DurableMessage.type, 1),
937
+ seq: 0,
938
+ aggregateID,
939
+ data: durableData(aggregateID, "claimed"),
940
+ },
941
+ { ownerID: "owner-1" },
942
+ )
943
+
944
+ const exit = yield* events
945
+ .replay(
946
+ {
947
+ id: EventV2.ID.create(),
948
+ type: EventV2.versionedType(DurableMessage.type, 1),
949
+ seq: 1,
950
+ aggregateID,
951
+ data: durableData(aggregateID, "conflict"),
952
+ },
953
+ { ownerID: "owner-2", strictOwner: true },
954
+ )
955
+ .pipe(Effect.exit)
956
+
957
+ expect(String(exit)).toContain("Replay owner mismatch")
958
+ }),
959
+ )
960
+
961
+ it.effect("publishes accepted replay with its durable sequence and suppresses stale replay", () =>
962
+ Effect.gen(function* () {
963
+ const events = yield* EventV2.Service
964
+ const received = new Array<EventV2.Payload>()
965
+ const aggregateID = Session.ID.create()
966
+ yield* events.listen((event) => Effect.sync(() => received.push(event)))
967
+ const replayed = {
968
+ id: EventV2.ID.create(),
969
+ type: EventV2.versionedType(DurableMessage.type, 1),
970
+ seq: 0,
971
+ aggregateID,
972
+ data: durableData(aggregateID, "replayed"),
973
+ }
974
+
975
+ yield* events.replay(replayed, { publish: true })
976
+ yield* events.replay(replayed, { publish: true })
977
+
978
+ expect(received).toMatchObject([{ id: replayed.id, durable: { seq: 0, version: 1 }, data: replayed.data }])
979
+ }),
980
+ )
981
+
982
+ it.effect("rejects divergent stale replay without publishing it", () =>
983
+ Effect.gen(function* () {
984
+ const events = yield* EventV2.Service
985
+ const received = new Array<EventV2.Payload>()
986
+ const aggregateID = Session.ID.create()
987
+ const replayed = {
988
+ id: EventV2.ID.create(),
989
+ type: EventV2.versionedType(DurableMessage.type, 1),
990
+ seq: 0,
991
+ aggregateID,
992
+ data: durableData(aggregateID, "original"),
993
+ }
994
+ yield* events.listen((event) => Effect.sync(() => received.push(event)))
995
+ yield* events.replay(replayed, { publish: true })
996
+
997
+ const exit = yield* events
998
+ .replay({ ...replayed, data: durableData(aggregateID, "divergent") }, { publish: true })
999
+ .pipe(Effect.exit)
1000
+
1001
+ expect(String(exit)).toContain("Replay diverged")
1002
+ expect(received).toHaveLength(1)
1003
+ }),
1004
+ )
1005
+
1006
+ it.effect("rejects an event ID reused at another aggregate position", () =>
1007
+ Effect.gen(function* () {
1008
+ const events = yield* EventV2.Service
1009
+ const aggregateID = Session.ID.create()
1010
+ const id = EventV2.ID.create()
1011
+ yield* events.replay({
1012
+ id,
1013
+ type: EventV2.versionedType(DurableMessage.type, 1),
1014
+ seq: 0,
1015
+ aggregateID,
1016
+ data: durableData(aggregateID, "first"),
1017
+ })
1018
+
1019
+ const exit = yield* events
1020
+ .replay({
1021
+ id,
1022
+ type: EventV2.versionedType(DurableMessage.type, 1),
1023
+ seq: 1,
1024
+ aggregateID,
1025
+ data: durableData(aggregateID, "second"),
1026
+ })
1027
+ .pipe(Effect.exit)
1028
+
1029
+ expect(String(exit)).toContain(`Event ${id} already exists`)
1030
+ }),
1031
+ )
1032
+
1033
+ it.effect("replay from a different owner leaves claimed sequence unchanged", () =>
1034
+ Effect.gen(function* () {
1035
+ const events = yield* EventV2.Service
1036
+ const { db } = yield* Database.Service
1037
+ const aggregateID = Session.ID.create()
1038
+ const received = new Array<EventV2.Payload>()
1039
+ yield* events.listen((event) => Effect.sync(() => received.push(event)))
1040
+
1041
+ yield* events.replay(
1042
+ {
1043
+ id: EventV2.ID.create(),
1044
+ type: EventV2.versionedType(DurableMessage.type, 1),
1045
+ seq: 0,
1046
+ aggregateID,
1047
+ data: durableData(aggregateID, "first"),
1048
+ },
1049
+ { ownerID: "owner-1" },
1050
+ )
1051
+ yield* events.replay(
1052
+ {
1053
+ id: EventV2.ID.create(),
1054
+ type: EventV2.versionedType(DurableMessage.type, 1),
1055
+ seq: 1,
1056
+ aggregateID,
1057
+ data: durableData(aggregateID, "ignored"),
1058
+ },
1059
+ { ownerID: "owner-2", publish: true },
1060
+ )
1061
+ const rows = yield* db
1062
+ .select()
1063
+ .from(EventTable)
1064
+ .where(eq(EventTable.aggregate_id, aggregateID))
1065
+ .all()
1066
+ .pipe(Effect.orDie)
1067
+ const sequence = yield* db
1068
+ .select({ seq: EventSequenceTable.seq, ownerID: EventSequenceTable.owner_id })
1069
+ .from(EventSequenceTable)
1070
+ .where(eq(EventSequenceTable.aggregate_id, aggregateID))
1071
+ .get()
1072
+ .pipe(Effect.orDie)
1073
+
1074
+ expect(rows).toHaveLength(1)
1075
+ expect(sequence).toEqual({ seq: 0, ownerID: "owner-1" })
1076
+ expect(received).toHaveLength(0)
1077
+ }),
1078
+ )
1079
+
1080
+ it.effect("claim updates the event sequence owner", () =>
1081
+ Effect.gen(function* () {
1082
+ const events = yield* EventV2.Service
1083
+ const { db } = yield* Database.Service
1084
+ const aggregateID = EventV2.ID.create()
1085
+
1086
+ yield* events.publish(SyncMessage, { id: aggregateID, text: "claimed" })
1087
+ yield* events.claim(aggregateID, "owner-1")
1088
+ yield* events.claim(aggregateID, "owner-2")
1089
+ const row = yield* db
1090
+ .select({ seq: EventSequenceTable.seq, ownerID: EventSequenceTable.owner_id })
1091
+ .from(EventSequenceTable)
1092
+ .where(eq(EventSequenceTable.aggregate_id, aggregateID))
1093
+ .get()
1094
+ .pipe(Effect.orDie)
1095
+
1096
+ expect(row).toEqual({ seq: 0, ownerID: "owner-2" })
1097
+ }),
1098
+ )
1099
+
1100
+ it.effect("remove clears durable event sequence", () =>
1101
+ Effect.gen(function* () {
1102
+ const events = yield* EventV2.Service
1103
+ const received = new Array<EventV2.Payload>()
1104
+ const aggregateID = Session.ID.create()
1105
+ yield* events.publish(DurableMessage, durableData(aggregateID, "seed"))
1106
+ yield* events.remove(aggregateID)
1107
+ yield* events.project(DurableMessage, (event) =>
1108
+ Effect.sync(() => {
1109
+ received.push(event)
1110
+ }),
1111
+ )
1112
+
1113
+ yield* events.replay({
1114
+ id: EventV2.ID.create(),
1115
+ type: EventV2.versionedType(DurableMessage.type, 1),
1116
+ seq: 0,
1117
+ aggregateID,
1118
+ data: durableData(aggregateID, "replayed"),
1119
+ })
1120
+
1121
+ expect(received[0]?.data).toEqual(durableData(aggregateID, "replayed"))
1122
+ }),
1123
+ )
1124
+ })