@reventlessdev/reventless-core 3.0.0-alpha.259 → 3.0.0-alpha.261

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 (209) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +11 -11
  3. package/src/Behavior.res +1 -4
  4. package/src/Message.res +17 -14
  5. package/src/Projection.res +51 -25
  6. package/src/RequestContext.res +8 -9
  7. package/src/ResourceAttribution.res +28 -14
  8. package/src/adapter/Adapter.res +48 -4
  9. package/src/adapter/EventLogProvisioning/EventLogProvisioning.res +5 -5
  10. package/src/adapter/Messaging/Messaging_Log_Backend.res +10 -7
  11. package/src/adapter/Messaging/Messaging_Log_Backend.res.mjs +2 -4
  12. package/src/adapter/Monitoring/Monitoring.res +16 -17
  13. package/src/adapter/QueryInterception/QueryInterception.res +0 -1
  14. package/src/adapter/Runtime/AggregateRuntime_Builder_Common.res +12 -5
  15. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +11 -7
  16. package/src/adapter/Runtime/PluginRuntime_Builder_Micro.res +1 -7
  17. package/src/adapter/Runtime/PluginRuntime_Builder_Single.res +1 -7
  18. package/src/adapter/RuntimeExtension/RuntimeExtension.res +0 -1
  19. package/src/admin/Platform_Admin.res +17 -6
  20. package/src/admin/Platform_AdminApi.res +14 -9
  21. package/src/admin/Platform_Admin_Callback.res +1 -1
  22. package/src/admin/Platform_BakedManifest.res +34 -34
  23. package/src/admin/Platform_ComponentDefinitionsApi.res +9 -21
  24. package/src/admin/Platform_PluginStructuresApi.res +15 -12
  25. package/src/admin/UiFragmentRegistry/StateChange/UiFragmentRegistry.res +1 -1
  26. package/src/components/Aggregate/Aggregate.res +13 -12
  27. package/src/components/Aggregate/Aggregate_Builder.res +22 -21
  28. package/src/components/Aggregate/Aggregate_Callback.res +91 -86
  29. package/src/components/Api/ApiNoApiHelpers.res +1 -3
  30. package/src/components/Api/Api_Ids.res +0 -1
  31. package/src/components/Api/Api_Naming.res +41 -18
  32. package/src/components/Api/GraphQL_FragmentGenerator.res +42 -52
  33. package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +11 -4
  34. package/src/components/Api/GraphQL_SchemaInspector.res +5 -3
  35. package/src/components/Api/GraphQL_Stitcher.res +9 -10
  36. package/src/components/Api/MCP_SchemaGenerator.res +8 -12
  37. package/src/components/Api/OwnerScopeDiagnostics.res +1 -2
  38. package/src/components/Api/QueryDbListQuery.res +13 -6
  39. package/src/components/Api/SchemaType.res +40 -20
  40. package/src/components/Api/SchemaType.res.mjs +16 -0
  41. package/src/components/Api/SuryToJsonSchema.res +18 -10
  42. package/src/components/Api/SuryToJsonSchema.res.mjs +135 -121
  43. package/src/components/AutomationSlice/AutomationSlice.res +4 -4
  44. package/src/components/AutomationSlice/AutomationSlice_Builder.res +15 -14
  45. package/src/components/AutomationSlice/AutomationSlice_Callback.res +3 -3
  46. package/src/components/Builder_Helpers.res +12 -23
  47. package/src/components/CommandGenerator/CommandGenerator.res +6 -1
  48. package/src/components/CommandGenerator/CommandGenerator_Builder.res +29 -21
  49. package/src/components/CommandGenerator/CommandGenerator_Callback.res +17 -21
  50. package/src/components/CommandTopic/CommandTopic.res +3 -3
  51. package/src/components/CommandTopic/CommandTopic_Builder.res +10 -10
  52. package/src/components/CommandTopic/CommandTopic_Callback.res +1 -4
  53. package/src/components/CommandTopic/CommandTopic_Helpers.res +1 -2
  54. package/src/components/Counter/Counter_Builder.res +1 -6
  55. package/src/components/Counter/Counter_Callback.res +3 -3
  56. package/src/components/Dcb/Dcb_Builder.res +970 -956
  57. package/src/components/DcbEventLog/DcbEventLog.res +4 -4
  58. package/src/components/DcbEventLog/DcbEventLog_Builder.res +0 -1
  59. package/src/components/DcbEventLog/DcbEventLog_Operations.res +17 -18
  60. package/src/components/EventCollector/EventCollector_Builder.res +2 -1
  61. package/src/components/EventLog/EventLog.res +5 -1
  62. package/src/components/EventLog/EventLog_Operations.res +12 -8
  63. package/src/components/EventMapper/EventMapper.res +8 -10
  64. package/src/components/EventMapper/EventMapper_Builder.res +4 -1
  65. package/src/components/EventMapper/EventMapper_Callback.res +46 -31
  66. package/src/components/EventTopic/EventTopic.res +3 -3
  67. package/src/components/EventTopic/EventTopic_Builder.res +1 -1
  68. package/src/components/Extension/Extension.res +4 -1
  69. package/src/components/Extension/Extension_Operations.res +11 -3
  70. package/src/components/ExtensionPoint/ExtensionPoint.res +3 -3
  71. package/src/components/ExtensionPoint/ExtensionPoint_Builder.res +9 -10
  72. package/src/components/ExtensionPoint/ExtensionPoint_Callback.res +2 -6
  73. package/src/components/ExtensionPoint/ExtensionPoint_Operations.res +13 -5
  74. package/src/components/Heartbeat/Heartbeat_Callback.res +3 -3
  75. package/src/components/InboundTranslationSlice/InboundTranslationSlice.res +4 -4
  76. package/src/components/InboundTranslationSlice/InboundTranslationSlice_Builder.res +7 -10
  77. package/src/components/OutboundTranslationSlice/OutboundTranslationSlice.res +4 -4
  78. package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Builder.res +15 -14
  79. package/src/components/QueryDb/QueryDb_Adapter.res +1 -3
  80. package/src/components/QueryDb/QueryDb_Operations.res +10 -10
  81. package/src/components/ReadModel/ReadModel.res +3 -3
  82. package/src/components/ReadModel/ReadModel_Builder.res +1 -2
  83. package/src/components/ReadModel/ReadModel_Callback.res +2 -1
  84. package/src/components/SideEffectHandler/SideEffectHandler_Callback.res +5 -2
  85. package/src/components/StateChangeSlice/StateChangeSlice.res +3 -3
  86. package/src/components/StateChangeSlice/StateChangeSlice_Builder.res +23 -4
  87. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +22 -6
  88. package/src/components/StateTopic.res +1 -3
  89. package/src/components/StateViewSlice/StateViewSlice.res +4 -4
  90. package/src/components/StateViewSlice/StateViewSlice_Builder.res +79 -68
  91. package/src/components/StateViewSlice/StateViewSlice_Callback.res +3 -1
  92. package/src/components/Task/Task.res +3 -3
  93. package/src/components/Task/Task_Adapter.res +4 -1
  94. package/src/components/Task/Task_Builder.res +24 -21
  95. package/src/plugin/api/PluginBaseFragment.res +9 -3
  96. package/src/plugin/component/Capability_Inference.res +8 -10
  97. package/src/plugin/component/Plugin.res +6 -6
  98. package/src/plugin/component/Plugin_Builder.res +54 -50
  99. package/src/plugin/component/Plugin_Callback.res +10 -6
  100. package/src/plugin/component/Plugin_Helpers.res +264 -230
  101. package/src/plugin/component/Plugin_Structure.res +370 -324
  102. package/src/plugin/component/Plugin_SubscriptionSchema.res +8 -6
  103. package/src/plugin/component/SchemaWalker.res +11 -12
  104. package/src/plugin/connect/PluginConnectExtension_Mapping.res +45 -50
  105. package/src/plugin/lifecycle/PluginBehavior.res +3 -4
  106. package/src/plugin/lifecycle/PluginSpec.res +1 -2
  107. package/src/plugin/lifecycle/PluginsProjection.res +16 -5
  108. package/src/plugin/lifecycle/PluginsReadModelSpec.res +0 -2
  109. package/src/util/CommandPublisher.res +9 -2
  110. package/src/util/CsvStream.res +3 -3
  111. package/src/util/Interstack.res +7 -15
  112. package/src/util/Interstack.resi +3 -3
  113. package/src/util/LogFormat.res +5 -9
  114. package/src/util/Logger.res +1 -2
  115. package/src/util/Lru.res +2 -1
  116. package/src/util/Lru.resi +2 -3
  117. package/src/util/ScheduleOps.res +24 -27
  118. package/src/util/StorePrefixCollision.res +6 -9
  119. package/src/util/Util_Promise.res +4 -1
  120. package/src/util/Util_Promise.res.mjs +1 -1
  121. package/tests/IdentityTest.res +166 -113
  122. package/tests/ResourceAttributionTest.res +28 -25
  123. package/tests/StorePrefixCollisionTest.res +2 -1
  124. package/tests/TaskBucketNamingTest.res +3 -2
  125. package/tests/adapter/EventLogProvisioningTest.res +51 -48
  126. package/tests/adapter/MonitoringTest.res +81 -38
  127. package/tests/adapter/QueryInterceptionTest.res +2 -2
  128. package/tests/adapter/RefusalVocabularyTest.res +31 -23
  129. package/tests/adapter/RuntimeExtensionHookReachTest.res +9 -8
  130. package/tests/adapter/RuntimeExtensionTest.res +5 -3
  131. package/tests/admin/AdminApiSchemaDriftTest.res +22 -16
  132. package/tests/admin/Platform_Admin_StructureTest.res +10 -11
  133. package/tests/admin/Platform_BakedManifestTest.res +31 -35
  134. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +18 -15
  135. package/tests/admin/Platform_PluginStructuresApiTest.res +5 -3
  136. package/tests/admin/Platform_UIFragmentsApiTest.res +6 -3
  137. package/tests/aggregate/AggregateCacheTest.res +10 -12
  138. package/tests/aggregate/AggregateCallbackTest.res +75 -46
  139. package/tests/aggregate/AggregateCausationTest.res +19 -20
  140. package/tests/aggregate/AggregateConflictTest.res +41 -19
  141. package/tests/aggregate/AggregateFixtures.res +15 -5
  142. package/tests/aggregate/AggregateRejectionTest.res +97 -80
  143. package/tests/aggregate/AggregateSnapshotTest.res +44 -16
  144. package/tests/api/Api_IdsTest.res +10 -4
  145. package/tests/api/BrandedMarkerCompositionTest.res +48 -36
  146. package/tests/api/GeolocationSemanticTest.res +3 -3
  147. package/tests/api/GraphQL_FragmentGeneratorTest.res +66 -54
  148. package/tests/api/OwnerScopeDiagnosticsTest.res +31 -27
  149. package/tests/api/StorageRefFieldsTest.res +30 -18
  150. package/tests/api/SuryToJsonSchemaTest.res +1338 -981
  151. package/tests/api/SuryToJsonSchemaTest.res.mjs +31 -0
  152. package/tests/api/TaggedUnionSchemaTest.res +17 -24
  153. package/tests/commandgenerator/CommandGeneratorCallbackTest.res +235 -179
  154. package/tests/commandgenerator/CommandGeneratorFixtures.res +8 -13
  155. package/tests/commandgenerator/OwnerStampingTest.res +91 -76
  156. package/tests/commandtopic/CommandOutcomeHookTest.res +198 -155
  157. package/tests/commandtopic/CommandTopicCallbackFixtures.res +5 -4
  158. package/tests/commandtopic/CommandTopicCallbackTest.res +61 -43
  159. package/tests/commandtopic/CommandTopicHelpersRejectionTest.res +70 -55
  160. package/tests/counter/CounterCallbackTest.res +40 -34
  161. package/tests/counter/CounterFixtures.res +4 -4
  162. package/tests/dcb/DcbDecodeTest.res +115 -84
  163. package/tests/dcb/DcbEventLogOperationsTest.res +194 -143
  164. package/tests/dcb/DcbEventLogSchemaTest.res +13 -13
  165. package/tests/dcb/DcbFixtures.res +4 -5
  166. package/tests/dcb/DcbScopeInferenceTest.res +173 -114
  167. package/tests/dcb/DcbStateChangeSliceTest.res +391 -262
  168. package/tests/dcb/DcbTagTest.res +22 -11
  169. package/tests/dcb/DcbValidationTest.res +288 -218
  170. package/tests/eventlog/EventLogFixtures.res +4 -4
  171. package/tests/eventlog/EventLogOperationsTest.res +102 -67
  172. package/tests/eventlog/EventLogRetryTest.res +104 -65
  173. package/tests/eventmapper/EventMapperCallbackTest.res +118 -83
  174. package/tests/eventmapper/EventMapperFixtures.res +15 -23
  175. package/tests/eventmapper/EventMapperTest.res +54 -38
  176. package/tests/eventtopic/EventTopicFixtures.res +3 -4
  177. package/tests/eventtopic/EventTopicOperationsTest.res +41 -29
  178. package/tests/extensionpoint/ExtensionPointCallbackTest.res +58 -45
  179. package/tests/extensionpoint/ExtensionPointFixtures.res +16 -20
  180. package/tests/extensionpoint/ExtensionPointOperationsTest.res +70 -49
  181. package/tests/extensionpoint/PluginDisconnectScheduleTest.res +2 -1
  182. package/tests/logger/LogFormatTest.res +104 -82
  183. package/tests/logger/LoggerLazyTest.res +21 -12
  184. package/tests/message/MessageTest.res +96 -93
  185. package/tests/message/MetaEnvelopeTest.res +26 -30
  186. package/tests/plugin/HeartbeatDisconnectGraceTest.res +31 -25
  187. package/tests/plugin/ManifestVisibilityTest.res +20 -12
  188. package/tests/plugin/PluginDefinitionScalars.res +1 -2
  189. package/tests/plugin/PluginLifecycleCorpusTest.res +15 -12
  190. package/tests/plugin/PluginSpecExperiment.res +0 -1
  191. package/tests/plugin/PluginStructureTest.res +1533 -1030
  192. package/tests/plugin/PluginStructureTest.res.mjs +6 -0
  193. package/tests/plugin/Plugin_EventQuerySchemaTest.res +21 -10
  194. package/tests/plugin/SchemaWalkerTest.res +116 -70
  195. package/tests/plugin/StateChange/PsReserveStock.res +4 -4
  196. package/tests/plugin/StateView/PsAnnotatedView.res +6 -4
  197. package/tests/plugin/StateView/PsAnnotatedView.res.mjs +7 -3
  198. package/tests/plugin/StateView/PsCustomersView.res +4 -4
  199. package/tests/plugin/StateView/PsShipmentsView.res +3 -2
  200. package/tests/projection/LifecycleTrailTest.res +1 -4
  201. package/tests/projection/ProjectionOptimizeTest.res +13 -9
  202. package/tests/querydb/QueryDbFixtures.res +0 -1
  203. package/tests/querydb/QueryDbOperationsTest.res +99 -64
  204. package/tests/querydb/TaggedUnionStampTest.res +10 -8
  205. package/tests/sideeffecthandler/SideEffectHandlerCallbackTest.res +50 -35
  206. package/tests/sideeffecthandler/SideEffectHandlerFixtures.res +4 -10
  207. package/tests/util/CommandPublisherTest.res +0 -1
  208. package/tests/util/CsvStreamTest.res +3 -3
  209. package/tests/util/LruTest.res +84 -56
@@ -1,6 +1,5 @@
1
1
  open JestGlobals
2
2
 
3
-
4
3
  // Driven through `makeGenerateCommand` rather than by calling `stampOwnerFields`
5
4
  // directly. The unit under test is not really the stamp — it is the claim that
6
5
  // the stamp is reached on the path a command actually takes, which is the half a
@@ -16,7 +15,10 @@ type command =
16
15
  })
17
16
  | // The control variant: same union, no owner. Whatever the caller sends here
18
17
  // must survive untouched, or the stamp is reaching across constructors.
19
- ImportProducts({batchId: @s.matches(Reventless.DcbTag.string) string, source: string})
18
+ ImportProducts({
19
+ batchId: @s.matches(Reventless.DcbTag.string) string,
20
+ source: string,
21
+ })
20
22
 
21
23
  let published: ref<array<Message.commandJson>> = ref([])
22
24
 
@@ -86,30 +88,27 @@ describe("@owner stamping on the command path:", () => {
86
88
  // The decisive case. An implementation that fills only the absent field passes
87
89
  // every other test in this file and hands one customer another's orders.
88
90
  testPromise("a FORGED owner field is overwritten, not respected", async () => {
89
- let _ =
90
- await generate(
91
- placeOrder(~customerId="cust-B", ~identity=cognito(~userId="cust-A", ~groups=["User"])),
92
- )->Effect.runPromise
91
+ let _ = await generate(
92
+ placeOrder(~customerId="cust-B", ~identity=cognito(~userId="cust-A", ~groups=["User"])),
93
+ )->Effect.runPromise
93
94
  expect(lastPublished()->fieldOf("customerId"))->toEqual(Some(str("cust-A")))
94
95
  })
95
96
 
96
97
  testPromise("non-owner fields of the same command are untouched", async () => {
97
- let _ =
98
- await generate(
99
- placeOrder(~customerId="cust-B", ~identity=cognito(~userId="cust-A", ~groups=["User"])),
100
- )->Effect.runPromise
98
+ let _ = await generate(
99
+ placeOrder(~customerId="cust-B", ~identity=cognito(~userId="cust-A", ~groups=["User"])),
100
+ )->Effect.runPromise
101
101
  expect(lastPublished()->fieldOf("note"))->toEqual(Some(str("gift wrap")))
102
102
  })
103
103
 
104
104
  testPromise("a command in the same union with no owner field is left alone", async () => {
105
- let _ =
106
- await generate(
107
- payload(
108
- ~command="ImportProducts",
109
- ~args=Dict.fromArray([("batchId", str("b-1")), ("source", str("acme"))]),
110
- ~identity=cognito(~userId="cust-A", ~groups=["User"]),
111
- ),
112
- )->Effect.runPromise
105
+ let _ = await generate(
106
+ payload(
107
+ ~command="ImportProducts",
108
+ ~args=Dict.fromArray([("batchId", str("b-1")), ("source", str("acme"))]),
109
+ ~identity=cognito(~userId="cust-A", ~groups=["User"]),
110
+ ),
111
+ )->Effect.runPromise
113
112
  expect((lastPublished()->fieldOf("source"), lastPublished()->fieldOf("customerId")))->toEqual((
114
113
  Some(str("acme")),
115
114
  None,
@@ -119,21 +118,25 @@ describe("@owner stamping on the command path:", () => {
119
118
  describe("exempt callers keep the value they sent:", () => {
120
119
  // Acting on another principal's behalf must stay possible for those who may
121
120
  // — otherwise an operator can never correct or back-fill anything.
122
- testPromise("an elevated caller's chosen customerId survives", async () => {
123
- let _ =
124
- await generate(
121
+ testPromise(
122
+ "an elevated caller's chosen customerId survives",
123
+ async () => {
124
+ let _ = await generate(
125
125
  placeOrder(~customerId="cust-B", ~identity=cognito(~userId="ops-1", ~groups=["Admin"])),
126
126
  )->Effect.runPromise
127
- expect(lastPublished()->fieldOf("customerId"))->toEqual(Some(str("cust-B")))
128
- })
127
+ expect(lastPublished()->fieldOf("customerId"))->toEqual(Some(str("cust-B")))
128
+ },
129
+ )
129
130
 
130
131
  // The IAM shape carries no userId at all. Stamping it would write
131
132
  // `undefined`; refusing it would break internal traffic. It is exempt.
132
- testPromise("an IAM system caller's chosen customerId survives", async () => {
133
- let _ =
134
- await generate(placeOrder(~customerId="cust-B", ~identity=iam))->Effect.runPromise
135
- expect(lastPublished()->fieldOf("customerId"))->toEqual(Some(str("cust-B")))
136
- })
133
+ testPromise(
134
+ "an IAM system caller's chosen customerId survives",
135
+ async () => {
136
+ let _ = await generate(placeOrder(~customerId="cust-B", ~identity=iam))->Effect.runPromise
137
+ expect(lastPublished()->fieldOf("customerId"))->toEqual(Some(str("cust-B")))
138
+ },
139
+ )
137
140
  })
138
141
 
139
142
  describe("a caller that cannot be identified is refused:", () => {
@@ -143,45 +146,56 @@ describe("@owner stamping on the command path:", () => {
143
146
  | exception _ => true
144
147
  }
145
148
 
146
- testPromise("an anonymous caller cannot place an owned command", async () =>
147
- expect(
148
- await refused(placeOrder(~customerId="cust-B", ~identity=Reventless.Identity.anonymous)),
149
- )->toBe(true)
149
+ testPromise(
150
+ "an anonymous caller cannot place an owned command",
151
+ async () =>
152
+ expect(
153
+ await refused(placeOrder(~customerId="cust-B", ~identity=Reventless.Identity.anonymous)),
154
+ )->toBe(true),
150
155
  )
151
156
 
152
157
  // The refusal describes the caller's own request, so a transport is allowed
153
158
  // to report it. Unmarked it arrives as "Unexpected error", which tells a
154
159
  // caller who needs to authenticate nothing at all.
155
- testPromise("the refusal is marked as the caller's fault", async () => {
156
- let failure = switch await generate(
157
- placeOrder(~customerId="cust-B", ~identity=Reventless.Identity.anonymous),
158
- )->Effect.runPromise {
159
- | _ => None
160
- | exception e => Some(e)
161
- }
162
- expect(failure->Option.mapOr(false, Plugin_ResolverError.isCallerFault))->toBe(true)
163
- })
160
+ testPromise(
161
+ "the refusal is marked as the caller's fault",
162
+ async () => {
163
+ let failure = switch await generate(
164
+ placeOrder(~customerId="cust-B", ~identity=Reventless.Identity.anonymous),
165
+ )->Effect.runPromise {
166
+ | _ => None
167
+ | exception e => Some(e)
168
+ }
169
+ expect(failure->Option.mapOr(false, Plugin_ResolverError.isCallerFault))->toBe(true)
170
+ },
171
+ )
164
172
 
165
- testPromise("nothing is published when the caller is refused", async () => {
166
- let _ = await refused(
167
- placeOrder(~customerId="cust-B", ~identity=Reventless.Identity.anonymous),
168
- )
169
- expect(published.contents->Array.length)->toBe(0)
170
- })
173
+ testPromise(
174
+ "nothing is published when the caller is refused",
175
+ async () => {
176
+ let _ = await refused(
177
+ placeOrder(~customerId="cust-B", ~identity=Reventless.Identity.anonymous),
178
+ )
179
+ expect(published.contents->Array.length)->toBe(0)
180
+ },
181
+ )
171
182
 
172
183
  // The refusal is scoped to commands that actually record an owner. An
173
184
  // anonymous caller invoking an unowned command has nothing to prove, and
174
185
  // refusing it here would silently narrow `AllowAnonymous`.
175
- testPromise("an anonymous caller may still issue an unowned command", async () => {
176
- let _ = await generate(
177
- payload(
178
- ~command="ImportProducts",
179
- ~args=Dict.fromArray([("batchId", str("b-1")), ("source", str("acme"))]),
180
- ~identity=Reventless.Identity.anonymous,
181
- ),
182
- )->Effect.runPromise
183
- expect(lastPublished()->fieldOf("source"))->toEqual(Some(str("acme")))
184
- })
186
+ testPromise(
187
+ "an anonymous caller may still issue an unowned command",
188
+ async () => {
189
+ let _ = await generate(
190
+ payload(
191
+ ~command="ImportProducts",
192
+ ~args=Dict.fromArray([("batchId", str("b-1")), ("source", str("acme"))]),
193
+ ~identity=Reventless.Identity.anonymous,
194
+ ),
195
+ )->Effect.runPromise
196
+ expect(lastPublished()->fieldOf("source"))->toEqual(Some(str("acme")))
197
+ },
198
+ )
185
199
  })
186
200
  })
187
201
 
@@ -207,10 +221,9 @@ describe("the schema a generator is built with must answer for its commands:", (
207
221
  )
208
222
 
209
223
  testPromise("a permissive schema silently stamps nothing — the defect, pinned", async () => {
210
- let _ =
211
- await permissive(
212
- placeOrder(~customerId="cust-B", ~identity=cognito(~userId="cust-A", ~groups=["User"])),
213
- )->Effect.runPromise
224
+ let _ = await permissive(
225
+ placeOrder(~customerId="cust-B", ~identity=cognito(~userId="cust-A", ~groups=["User"])),
226
+ )->Effect.runPromise
214
227
  // Not the behaviour we want anywhere — recorded so that a change making the
215
228
  // permissive path stamp correctly is a deliberate one, and so the contrast
216
229
  // with the case below cannot be read as incidental.
@@ -218,10 +231,9 @@ describe("the schema a generator is built with must answer for its commands:", (
218
231
  })
219
232
 
220
233
  testPromise("the real schema stamps, from the same payload and caller", async () => {
221
- let _ =
222
- await generate(
223
- placeOrder(~customerId="cust-B", ~identity=cognito(~userId="cust-A", ~groups=["User"])),
224
- )->Effect.runPromise
234
+ let _ = await generate(
235
+ placeOrder(~customerId="cust-B", ~identity=cognito(~userId="cust-A", ~groups=["User"])),
236
+ )->Effect.runPromise
225
237
  expect(lastPublished()->fieldOf("customerId"))->toEqual(Some(str("cust-A")))
226
238
  })
227
239
 
@@ -229,15 +241,18 @@ describe("the schema a generator is built with must answer for its commands:", (
229
241
  // maps now satisfy by construction: every command a spec exposes resolves to
230
242
  // its own owner fields through the schema that command will be dispatched
231
243
  // with. Reading `[]` here is exactly the state that stamps nothing.
232
- testPromise("every command of a spec resolves its owner fields through that spec's schema", async () => {
233
- let schema = commandSchema->S.castToUnknown
234
- expect(Reventless.Owner.variantFieldNames(schema, ~variant="PlaceOrder"))->toEqual([
235
- "customerId",
236
- ])
237
- expect(Reventless.Owner.variantFieldNames(schema, ~variant="ImportProducts"))->toEqual([])
238
- // The substitution, asked the same question.
239
- expect(
240
- Reventless.Owner.variantFieldNames(S.json->S.castToUnknown, ~variant="PlaceOrder"),
241
- )->toEqual([])
242
- })
244
+ testPromise(
245
+ "every command of a spec resolves its owner fields through that spec's schema",
246
+ async () => {
247
+ let schema = commandSchema->S.castToUnknown
248
+ expect(Reventless.Owner.variantFieldNames(schema, ~variant="PlaceOrder"))->toEqual([
249
+ "customerId",
250
+ ])
251
+ expect(Reventless.Owner.variantFieldNames(schema, ~variant="ImportProducts"))->toEqual([])
252
+ // The substitution, asked the same question.
253
+ expect(
254
+ Reventless.Owner.variantFieldNames(S.json->S.castToUnknown, ~variant="PlaceOrder"),
255
+ )->toEqual([])
256
+ },
257
+ )
243
258
  })
@@ -47,8 +47,10 @@ let sliceItem = (reference, command): CommandTopic.topicItem<
47
47
  }
48
48
 
49
49
  let runSlice = (reference, command) =>
50
- SliceHandler.handleCommands(dcbEventLog, Stream.fromIterable([sliceItem(reference, command)]))
51
- ->Effect.runPromise
50
+ SliceHandler.handleCommands(
51
+ dcbEventLog,
52
+ Stream.fromIterable([sliceItem(reference, command)]),
53
+ )->Effect.runPromise
52
54
 
53
55
  let runAgg = (~aggId="agg-1", reference, command) =>
54
56
  Stream.fromIterable([AggregateFixtures.makeTopicItem(~aggId, reference, command)])
@@ -78,9 +80,7 @@ let seen = () =>
78
80
  )
79
81
 
80
82
  let record = () =>
81
- CommandTopic_Helpers.registerCommandOutcome(r =>
82
- observed := observed.contents->Array.concat([r])
83
- )
83
+ CommandTopic_Helpers.registerCommandOutcome(r => observed := observed.contents->Array.concat([r]))
84
84
 
85
85
  let _ = beforeEach(() => {
86
86
  AggregateFixtures.mock.reset()
@@ -94,166 +94,209 @@ let _ = beforeEach(() => {
94
94
 
95
95
  describe("CommandTopic command-outcome hook:", () => {
96
96
  describe("aggregate", () => {
97
- testPromise("observes an accepted command with no side-channel installed", async () => {
98
- record()
99
- let results = await runAgg("ref-1", AggregateFixtures.AggSpec.Create({name: "Widget"}))
100
-
101
- expect(results)->toEqual([Ok("ref-1")])
102
- expect(seen())->toEqual([("TestAggregate", "ref-1", "accepted", "", 1)])
103
- })
104
-
105
- testPromise("carries the entity id of an accepted command", async () => {
106
- record()
107
- let _ = await runAgg(~aggId="agg-7", "ref-1", AggregateFixtures.AggSpec.Create({name: "W"}))
108
-
109
- let entityId = switch (observed.contents->Array.getUnsafe(0)).outcome {
110
- | OutcomeAccepted({entityId}) => entityId
111
- | OutcomeRejected(_) => None
112
- }
113
- expect(entityId)->toEqual(Some("agg-7"))
114
- })
115
-
116
- testPromise("reports a decide rejection as domain, with the declared error code", async () => {
117
- // Seed before registering, so only the second (rejected) Create is observed.
118
- let _ = await runAgg("ref-seed", AggregateFixtures.AggSpec.Create({name: "Widget"}))
119
- record()
120
- let results = await runAgg("ref-1", AggregateFixtures.AggSpec.Create({name: "Widget"}))
121
-
122
- expect(results)->toEqual([Ok("ref-1")])
123
- expect(seen())->toEqual([
124
- ("TestAggregate", "ref-1", "rejected/domain", "AlreadyExists", 0),
125
- ])
126
- })
127
-
128
- testPromise("reports a failed append as infrastructure, not a domain rejection", async () => {
129
- record()
130
- AggregateFixtures.mock.failNextAppend := true
131
- let _ = await runAgg("ref-1", AggregateFixtures.AggSpec.Create({name: "Widget"}))
132
-
133
- // `decide` said Ok here — the store is what failed, and the hook has to say so.
134
- expect(seen())->toEqual([
135
- ("TestAggregate", "ref-1", "rejected/infrastructure", "AppendFailed", 0),
136
- ])
137
- })
138
-
139
- testPromise("observes every command of a mixed batch", async () => {
140
- let _ = await runAgg("ref-seed", AggregateFixtures.AggSpec.Create({name: "Widget"}))
141
- record()
142
- let _ =
143
- await Stream.fromIterable([
144
- AggregateFixtures.makeTopicItem("ref-1", AggregateFixtures.AggSpec.Rename({newName: "A"})),
97
+ testPromise(
98
+ "observes an accepted command with no side-channel installed",
99
+ async () => {
100
+ record()
101
+ let results = await runAgg("ref-1", AggregateFixtures.AggSpec.Create({name: "Widget"}))
102
+
103
+ expect(results)->toEqual([Ok("ref-1")])
104
+ expect(seen())->toEqual([("TestAggregate", "ref-1", "accepted", "", 1)])
105
+ },
106
+ )
107
+
108
+ testPromise(
109
+ "carries the entity id of an accepted command",
110
+ async () => {
111
+ record()
112
+ let _ = await runAgg(~aggId="agg-7", "ref-1", AggregateFixtures.AggSpec.Create({name: "W"}))
113
+
114
+ let entityId = switch (observed.contents->Array.getUnsafe(0)).outcome {
115
+ | OutcomeAccepted({entityId}) => entityId
116
+ | OutcomeRejected(_) => None
117
+ }
118
+ expect(entityId)->toEqual(Some("agg-7"))
119
+ },
120
+ )
121
+
122
+ testPromise(
123
+ "reports a decide rejection as domain, with the declared error code",
124
+ async () => {
125
+ // Seed before registering, so only the second (rejected) Create is observed.
126
+ let _ = await runAgg("ref-seed", AggregateFixtures.AggSpec.Create({name: "Widget"}))
127
+ record()
128
+ let results = await runAgg("ref-1", AggregateFixtures.AggSpec.Create({name: "Widget"}))
129
+
130
+ expect(results)->toEqual([Ok("ref-1")])
131
+ expect(seen())->toEqual([("TestAggregate", "ref-1", "rejected/domain", "AlreadyExists", 0)])
132
+ },
133
+ )
134
+
135
+ testPromise(
136
+ "reports a failed append as infrastructure, not a domain rejection",
137
+ async () => {
138
+ record()
139
+ AggregateFixtures.mock.failNextAppend := true
140
+ let _ = await runAgg("ref-1", AggregateFixtures.AggSpec.Create({name: "Widget"}))
141
+
142
+ // `decide` said Ok here — the store is what failed, and the hook has to say so.
143
+ expect(seen())->toEqual([
144
+ ("TestAggregate", "ref-1", "rejected/infrastructure", "AppendFailed", 0),
145
+ ])
146
+ },
147
+ )
148
+
149
+ testPromise(
150
+ "observes every command of a mixed batch",
151
+ async () => {
152
+ let _ = await runAgg("ref-seed", AggregateFixtures.AggSpec.Create({name: "Widget"}))
153
+ record()
154
+ let _ = await Stream.fromIterable([
155
+ AggregateFixtures.makeTopicItem(
156
+ "ref-1",
157
+ AggregateFixtures.AggSpec.Rename({newName: "A"}),
158
+ ),
145
159
  AggregateFixtures.makeTopicItem("ref-2", AggregateFixtures.AggSpec.Create({name: "B"})),
146
- AggregateFixtures.makeTopicItem("ref-3", AggregateFixtures.AggSpec.Rename({newName: "C"})),
160
+ AggregateFixtures.makeTopicItem(
161
+ "ref-3",
162
+ AggregateFixtures.AggSpec.Rename({newName: "C"}),
163
+ ),
147
164
  ])
148
165
  ->AggregateFixtures.TestHandler.handleCommands
149
166
  ->Effect.runPromise
150
167
 
151
- expect(seen())->toEqual([
152
- ("TestAggregate", "ref-1", "accepted", "", 2),
153
- ("TestAggregate", "ref-2", "rejected/domain", "AlreadyExists", 0),
154
- ("TestAggregate", "ref-3", "accepted", "", 2),
155
- ])
156
- })
168
+ expect(seen())->toEqual([
169
+ ("TestAggregate", "ref-1", "accepted", "", 2),
170
+ ("TestAggregate", "ref-2", "rejected/domain", "AlreadyExists", 0),
171
+ ("TestAggregate", "ref-3", "accepted", "", 2),
172
+ ])
173
+ },
174
+ )
157
175
  })
158
176
 
159
177
  describe("state-change slice", () => {
160
- testPromise("observes an accepted command with no side-channel installed", async () => {
161
- record()
162
- let _ = await runSlice(
163
- "ref-1",
164
- DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"}),
165
- )
166
-
167
- expect(seen())->toEqual([
168
- ("TestStateChangeSlice", DcbFixtures.testMeta.msgId, "accepted", "", 1),
169
- ])
170
- })
171
-
172
- testPromise("reports a decide rejection as domain, with the declared error code", async () => {
173
- let _ = await runSlice(
174
- "ref-seed",
175
- DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"}),
176
- )
177
- record()
178
- let _ = await runSlice(
179
- "ref-1",
180
- DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Again"}),
181
- )
182
-
183
- expect(seen())->toEqual([
184
- (
185
- "TestStateChangeSlice",
186
- DcbFixtures.testMeta.msgId,
187
- "rejected/domain",
188
- "ItemAlreadyExists",
189
- 0,
190
- ),
191
- ])
192
- })
193
-
194
- testPromise("reports exhausted append conflicts as infrastructure", async () => {
195
- record()
196
- // One more failure than the retry loop can absorb.
197
- dcbMock.failNextAppends := 4
198
- let _ = await runSlice(
199
- "ref-1",
200
- DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"}),
201
- )
202
-
203
- expect(seen())->toEqual([
204
- ("TestStateChangeSlice", DcbFixtures.testMeta.msgId, "rejected/infrastructure", "Conflict", 0),
205
- ])
206
- })
178
+ testPromise(
179
+ "observes an accepted command with no side-channel installed",
180
+ async () => {
181
+ record()
182
+ let _ = await runSlice(
183
+ "ref-1",
184
+ DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"}),
185
+ )
186
+
187
+ expect(seen())->toEqual([
188
+ ("TestStateChangeSlice", DcbFixtures.testMeta.msgId, "accepted", "", 1),
189
+ ])
190
+ },
191
+ )
192
+
193
+ testPromise(
194
+ "reports a decide rejection as domain, with the declared error code",
195
+ async () => {
196
+ let _ = await runSlice(
197
+ "ref-seed",
198
+ DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"}),
199
+ )
200
+ record()
201
+ let _ = await runSlice(
202
+ "ref-1",
203
+ DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Again"}),
204
+ )
205
+
206
+ expect(seen())->toEqual([
207
+ (
208
+ "TestStateChangeSlice",
209
+ DcbFixtures.testMeta.msgId,
210
+ "rejected/domain",
211
+ "ItemAlreadyExists",
212
+ 0,
213
+ ),
214
+ ])
215
+ },
216
+ )
217
+
218
+ testPromise(
219
+ "reports exhausted append conflicts as infrastructure",
220
+ async () => {
221
+ record()
222
+ // One more failure than the retry loop can absorb.
223
+ dcbMock.failNextAppends := 4
224
+ let _ = await runSlice(
225
+ "ref-1",
226
+ DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"}),
227
+ )
228
+
229
+ expect(seen())->toEqual([
230
+ (
231
+ "TestStateChangeSlice",
232
+ DcbFixtures.testMeta.msgId,
233
+ "rejected/infrastructure",
234
+ "Conflict",
235
+ 0,
236
+ ),
237
+ ])
238
+ },
239
+ )
207
240
  })
208
241
 
209
242
  describe("registration", () => {
210
- testPromise("with nothing registered nothing is observed and the command is unaffected", async () => {
211
- let results = await runAgg("ref-1", AggregateFixtures.AggSpec.Create({name: "Widget"}))
212
-
213
- expect(results)->toEqual([Ok("ref-1")])
214
- expect(AggregateFixtures.mock.getAll()->Array.length)->toBe(1)
215
- expect(observed.contents->Array.length)->toBe(0)
216
- })
217
-
218
- testPromise("clearCommandOutcome stops observation", async () => {
219
- record()
220
- CommandTopic_Helpers.clearCommandOutcome()
221
- let _ = await runAgg("ref-1", AggregateFixtures.AggSpec.Create({name: "Widget"}))
222
-
223
- expect(observed.contents->Array.length)->toBe(0)
224
- })
225
-
226
- testPromise("an out-of-tree extension can register it from onColdStart", async () => {
227
- // The hook needs no registration path of its own: it has the shape the cold-start
228
- // seam already reaches, so an extension picks it up alongside the other four.
229
- module Accountant: RuntimeExtension.Extension = {
230
- let moduleUrl = "file:///pkg/accountant.res.mjs"
231
- let companionModuleUrls = []
232
- let onColdStart = (~runtimeKind as _, ~component as _, ~plugin as _, ~platform as _) =>
233
- record()
234
- }
235
- RuntimeExtension.use(module(Accountant: RuntimeExtension.Extension))
236
- RuntimeExtension.notifyColdStart(
237
- ~runtimeKind=ComponentType.Aggregate,
238
- ~component="AllAggregates",
239
- ~plugin=Some("Catalog"),
240
- ~platform=Some("Shop"),
241
- )
242
-
243
- let _ = await runAgg("ref-1", AggregateFixtures.AggSpec.Create({name: "Widget"}))
244
-
245
- expect(seen())->toEqual([("TestAggregate", "ref-1", "accepted", "", 1)])
246
- })
247
-
248
- testPromise("a throwing hook is swallowed and the command still succeeds", async () => {
249
- CommandTopic_Helpers.registerCommandOutcome(_ =>
250
- JsError.throwWithMessage("hook exploded")
251
- )
252
- let results = await runAgg("ref-1", AggregateFixtures.AggSpec.Create({name: "Widget"}))
253
-
254
- // The outcome the hook observes has already happened — an observer must not undo it.
255
- expect(results)->toEqual([Ok("ref-1")])
256
- expect(AggregateFixtures.mock.getAll()->Array.length)->toBe(1)
257
- })
243
+ testPromise(
244
+ "with nothing registered nothing is observed and the command is unaffected",
245
+ async () => {
246
+ let results = await runAgg("ref-1", AggregateFixtures.AggSpec.Create({name: "Widget"}))
247
+
248
+ expect(results)->toEqual([Ok("ref-1")])
249
+ expect(AggregateFixtures.mock.getAll()->Array.length)->toBe(1)
250
+ expect(observed.contents->Array.length)->toBe(0)
251
+ },
252
+ )
253
+
254
+ testPromise(
255
+ "clearCommandOutcome stops observation",
256
+ async () => {
257
+ record()
258
+ CommandTopic_Helpers.clearCommandOutcome()
259
+ let _ = await runAgg("ref-1", AggregateFixtures.AggSpec.Create({name: "Widget"}))
260
+
261
+ expect(observed.contents->Array.length)->toBe(0)
262
+ },
263
+ )
264
+
265
+ testPromise(
266
+ "an out-of-tree extension can register it from onColdStart",
267
+ async () => {
268
+ // The hook needs no registration path of its own: it has the shape the cold-start
269
+ // seam already reaches, so an extension picks it up alongside the other four.
270
+ module Accountant: RuntimeExtension.Extension = {
271
+ let moduleUrl = "file:///pkg/accountant.res.mjs"
272
+ let companionModuleUrls = []
273
+ let onColdStart = (~runtimeKind as _, ~component as _, ~plugin as _, ~platform as _) =>
274
+ record()
275
+ }
276
+ RuntimeExtension.use(module(Accountant: RuntimeExtension.Extension))
277
+ RuntimeExtension.notifyColdStart(
278
+ ~runtimeKind=ComponentType.Aggregate,
279
+ ~component="AllAggregates",
280
+ ~plugin=Some("Catalog"),
281
+ ~platform=Some("Shop"),
282
+ )
283
+
284
+ let _ = await runAgg("ref-1", AggregateFixtures.AggSpec.Create({name: "Widget"}))
285
+
286
+ expect(seen())->toEqual([("TestAggregate", "ref-1", "accepted", "", 1)])
287
+ },
288
+ )
289
+
290
+ testPromise(
291
+ "a throwing hook is swallowed and the command still succeeds",
292
+ async () => {
293
+ CommandTopic_Helpers.registerCommandOutcome(_ => JsError.throwWithMessage("hook exploded"))
294
+ let results = await runAgg("ref-1", AggregateFixtures.AggSpec.Create({name: "Widget"}))
295
+
296
+ // The outcome the hook observes has already happened — an observer must not undo it.
297
+ expect(results)->toEqual([Ok("ref-1")])
298
+ expect(AggregateFixtures.mock.getAll()->Array.length)->toBe(1)
299
+ },
300
+ )
258
301
  })
259
302
  })
@@ -1,4 +1,3 @@
1
-
2
1
  // ─────────────────────────────────────────────────────────────
3
2
  // Command spec for CommandTopic_Callback tests
4
3
  // ─────────────────────────────────────────────────────────────
@@ -43,9 +42,11 @@ module TestOps: CommandTopic_Callback.Ops with module Spec = TestSpec = {
43
42
  }
44
43
  capturedItems :=
45
44
  capturedItems.contents->Array.concat(
46
- items->Array.map((item: ReventlessInfra.CommandTopic.topicItem<
47
- Message.command'<TestSpec.Id.t, TestSpec.command>,
48
- >) => {
45
+ items->Array.map((
46
+ item: ReventlessInfra.CommandTopic.topicItem<
47
+ Message.command'<TestSpec.Id.t, TestSpec.command>,
48
+ >,
49
+ ) => {
49
50
  reference: item.reference,
50
51
  command: item.command.command,
51
52
  }),