@reventlessdev/reventless-core 3.0.0-alpha.215 → 3.0.0-alpha.216

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 (30) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/package.json +6 -6
  3. package/src/admin/Platform_Admin_Structure.res +4 -0
  4. package/src/admin/Platform_Admin_Structure.res.mjs +5 -0
  5. package/src/admin/Platform_ComponentDefinitionsApi.res +16 -1
  6. package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +24 -1
  7. package/src/components/Aggregate/Aggregate_Callback.res +14 -2
  8. package/src/components/Aggregate/Aggregate_Callback.res.mjs +3 -3
  9. package/src/components/CommandTopic/CommandTopic.res.mjs +12 -0
  10. package/src/components/CommandTopic/CommandTopic_Helpers.res +88 -2
  11. package/src/components/CommandTopic/CommandTopic_Helpers.res.mjs +48 -2
  12. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +13 -1
  13. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +4 -4
  14. package/src/plugin/component/Plugin_Builder.res +1 -0
  15. package/src/plugin/component/Plugin_Builder.res.mjs +1 -0
  16. package/src/plugin/component/Plugin_Structure.res +66 -51
  17. package/src/plugin/component/Plugin_Structure.res.mjs +61 -46
  18. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +21 -0
  19. package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +21 -0
  20. package/tests/commandtopic/CommandOutcomeHookTest.res +258 -0
  21. package/tests/commandtopic/CommandOutcomeHookTest.res.mjs +372 -0
  22. package/tests/commandtopic/CommandTopicHelpersRejectionTest.res +9 -1
  23. package/tests/commandtopic/CommandTopicHelpersRejectionTest.res.mjs +3 -3
  24. package/tests/message/MessageTest.res +1 -0
  25. package/tests/message/MessageTest.res.mjs +1 -0
  26. package/tests/plugin/PluginStructureTest.res +27 -0
  27. package/tests/plugin/PluginStructureTest.res.mjs +27 -0
  28. package/tests/plugin/StateChangeSlice/PsShipOrder.res +3 -1
  29. package/tests/plugin/StateChangeSlice/PsShipOrder.res.mjs +7 -1
  30. package/tests/plugin/pluginDefinitionRequiredScalars.txt +8 -0
@@ -0,0 +1,372 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Stream from "effect/Stream";
6
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
7
+ import * as DcbFixtures$ReventlessCore from "../dcb/DcbFixtures.res.mjs";
8
+ import * as RuntimeExtension$ReventlessCore from "../../src/adapter/RuntimeExtension/RuntimeExtension.res.mjs";
9
+ import * as AggregateFixtures$ReventlessCore from "../aggregate/AggregateFixtures.res.mjs";
10
+ import * as CommandTopic_Helpers$ReventlessCore from "../../src/components/CommandTopic/CommandTopic_Helpers.res.mjs";
11
+ import * as DcbEventLog_Operations$ReventlessCore from "../../src/components/DcbEventLog/DcbEventLog_Operations.res.mjs";
12
+ import * as StateChangeSlice_Callback$ReventlessCore from "../../src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs";
13
+
14
+ let dcbMock = DcbFixtures$ReventlessCore.makeMockStorage();
15
+
16
+ let storage = dcbMock.operations;
17
+
18
+ let publishJson = dcbMock.mockPublishJson;
19
+
20
+ let TestDcbOps = {
21
+ name: "TestDcbEventLog",
22
+ serviceName: "TestDcbEventLog",
23
+ storage: storage,
24
+ publishJson: publishJson
25
+ };
26
+
27
+ let EventLogOps = DcbEventLog_Operations$ReventlessCore.Make(TestDcbOps);
28
+
29
+ let dcbEventLog_read = EventLogOps.read;
30
+
31
+ let dcbEventLog_append = EventLogOps.append;
32
+
33
+ let dcbEventLog_readStream = EventLogOps.readStream;
34
+
35
+ let dcbEventLog_appendStream = EventLogOps.appendStream;
36
+
37
+ let dcbEventLog = {
38
+ read: dcbEventLog_read,
39
+ append: dcbEventLog_append,
40
+ readStream: dcbEventLog_readStream,
41
+ appendStream: dcbEventLog_appendStream
42
+ };
43
+
44
+ let initialState = DcbFixtures$ReventlessCore.TestCommandSpec.initialState;
45
+
46
+ let evolve = DcbFixtures$ReventlessCore.TestCommandSpec.evolve;
47
+
48
+ let decide = DcbFixtures$ReventlessCore.TestCommandSpec.decide;
49
+
50
+ let moduleUrl = DcbFixtures$ReventlessCore.TestCommandSpec.moduleUrl;
51
+
52
+ let SliceBehavior = {
53
+ initialState: initialState,
54
+ evolve: evolve,
55
+ decide: decide,
56
+ moduleUrl: moduleUrl
57
+ };
58
+
59
+ let SliceHandler = StateChangeSlice_Callback$ReventlessCore.Make({
60
+ name: DcbFixtures$ReventlessCore.TestCommandSpec.name,
61
+ moduleUrl: DcbFixtures$ReventlessCore.TestCommandSpec.moduleUrl,
62
+ Id: Id$Reventless.$$String,
63
+ consumedEventSchema: DcbFixtures$ReventlessCore.TestCommandSpec.consumedEventSchema,
64
+ errorSchema: DcbFixtures$ReventlessCore.TestCommandSpec.errorSchema,
65
+ eventSchema: DcbFixtures$ReventlessCore.TestCommandSpec.eventSchema,
66
+ commandSchema: DcbFixtures$ReventlessCore.TestCommandSpec.commandSchema,
67
+ commandAuthorization: DcbFixtures$ReventlessCore.TestCommandSpec.commandAuthorization,
68
+ readConsistency: DcbFixtures$ReventlessCore.TestCommandSpec.readConsistency
69
+ })(SliceBehavior);
70
+
71
+ function sliceItem(reference, command) {
72
+ return {
73
+ command: {
74
+ id: Id$Reventless.$$String.makeFromString("cmd-" + reference),
75
+ meta: DcbFixtures$ReventlessCore.testMeta,
76
+ command: command
77
+ },
78
+ reference: reference
79
+ };
80
+ }
81
+
82
+ function runSlice(reference, command) {
83
+ return Effect.runPromise(SliceHandler.handleCommands(undefined, undefined, dcbEventLog, Stream.fromIterable([sliceItem(reference, command)])));
84
+ }
85
+
86
+ function runAgg(aggIdOpt, reference, command) {
87
+ let aggId = aggIdOpt !== undefined ? aggIdOpt : "agg-1";
88
+ return Effect.runPromise(AggregateFixtures$ReventlessCore.TestHandler.handleCommands(Stream.fromIterable([AggregateFixtures$ReventlessCore.makeTopicItem(aggId, reference, command)])));
89
+ }
90
+
91
+ let observed = {
92
+ contents: []
93
+ };
94
+
95
+ function seen() {
96
+ return observed.contents.map(r => {
97
+ let match = r.outcome;
98
+ if (match.TAG === "OutcomeAccepted") {
99
+ return [
100
+ r.component,
101
+ r.reference,
102
+ "accepted",
103
+ "",
104
+ match.eventCount
105
+ ];
106
+ }
107
+ let tmp;
108
+ tmp = match.cause === "DomainRejection" ? "rejected/domain" : "rejected/infrastructure";
109
+ return [
110
+ r.component,
111
+ r.reference,
112
+ tmp,
113
+ match.errorCode,
114
+ 0
115
+ ];
116
+ });
117
+ }
118
+
119
+ function record() {
120
+ CommandTopic_Helpers$ReventlessCore.registerCommandOutcome(r => {
121
+ observed.contents = observed.contents.concat([r]);
122
+ });
123
+ }
124
+
125
+ globalThis.beforeEach(() => {
126
+ AggregateFixtures$ReventlessCore.mock.reset();
127
+ AggregateFixtures$ReventlessCore.TestHandler.resetCache();
128
+ dcbMock.reset();
129
+ SliceHandler.resetCache();
130
+ observed.contents = [];
131
+ CommandTopic_Helpers$ReventlessCore.clearCommandOutcome();
132
+ RuntimeExtension$ReventlessCore.reset();
133
+ });
134
+
135
+ globalThis.describe("CommandTopic command-outcome hook:", () => {
136
+ globalThis.describe("aggregate", () => {
137
+ globalThis.test("observes an accepted command with no side-channel installed", async () => {
138
+ record();
139
+ let results = await runAgg(undefined, "ref-1", {
140
+ TAG: "Create",
141
+ name: "Widget"
142
+ });
143
+ globalThis.expect(results).toEqual([{
144
+ TAG: "Ok",
145
+ _0: "ref-1"
146
+ }]);
147
+ globalThis.expect(seen()).toEqual([[
148
+ "TestAggregate",
149
+ "ref-1",
150
+ "accepted",
151
+ "",
152
+ 1
153
+ ]]);
154
+ });
155
+ globalThis.test("carries the entity id of an accepted command", async () => {
156
+ record();
157
+ await runAgg("agg-7", "ref-1", {
158
+ TAG: "Create",
159
+ name: "W"
160
+ });
161
+ let match = observed.contents[0].outcome;
162
+ let entityId;
163
+ entityId = match.TAG === "OutcomeAccepted" ? match.entityId : undefined;
164
+ globalThis.expect(entityId).toEqual("agg-7");
165
+ });
166
+ globalThis.test("reports a decide rejection as domain, with the declared error code", async () => {
167
+ await runAgg(undefined, "ref-seed", {
168
+ TAG: "Create",
169
+ name: "Widget"
170
+ });
171
+ record();
172
+ let results = await runAgg(undefined, "ref-1", {
173
+ TAG: "Create",
174
+ name: "Widget"
175
+ });
176
+ globalThis.expect(results).toEqual([{
177
+ TAG: "Ok",
178
+ _0: "ref-1"
179
+ }]);
180
+ globalThis.expect(seen()).toEqual([[
181
+ "TestAggregate",
182
+ "ref-1",
183
+ "rejected/domain",
184
+ "AlreadyExists",
185
+ 0
186
+ ]]);
187
+ });
188
+ globalThis.test("reports a failed append as infrastructure, not a domain rejection", async () => {
189
+ record();
190
+ AggregateFixtures$ReventlessCore.mock.failNextAppend.contents = true;
191
+ await runAgg(undefined, "ref-1", {
192
+ TAG: "Create",
193
+ name: "Widget"
194
+ });
195
+ globalThis.expect(seen()).toEqual([[
196
+ "TestAggregate",
197
+ "ref-1",
198
+ "rejected/infrastructure",
199
+ "AppendFailed",
200
+ 0
201
+ ]]);
202
+ });
203
+ globalThis.test("observes every command of a mixed batch", async () => {
204
+ await runAgg(undefined, "ref-seed", {
205
+ TAG: "Create",
206
+ name: "Widget"
207
+ });
208
+ record();
209
+ await Effect.runPromise(AggregateFixtures$ReventlessCore.TestHandler.handleCommands(Stream.fromIterable([
210
+ AggregateFixtures$ReventlessCore.makeTopicItem(undefined, "ref-1", {
211
+ TAG: "Rename",
212
+ newName: "A"
213
+ }),
214
+ AggregateFixtures$ReventlessCore.makeTopicItem(undefined, "ref-2", {
215
+ TAG: "Create",
216
+ name: "B"
217
+ }),
218
+ AggregateFixtures$ReventlessCore.makeTopicItem(undefined, "ref-3", {
219
+ TAG: "Rename",
220
+ newName: "C"
221
+ })
222
+ ])));
223
+ globalThis.expect(seen()).toEqual([
224
+ [
225
+ "TestAggregate",
226
+ "ref-1",
227
+ "accepted",
228
+ "",
229
+ 2
230
+ ],
231
+ [
232
+ "TestAggregate",
233
+ "ref-2",
234
+ "rejected/domain",
235
+ "AlreadyExists",
236
+ 0
237
+ ],
238
+ [
239
+ "TestAggregate",
240
+ "ref-3",
241
+ "accepted",
242
+ "",
243
+ 2
244
+ ]
245
+ ]);
246
+ });
247
+ });
248
+ globalThis.describe("state-change slice", () => {
249
+ globalThis.test("observes an accepted command with no side-channel installed", async () => {
250
+ record();
251
+ await runSlice("ref-1", {
252
+ TAG: "CreateItem",
253
+ itemId: "item-1",
254
+ name: "Test"
255
+ });
256
+ globalThis.expect(seen()).toEqual([[
257
+ "TestStateChangeSlice",
258
+ DcbFixtures$ReventlessCore.testMeta.msgId,
259
+ "accepted",
260
+ "",
261
+ 1
262
+ ]]);
263
+ });
264
+ globalThis.test("reports a decide rejection as domain, with the declared error code", async () => {
265
+ await runSlice("ref-seed", {
266
+ TAG: "CreateItem",
267
+ itemId: "item-1",
268
+ name: "Test"
269
+ });
270
+ record();
271
+ await runSlice("ref-1", {
272
+ TAG: "CreateItem",
273
+ itemId: "item-1",
274
+ name: "Again"
275
+ });
276
+ globalThis.expect(seen()).toEqual([[
277
+ "TestStateChangeSlice",
278
+ DcbFixtures$ReventlessCore.testMeta.msgId,
279
+ "rejected/domain",
280
+ "ItemAlreadyExists",
281
+ 0
282
+ ]]);
283
+ });
284
+ globalThis.test("reports exhausted append conflicts as infrastructure", async () => {
285
+ record();
286
+ dcbMock.failNextAppends.contents = 4;
287
+ await runSlice("ref-1", {
288
+ TAG: "CreateItem",
289
+ itemId: "item-1",
290
+ name: "Test"
291
+ });
292
+ globalThis.expect(seen()).toEqual([[
293
+ "TestStateChangeSlice",
294
+ DcbFixtures$ReventlessCore.testMeta.msgId,
295
+ "rejected/infrastructure",
296
+ "Conflict",
297
+ 0
298
+ ]]);
299
+ });
300
+ });
301
+ globalThis.describe("registration", () => {
302
+ globalThis.test("with nothing registered nothing is observed and the command is unaffected", async () => {
303
+ let results = await runAgg(undefined, "ref-1", {
304
+ TAG: "Create",
305
+ name: "Widget"
306
+ });
307
+ globalThis.expect(results).toEqual([{
308
+ TAG: "Ok",
309
+ _0: "ref-1"
310
+ }]);
311
+ globalThis.expect(AggregateFixtures$ReventlessCore.mock.getAll().length).toBe(1);
312
+ globalThis.expect(observed.contents.length).toBe(0);
313
+ });
314
+ globalThis.test("clearCommandOutcome stops observation", async () => {
315
+ record();
316
+ CommandTopic_Helpers$ReventlessCore.clearCommandOutcome();
317
+ await runAgg(undefined, "ref-1", {
318
+ TAG: "Create",
319
+ name: "Widget"
320
+ });
321
+ globalThis.expect(observed.contents.length).toBe(0);
322
+ });
323
+ globalThis.test("an out-of-tree extension can register it from onColdStart", async () => {
324
+ let onColdStart = (param, param$1, param$2, param$3) => record();
325
+ let Accountant = {
326
+ moduleUrl: "file:///pkg/accountant.res.mjs",
327
+ onColdStart: onColdStart
328
+ };
329
+ RuntimeExtension$ReventlessCore.use(Accountant);
330
+ RuntimeExtension$ReventlessCore.notifyColdStart("Aggregate", "AllAggregates", "Catalog", "Shop");
331
+ await runAgg(undefined, "ref-1", {
332
+ TAG: "Create",
333
+ name: "Widget"
334
+ });
335
+ globalThis.expect(seen()).toEqual([[
336
+ "TestAggregate",
337
+ "ref-1",
338
+ "accepted",
339
+ "",
340
+ 1
341
+ ]]);
342
+ });
343
+ globalThis.test("a throwing hook is swallowed and the command still succeeds", async () => {
344
+ CommandTopic_Helpers$ReventlessCore.registerCommandOutcome(param => Stdlib_JsError.throwWithMessage("hook exploded"));
345
+ let results = await runAgg(undefined, "ref-1", {
346
+ TAG: "Create",
347
+ name: "Widget"
348
+ });
349
+ globalThis.expect(results).toEqual([{
350
+ TAG: "Ok",
351
+ _0: "ref-1"
352
+ }]);
353
+ globalThis.expect(AggregateFixtures$ReventlessCore.mock.getAll().length).toBe(1);
354
+ });
355
+ });
356
+ });
357
+
358
+ export {
359
+ dcbMock,
360
+ TestDcbOps,
361
+ EventLogOps,
362
+ dcbEventLog,
363
+ SliceBehavior,
364
+ SliceHandler,
365
+ sliceItem,
366
+ runSlice,
367
+ runAgg,
368
+ observed,
369
+ seen,
370
+ record,
371
+ }
372
+ /* dcbMock Not a pure module */
@@ -26,8 +26,14 @@ describe("CommandTopic_Helpers.runInlineAndCollect:", () => {
26
26
  ->Effect.map(items => {
27
27
  items->Array.forEach(item => {
28
28
  // Both channels report for the same reference; rejected wins.
29
- CommandTopic_Helpers.reportAccepted(item.reference, {entityId: "agg-1", eventCount: 0})
29
+ CommandTopic_Helpers.reportAccepted(
30
+ ~component="TestService",
31
+ item.reference,
32
+ {entityId: "agg-1", eventCount: 0},
33
+ )
30
34
  CommandTopic_Helpers.reportRejected(
35
+ ~component="TestService",
36
+ ~cause=DomainRejection,
31
37
  item.reference,
32
38
  {errorCode: "AlreadyExists", errorDetail: ""},
33
39
  )
@@ -53,6 +59,8 @@ describe("CommandTopic_Helpers.runInlineAndCollect:", () => {
53
59
  ->Effect.map(items => {
54
60
  items->Array.forEach(item =>
55
61
  CommandTopic_Helpers.reportRejected(
62
+ ~component="TestService",
63
+ ~cause=DomainRejection,
56
64
  item.reference,
57
65
  {errorCode: "BusinessRuleViolated", errorDetail: "{\"reason\":\"x\"}"},
58
66
  )
@@ -30,11 +30,11 @@ globalThis.describe("CommandTopic_Helpers.runInlineAndCollect:", () => {
30
30
  globalThis.test("Rejected from rejectedResultChannel beats Accepted from acceptedResultChannel", async () => {
31
31
  let handler = stream => Effect.map(Stream.runCollect(stream), items => {
32
32
  items.forEach(item => {
33
- CommandTopic_Helpers$ReventlessCore.reportAccepted(item.reference, {
33
+ CommandTopic_Helpers$ReventlessCore.reportAccepted("TestService", item.reference, {
34
34
  entityId: "agg-1",
35
35
  eventCount: 0
36
36
  });
37
- CommandTopic_Helpers$ReventlessCore.reportRejected(item.reference, {
37
+ CommandTopic_Helpers$ReventlessCore.reportRejected("TestService", "DomainRejection", item.reference, {
38
38
  errorCode: "AlreadyExists",
39
39
  errorDetail: ""
40
40
  });
@@ -61,7 +61,7 @@ globalThis.describe("CommandTopic_Helpers.runInlineAndCollect:", () => {
61
61
  });
62
62
  globalThis.test("Rejected from rejectedResultChannel beats Error result (synthesized Conflict)", async () => {
63
63
  let handler = stream => Effect.map(Stream.runCollect(stream), items => {
64
- items.forEach(item => CommandTopic_Helpers$ReventlessCore.reportRejected(item.reference, {
64
+ items.forEach(item => CommandTopic_Helpers$ReventlessCore.reportRejected("TestService", "DomainRejection", item.reference, {
65
65
  errorCode: "BusinessRuleViolated",
66
66
  errorDetail: "{\"reason\":\"x\"}"
67
67
  }));
@@ -122,6 +122,7 @@ describe("Message should", () => {
122
122
  linkedViews: [],
123
123
  consistencyRead: None,
124
124
  events: [],
125
+ errors: [],
125
126
  chapter: None,
126
127
  },
127
128
  ],
@@ -114,6 +114,7 @@ globalThis.describe("Message should", () => {
114
114
  linkedViews: [],
115
115
  consistencyRead: undefined,
116
116
  events: [],
117
+ errors: [],
117
118
  chapter: undefined
118
119
  }],
119
120
  automationSlices: [],
@@ -245,6 +245,33 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
245
245
  let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
246
246
  expect(shipOrder.producedEventTypes)->toEqual(["TestPlugin.OrderShipped"])
247
247
  })
248
+
249
+ // A slice's declared errors are the third family of its contract, beside the
250
+ // commands it accepts and the events it emits — what a caller has to handle
251
+ // when a decision is refused.
252
+ testSync("PlaceOrder: the single payload-less error variant is surfaced", () => {
253
+ let placeOrder = structure.stateChangeSlices->Array.getUnsafe(0)
254
+ expect(placeOrder.errors->Array.map(e => e.name))->toEqual(["AlreadyPlaced"])
255
+ })
256
+
257
+ testSync("ShipOrder: both error variants surface, in declaration order", () => {
258
+ let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
259
+ expect(shipOrder.errors->Array.map(e => e.name))->toEqual(["OrderNotFound", "NotShippable"])
260
+ })
261
+
262
+ testSync("ShipOrder: a payload-carrying error carries its field schema", () => {
263
+ let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
264
+ let notShippable =
265
+ shipOrder.errors->Array.find(e => e.name == "NotShippable")->Option.getOrThrow
266
+ expect(notShippable.schema->String.includes("\"reason\""))->toBe(true)
267
+ })
268
+
269
+ testSync("ShipOrder: a payload-less error carries no fields and no references", () => {
270
+ let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
271
+ let notFound = shipOrder.errors->Array.find(e => e.name == "OrderNotFound")->Option.getOrThrow
272
+ expect((notFound.schema->String.includes("\"reason\""), notFound.references->Array.length))
273
+ ->toEqual((false, 0))
274
+ })
248
275
  })
249
276
 
250
277
  describe("stateViewSlices", () => {
@@ -398,6 +398,33 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
398
398
  let shipOrder = structure.stateChangeSlices[1];
399
399
  globalThis.expect(shipOrder.producedEventTypes).toEqual(["TestPlugin.OrderShipped"]);
400
400
  });
401
+ globalThis.test("PlaceOrder: the single payload-less error variant is surfaced", () => {
402
+ let placeOrder = structure.stateChangeSlices[0];
403
+ globalThis.expect(placeOrder.errors.map(e => e.name)).toEqual(["AlreadyPlaced"]);
404
+ });
405
+ globalThis.test("ShipOrder: both error variants surface, in declaration order", () => {
406
+ let shipOrder = structure.stateChangeSlices[1];
407
+ globalThis.expect(shipOrder.errors.map(e => e.name)).toEqual([
408
+ "OrderNotFound",
409
+ "NotShippable"
410
+ ]);
411
+ });
412
+ globalThis.test("ShipOrder: a payload-carrying error carries its field schema", () => {
413
+ let shipOrder = structure.stateChangeSlices[1];
414
+ let notShippable = Stdlib_Option.getOrThrow(shipOrder.errors.find(e => e.name === "NotShippable"), undefined);
415
+ globalThis.expect(notShippable.schema.includes("\"reason\"")).toBe(true);
416
+ });
417
+ globalThis.test("ShipOrder: a payload-less error carries no fields and no references", () => {
418
+ let shipOrder = structure.stateChangeSlices[1];
419
+ let notFound = Stdlib_Option.getOrThrow(shipOrder.errors.find(e => e.name === "OrderNotFound"), undefined);
420
+ globalThis.expect([
421
+ notFound.schema.includes("\"reason\""),
422
+ notFound.references.length
423
+ ]).toEqual([
424
+ false,
425
+ 0
426
+ ]);
427
+ });
401
428
  });
402
429
  globalThis.describe("stateViewSlices", () => {
403
430
  globalThis.test("produces four SVS entries in declaration order", () => {
@@ -21,8 +21,10 @@ let evolve = (_state, _event) => true
21
21
  @schema
22
22
  type command = @targetState("Shipped") ShipOrder({orderId: string}) | @noApi CancelShipment
23
23
 
24
+ // Two variants, one payload-less and one carrying a field — the same two branches
25
+ // `events` exercises, so the structure's `errors` list is pinned on both.
24
26
  @schema
25
- type error = OrderNotFound
27
+ type error = OrderNotFound | NotShippable({reason: string})
26
28
 
27
29
  // ShipmentVoided is payload-less — excluded from producedEventTypes (DCB filter),
28
30
  // but still surfaced in `events` (toEventDef's payload-less branch).
@@ -22,7 +22,13 @@ let commandSchema = S.union([
22
22
  S.literal("CancelShipment")
23
23
  ]);
24
24
 
25
- let errorSchema = S.literal("OrderNotFound");
25
+ let errorSchema = S.union([
26
+ S.literal("OrderNotFound"),
27
+ S.schema(s => ({
28
+ TAG: "NotShippable",
29
+ reason: s.m(S.string)
30
+ }))
31
+ ]);
26
32
 
27
33
  let eventSchema = S.union([
28
34
  S.schema(s => ({
@@ -43,6 +43,10 @@
43
43
  .structure.aggregates[].commands[].references[].fieldName: string
44
44
  .structure.aggregates[].commands[].schema: string
45
45
  .structure.aggregates[].consumedEventTypes[]: string
46
+ .structure.aggregates[].errors[].name: string
47
+ .structure.aggregates[].errors[].references[].entity: string
48
+ .structure.aggregates[].errors[].references[].fieldName: string
49
+ .structure.aggregates[].errors[].schema: string
46
50
  .structure.aggregates[].events[].name: string
47
51
  .structure.aggregates[].events[].references[].entity: string
48
52
  .structure.aggregates[].events[].references[].fieldName: string
@@ -86,6 +90,10 @@
86
90
  .structure.stateChangeSlices[].commands[].references[].fieldName: string
87
91
  .structure.stateChangeSlices[].commands[].schema: string
88
92
  .structure.stateChangeSlices[].consumedEventTypes[]: string
93
+ .structure.stateChangeSlices[].errors[].name: string
94
+ .structure.stateChangeSlices[].errors[].references[].entity: string
95
+ .structure.stateChangeSlices[].errors[].references[].fieldName: string
96
+ .structure.stateChangeSlices[].errors[].schema: string
89
97
  .structure.stateChangeSlices[].events[].name: string
90
98
  .structure.stateChangeSlices[].events[].references[].entity: string
91
99
  .structure.stateChangeSlices[].events[].references[].fieldName: string