@reventlessdev/reventless-gwt 1.0.0-alpha.74

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 (172) hide show
  1. package/CHANGELOG.md +698 -0
  2. package/LICENSE +202 -0
  3. package/bin/reventless-dev.mjs +21 -0
  4. package/package.json +54 -0
  5. package/rescript.json +36 -0
  6. package/src/.gwtignore +10 -0
  7. package/src/Automation_GWT.res +207 -0
  8. package/src/Automation_GWT.res.mjs +160 -0
  9. package/src/Behavior_GWT.res +603 -0
  10. package/src/Behavior_GWT.res.mjs +426 -0
  11. package/src/Bind.res +29 -0
  12. package/src/Bind.res.mjs +22 -0
  13. package/src/BuildClassifier.res +137 -0
  14. package/src/BuildClassifier.res.mjs +136 -0
  15. package/src/Cancellation.res +31 -0
  16. package/src/Cancellation.res.mjs +45 -0
  17. package/src/ChildProcess.res +84 -0
  18. package/src/ChildProcess.res.mjs +92 -0
  19. package/src/Cli.res +671 -0
  20. package/src/Cli.res.mjs +754 -0
  21. package/src/Collector.res +201 -0
  22. package/src/Collector.res.mjs +215 -0
  23. package/src/ComponentMeta.res +104 -0
  24. package/src/ComponentMeta.res.mjs +120 -0
  25. package/src/ComponentScan.res +103 -0
  26. package/src/ComponentScan.res.mjs +105 -0
  27. package/src/Delegate_GWT.res +303 -0
  28. package/src/Delegate_GWT.res.mjs +372 -0
  29. package/src/Diff.res +91 -0
  30. package/src/Diff.res.mjs +98 -0
  31. package/src/Discovery.res +105 -0
  32. package/src/Discovery.res.mjs +103 -0
  33. package/src/DomainDeadCode.res +93 -0
  34. package/src/DomainDeadCode.res.mjs +69 -0
  35. package/src/DomainGraph.res +0 -0
  36. package/src/DomainGraph.res.mjs +0 -0
  37. package/src/EventMapping_GWT.res +29 -0
  38. package/src/EventMapping_GWT.res.mjs +44 -0
  39. package/src/Filter.res +36 -0
  40. package/src/Filter.res.mjs +40 -0
  41. package/src/Flow_GWT.res +664 -0
  42. package/src/Flow_GWT.res.mjs +604 -0
  43. package/src/FormatterHuman.res +138 -0
  44. package/src/FormatterHuman.res.mjs +122 -0
  45. package/src/FormatterJson.res +257 -0
  46. package/src/FormatterJson.res.mjs +320 -0
  47. package/src/FormatterJunit.res +93 -0
  48. package/src/FormatterJunit.res.mjs +90 -0
  49. package/src/FormatterTap.res +139 -0
  50. package/src/FormatterTap.res.mjs +149 -0
  51. package/src/FormatterVsCode.res +362 -0
  52. package/src/FormatterVsCode.res.mjs +564 -0
  53. package/src/Hint.res +119 -0
  54. package/src/Hint.res.mjs +124 -0
  55. package/src/InboundTranslation_GWT.res +83 -0
  56. package/src/InboundTranslation_GWT.res.mjs +94 -0
  57. package/src/JestBind.res +70 -0
  58. package/src/JestBind.res.mjs +63 -0
  59. package/src/Loader.res +24 -0
  60. package/src/Loader.res.mjs +23 -0
  61. package/src/LocalHost.res +135 -0
  62. package/src/LocalHost.res.mjs +127 -0
  63. package/src/Mapping_GWT.res +474 -0
  64. package/src/Mapping_GWT.res.mjs +322 -0
  65. package/src/MultiSourceProjection_GWT.res +316 -0
  66. package/src/MultiSourceProjection_GWT.res.mjs +354 -0
  67. package/src/OutboundTranslation_GWT.res +238 -0
  68. package/src/OutboundTranslation_GWT.res.mjs +186 -0
  69. package/src/Outcome.res +161 -0
  70. package/src/Outcome.res.mjs +140 -0
  71. package/src/PackageScan.res +80 -0
  72. package/src/PackageScan.res.mjs +81 -0
  73. package/src/PlatformRunner.res +160 -0
  74. package/src/PlatformRunner.res.mjs +155 -0
  75. package/src/PlatformScan.res +138 -0
  76. package/src/PlatformScan.res.mjs +131 -0
  77. package/src/ProcessManager.res +91 -0
  78. package/src/ProcessManager.res.mjs +80 -0
  79. package/src/Projection_GWT.res +312 -0
  80. package/src/Projection_GWT.res.mjs +332 -0
  81. package/src/Query_GWT.res +379 -0
  82. package/src/Query_GWT.res.mjs +367 -0
  83. package/src/RenderRescript.res +125 -0
  84. package/src/RenderRescript.res.mjs +141 -0
  85. package/src/RunnerTypes.res +68 -0
  86. package/src/RunnerTypes.res.mjs +60 -0
  87. package/src/SideEffect_GWT.res +157 -0
  88. package/src/SideEffect_GWT.res.mjs +113 -0
  89. package/src/StateChange/ExternalAddCategorySlice.res +25 -0
  90. package/src/StateChange/ExternalAddCategorySlice.res.mjs +34 -0
  91. package/src/StateChange/ExternalAddCategorySlice_Behavior.res +19 -0
  92. package/src/StateChange/ExternalAddCategorySlice_Behavior.res.mjs +46 -0
  93. package/src/StateChange/WithFixtures.res +22 -0
  94. package/src/StateChange/WithFixtures.res.mjs +31 -0
  95. package/src/StateChange/WithFixtures_Behavior.res +17 -0
  96. package/src/StateChange/WithFixtures_Behavior.res.mjs +37 -0
  97. package/src/StateChange/WithManualOpen.res +23 -0
  98. package/src/StateChange/WithManualOpen.res.mjs +31 -0
  99. package/src/StateChange/WithManualOpen_Behavior.res +17 -0
  100. package/src/StateChange/WithManualOpen_Behavior.res.mjs +37 -0
  101. package/src/StubRuntime.res +53 -0
  102. package/src/StubRuntime.res.mjs +54 -0
  103. package/src/TestFixtures.res +16 -0
  104. package/src/TestFixtures.res.mjs +34 -0
  105. package/src/Watch.res +92 -0
  106. package/src/Watch.res.mjs +83 -0
  107. package/src/WatcherProbe.res +48 -0
  108. package/src/WatcherProbe.res.mjs +46 -0
  109. package/tests/AutomationGwtTest.res +71 -0
  110. package/tests/AutomationGwtTest.res.mjs +189 -0
  111. package/tests/BuildClassifierTest.res +94 -0
  112. package/tests/BuildClassifierTest.res.mjs +132 -0
  113. package/tests/ComponentMetaTest.res +52 -0
  114. package/tests/ComponentMetaTest.res.mjs +57 -0
  115. package/tests/DelegateGwtTest.res +487 -0
  116. package/tests/DelegateGwtTest.res.mjs +891 -0
  117. package/tests/DiscoveryTest.res +42 -0
  118. package/tests/DiscoveryTest.res.mjs +43 -0
  119. package/tests/DomainDeadCodeTest.res +116 -0
  120. package/tests/DomainDeadCodeTest.res.mjs +155 -0
  121. package/tests/DomainGraphTest.res +365 -0
  122. package/tests/DomainGraphTest.res.mjs +413 -0
  123. package/tests/FlowAggregateGwtTest.res +169 -0
  124. package/tests/FlowAggregateGwtTest.res.mjs +269 -0
  125. package/tests/FlowCrossPluginGwtTest.res +340 -0
  126. package/tests/FlowCrossPluginGwtTest.res.mjs +798 -0
  127. package/tests/FlowGwtTest.res +222 -0
  128. package/tests/FlowGwtTest.res.mjs +432 -0
  129. package/tests/FormatterVsCodeMessageTest.res +41 -0
  130. package/tests/FormatterVsCodeMessageTest.res.mjs +42 -0
  131. package/tests/InboundTranslationGwtTest.res +34 -0
  132. package/tests/InboundTranslationGwtTest.res.mjs +98 -0
  133. package/tests/LocalHostIntegration.res +70 -0
  134. package/tests/LocalHostIntegration.res.mjs +64 -0
  135. package/tests/LocalHostTest.res +71 -0
  136. package/tests/LocalHostTest.res.mjs +63 -0
  137. package/tests/MappingGwtTest.res +348 -0
  138. package/tests/MappingGwtTest.res.mjs +592 -0
  139. package/tests/OutboundTranslationGwtTest.res +59 -0
  140. package/tests/OutboundTranslationGwtTest.res.mjs +138 -0
  141. package/tests/PlatformRunnerTest.res +66 -0
  142. package/tests/PlatformRunnerTest.res.mjs +85 -0
  143. package/tests/PlatformScanTest.res +61 -0
  144. package/tests/PlatformScanTest.res.mjs +56 -0
  145. package/tests/QueryGwtTest.res +161 -0
  146. package/tests/QueryGwtTest.res.mjs +314 -0
  147. package/tests/QueryResolverGwtTest.res +71 -0
  148. package/tests/QueryResolverGwtTest.res.mjs +136 -0
  149. package/tests/RunnerUnitTest.res +219 -0
  150. package/tests/RunnerUnitTest.res.mjs +244 -0
  151. package/tests/SideEffect/FakeOrderNotification.res +23 -0
  152. package/tests/SideEffect/FakeOrderNotification.res.mjs +36 -0
  153. package/tests/SideEffect/FakeOrderNotificationGwtTest.res +35 -0
  154. package/tests/SideEffect/FakeOrderNotificationGwtTest.res.mjs +73 -0
  155. package/tests/SideEffect/MockEmail.res +18 -0
  156. package/tests/SideEffect/MockEmail.res.mjs +47 -0
  157. package/tests/StateChange/ExternalAddCategorySlice_GWT.res +27 -0
  158. package/tests/StateChange/ExternalAddCategorySlice_GWT.res.mjs +70 -0
  159. package/tests/StateChange/WithFixtures_Fixtures.res +9 -0
  160. package/tests/StateChange/WithFixtures_Fixtures.res.mjs +20 -0
  161. package/tests/StateChange/WithFixtures_GWT.res +23 -0
  162. package/tests/StateChange/WithFixtures_GWT.res.mjs +56 -0
  163. package/tests/StateChange/WithManualOpen_Fixtures.res +7 -0
  164. package/tests/StateChange/WithManualOpen_Fixtures.res.mjs +20 -0
  165. package/tests/StateChange/WithManualOpen_GWT.res +17 -0
  166. package/tests/StateChange/WithManualOpen_GWT.res.mjs +56 -0
  167. package/tests/StateChangeSliceGwtTest.res +160 -0
  168. package/tests/StateChangeSliceGwtTest.res.mjs +280 -0
  169. package/tests/StateViewSliceGwtTest.res +64 -0
  170. package/tests/StateViewSliceGwtTest.res.mjs +161 -0
  171. package/tests/WatchDebounceTest.res +78 -0
  172. package/tests/WatchDebounceTest.res.mjs +97 -0
@@ -0,0 +1,348 @@
1
+ // Worked examples for Mapping_GWT — one for each producer/consumer combination
2
+ // (Aggr→Aggr, Aggr→DCB, DCB→Aggr, DCB→DCB). See
3
+ // `docs/analysis/event-source-connection-matrix.md` for the connection matrix
4
+ // and `docs/plans/reventless-gwt.md` Stage 5 for the plan context.
5
+ //
6
+ // Each test exercises the full source-command → mapping → target-command →
7
+ // target-decide pipeline end-to-end against the unified `GwtSource`/`GwtTarget`
8
+ // module types.
9
+
10
+ S.enableJson()
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Shared Aggregate specs + behaviours used by the Aggr→* cases below.
14
+ // ---------------------------------------------------------------------------
15
+
16
+ module CategorySpec = {
17
+ module Id = Reventless.Id.StringPure
18
+ let name = "Category"
19
+
20
+ @schema
21
+ type command = AddCategory({categoryId: string, name: string})
22
+
23
+ @schema
24
+ type event = CategoryAdded({categoryId: string, name: string})
25
+
26
+ @schema
27
+ type error = CategoryAlreadyExists
28
+
29
+ let moduleUrl = ""
30
+ }
31
+
32
+ module CategoryBehavior = {
33
+ module Spec = CategorySpec
34
+ type state = NotCreated | Created
35
+ let initialState = NotCreated
36
+
37
+ let evolve = (_state, event: CategorySpec.event) =>
38
+ switch event {
39
+ | CategoryAdded(_) => Created
40
+ }
41
+
42
+ let decide = (state, cmd: CategorySpec.command) =>
43
+ switch (state, cmd) {
44
+ | (NotCreated, AddCategory({categoryId, name})) =>
45
+ Ok([CategorySpec.CategoryAdded({categoryId, name})])
46
+ | (Created, AddCategory(_)) => Error(CategorySpec.CategoryAlreadyExists)
47
+ }
48
+
49
+ let moduleUrl = ""
50
+ }
51
+
52
+ // Target aggregate for Aggr→Aggr and DCB→Aggr cases.
53
+ module ProductSpec = {
54
+ module Id = Reventless.Id.StringPure
55
+ let name = "Product"
56
+
57
+ @schema
58
+ type command = MirrorCategory({productId: string, categoryId: string, name: string})
59
+
60
+ @schema
61
+ type event = ProductCategoryMirrored({productId: string, categoryId: string, name: string})
62
+
63
+ @schema
64
+ type error = ProductAlreadyMirrored
65
+
66
+ let moduleUrl = ""
67
+ }
68
+
69
+ module ProductBehavior = {
70
+ module Spec = ProductSpec
71
+ type state = Pristine | Mirrored
72
+ let initialState = Pristine
73
+
74
+ let evolve = (_state, event: ProductSpec.event) =>
75
+ switch event {
76
+ | ProductCategoryMirrored(_) => Mirrored
77
+ }
78
+
79
+ let decide = (state, cmd: ProductSpec.command) =>
80
+ switch (state, cmd) {
81
+ | (Pristine, MirrorCategory({productId, categoryId, name})) =>
82
+ Ok([ProductSpec.ProductCategoryMirrored({productId, categoryId, name})])
83
+ | (Mirrored, MirrorCategory(_)) => Error(ProductSpec.ProductAlreadyMirrored)
84
+ }
85
+
86
+ let moduleUrl = ""
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Shared StateChangeSlice specs used by the *→DCB and DCB→* cases.
91
+ // ---------------------------------------------------------------------------
92
+
93
+ module NotificationSlice = {
94
+ let name = "Notification"
95
+
96
+ type state = {sent: bool}
97
+ let initialState = {sent: false}
98
+
99
+ @schema
100
+ type consumedEvent =
101
+ | NotificationSent({
102
+ notificationId: @s.matches(Reventless.DcbTag.string) string,
103
+ })
104
+
105
+ let evolve = (_state, event) =>
106
+ switch event {
107
+ | NotificationSent(_) => {sent: true}
108
+ }
109
+
110
+ @schema
111
+ type command =
112
+ | SendNotification({
113
+ notificationId: @s.matches(Reventless.DcbTag.string) string,
114
+ message: string,
115
+ })
116
+
117
+ @schema
118
+ type error = NotificationAlreadySent
119
+
120
+ @schema
121
+ type event =
122
+ | NotificationSent({
123
+ notificationId: @s.matches(Reventless.DcbTag.string) string,
124
+ message: string,
125
+ })
126
+
127
+ let decide = (state, command) =>
128
+ switch command {
129
+ | SendNotification({notificationId, message}) =>
130
+ state.sent
131
+ ? Error(NotificationAlreadySent)
132
+ : Ok([NotificationSent({notificationId, message})])
133
+ }
134
+ }
135
+
136
+ module InventorySlice = {
137
+ let name = "Inventory"
138
+
139
+ type state = {reserved: bool}
140
+ let initialState = {reserved: false}
141
+
142
+ @schema
143
+ type consumedEvent =
144
+ | StockReserved({itemId: @s.matches(Reventless.DcbTag.string) string})
145
+
146
+ let evolve = (_state, event) =>
147
+ switch event {
148
+ | StockReserved(_) => {reserved: true}
149
+ }
150
+
151
+ @schema
152
+ type command =
153
+ | ReserveStock({
154
+ itemId: @s.matches(Reventless.DcbTag.string) string,
155
+ quantity: int,
156
+ })
157
+
158
+ @schema
159
+ type error = OutOfStock
160
+
161
+ @schema
162
+ type event =
163
+ | StockReserved({
164
+ itemId: @s.matches(Reventless.DcbTag.string) string,
165
+ quantity: int,
166
+ })
167
+
168
+ let decide = (state, command) =>
169
+ switch command {
170
+ | ReserveStock({itemId, quantity}) =>
171
+ state.reserved ? Error(OutOfStock) : Ok([StockReserved({itemId, quantity})])
172
+ }
173
+ }
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // Adapter aliases — built once, shared across mappings.
177
+ // ---------------------------------------------------------------------------
178
+
179
+ module CategorySource = Mapping_GWT.FromBehavior(CategorySpec, CategoryBehavior)
180
+ module ProductTarget = Mapping_GWT.FromBehavior(ProductSpec, ProductBehavior)
181
+ module NotificationSource = Mapping_GWT.FromStateChangeSlice(NotificationSlice)
182
+ module NotificationTarget = Mapping_GWT.FromStateChangeSlice(NotificationSlice)
183
+ module InventoryTarget = Mapping_GWT.FromStateChangeSlice(InventorySlice)
184
+
185
+ // ---------------------------------------------------------------------------
186
+ // Case 1 — Aggr → Aggr: CategoryAdded triggers MirrorCategory on Product.
187
+ // ---------------------------------------------------------------------------
188
+
189
+ module AggrToAggrMapping = {
190
+ module Source = CategorySource
191
+ module Target = ProductTarget
192
+
193
+ let map = (_sourceId, event: CategorySpec.event, _q) =>
194
+ switch event {
195
+ | CategoryAdded({categoryId, name}) => [
196
+ Reventless.EventMapping.Publish(
197
+ ("prod-for-" ++ categoryId)->ProductSpec.Id.makeFromString,
198
+ ProductSpec.MirrorCategory({productId: "prod-for-" ++ categoryId, categoryId, name}),
199
+ ),
200
+ ]
201
+ }
202
+ }
203
+
204
+ module AggrToAggrGwt = Mapping_GWT.Make(AggrToAggrMapping)
205
+
206
+ AggrToAggrGwt.describe("Category → Product (Aggr → Aggr)", () =>
207
+ AggrToAggrGwt.test("AddCategory produces ProductCategoryMirrored on product", () =>
208
+ AggrToAggrGwt.givenSourceEvents([])
209
+ ->AggrToAggrGwt.andTargetEvents([])
210
+ ->AggrToAggrGwt.whenSourceCmd("c1", AddCategory({categoryId: "c1", name: "Books"}))
211
+ ->AggrToAggrGwt.thenTargetEvent(
212
+ "prod-for-c1",
213
+ ProductCategoryMirrored({productId: "prod-for-c1", categoryId: "c1", name: "Books"}),
214
+ )
215
+ )
216
+ )
217
+
218
+ // ---------------------------------------------------------------------------
219
+ // Case 2 — Aggr → DCB: CategoryAdded triggers SendNotification on Notification slice.
220
+ // ---------------------------------------------------------------------------
221
+
222
+ module AggrToDcbMapping = {
223
+ module Source = CategorySource
224
+ module Target = NotificationTarget
225
+
226
+ let map = (_sourceId, event: CategorySpec.event, _q) =>
227
+ switch event {
228
+ | CategoryAdded({categoryId, name}) => [
229
+ Reventless.EventMapping.Publish(
230
+ ("notif-" ++ categoryId)->NotificationTarget.Id.makeFromString,
231
+ NotificationSlice.SendNotification({
232
+ notificationId: "notif-" ++ categoryId,
233
+ message: `Category ${name} was created`,
234
+ }),
235
+ ),
236
+ ]
237
+ }
238
+ }
239
+
240
+ module AggrToDcbGwt = Mapping_GWT.Make(AggrToDcbMapping)
241
+
242
+ AggrToDcbGwt.describe("Category → Notification (Aggr → DCB)", () =>
243
+ AggrToDcbGwt.test("AddCategory triggers NotificationSent on the DCB slice", () =>
244
+ AggrToDcbGwt.givenSourceEvents([])
245
+ ->AggrToDcbGwt.andTargetEvents([])
246
+ ->AggrToDcbGwt.whenSourceCmd("c2", AddCategory({categoryId: "c2", name: "Media"}))
247
+ ->AggrToDcbGwt.thenTargetEvent(
248
+ "notif-c2",
249
+ NotificationSlice.NotificationSent({
250
+ notificationId: "notif-c2",
251
+ message: "Category Media was created",
252
+ }),
253
+ )
254
+ )
255
+ )
256
+
257
+ // ---------------------------------------------------------------------------
258
+ // Case 3 — DCB → Aggr: Inventory StockReserved triggers MirrorCategory on Product.
259
+ // ---------------------------------------------------------------------------
260
+
261
+ module InventorySource = Mapping_GWT.FromStateChangeSlice(InventorySlice)
262
+
263
+ module DcbToAggrMapping = {
264
+ module Source = InventorySource
265
+ module Target = ProductTarget
266
+
267
+ let map = (_sourceId, event: InventorySlice.event, _q) =>
268
+ switch event {
269
+ | StockReserved({itemId, quantity: _}) => [
270
+ Reventless.EventMapping.Publish(
271
+ ("prod-" ++ itemId)->ProductSpec.Id.makeFromString,
272
+ ProductSpec.MirrorCategory({
273
+ productId: "prod-" ++ itemId,
274
+ categoryId: itemId,
275
+ name: itemId,
276
+ }),
277
+ ),
278
+ ]
279
+ }
280
+ }
281
+
282
+ module DcbToAggrGwt = Mapping_GWT.Make(DcbToAggrMapping)
283
+
284
+ DcbToAggrGwt.describe("Inventory → Product (DCB → Aggr)", () =>
285
+ DcbToAggrGwt.test("ReserveStock triggers ProductCategoryMirrored", () =>
286
+ DcbToAggrGwt.givenSourceEvents([])
287
+ ->DcbToAggrGwt.andTargetEvents([])
288
+ ->DcbToAggrGwt.whenSourceCmd("i1", ReserveStock({itemId: "i1", quantity: 5}))
289
+ ->DcbToAggrGwt.thenTargetEvent(
290
+ "prod-i1",
291
+ ProductCategoryMirrored({productId: "prod-i1", categoryId: "i1", name: "i1"}),
292
+ )
293
+ )
294
+ )
295
+
296
+ // ---------------------------------------------------------------------------
297
+ // Case 4 — DCB → DCB: Inventory StockReserved triggers SendNotification on Notification slice.
298
+ // ---------------------------------------------------------------------------
299
+
300
+ module DcbToDcbMapping = {
301
+ module Source = InventorySource
302
+ module Target = NotificationTarget
303
+
304
+ let map = (_sourceId, event: InventorySlice.event, _q) =>
305
+ switch event {
306
+ | StockReserved({itemId, quantity}) => [
307
+ Reventless.EventMapping.Publish(
308
+ ("reserved-" ++ itemId)->NotificationTarget.Id.makeFromString,
309
+ NotificationSlice.SendNotification({
310
+ notificationId: "reserved-" ++ itemId,
311
+ message: `Reserved ${quantity->Int.toString} of ${itemId}`,
312
+ }),
313
+ ),
314
+ ]
315
+ }
316
+ }
317
+
318
+ module DcbToDcbGwt = Mapping_GWT.Make(DcbToDcbMapping)
319
+
320
+ DcbToDcbGwt.describe("Inventory → Notification (DCB → DCB)", () => {
321
+ DcbToDcbGwt.test("ReserveStock triggers NotificationSent", () =>
322
+ DcbToDcbGwt.givenSourceEvents([])
323
+ ->DcbToDcbGwt.andTargetEvents([])
324
+ ->DcbToDcbGwt.whenSourceCmd("i7", ReserveStock({itemId: "i7", quantity: 2}))
325
+ ->DcbToDcbGwt.thenTargetEvent(
326
+ "reserved-i7",
327
+ NotificationSlice.NotificationSent({
328
+ notificationId: "reserved-i7",
329
+ message: "Reserved 2 of i7",
330
+ }),
331
+ )
332
+ )
333
+
334
+ // Target-error coverage: if the notification was already sent, SendNotification
335
+ // fails with `NotificationAlreadySent`. `thenTargetError` surfaces the target's
336
+ // decide error without requiring the source to produce no events.
337
+ DcbToDcbGwt.test("existing NotificationSent causes target decide to reject", () =>
338
+ DcbToDcbGwt.givenSourceEvents([])
339
+ ->DcbToDcbGwt.andTargetEvents([
340
+ (
341
+ "reserved-i7",
342
+ [NotificationSlice.NotificationSent({notificationId: "reserved-i7"})],
343
+ ),
344
+ ])
345
+ ->DcbToDcbGwt.whenSourceCmd("i7", ReserveStock({itemId: "i7", quantity: 2}))
346
+ ->DcbToDcbGwt.thenTargetError(NotificationAlreadySent)
347
+ )
348
+ })