@reventlessdev/reventless-local 3.0.0-alpha.249 → 3.0.0-alpha.250

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 (170) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/package.json +13 -13
  3. package/src/BakedManifest.res +5 -6
  4. package/src/EmitCapabilities.res +3 -1
  5. package/src/LocalPlatformRegistry.res +3 -1
  6. package/src/LocalPlatformStart.res +7 -6
  7. package/src/LocalSeedTarget.res +3 -1
  8. package/src/Platform.res +291 -215
  9. package/src/ShellConfig.res +10 -17
  10. package/src/UiHints.res +8 -14
  11. package/src/UiSlots.res +10 -17
  12. package/src/adapter/Api/LocalEvents_Server.res +5 -3
  13. package/src/adapter/Api/LocalGraphQL_Adapter.res +4 -4
  14. package/src/adapter/Auth/LocalAuth.res +11 -15
  15. package/src/adapter/Auth/UserStore.res +5 -7
  16. package/src/adapter/Backend.res +1 -2
  17. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +42 -26
  18. package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res +13 -3
  19. package/src/adapter/CommandTopic/LocalCommandTopicChannel.res +15 -17
  20. package/src/adapter/CommandTopic/LocalCommandTopicRemoteChannel.res +18 -22
  21. package/src/adapter/Counter/LocalCounterHandler.res +1 -1
  22. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +33 -22
  23. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +57 -57
  24. package/src/adapter/DomainGraphQL_Server.res +35 -34
  25. package/src/adapter/DomainMCP_Server.res +45 -41
  26. package/src/adapter/EventCollector/LocalEventCollectorChannel.res +47 -36
  27. package/src/adapter/EventHistory/EventHistoryResolvers_GraphQL.res +15 -12
  28. package/src/adapter/EventLog/EventLogStorage_InMemory.res +9 -4
  29. package/src/adapter/EventLog/EventLogStorage_Sqlite.res +36 -24
  30. package/src/adapter/EventTopic/LocalEventTopicPublisher.res +9 -3
  31. package/src/adapter/Heartbeat/LocalHeartbeatRunner.res +9 -14
  32. package/src/adapter/LocalBus.res +12 -12
  33. package/src/adapter/LocalCapabilities.res.mjs +1 -1
  34. package/src/adapter/LocalGeocodeResolvers.res +1 -4
  35. package/src/adapter/LocalSideEffectHandler.res +1 -1
  36. package/src/adapter/LocalStateChangeDescriptor.res +2 -1
  37. package/src/adapter/MCP_ServerInstance.res +51 -39
  38. package/src/adapter/ObjectStore/LocalObjectStore.res +5 -2
  39. package/src/adapter/ObjectStore/ObjectStoreStorage_FileSystem.res +1 -2
  40. package/src/adapter/PlatformGraphQL_Server.res +9 -5
  41. package/src/adapter/ProjectionCheckpoint.res +8 -1
  42. package/src/adapter/ProjectionPending.res +1 -3
  43. package/src/adapter/QueryDb/LocalQueryDbStorage.res +2 -1
  44. package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +367 -269
  45. package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +5 -3
  46. package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +63 -40
  47. package/src/adapter/QueryDb/SortKey_Filter.res +14 -6
  48. package/src/adapter/Runtime/LocalAggregateRuntime_Builder.res +12 -10
  49. package/src/adapter/Runtime/LocalEventCollectorRuntime_Builder.res +8 -2
  50. package/src/adapter/Runtime/LocalPluginRuntime_Builder.res +1 -4
  51. package/src/adapter/Scheduler/LocalScheduledPublisher.res +12 -12
  52. package/src/adapter/SqliteDriver.res +16 -4
  53. package/src/adapter/Task/TaskBucket_Sqlite.res +5 -6
  54. package/src/components/Aggregate_Builder.res +6 -1
  55. package/src/components/Counter_Builder.res +1 -5
  56. package/src/components/DcbEventLog_Builder.res +5 -3
  57. package/src/components/ExtensionPoint_Builder.res +13 -15
  58. package/src/components/InboundTranslationSlice_Builder.res +3 -2
  59. package/src/components/OutboundTranslationSlice_Builder.res +3 -2
  60. package/src/components/ReadModel_Builder.res +18 -20
  61. package/src/components/StateViewSlice_Builder.res +3 -2
  62. package/src/components/Task_Builder.res +12 -13
  63. package/src/reset/LocalSeedReset.res +2 -5
  64. package/src/test/Mocks/MockDcbEventLogStorage.res +5 -1
  65. package/src/test/Mocks/MockEventLogStorage.res +8 -3
  66. package/src/test/Mocks/MockQueryDbStorage.res +3 -2
  67. package/src/test/TestRunner.res +1 -1
  68. package/tests/BakedManifestFilesTest.res +7 -10
  69. package/tests/LocalEventTapTest.res +26 -21
  70. package/tests/LocalPlatformStartTest.res +33 -28
  71. package/tests/PluginEventDecodeTest.res +3 -3
  72. package/tests/ShellConfigTest.res +30 -21
  73. package/tests/SplitApiFixtures.res +34 -10
  74. package/tests/SplitApiTest.res +92 -62
  75. package/tests/adapter/BackendParityTest.res +180 -130
  76. package/tests/adapter/CollectNEventsTest.res +6 -4
  77. package/tests/adapter/CommandAuthorizationTest.res +47 -41
  78. package/tests/adapter/CommandTopicChannelTest.res +163 -139
  79. package/tests/adapter/CounterHandlerTest.res +95 -77
  80. package/tests/adapter/DcbEventLogStorageSqliteTest.res +171 -168
  81. package/tests/adapter/DcbEventLogStorageTest.res +264 -207
  82. package/tests/adapter/DomainGraphQL_ServerTest.res +53 -43
  83. package/tests/adapter/EventCollectorChannelTest.res +153 -124
  84. package/tests/adapter/EventHistoryResolverTest.res +46 -18
  85. package/tests/adapter/EventLogSnapshotParityTest.res +57 -45
  86. package/tests/adapter/EventLogStorageSqliteTest.res +9 -10
  87. package/tests/adapter/EventTapTest.res +37 -20
  88. package/tests/adapter/EventTopicPublisherTest.res +74 -50
  89. package/tests/adapter/GraphQLSubscriptionBridgeTest.res +4 -3
  90. package/tests/adapter/GraphQL_SchemaInspectorTest.res +666 -568
  91. package/tests/adapter/GraphQL_SubscriptionResolversTest.res +105 -96
  92. package/tests/adapter/HeartbeatRunnerTest.res +99 -81
  93. package/tests/adapter/InboundTranslationMutationTest.res +4 -1
  94. package/tests/adapter/LocalAuthHttpTest.res +79 -85
  95. package/tests/adapter/LocalAuthLoginTest.res +42 -50
  96. package/tests/adapter/LocalAuthTest.res +8 -16
  97. package/tests/adapter/LocalAuthUserStoreTest.res +2 -7
  98. package/tests/adapter/LocalBusBoundedTest.res +187 -117
  99. package/tests/adapter/LocalBusPubSubTest.res +118 -76
  100. package/tests/adapter/LocalDispatchContextTest.res +1 -2
  101. package/tests/adapter/LocalEvents_ServerTest.res +242 -187
  102. package/tests/adapter/PgProjectionCatchupTest.res +44 -10
  103. package/tests/adapter/ProjectionCheckpointTest.res +261 -206
  104. package/tests/adapter/QueryDbGsiTtlTest.res +123 -83
  105. package/tests/adapter/QueryDbListPushdownParityTest.res +107 -67
  106. package/tests/adapter/QueryDbListResolverTest.res +225 -208
  107. package/tests/adapter/QueryDbResolvedFieldsTest.res +17 -14
  108. package/tests/adapter/QueryDbStorageSqliteTest.res +33 -49
  109. package/tests/adapter/QueryDbStorageTest.res +163 -117
  110. package/tests/adapter/QueryDbUnionRoundTripTest.res +8 -10
  111. package/tests/adapter/QueryEngineTest.res +179 -126
  112. package/tests/adapter/ScheduledPublisherTest.res +171 -135
  113. package/tests/adapter/ServedBucketHttpTest.res +65 -59
  114. package/tests/adapter/SortKey_FilterTest.res +19 -3
  115. package/tests/adapter/SqliteDriverTest.res +13 -10
  116. package/tests/adapter/TaskBucketTest.res +94 -80
  117. package/tests/components/aggregate/AggregateTest.res +31 -18
  118. package/tests/components/automationslice/AutomationSliceCallbackTest.res +259 -222
  119. package/tests/components/automationslice/AutomationSliceFixtures.res +4 -2
  120. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +38 -38
  121. package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +12 -14
  122. package/tests/components/automationslice/MixedSourceAutomationSliceTest.res +8 -32
  123. package/tests/components/commandgenerator/CommandGeneratorFixtures.res +7 -5
  124. package/tests/components/commandgenerator/CommandGeneratorTest.res +40 -31
  125. package/tests/components/commandtopic/CommandTopicFixtures.res +3 -4
  126. package/tests/components/commandtopic/CommandTopicStreamFixtures.res +7 -5
  127. package/tests/components/commandtopic/CommandTopicTest.res +24 -15
  128. package/tests/components/counter/CounterFixtures.res +6 -8
  129. package/tests/components/dcb/DcbCrossPartitionFixtures.res +8 -2
  130. package/tests/components/dcb/DcbCrossPartitionReadTest.res +20 -13
  131. package/tests/components/dcb/DcbEventLogAppendStreamTest.res +12 -8
  132. package/tests/components/dcb/DcbEventLogStreamTest.res +88 -61
  133. package/tests/components/dcb/DcbFixtures.res +4 -1
  134. package/tests/components/dcb/DcbTest.res +9 -6
  135. package/tests/components/eventcollector/EventCollectorFixtures.res +0 -1
  136. package/tests/components/eventcollector/EventCollectorTest.res +6 -3
  137. package/tests/components/eventlog/EventLogAppendStreamTest.res +88 -68
  138. package/tests/components/eventlog/EventLogConflictTest.res +8 -4
  139. package/tests/components/eventlog/EventLogFixtures.res +3 -3
  140. package/tests/components/eventlog/EventLogProvisioningSeamTest.res +26 -13
  141. package/tests/components/eventlog/EventLogStreamTest.res +119 -56
  142. package/tests/components/eventlog/EventLogTest.res +20 -4
  143. package/tests/components/eventtopic/EventTopicFixtures.res +3 -3
  144. package/tests/components/eventtopic/EventTopicStreamFixtures.res +1 -2
  145. package/tests/components/eventtopic/EventTopicStreamTest.res +7 -1
  146. package/tests/components/extensionpoint/ExtensionPointFixtures.res +14 -9
  147. package/tests/components/extensionpoint/ExtensionPointTest.res +3 -2
  148. package/tests/components/heartbeat/HeartbeatFixtures.res +5 -6
  149. package/tests/components/heartbeat/HeartbeatTest.res +56 -47
  150. package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res +271 -249
  151. package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res +3 -2
  152. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +361 -276
  153. package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res +8 -10
  154. package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res +37 -43
  155. package/tests/components/querydb/QueryDbFixtures.res +1 -5
  156. package/tests/components/querydb/QueryDbTest.res +2 -4
  157. package/tests/components/readmodel/DcbReadModelE2EFixtures.res +15 -18
  158. package/tests/components/readmodel/MixedSourceReadModelFixtures.res +5 -7
  159. package/tests/components/readmodel/ReadModelFixtures.res +8 -8
  160. package/tests/components/scheduler/SchedulerTest.res +76 -58
  161. package/tests/components/sideeffecthandler/SideEffectHandlerFixtures.res +4 -4
  162. package/tests/components/stateviewslice/StateViewSliceDisplayNameFixtures.res +5 -7
  163. package/tests/components/stateviewslice/StateViewSliceFixtures.res +8 -6
  164. package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res +17 -11
  165. package/tests/components/stateviewslice/StateViewSliceSubIdTest.res +12 -2
  166. package/tests/components/task/TaskFixtures.res +5 -5
  167. package/tests/components/task/TaskTest.res +95 -80
  168. package/tests/plugin/PluginsProjection_GWT.res +3 -1
  169. package/tests/plugin/UiFragments_GWT.res +6 -1
  170. package/tests/reset/LocalSeedResetTest.res +1 -0
@@ -28,8 +28,12 @@ open ReventlessCore.OutboundTranslationSlice_Callback
28
28
 
29
29
  describe("OutboundTranslationSlice Callback", () => {
30
30
  let _ = beforeEach(() => {
31
- FireForgetCallback.todoItems->Dict.keysToArray->Array.forEach(k => FireForgetCallback.todoItems->Dict.delete(k))
32
- CommandBackCallback.todoItems->Dict.keysToArray->Array.forEach(k => CommandBackCallback.todoItems->Dict.delete(k))
31
+ FireForgetCallback.todoItems
32
+ ->Dict.keysToArray
33
+ ->Array.forEach(k => FireForgetCallback.todoItems->Dict.delete(k))
34
+ CommandBackCallback.todoItems
35
+ ->Dict.keysToArray
36
+ ->Array.forEach(k => CommandBackCallback.todoItems->Dict.delete(k))
33
37
  // Reset the translate function to its default
34
38
  ProcessPaymentSpec.translateFn :=
35
39
  (async (id, _item) => Ok(Some((id, ProcessPaymentSpec.ConfirmPayment({orderId: id})))))
@@ -38,304 +42,385 @@ describe("OutboundTranslationSlice Callback", () => {
38
42
  })
39
43
 
40
44
  describe("Phase 1 — collect", () => {
41
- testPromise("OrderShipped event creates a pending outbound item", async () => {
42
- FireForgetCallback.phase1([("evt", OrderShipped({orderId: "ord-1", email: "test@example.com"}))])
43
- let items = FireForgetCallback.todoItems
44
- expect(items->Dict.toArray->Array.length)->toBe(1)
45
- let row = items->Dict.get("ord-1")
46
- expect(row->Option.isSome)->toBe(true)
47
- let r = row->Option.getOrThrow
48
- expect(r.status)->toBe(Pending)
49
- expect(r.retryCount)->toBe(0)
50
- })
51
-
52
- testPromise("duplicate event does not create a second item (idempotent)", async () => {
53
- FireForgetCallback.phase1([("evt", OrderShipped({orderId: "ord-1", email: "test@example.com"}))])
54
- FireForgetCallback.phase1([("evt", OrderShipped({orderId: "ord-1", email: "other@example.com"}))])
55
- let items = FireForgetCallback.todoItems
56
- expect(items->Dict.toArray->Array.length)->toBe(1)
57
- })
58
-
59
- testPromise("multiple different events create multiple items", async () => {
60
- FireForgetCallback.phase1([
61
- ("evt", OrderShipped({orderId: "ord-1", email: "a@example.com"})),
62
- ("evt", OrderShipped({orderId: "ord-2", email: "b@example.com"})),
63
- ])
64
- let items = FireForgetCallback.todoItems
65
- expect(items->Dict.toArray->Array.length)->toBe(2)
66
- })
67
-
45
+ testPromise(
46
+ "OrderShipped event creates a pending outbound item",
47
+ async () => {
48
+ FireForgetCallback.phase1([
49
+ ("evt", OrderShipped({orderId: "ord-1", email: "test@example.com"})),
50
+ ])
51
+ let items = FireForgetCallback.todoItems
52
+ expect(items->Dict.toArray->Array.length)->toBe(1)
53
+ let row = items->Dict.get("ord-1")
54
+ expect(row->Option.isSome)->toBe(true)
55
+ let r = row->Option.getOrThrow
56
+ expect(r.status)->toBe(Pending)
57
+ expect(r.retryCount)->toBe(0)
58
+ },
59
+ )
60
+
61
+ testPromise(
62
+ "duplicate event does not create a second item (idempotent)",
63
+ async () => {
64
+ FireForgetCallback.phase1([
65
+ ("evt", OrderShipped({orderId: "ord-1", email: "test@example.com"})),
66
+ ])
67
+ FireForgetCallback.phase1([
68
+ ("evt", OrderShipped({orderId: "ord-1", email: "other@example.com"})),
69
+ ])
70
+ let items = FireForgetCallback.todoItems
71
+ expect(items->Dict.toArray->Array.length)->toBe(1)
72
+ },
73
+ )
74
+
75
+ testPromise(
76
+ "multiple different events create multiple items",
77
+ async () => {
78
+ FireForgetCallback.phase1([
79
+ ("evt", OrderShipped({orderId: "ord-1", email: "a@example.com"})),
80
+ ("evt", OrderShipped({orderId: "ord-2", email: "b@example.com"})),
81
+ ])
82
+ let items = FireForgetCallback.todoItems
83
+ expect(items->Dict.toArray->Array.length)->toBe(2)
84
+ },
85
+ )
68
86
  })
69
87
 
70
88
  describe("Phase 2 — translate (fire-and-forget)", () => {
71
- testPromise("Ok(None) marks item as Completed, no command published", async () => {
72
- FireForgetCallback.phase1([("evt", OrderShipped({orderId: "ord-1", email: "test@example.com"}))])
73
- let publishedCommands = ref([])
74
- let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
75
- publishedCommands := cmds
76
- }
77
- await FireForgetCallback.phase2(mockPublish, ~capabilities=Reventless.Capabilities.none)
78
- expect(publishedCommands.contents->Array.length)->toBe(0)
79
- let row = FireForgetCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
80
- expect(row.status)->toBe(Completed)
81
- })
82
-
83
- testPromise("empty TODO list produces no commands", async () => {
84
- let publishedCommands = ref([])
85
- let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
86
- publishedCommands := cmds
87
- }
88
- await FireForgetCallback.phase2(mockPublish, ~capabilities=Reventless.Capabilities.none)
89
- expect(publishedCommands.contents->Array.length)->toBe(0)
90
- })
89
+ testPromise(
90
+ "Ok(None) marks item as Completed, no command published",
91
+ async () => {
92
+ FireForgetCallback.phase1([
93
+ ("evt", OrderShipped({orderId: "ord-1", email: "test@example.com"})),
94
+ ])
95
+ let publishedCommands = ref([])
96
+ let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
97
+ publishedCommands := cmds
98
+ }
99
+ await FireForgetCallback.phase2(mockPublish, ~capabilities=Reventless.Capabilities.none)
100
+ expect(publishedCommands.contents->Array.length)->toBe(0)
101
+ let row = FireForgetCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
102
+ expect(row.status)->toBe(Completed)
103
+ },
104
+ )
105
+
106
+ testPromise(
107
+ "empty TODO list produces no commands",
108
+ async () => {
109
+ let publishedCommands = ref([])
110
+ let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
111
+ publishedCommands := cmds
112
+ }
113
+ await FireForgetCallback.phase2(mockPublish, ~capabilities=Reventless.Capabilities.none)
114
+ expect(publishedCommands.contents->Array.length)->toBe(0)
115
+ },
116
+ )
91
117
  })
92
118
 
93
119
  describe("Phase 2 — translate (command-back)", () => {
94
- testPromise("Ok(Some(...)) marks item as Completed and publishes command", async () => {
95
- CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
96
- let publishedCommands = ref([])
97
- let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
98
- publishedCommands := Array.concat(publishedCommands.contents, cmds)
99
- }
100
- await CommandBackCallback.phase2(mockPublish, ~capabilities=Reventless.Capabilities.none)
101
- expect(publishedCommands.contents->Array.length)->toBe(1)
102
- let cmd = publishedCommands.contents->Array.getUnsafe(0)
103
- expect(cmd.id)->toBe("ord-1")
104
- let row = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
105
- expect(row.status)->toBe(Completed)
106
- })
120
+ testPromise(
121
+ "Ok(Some(...)) marks item as Completed and publishes command",
122
+ async () => {
123
+ CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
124
+ let publishedCommands = ref([])
125
+ let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
126
+ publishedCommands := Array.concat(publishedCommands.contents, cmds)
127
+ }
128
+ await CommandBackCallback.phase2(mockPublish, ~capabilities=Reventless.Capabilities.none)
129
+ expect(publishedCommands.contents->Array.length)->toBe(1)
130
+ let cmd = publishedCommands.contents->Array.getUnsafe(0)
131
+ expect(cmd.id)->toBe("ord-1")
132
+ let row = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
133
+ expect(row.status)->toBe(Completed)
134
+ },
135
+ )
107
136
 
108
137
  // The published command's `meta.service` must name the TARGET, not this
109
138
  // slice. An aggregate derives its event's meta from the command's, and
110
139
  // ReadModel/AutomationSlice callbacks dispatch mappings on `meta.service` —
111
140
  // so a slice that stamps its own name here makes the target's events match
112
141
  // no mapping and project as zero actions, with no error anywhere.
113
- testPromise("published command is tagged with the target's name", async () => {
114
- CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
115
- let publishedCommands = ref([])
116
- let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
117
- publishedCommands := Array.concat(publishedCommands.contents, cmds)
118
- }
119
- await CommandBackCallback.phase2(mockPublish, ~capabilities=Reventless.Capabilities.none)
120
- let cmd = publishedCommands.contents->Array.getUnsafe(0)
121
- expect(cmd.meta.service)->toBe("ConfirmPayment")
122
- })
123
-
124
- testPromise("Error marks item as Failed with incremented retryCount", async () => {
125
- ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("gateway timeout"))
126
-
127
- CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
128
- let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => ()
129
- await CommandBackCallback.phase2(mockPublish, ~capabilities=Reventless.Capabilities.none)
130
-
131
- let row = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
132
- expect(row.status)->toBe(Failed)
133
- expect(row.retryCount)->toBe(1)
134
- })
135
-
136
- testPromise("failed items with retryCount < maxRetries are retried", async () => {
137
- // First attempt fails
138
- ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("timeout"))
139
- CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
140
- let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => ()
141
- await CommandBackCallback.phase2(mockPublish, ~capabilities=Reventless.Capabilities.none)
142
- let row1 = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
143
- expect(row1.retryCount)->toBe(1)
144
-
145
- // Second attempt succeeds
146
- ProcessPaymentSpec.translateFn :=
147
- (async (id, _item) => Ok(Some((id, ProcessPaymentSpec.ConfirmPayment({orderId: id})))))
148
- let publishedCommands = ref([])
149
- let successPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
150
- publishedCommands := cmds
151
- }
152
- await CommandBackCallback.phase2(successPublish, ~capabilities=Reventless.Capabilities.none)
153
- expect(publishedCommands.contents->Array.length)->toBe(1)
154
- let row2 = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
155
- expect(row2.status)->toBe(Completed)
156
- })
157
-
158
- testPromise("failed items beyond maxRetries are not retried", async () => {
159
- // ProcessPaymentSpec.maxRetries = 2, so after 2 failures it should stop
160
- ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("timeout"))
161
- CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
162
- let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => ()
163
-
164
- // Fail once — still retriable, and the status says so.
165
- await CommandBackCallback.phase2(mockPublish, ~capabilities=Reventless.Capabilities.none)
166
- expect((CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)
167
- ->toBe(Failed)
168
-
169
- // The second failure spends the budget, and is marked at that moment rather
170
- // than by falling out of the next pass's filter.
171
- await CommandBackCallback.phase2(mockPublish, ~capabilities=Reventless.Capabilities.none)
172
- let row = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
173
- expect(row.retryCount)->toBe(2)
174
- expect(row.status)->toBe(Abandoned)
175
-
176
- // Third attempt should not be tried
177
- let publishedCommands = ref([])
178
- let trackPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
179
- publishedCommands := cmds
180
- }
181
- await CommandBackCallback.phase2(trackPublish, ~capabilities=Reventless.Capabilities.none)
182
- expect(publishedCommands.contents->Array.length)->toBe(0)
183
- let row2 = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
184
- // retryCount should still be 2 — not retried
185
- expect(row2.retryCount)->toBe(2)
186
- expect(row2.status)->toBe(Abandoned)
187
- })
142
+ testPromise(
143
+ "published command is tagged with the target's name",
144
+ async () => {
145
+ CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
146
+ let publishedCommands = ref([])
147
+ let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
148
+ publishedCommands := Array.concat(publishedCommands.contents, cmds)
149
+ }
150
+ await CommandBackCallback.phase2(mockPublish, ~capabilities=Reventless.Capabilities.none)
151
+ let cmd = publishedCommands.contents->Array.getUnsafe(0)
152
+ expect(cmd.meta.service)->toBe("ConfirmPayment")
153
+ },
154
+ )
155
+
156
+ testPromise(
157
+ "Error marks item as Failed with incremented retryCount",
158
+ async () => {
159
+ ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("gateway timeout"))
160
+
161
+ CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
162
+ let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => ()
163
+ await CommandBackCallback.phase2(mockPublish, ~capabilities=Reventless.Capabilities.none)
164
+
165
+ let row = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
166
+ expect(row.status)->toBe(Failed)
167
+ expect(row.retryCount)->toBe(1)
168
+ },
169
+ )
170
+
171
+ testPromise(
172
+ "failed items with retryCount < maxRetries are retried",
173
+ async () => {
174
+ // First attempt fails
175
+ ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("timeout"))
176
+ CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
177
+ let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => ()
178
+ await CommandBackCallback.phase2(mockPublish, ~capabilities=Reventless.Capabilities.none)
179
+ let row1 = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
180
+ expect(row1.retryCount)->toBe(1)
181
+
182
+ // Second attempt succeeds
183
+ ProcessPaymentSpec.translateFn :=
184
+ (async (id, _item) => Ok(Some((id, ProcessPaymentSpec.ConfirmPayment({orderId: id})))))
185
+ let publishedCommands = ref([])
186
+ let successPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
187
+ publishedCommands := cmds
188
+ }
189
+ await CommandBackCallback.phase2(successPublish, ~capabilities=Reventless.Capabilities.none)
190
+ expect(publishedCommands.contents->Array.length)->toBe(1)
191
+ let row2 = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
192
+ expect(row2.status)->toBe(Completed)
193
+ },
194
+ )
195
+
196
+ testPromise(
197
+ "failed items beyond maxRetries are not retried",
198
+ async () => {
199
+ // ProcessPaymentSpec.maxRetries = 2, so after 2 failures it should stop
200
+ ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("timeout"))
201
+ CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
202
+ let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => ()
203
+
204
+ // Fail once — still retriable, and the status says so.
205
+ await CommandBackCallback.phase2(mockPublish, ~capabilities=Reventless.Capabilities.none)
206
+ expect((CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)->toBe(
207
+ Failed,
208
+ )
209
+
210
+ // The second failure spends the budget, and is marked at that moment rather
211
+ // than by falling out of the next pass's filter.
212
+ await CommandBackCallback.phase2(mockPublish, ~capabilities=Reventless.Capabilities.none)
213
+ let row = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
214
+ expect(row.retryCount)->toBe(2)
215
+ expect(row.status)->toBe(Abandoned)
216
+
217
+ // Third attempt should not be tried
218
+ let publishedCommands = ref([])
219
+ let trackPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
220
+ publishedCommands := cmds
221
+ }
222
+ await CommandBackCallback.phase2(trackPublish, ~capabilities=Reventless.Capabilities.none)
223
+ expect(publishedCommands.contents->Array.length)->toBe(0)
224
+ let row2 = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
225
+ // retryCount should still be 2 — not retried
226
+ expect(row2.retryCount)->toBe(2)
227
+ expect(row2.status)->toBe(Abandoned)
228
+ },
229
+ )
188
230
 
189
231
  // What an older build wrote, and what a Spec that lowers maxRetries leaves
190
232
  // behind: Failed at the ceiling, a status promising a retry no pass will make.
191
- testPromise("a row left stranded at the ceiling is normalised on the next pass", async () => {
192
- ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("timeout"))
193
- CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
194
- let row = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
195
- CommandBackCallback.todoItems->Dict.set("ord-1", {...row, status: Failed, retryCount: 2})
196
-
197
- await CommandBackCallback.phase2(
198
- async _cmds => (),
199
- ~capabilities=Reventless.Capabilities.none,
200
- )
201
- expect((CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)
202
- ->toBe(Abandoned)
203
- })
204
-
205
- testPromise("the last error survives the transition to Abandoned", async () => {
206
- ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("gateway down"))
207
- CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
208
- let publish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => ()
209
- await CommandBackCallback.phase2(publish, ~capabilities=Reventless.Capabilities.none)
210
- await CommandBackCallback.phase2(publish, ~capabilities=Reventless.Capabilities.none)
211
- let row = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
212
- expect(row.status)->toBe(Abandoned)
213
- expect(row.lastError)->toEqual(Some("gateway down"))
214
- })
215
-
216
- testPromise("the row carries the ceiling its count is racing", async () => {
217
- CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
218
- expect((CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow).maxRetries)
219
- ->toEqual(Some(ProcessPaymentSpec.maxRetries))
220
- })
233
+ testPromise(
234
+ "a row left stranded at the ceiling is normalised on the next pass",
235
+ async () => {
236
+ ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("timeout"))
237
+ CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
238
+ let row = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
239
+ CommandBackCallback.todoItems->Dict.set("ord-1", {...row, status: Failed, retryCount: 2})
240
+
241
+ await CommandBackCallback.phase2(
242
+ async _cmds => (),
243
+ ~capabilities=Reventless.Capabilities.none,
244
+ )
245
+ expect((CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)->toBe(
246
+ Abandoned,
247
+ )
248
+ },
249
+ )
250
+
251
+ testPromise(
252
+ "the last error survives the transition to Abandoned",
253
+ async () => {
254
+ ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("gateway down"))
255
+ CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
256
+ let publish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => ()
257
+ await CommandBackCallback.phase2(publish, ~capabilities=Reventless.Capabilities.none)
258
+ await CommandBackCallback.phase2(publish, ~capabilities=Reventless.Capabilities.none)
259
+ let row = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
260
+ expect(row.status)->toBe(Abandoned)
261
+ expect(row.lastError)->toEqual(Some("gateway down"))
262
+ },
263
+ )
264
+
265
+ testPromise(
266
+ "the row carries the ceiling its count is racing",
267
+ async () => {
268
+ CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
269
+ expect(
270
+ (CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow).maxRetries,
271
+ )->toEqual(Some(ProcessPaymentSpec.maxRetries))
272
+ },
273
+ )
221
274
 
222
275
  // Abandonment is an outcome, and a slice that has one to report gets to say
223
276
  // so — the row going quiet is otherwise the only trace.
224
- testPromise("a slice that answers onExhausted has its command published", async () => {
225
- ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("gateway down"))
226
- ProcessPaymentSpec.onExhaustedFn :=
227
- ((id, _item, _lastError) => Some((id, ProcessPaymentSpec.ConfirmPayment({orderId: id}))))
228
-
229
- CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
230
- let published = ref([])
231
- let publish: ReventlessInfra.CommandTopic.publishJsons = async cmds =>
232
- published := Array.concat(published.contents, cmds)
233
-
234
- await CommandBackCallback.phase2(publish, ~capabilities=Reventless.Capabilities.none)
235
- expect(published.contents->Array.length)->toBe(0) // still retriable
236
- await CommandBackCallback.phase2(publish, ~capabilities=Reventless.Capabilities.none)
237
-
238
- expect((CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)
239
- ->toBe(Abandoned)
240
- expect(published.contents->Array.length)->toBe(1)
241
- expect((published.contents->Array.getUnsafe(0)).id)->toBe("ord-1")
242
- // The target's name, not the slice's — the same rule the success path follows.
243
- expect((published.contents->Array.getUnsafe(0)).meta.service)->toBe("ConfirmPayment")
244
- })
245
-
246
- testPromise("the hook is handed the error that ended it", async () => {
247
- ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("gateway down"))
248
- let seen = ref(None)
249
- ProcessPaymentSpec.onExhaustedFn :=
250
- ((_id, _item, lastError) => {
251
- seen := lastError
252
- None
253
- })
254
-
255
- CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
256
- let publish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => ()
257
- await CommandBackCallback.phase2(publish, ~capabilities=Reventless.Capabilities.none)
258
- await CommandBackCallback.phase2(publish, ~capabilities=Reventless.Capabilities.none)
259
- expect(seen.contents)->toEqual(Some("gateway down"))
260
- })
261
-
262
- testPromise("a silent slice publishes nothing, and the row is still Abandoned", async () => {
263
- ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("gateway down"))
264
- // onExhaustedFn stays at its default None (reset in beforeEach).
265
- CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
266
- let published = ref([])
267
- let publish: ReventlessInfra.CommandTopic.publishJsons = async cmds =>
268
- published := Array.concat(published.contents, cmds)
269
- await CommandBackCallback.phase2(publish, ~capabilities=Reventless.Capabilities.none)
270
- await CommandBackCallback.phase2(publish, ~capabilities=Reventless.Capabilities.none)
271
- expect(published.contents->Array.length)->toBe(0)
272
- expect((CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)
273
- ->toBe(Abandoned)
274
- })
277
+ testPromise(
278
+ "a slice that answers onExhausted has its command published",
279
+ async () => {
280
+ ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("gateway down"))
281
+ ProcessPaymentSpec.onExhaustedFn :=
282
+ ((id, _item, _lastError) => Some((id, ProcessPaymentSpec.ConfirmPayment({orderId: id}))))
283
+
284
+ CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
285
+ let published = ref([])
286
+ let publish: ReventlessInfra.CommandTopic.publishJsons = async cmds =>
287
+ published := Array.concat(published.contents, cmds)
288
+
289
+ await CommandBackCallback.phase2(publish, ~capabilities=Reventless.Capabilities.none)
290
+ expect(published.contents->Array.length)->toBe(0) // still retriable
291
+ await CommandBackCallback.phase2(publish, ~capabilities=Reventless.Capabilities.none)
292
+
293
+ expect((CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)->toBe(
294
+ Abandoned,
295
+ )
296
+ expect(published.contents->Array.length)->toBe(1)
297
+ expect((published.contents->Array.getUnsafe(0)).id)->toBe("ord-1")
298
+ // The target's name, not the slice's — the same rule the success path follows.
299
+ expect((published.contents->Array.getUnsafe(0)).meta.service)->toBe("ConfirmPayment")
300
+ },
301
+ )
302
+
303
+ testPromise(
304
+ "the hook is handed the error that ended it",
305
+ async () => {
306
+ ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("gateway down"))
307
+ let seen = ref(None)
308
+ ProcessPaymentSpec.onExhaustedFn :=
309
+ (
310
+ (_id, _item, lastError) => {
311
+ seen := lastError
312
+ None
313
+ }
314
+ )
315
+
316
+ CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
317
+ let publish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => ()
318
+ await CommandBackCallback.phase2(publish, ~capabilities=Reventless.Capabilities.none)
319
+ await CommandBackCallback.phase2(publish, ~capabilities=Reventless.Capabilities.none)
320
+ expect(seen.contents)->toEqual(Some("gateway down"))
321
+ },
322
+ )
323
+
324
+ testPromise(
325
+ "a silent slice publishes nothing, and the row is still Abandoned",
326
+ async () => {
327
+ ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("gateway down"))
328
+ // onExhaustedFn stays at its default None (reset in beforeEach).
329
+ CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
330
+ let published = ref([])
331
+ let publish: ReventlessInfra.CommandTopic.publishJsons = async cmds =>
332
+ published := Array.concat(published.contents, cmds)
333
+ await CommandBackCallback.phase2(publish, ~capabilities=Reventless.Capabilities.none)
334
+ await CommandBackCallback.phase2(publish, ~capabilities=Reventless.Capabilities.none)
335
+ expect(published.contents->Array.length)->toBe(0)
336
+ expect((CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)->toBe(
337
+ Abandoned,
338
+ )
339
+ },
340
+ )
275
341
 
276
342
  // The budget is what ran out, so there is nothing left to retry with — a row
277
343
  // must not fall back to Failed and re-enter the sweep on a publish error.
278
- testPromise("a failed announcement leaves the row Abandoned", async () => {
279
- ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("gateway down"))
280
- ProcessPaymentSpec.onExhaustedFn :=
281
- ((id, _item, _lastError) => Some((id, ProcessPaymentSpec.ConfirmPayment({orderId: id}))))
282
-
283
- CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
284
- let failOnAbandonment: ReventlessInfra.CommandTopic.publishJsons = async _cmds =>
285
- JsError.throwWithMessage("topic unavailable")
286
- await CommandBackCallback.phase2(failOnAbandonment, ~capabilities=Reventless.Capabilities.none)
287
- await CommandBackCallback.phase2(failOnAbandonment, ~capabilities=Reventless.Capabilities.none)
288
- let row = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
289
- expect(row.status)->toBe(Abandoned)
290
- expect(row.retryCount)->toBe(2)
291
- })
344
+ testPromise(
345
+ "a failed announcement leaves the row Abandoned",
346
+ async () => {
347
+ ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("gateway down"))
348
+ ProcessPaymentSpec.onExhaustedFn :=
349
+ ((id, _item, _lastError) => Some((id, ProcessPaymentSpec.ConfirmPayment({orderId: id}))))
350
+
351
+ CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
352
+ let failOnAbandonment: ReventlessInfra.CommandTopic.publishJsons = async _cmds =>
353
+ JsError.throwWithMessage("topic unavailable")
354
+ await CommandBackCallback.phase2(
355
+ failOnAbandonment,
356
+ ~capabilities=Reventless.Capabilities.none,
357
+ )
358
+ await CommandBackCallback.phase2(
359
+ failOnAbandonment,
360
+ ~capabilities=Reventless.Capabilities.none,
361
+ )
362
+ let row = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
363
+ expect(row.status)->toBe(Abandoned)
364
+ expect(row.retryCount)->toBe(2)
365
+ },
366
+ )
292
367
 
293
368
  // A row an older build stranded reaches the hook too — it is being abandoned
294
369
  // now, as far as anything downstream is concerned.
295
- testPromise("a stranded row announces when it is normalised", async () => {
296
- ProcessPaymentSpec.onExhaustedFn :=
297
- ((id, _item, _lastError) => Some((id, ProcessPaymentSpec.ConfirmPayment({orderId: id}))))
298
- CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
299
- let row = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
300
- CommandBackCallback.todoItems->Dict.set("ord-1", {...row, status: Failed, retryCount: 2})
301
-
302
- let published = ref([])
303
- await CommandBackCallback.phase2(
304
- async cmds => published := cmds,
305
- ~capabilities=Reventless.Capabilities.none,
306
- )
307
- expect(published.contents->Array.length)->toBe(1)
308
- expect((CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)
309
- ->toBe(Abandoned)
310
- })
311
-
312
- testPromise("individual item failure does not affect other items", async () => {
313
- let callCount = ref(0)
314
- ProcessPaymentSpec.translateFn := (async (id, _item) => {
315
- callCount := callCount.contents + 1
316
- if callCount.contents == 1 {
317
- Error("first item fails")
318
- } else {
319
- Ok(Some((id, ProcessPaymentSpec.ConfirmPayment({orderId: id}))))
370
+ testPromise(
371
+ "a stranded row announces when it is normalised",
372
+ async () => {
373
+ ProcessPaymentSpec.onExhaustedFn :=
374
+ ((id, _item, _lastError) => Some((id, ProcessPaymentSpec.ConfirmPayment({orderId: id}))))
375
+ CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
376
+ let row = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
377
+ CommandBackCallback.todoItems->Dict.set("ord-1", {...row, status: Failed, retryCount: 2})
378
+
379
+ let published = ref([])
380
+ await CommandBackCallback.phase2(
381
+ async cmds => published := cmds,
382
+ ~capabilities=Reventless.Capabilities.none,
383
+ )
384
+ expect(published.contents->Array.length)->toBe(1)
385
+ expect((CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)->toBe(
386
+ Abandoned,
387
+ )
388
+ },
389
+ )
390
+
391
+ testPromise(
392
+ "individual item failure does not affect other items",
393
+ async () => {
394
+ let callCount = ref(0)
395
+ ProcessPaymentSpec.translateFn :=
396
+ (
397
+ async (id, _item) => {
398
+ callCount := callCount.contents + 1
399
+ if callCount.contents == 1 {
400
+ Error("first item fails")
401
+ } else {
402
+ Ok(Some((id, ProcessPaymentSpec.ConfirmPayment({orderId: id}))))
403
+ }
404
+ }
405
+ )
406
+
407
+ CommandBackCallback.phase1([
408
+ ("evt", PaymentReceived({orderId: "ord-1", amount: 50.0})),
409
+ ("evt", PaymentReceived({orderId: "ord-2", amount: 75.0})),
410
+ ])
411
+ let publishedCommands = ref([])
412
+ let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
413
+ publishedCommands := Array.concat(publishedCommands.contents, cmds)
320
414
  }
321
- })
322
-
323
- CommandBackCallback.phase1([
324
- ("evt", PaymentReceived({orderId: "ord-1", amount: 50.0})),
325
- ("evt", PaymentReceived({orderId: "ord-2", amount: 75.0})),
326
- ])
327
- let publishedCommands = ref([])
328
- let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
329
- publishedCommands := Array.concat(publishedCommands.contents, cmds)
330
- }
331
- await CommandBackCallback.phase2(mockPublish, ~capabilities=Reventless.Capabilities.none)
332
-
333
- // ord-1 should be Failed, ord-2 should be Completed
334
- let row1 = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
335
- expect(row1.status)->toBe(Failed)
336
- let row2 = CommandBackCallback.todoItems->Dict.get("ord-2")->Option.getOrThrow
337
- expect(row2.status)->toBe(Completed)
338
- expect(publishedCommands.contents->Array.length)->toBe(1)
339
- })
415
+ await CommandBackCallback.phase2(mockPublish, ~capabilities=Reventless.Capabilities.none)
416
+
417
+ // ord-1 should be Failed, ord-2 should be Completed
418
+ let row1 = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
419
+ expect(row1.status)->toBe(Failed)
420
+ let row2 = CommandBackCallback.todoItems->Dict.get("ord-2")->Option.getOrThrow
421
+ expect(row2.status)->toBe(Completed)
422
+ expect(publishedCommands.contents->Array.length)->toBe(1)
423
+ },
424
+ )
340
425
  })
341
426
  })