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

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 (174) hide show
  1. package/CHANGELOG.md +30 -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 +12 -16
  15. package/src/adapter/Auth/LocalAuth.res.mjs +2 -1
  16. package/src/adapter/Auth/UserStore.res +13 -40
  17. package/src/adapter/Auth/UserStore.res.mjs +11 -67
  18. package/src/adapter/Backend.res +1 -2
  19. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +42 -26
  20. package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res +13 -3
  21. package/src/adapter/CommandTopic/LocalCommandTopicChannel.res +15 -17
  22. package/src/adapter/CommandTopic/LocalCommandTopicRemoteChannel.res +18 -22
  23. package/src/adapter/Counter/LocalCounterHandler.res +1 -1
  24. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +33 -22
  25. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +57 -57
  26. package/src/adapter/DomainGraphQL_Server.res +35 -34
  27. package/src/adapter/DomainMCP_Server.res +45 -41
  28. package/src/adapter/EventCollector/LocalEventCollectorChannel.res +47 -36
  29. package/src/adapter/EventHistory/EventHistoryResolvers_GraphQL.res +15 -12
  30. package/src/adapter/EventLog/EventLogStorage_InMemory.res +9 -4
  31. package/src/adapter/EventLog/EventLogStorage_Sqlite.res +36 -24
  32. package/src/adapter/EventTopic/LocalEventTopicPublisher.res +9 -3
  33. package/src/adapter/Heartbeat/LocalHeartbeatRunner.res +9 -14
  34. package/src/adapter/LocalBus.res +12 -12
  35. package/src/adapter/LocalCapabilities.res +6 -0
  36. package/src/adapter/LocalCapabilities.res.mjs +5 -2
  37. package/src/adapter/LocalGeocodeResolvers.res +1 -4
  38. package/src/adapter/LocalSideEffectHandler.res +1 -1
  39. package/src/adapter/LocalStateChangeDescriptor.res +2 -1
  40. package/src/adapter/MCP_ServerInstance.res +51 -39
  41. package/src/adapter/ObjectStore/LocalObjectStore.res +5 -2
  42. package/src/adapter/ObjectStore/ObjectStoreStorage_FileSystem.res +1 -2
  43. package/src/adapter/PlatformGraphQL_Server.res +10 -6
  44. package/src/adapter/PlatformGraphQL_Server.res.mjs +2 -1
  45. package/src/adapter/ProjectionCheckpoint.res +8 -1
  46. package/src/adapter/ProjectionPending.res +1 -3
  47. package/src/adapter/QueryDb/LocalQueryDbStorage.res +2 -1
  48. package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +367 -269
  49. package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +5 -3
  50. package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +63 -40
  51. package/src/adapter/QueryDb/SortKey_Filter.res +14 -6
  52. package/src/adapter/Runtime/LocalAggregateRuntime_Builder.res +12 -10
  53. package/src/adapter/Runtime/LocalEventCollectorRuntime_Builder.res +8 -2
  54. package/src/adapter/Runtime/LocalPluginRuntime_Builder.res +1 -4
  55. package/src/adapter/Scheduler/LocalScheduledPublisher.res +12 -12
  56. package/src/adapter/SqliteDriver.res +16 -4
  57. package/src/adapter/Task/TaskBucket_Sqlite.res +5 -6
  58. package/src/components/Aggregate_Builder.res +6 -1
  59. package/src/components/Counter_Builder.res +1 -5
  60. package/src/components/DcbEventLog_Builder.res +5 -3
  61. package/src/components/ExtensionPoint_Builder.res +13 -15
  62. package/src/components/InboundTranslationSlice_Builder.res +3 -2
  63. package/src/components/OutboundTranslationSlice_Builder.res +3 -2
  64. package/src/components/ReadModel_Builder.res +18 -20
  65. package/src/components/StateViewSlice_Builder.res +3 -2
  66. package/src/components/Task_Builder.res +12 -13
  67. package/src/reset/LocalSeedReset.res +2 -5
  68. package/src/test/Mocks/MockDcbEventLogStorage.res +5 -1
  69. package/src/test/Mocks/MockEventLogStorage.res +8 -3
  70. package/src/test/Mocks/MockQueryDbStorage.res +3 -2
  71. package/src/test/TestRunner.res +1 -1
  72. package/tests/BakedManifestFilesTest.res +7 -10
  73. package/tests/LocalEventTapTest.res +26 -21
  74. package/tests/LocalPlatformStartTest.res +33 -28
  75. package/tests/PluginEventDecodeTest.res +3 -3
  76. package/tests/ShellConfigTest.res +30 -21
  77. package/tests/SplitApiFixtures.res +34 -10
  78. package/tests/SplitApiTest.res +92 -62
  79. package/tests/adapter/BackendParityTest.res +180 -130
  80. package/tests/adapter/CollectNEventsTest.res +6 -4
  81. package/tests/adapter/CommandAuthorizationTest.res +47 -41
  82. package/tests/adapter/CommandTopicChannelTest.res +163 -139
  83. package/tests/adapter/CounterHandlerTest.res +95 -77
  84. package/tests/adapter/DcbEventLogStorageSqliteTest.res +171 -168
  85. package/tests/adapter/DcbEventLogStorageTest.res +264 -207
  86. package/tests/adapter/DomainGraphQL_ServerTest.res +53 -43
  87. package/tests/adapter/EventCollectorChannelTest.res +153 -124
  88. package/tests/adapter/EventHistoryResolverTest.res +46 -18
  89. package/tests/adapter/EventLogSnapshotParityTest.res +57 -45
  90. package/tests/adapter/EventLogStorageSqliteTest.res +9 -10
  91. package/tests/adapter/EventTapTest.res +37 -20
  92. package/tests/adapter/EventTopicPublisherTest.res +74 -50
  93. package/tests/adapter/GraphQLSubscriptionBridgeTest.res +4 -3
  94. package/tests/adapter/GraphQL_SchemaInspectorTest.res +666 -568
  95. package/tests/adapter/GraphQL_SubscriptionResolversTest.res +105 -96
  96. package/tests/adapter/HeartbeatRunnerTest.res +99 -81
  97. package/tests/adapter/InboundTranslationMutationTest.res +4 -1
  98. package/tests/adapter/LocalAuthHttpTest.res +79 -85
  99. package/tests/adapter/LocalAuthLoginTest.res +42 -50
  100. package/tests/adapter/LocalAuthTest.res +8 -16
  101. package/tests/adapter/LocalAuthUserStoreTest.res +2 -7
  102. package/tests/adapter/LocalBusBoundedTest.res +187 -117
  103. package/tests/adapter/LocalBusPubSubTest.res +118 -76
  104. package/tests/adapter/LocalDispatchContextTest.res +1 -2
  105. package/tests/adapter/LocalEvents_ServerTest.res +242 -187
  106. package/tests/adapter/PgProjectionCatchupTest.res +44 -10
  107. package/tests/adapter/ProjectionCheckpointTest.res +261 -206
  108. package/tests/adapter/QueryDbGsiTtlTest.res +123 -83
  109. package/tests/adapter/QueryDbListPushdownParityTest.res +107 -67
  110. package/tests/adapter/QueryDbListResolverTest.res +225 -208
  111. package/tests/adapter/QueryDbResolvedFieldsTest.res +17 -14
  112. package/tests/adapter/QueryDbStorageSqliteTest.res +33 -49
  113. package/tests/adapter/QueryDbStorageTest.res +163 -117
  114. package/tests/adapter/QueryDbUnionRoundTripTest.res +8 -10
  115. package/tests/adapter/QueryEngineTest.res +179 -126
  116. package/tests/adapter/ScheduledPublisherTest.res +171 -135
  117. package/tests/adapter/ServedBucketHttpTest.res +65 -59
  118. package/tests/adapter/SortKey_FilterTest.res +19 -3
  119. package/tests/adapter/SqliteDriverTest.res +13 -10
  120. package/tests/adapter/TaskBucketTest.res +94 -80
  121. package/tests/components/aggregate/AggregateTest.res +31 -18
  122. package/tests/components/automationslice/AutomationSliceCallbackTest.res +259 -222
  123. package/tests/components/automationslice/AutomationSliceFixtures.res +4 -2
  124. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +38 -38
  125. package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +12 -14
  126. package/tests/components/automationslice/MixedSourceAutomationSliceTest.res +8 -32
  127. package/tests/components/commandgenerator/CommandGeneratorFixtures.res +7 -5
  128. package/tests/components/commandgenerator/CommandGeneratorTest.res +40 -31
  129. package/tests/components/commandtopic/CommandTopicFixtures.res +3 -4
  130. package/tests/components/commandtopic/CommandTopicStreamFixtures.res +7 -5
  131. package/tests/components/commandtopic/CommandTopicTest.res +24 -15
  132. package/tests/components/counter/CounterFixtures.res +6 -8
  133. package/tests/components/dcb/DcbCrossPartitionFixtures.res +8 -2
  134. package/tests/components/dcb/DcbCrossPartitionReadTest.res +20 -13
  135. package/tests/components/dcb/DcbEventLogAppendStreamTest.res +12 -8
  136. package/tests/components/dcb/DcbEventLogStreamTest.res +88 -61
  137. package/tests/components/dcb/DcbFixtures.res +4 -1
  138. package/tests/components/dcb/DcbTest.res +9 -6
  139. package/tests/components/eventcollector/EventCollectorFixtures.res +0 -1
  140. package/tests/components/eventcollector/EventCollectorTest.res +6 -3
  141. package/tests/components/eventlog/EventLogAppendStreamTest.res +88 -68
  142. package/tests/components/eventlog/EventLogConflictTest.res +8 -4
  143. package/tests/components/eventlog/EventLogFixtures.res +3 -3
  144. package/tests/components/eventlog/EventLogProvisioningSeamTest.res +26 -13
  145. package/tests/components/eventlog/EventLogStreamTest.res +119 -56
  146. package/tests/components/eventlog/EventLogTest.res +20 -4
  147. package/tests/components/eventtopic/EventTopicFixtures.res +3 -3
  148. package/tests/components/eventtopic/EventTopicStreamFixtures.res +1 -2
  149. package/tests/components/eventtopic/EventTopicStreamTest.res +7 -1
  150. package/tests/components/extensionpoint/ExtensionPointFixtures.res +14 -9
  151. package/tests/components/extensionpoint/ExtensionPointTest.res +3 -2
  152. package/tests/components/heartbeat/HeartbeatFixtures.res +5 -6
  153. package/tests/components/heartbeat/HeartbeatTest.res +56 -47
  154. package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res +271 -249
  155. package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res +3 -2
  156. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +361 -276
  157. package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res +8 -10
  158. package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res +37 -43
  159. package/tests/components/querydb/QueryDbFixtures.res +1 -5
  160. package/tests/components/querydb/QueryDbTest.res +2 -4
  161. package/tests/components/readmodel/DcbReadModelE2EFixtures.res +15 -18
  162. package/tests/components/readmodel/MixedSourceReadModelFixtures.res +5 -7
  163. package/tests/components/readmodel/ReadModelFixtures.res +8 -8
  164. package/tests/components/scheduler/SchedulerTest.res +76 -58
  165. package/tests/components/sideeffecthandler/SideEffectHandlerFixtures.res +4 -4
  166. package/tests/components/stateviewslice/StateViewSliceDisplayNameFixtures.res +5 -7
  167. package/tests/components/stateviewslice/StateViewSliceFixtures.res +8 -6
  168. package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res +17 -11
  169. package/tests/components/stateviewslice/StateViewSliceSubIdTest.res +12 -2
  170. package/tests/components/task/TaskFixtures.res +5 -5
  171. package/tests/components/task/TaskTest.res +95 -80
  172. package/tests/plugin/PluginsProjection_GWT.res +3 -1
  173. package/tests/plugin/UiFragments_GWT.res +6 -1
  174. package/tests/reset/LocalSeedResetTest.res +1 -0
@@ -5,10 +5,7 @@
5
5
  open JestGlobals
6
6
  open AutomationSliceFixtures
7
7
 
8
- module Callback = ReventlessCore.AutomationSlice_Callback.Make(
9
- ShipOrderSpec,
10
- ShipOrderAutomation,
11
- )
8
+ module Callback = ReventlessCore.AutomationSlice_Callback.Make(ShipOrderSpec, ShipOrderAutomation)
12
9
 
13
10
  module SkipCallback = ReventlessCore.AutomationSlice_Callback.Make(
14
11
  SkipProcessSpec,
@@ -28,155 +25,182 @@ describe("AutomationSlice Callback (mixed-source)", () => {
28
25
  })
29
26
 
30
27
  describe("Phase 1 — collect", () => {
31
- testPromise("OrderPlaced JSON creates a pending TODO item", async () => {
32
- let json = encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))
33
- Callback.phase1([json], testContext)
34
- let items = Callback.todoItems
35
- expect(items->Dict.toArray->Array.length)->toBe(1)
36
- let row = items->Dict.get("ord-1")->Option.getOrThrow
37
- expect(row.status)->toBe(Pending)
38
- expect(row.retryCount)->toBe(0)
39
- })
40
-
41
- testPromise("duplicate OrderPlaced is idempotent", async () => {
42
- let j1 = encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))
43
- let j2 = encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "456 Oak Ave"}))
44
- Callback.phase1([j1], testContext)
45
- Callback.phase1([j2], testContext)
46
- expect(Callback.todoItems->Dict.toArray->Array.length)->toBe(1)
47
- })
48
-
49
- testPromise("multiple OrderPlaced events create multiple TODO items", async () => {
50
- let j1 = encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))
51
- let j2 = encodeShipOrderEvent(OrderPlaced({orderId: "ord-2", address: "456 Oak Ave"}))
52
- Callback.phase1([j1, j2], testContext)
53
- expect(Callback.todoItems->Dict.toArray->Array.length)->toBe(2)
54
- })
55
-
56
- testPromise("ShipmentCreated does not create a TODO item", async () => {
57
- let json = encodeShipOrderEvent(ShipmentCreated({orderId: "ord-1"}))
58
- Callback.phase1([json], testContext)
59
- expect(Callback.todoItems->Dict.toArray->Array.length)->toBe(0)
60
- })
28
+ testPromise(
29
+ "OrderPlaced JSON creates a pending TODO item",
30
+ async () => {
31
+ let json = encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))
32
+ Callback.phase1([json], testContext)
33
+ let items = Callback.todoItems
34
+ expect(items->Dict.toArray->Array.length)->toBe(1)
35
+ let row = items->Dict.get("ord-1")->Option.getOrThrow
36
+ expect(row.status)->toBe(Pending)
37
+ expect(row.retryCount)->toBe(0)
38
+ },
39
+ )
40
+
41
+ testPromise(
42
+ "duplicate OrderPlaced is idempotent",
43
+ async () => {
44
+ let j1 = encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))
45
+ let j2 = encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "456 Oak Ave"}))
46
+ Callback.phase1([j1], testContext)
47
+ Callback.phase1([j2], testContext)
48
+ expect(Callback.todoItems->Dict.toArray->Array.length)->toBe(1)
49
+ },
50
+ )
51
+
52
+ testPromise(
53
+ "multiple OrderPlaced events create multiple TODO items",
54
+ async () => {
55
+ let j1 = encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))
56
+ let j2 = encodeShipOrderEvent(OrderPlaced({orderId: "ord-2", address: "456 Oak Ave"}))
57
+ Callback.phase1([j1, j2], testContext)
58
+ expect(Callback.todoItems->Dict.toArray->Array.length)->toBe(2)
59
+ },
60
+ )
61
+
62
+ testPromise(
63
+ "ShipmentCreated does not create a TODO item",
64
+ async () => {
65
+ let json = encodeShipOrderEvent(ShipmentCreated({orderId: "ord-1"}))
66
+ Callback.phase1([json], testContext)
67
+ expect(Callback.todoItems->Dict.toArray->Array.length)->toBe(0)
68
+ },
69
+ )
61
70
  })
62
71
 
63
72
  describe("Phase 1 — resolve", () => {
64
- testPromise("ShipmentCreated marks pending TODO item as completed", async () => {
65
- Callback.phase1(
66
- [encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))],
67
- testContext,
68
- )
69
- Callback.phase1(
70
- [encodeShipOrderEvent(ShipmentCreated({orderId: "ord-1"}))],
71
- testContext,
72
- )
73
- let row = Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow
74
- expect(row.status)->toBe(Completed)
75
- })
76
-
77
- testPromise("ShipmentCreated for unknown id is a no-op", async () => {
78
- Callback.phase1(
79
- [encodeShipOrderEvent(ShipmentCreated({orderId: "unknown"}))],
80
- testContext,
81
- )
82
- expect(Callback.todoItems->Dict.toArray->Array.length)->toBe(0)
83
- })
73
+ testPromise(
74
+ "ShipmentCreated marks pending TODO item as completed",
75
+ async () => {
76
+ Callback.phase1(
77
+ [encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))],
78
+ testContext,
79
+ )
80
+ Callback.phase1([encodeShipOrderEvent(ShipmentCreated({orderId: "ord-1"}))], testContext)
81
+ let row = Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow
82
+ expect(row.status)->toBe(Completed)
83
+ },
84
+ )
85
+
86
+ testPromise(
87
+ "ShipmentCreated for unknown id is a no-op",
88
+ async () => {
89
+ Callback.phase1([encodeShipOrderEvent(ShipmentCreated({orderId: "unknown"}))], testContext)
90
+ expect(Callback.todoItems->Dict.toArray->Array.length)->toBe(0)
91
+ },
92
+ )
84
93
  })
85
94
 
86
95
  describe("Phase 2 — process", () => {
87
- testPromise("pending items produce commands via publishJsons", async () => {
88
- Callback.phase1(
89
- [encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))],
90
- testContext,
91
- )
92
- let publishedCommands = ref([])
93
- let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
94
- publishedCommands := cmds
95
- }
96
- await Callback.phase2(mockPublish)
97
-
98
- expect(publishedCommands.contents->Array.length)->toBe(1)
99
- let cmd = publishedCommands.contents->Array.getUnsafe(0)
100
- expect(cmd.id)->toBe("ord-1")
101
-
102
- let row = Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow
103
- expect(row.status)->toBe(Processing)
104
- })
105
-
106
- testPromise("completed items are not processed", async () => {
107
- Callback.phase1(
108
- [encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))],
109
- testContext,
110
- )
111
- Callback.phase1(
112
- [encodeShipOrderEvent(ShipmentCreated({orderId: "ord-1"}))],
113
- testContext,
114
- )
115
- let publishedCommands = ref([])
116
- let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
117
- publishedCommands := cmds
118
- }
119
- await Callback.phase2(mockPublish)
120
- expect(publishedCommands.contents->Array.length)->toBe(0)
121
- })
122
-
123
- testPromise("items where process returns None are skipped", async () => {
124
- let json = encodeSkipProcessEvent(OrderPlaced({orderId: "ord-1"}))
125
- SkipCallback.phase1([json], testContext)
126
- let publishedCommands = ref([])
127
- let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
128
- publishedCommands := cmds
129
- }
130
- await SkipCallback.phase2(mockPublish)
131
- expect(publishedCommands.contents->Array.length)->toBe(0)
132
- let row = SkipCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
133
- expect(row.status)->toBe(Pending)
134
- })
135
-
136
- testPromise("empty TODO list produces no commands", async () => {
137
- let publishedCommands = ref([])
138
- let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
139
- publishedCommands := cmds
140
- }
141
- await Callback.phase2(mockPublish)
142
- expect(publishedCommands.contents->Array.length)->toBe(0)
143
- })
144
-
145
- testPromise("publishJsons failure marks items as Failed", async () => {
146
- Callback.phase1(
147
- [encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))],
148
- testContext,
149
- )
150
- let failingPublish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => {
151
- JsError.throwWithMessage("publish failed")
152
- }
153
- await Callback.phase2(failingPublish)
154
- let row = Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow
155
- expect(row.status)->toBe(Failed)
156
- expect(row.retryCount)->toBe(1)
157
- })
158
-
159
- testPromise("failed items with retryCount < maxRetries are retried", async () => {
160
- Callback.phase1(
161
- [encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))],
162
- testContext,
163
- )
164
- let failingPublish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => {
165
- JsError.throwWithMessage("publish failed")
166
- }
167
- await Callback.phase2(failingPublish)
168
- let row1 = Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow
169
- expect(row1.retryCount)->toBe(1)
170
-
171
- let publishedCommands = ref([])
172
- let successPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
173
- publishedCommands := cmds
174
- }
175
- await Callback.phase2(successPublish)
176
- expect(publishedCommands.contents->Array.length)->toBe(1)
177
- let row2 = Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow
178
- expect(row2.status)->toBe(Processing)
179
- })
96
+ testPromise(
97
+ "pending items produce commands via publishJsons",
98
+ async () => {
99
+ Callback.phase1(
100
+ [encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))],
101
+ testContext,
102
+ )
103
+ let publishedCommands = ref([])
104
+ let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
105
+ publishedCommands := cmds
106
+ }
107
+ await Callback.phase2(mockPublish)
108
+
109
+ expect(publishedCommands.contents->Array.length)->toBe(1)
110
+ let cmd = publishedCommands.contents->Array.getUnsafe(0)
111
+ expect(cmd.id)->toBe("ord-1")
112
+
113
+ let row = Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow
114
+ expect(row.status)->toBe(Processing)
115
+ },
116
+ )
117
+
118
+ testPromise(
119
+ "completed items are not processed",
120
+ async () => {
121
+ Callback.phase1(
122
+ [encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))],
123
+ testContext,
124
+ )
125
+ Callback.phase1([encodeShipOrderEvent(ShipmentCreated({orderId: "ord-1"}))], testContext)
126
+ let publishedCommands = ref([])
127
+ let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
128
+ publishedCommands := cmds
129
+ }
130
+ await Callback.phase2(mockPublish)
131
+ expect(publishedCommands.contents->Array.length)->toBe(0)
132
+ },
133
+ )
134
+
135
+ testPromise(
136
+ "items where process returns None are skipped",
137
+ async () => {
138
+ let json = encodeSkipProcessEvent(OrderPlaced({orderId: "ord-1"}))
139
+ SkipCallback.phase1([json], testContext)
140
+ let publishedCommands = ref([])
141
+ let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
142
+ publishedCommands := cmds
143
+ }
144
+ await SkipCallback.phase2(mockPublish)
145
+ expect(publishedCommands.contents->Array.length)->toBe(0)
146
+ let row = SkipCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
147
+ expect(row.status)->toBe(Pending)
148
+ },
149
+ )
150
+
151
+ testPromise(
152
+ "empty TODO list produces no commands",
153
+ async () => {
154
+ let publishedCommands = ref([])
155
+ let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
156
+ publishedCommands := cmds
157
+ }
158
+ await Callback.phase2(mockPublish)
159
+ expect(publishedCommands.contents->Array.length)->toBe(0)
160
+ },
161
+ )
162
+
163
+ testPromise(
164
+ "publishJsons failure marks items as Failed",
165
+ async () => {
166
+ Callback.phase1(
167
+ [encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))],
168
+ testContext,
169
+ )
170
+ let failingPublish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => {
171
+ JsError.throwWithMessage("publish failed")
172
+ }
173
+ await Callback.phase2(failingPublish)
174
+ let row = Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow
175
+ expect(row.status)->toBe(Failed)
176
+ expect(row.retryCount)->toBe(1)
177
+ },
178
+ )
179
+
180
+ testPromise(
181
+ "failed items with retryCount < maxRetries are retried",
182
+ async () => {
183
+ Callback.phase1(
184
+ [encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))],
185
+ testContext,
186
+ )
187
+ let failingPublish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => {
188
+ JsError.throwWithMessage("publish failed")
189
+ }
190
+ await Callback.phase2(failingPublish)
191
+ let row1 = Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow
192
+ expect(row1.retryCount)->toBe(1)
193
+
194
+ let publishedCommands = ref([])
195
+ let successPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
196
+ publishedCommands := cmds
197
+ }
198
+ await Callback.phase2(successPublish)
199
+ expect(publishedCommands.contents->Array.length)->toBe(1)
200
+ let row2 = Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow
201
+ expect(row2.status)->toBe(Processing)
202
+ },
203
+ )
180
204
  })
181
205
 
182
206
  // A row used to leave the retry budget by falling out of phase2's filter, so
@@ -194,83 +218,96 @@ describe("AutomationSlice Callback (mixed-source)", () => {
194
218
 
195
219
  let statusOf = id => (Callback.todoItems->Dict.get(id)->Option.getOrThrow).status
196
220
 
197
- testPromise("the attempt that spends the budget writes Abandoned, not Failed", async () => {
198
- collectOne()
199
- // ShipOrderSpec.maxRetries is 3: attempts at retryCount 0, 1 and 2 leave the
200
- // row retriable, and the third failure is the one that ends it.
201
- await Callback.phase2(failingPublish)
202
- expect(statusOf("ord-1"))->toBe(Failed)
203
- await Callback.phase2(failingPublish)
204
- expect(statusOf("ord-1"))->toBe(Failed)
205
- await Callback.phase2(failingPublish)
206
- expect(statusOf("ord-1"))->toBe(Abandoned)
207
- expect((Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow).retryCount)->toBe(3)
208
- })
209
-
210
- testPromise("an abandoned row is not picked up again", async () => {
211
- collectOne()
212
- await Callback.phase2(failingPublish)
213
- await Callback.phase2(failingPublish)
214
- await Callback.phase2(failingPublish)
215
- expect(statusOf("ord-1"))->toBe(Abandoned)
216
-
217
- // A pass that would have succeeded: the row must still not be attempted,
218
- // which is what makes the status a decision rather than a label.
219
- let published = ref([])
220
- await Callback.phase2(async cmds => published := cmds)
221
- expect(published.contents->Array.length)->toBe(0)
222
- expect(statusOf("ord-1"))->toBe(Abandoned)
223
- expect((Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow).retryCount)->toBe(3)
224
- })
225
-
226
- testPromise("a row left stranded at the ceiling is normalised on the next pass", async () => {
227
- collectOne()
228
- // What an older build wrote, and what a Spec that lowered maxRetries leaves
229
- // behind: Failed at the ceiling, which no pass would ever pick up or mark.
230
- let row = Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow
231
- Callback.todoItems->Dict.set("ord-1", {...row, status: Failed, retryCount: 3})
232
-
233
- let published = ref([])
234
- await Callback.phase2(async cmds => published := cmds)
235
- expect(published.contents->Array.length)->toBe(0)
236
- expect(statusOf("ord-1"))->toBe(Abandoned)
237
- })
238
-
239
- testPromise("the row carries the ceiling its count is racing", async () => {
240
- collectOne()
241
- expect((Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow).maxRetries)
242
- ->toEqual(Some(ShipOrderSpec.maxRetries))
243
- })
221
+ testPromise(
222
+ "the attempt that spends the budget writes Abandoned, not Failed",
223
+ async () => {
224
+ collectOne()
225
+ // ShipOrderSpec.maxRetries is 3: attempts at retryCount 0, 1 and 2 leave the
226
+ // row retriable, and the third failure is the one that ends it.
227
+ await Callback.phase2(failingPublish)
228
+ expect(statusOf("ord-1"))->toBe(Failed)
229
+ await Callback.phase2(failingPublish)
230
+ expect(statusOf("ord-1"))->toBe(Failed)
231
+ await Callback.phase2(failingPublish)
232
+ expect(statusOf("ord-1"))->toBe(Abandoned)
233
+ expect((Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow).retryCount)->toBe(3)
234
+ },
235
+ )
236
+
237
+ testPromise(
238
+ "an abandoned row is not picked up again",
239
+ async () => {
240
+ collectOne()
241
+ await Callback.phase2(failingPublish)
242
+ await Callback.phase2(failingPublish)
243
+ await Callback.phase2(failingPublish)
244
+ expect(statusOf("ord-1"))->toBe(Abandoned)
245
+
246
+ // A pass that would have succeeded: the row must still not be attempted,
247
+ // which is what makes the status a decision rather than a label.
248
+ let published = ref([])
249
+ await Callback.phase2(async cmds => published := cmds)
250
+ expect(published.contents->Array.length)->toBe(0)
251
+ expect(statusOf("ord-1"))->toBe(Abandoned)
252
+ expect((Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow).retryCount)->toBe(3)
253
+ },
254
+ )
255
+
256
+ testPromise(
257
+ "a row left stranded at the ceiling is normalised on the next pass",
258
+ async () => {
259
+ collectOne()
260
+ // What an older build wrote, and what a Spec that lowered maxRetries leaves
261
+ // behind: Failed at the ceiling, which no pass would ever pick up or mark.
262
+ let row = Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow
263
+ Callback.todoItems->Dict.set("ord-1", {...row, status: Failed, retryCount: 3})
264
+
265
+ let published = ref([])
266
+ await Callback.phase2(async cmds => published := cmds)
267
+ expect(published.contents->Array.length)->toBe(0)
268
+ expect(statusOf("ord-1"))->toBe(Abandoned)
269
+ },
270
+ )
271
+
272
+ testPromise(
273
+ "the row carries the ceiling its count is racing",
274
+ async () => {
275
+ collectOne()
276
+ expect((Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow).maxRetries)->toEqual(
277
+ Some(ShipOrderSpec.maxRetries),
278
+ )
279
+ },
280
+ )
244
281
  })
245
282
 
246
283
  describe("full lifecycle", () => {
247
- testPromise("collect → process → resolve completes the TODO item", async () => {
248
- Callback.phase1(
249
- [encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))],
250
- testContext,
251
- )
252
- expect((Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)->toBe(Pending)
253
-
254
- let publishedCommands = ref([])
255
- let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
256
- publishedCommands := cmds
257
- }
258
- await Callback.phase2(mockPublish)
259
- expect(publishedCommands.contents->Array.length)->toBe(1)
260
- expect((Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)->toBe(Processing)
261
-
262
- Callback.phase1(
263
- [encodeShipOrderEvent(ShipmentCreated({orderId: "ord-1"}))],
264
- testContext,
265
- )
266
- expect((Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)->toBe(Completed)
267
-
268
- let publishedCommands2 = ref([])
269
- let mockPublish2: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
270
- publishedCommands2 := cmds
271
- }
272
- await Callback.phase2(mockPublish2)
273
- expect(publishedCommands2.contents->Array.length)->toBe(0)
274
- })
284
+ testPromise(
285
+ "collect → process → resolve completes the TODO item",
286
+ async () => {
287
+ Callback.phase1(
288
+ [encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))],
289
+ testContext,
290
+ )
291
+ expect((Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)->toBe(Pending)
292
+
293
+ let publishedCommands = ref([])
294
+ let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
295
+ publishedCommands := cmds
296
+ }
297
+ await Callback.phase2(mockPublish)
298
+ expect(publishedCommands.contents->Array.length)->toBe(1)
299
+ expect((Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)->toBe(Processing)
300
+
301
+ Callback.phase1([encodeShipOrderEvent(ShipmentCreated({orderId: "ord-1"}))], testContext)
302
+ expect((Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)->toBe(Completed)
303
+
304
+ let publishedCommands2 = ref([])
305
+ let mockPublish2: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
306
+ publishedCommands2 := cmds
307
+ }
308
+ await Callback.phase2(mockPublish2)
309
+ expect(publishedCommands2.contents->Array.length)->toBe(0)
310
+ },
311
+ )
275
312
  })
276
313
  })
@@ -106,8 +106,10 @@ module SkipProcessMapping = Reventless.AutomationSlice.Mapping.Make(
106
106
 
107
107
  module ShipOrderAutomation: Reventless.AutomationSlice.Automation
108
108
  with module Spec := ShipOrderSpec = {
109
- let process = (id, _item: ShipOrderSpec.todoItem) =>
110
- Some((id, ShipOrderSpec.CreateShipment({orderId: id})))
109
+ let process = (id, _item: ShipOrderSpec.todoItem) => Some((
110
+ id,
111
+ ShipOrderSpec.CreateShipment({orderId: id}),
112
+ ))
111
113
  let onExhausted = (_id, _item: ShipOrderSpec.todoItem) => None
112
114
  let moduleUrl: string = %raw(`import.meta.url`)
113
115
  module M = Reventless.AutomationSlice.Mappings.Make(ShipOrderSpec)
@@ -156,8 +156,7 @@ module FromDcb = AutomationSlice.Mapping.Make(
156
156
  )
157
157
 
158
158
  module AutoShipAutomation: AutomationSlice.Automation with module Spec := AutoShipSpec = {
159
- let process = (id, _item: AutoShipSpec.todoItem) =>
160
- Some((id, AutoShipSpec.Ship({orderId: id})))
159
+ let process = (id, _item: AutoShipSpec.todoItem) => Some((id, AutoShipSpec.Ship({orderId: id})))
161
160
  let onExhausted = (_id, _item: AutoShipSpec.todoItem) => None
162
161
  let moduleUrl: string = %raw(`import.meta.url`)
163
162
  module M = AutomationSlice.Mappings.Make(AutoShipSpec)
@@ -189,42 +188,40 @@ let dcbEventLog = DcbLogMaker.make(
189
188
  // ─────────────────────────────────────────────────────────────
190
189
 
191
190
  let publishJsons: ReventlessInfra.CommandTopic.publishJsons = async cmdJsons => {
192
- let _ =
193
- await cmdJsons
194
- ->Array.map(async cmdJson => {
195
- let typeName = switch cmdJson.commandJson {
196
- | JSON.Object(dict) =>
197
- dict
198
- ->Dict.get("TAG")
199
- ->Option.flatMap(j =>
200
- switch j {
201
- | JSON.String(s) => Some(s)
202
- | _ => None
203
- }
204
- )
205
- ->Option.getOr("")
206
- | _ => ""
207
- }
208
- let fullBody = JSON.Encode.object(
209
- Dict.fromArray([
210
- ("id", JSON.Encode.string(cmdJson.id)),
211
- ("meta", cmdJson.meta->Reventless.Util_Sury.toJson(Reventless.Message.metaSchema)),
212
- ("command", cmdJson.commandJson),
213
- ]),
191
+ let _ = await cmdJsons
192
+ ->Array.map(async cmdJson => {
193
+ let typeName = switch cmdJson.commandJson {
194
+ | JSON.Object(dict) =>
195
+ dict
196
+ ->Dict.get("TAG")
197
+ ->Option.flatMap(j =>
198
+ switch j {
199
+ | JSON.String(s) => Some(s)
200
+ | _ => None
201
+ }
214
202
  )
215
- let handlers = ReventlessCore.CommandTopic.getHandlers(typeName)
216
- let _ =
217
- await handlers
218
- ->Array.map(async entry => {
219
- let item: ReventlessInfra.CommandTopic.topicItem<JSON.t> = {
220
- reference: cmdJson.id,
221
- command: fullBody,
222
- }
223
- let _ = await entry.handler(Stream.fromIterable([item]))->Effect.runPromise
224
- })
225
- ->Promise.all
203
+ ->Option.getOr("")
204
+ | _ => ""
205
+ }
206
+ let fullBody = JSON.Encode.object(
207
+ Dict.fromArray([
208
+ ("id", JSON.Encode.string(cmdJson.id)),
209
+ ("meta", cmdJson.meta->Reventless.Util_Sury.toJson(Reventless.Message.metaSchema)),
210
+ ("command", cmdJson.commandJson),
211
+ ]),
212
+ )
213
+ let handlers = ReventlessCore.CommandTopic.getHandlers(typeName)
214
+ let _ = await handlers
215
+ ->Array.map(async entry => {
216
+ let item: ReventlessInfra.CommandTopic.topicItem<JSON.t> = {
217
+ reference: cmdJson.id,
218
+ command: fullBody,
219
+ }
220
+ let _ = await entry.handler(Stream.fromIterable([item]))->Effect.runPromise
226
221
  })
227
222
  ->Promise.all
223
+ })
224
+ ->Promise.all
228
225
  }
229
226
 
230
227
  let publishJsonsOutput = publishJsons->Pulumi.Output.make
@@ -239,7 +236,9 @@ let _placeSlice = PlaceMaker.make(~dcbEventLog, ~publishJsons=publishJsonsOutput
239
236
  module ShipMaker = StateChangeSlice_Builder.Make(ShipSpec, ShipBehavior)
240
237
  let _shipSlice = ShipMaker.make(~dcbEventLog, ~publishJsons=publishJsonsOutput)
241
238
 
242
- let dcbTopicOutputs: ReventlessInfra.EventTopic.outputs = (dcbEventLog->ReventlessInfra.Component.outputs).eventTopic
239
+ let dcbTopicOutputs: ReventlessInfra.EventTopic.outputs = (
240
+ dcbEventLog->ReventlessInfra.Component.outputs
241
+ ).eventTopic
243
242
  let allEventTopics: ReventlessInfra.EventTopic.allOutputs = Dict.fromArray([
244
243
  (DcbSource.name, dcbTopicOutputs),
245
244
  ])
@@ -266,8 +265,9 @@ let autoShipSlice = AutoShip.make(
266
265
  let placeCmdJson = (orderId: string): Reventless.Message.commandJson => {
267
266
  id: orderId,
268
267
  meta: testMeta,
269
- commandJson: PlaceSpec.Place({orderId: orderId})
270
- ->Reventless.Util_Sury.toJson(PlaceSpec.commandSchema),
268
+ commandJson: PlaceSpec.Place({orderId: orderId})->Reventless.Util_Sury.toJson(
269
+ PlaceSpec.commandSchema,
270
+ ),
271
271
  }
272
272
 
273
273
  // Promise-based timeout used to fail fast if the chain self-deadlocks.