@reventlessdev/reventless-core 3.0.0-alpha.250 → 3.0.0-alpha.252
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.
- package/CHANGELOG.md +40 -0
- package/package.json +7 -7
- package/src/admin/Platform_Admin_Structure.res +4 -1
- package/src/admin/Platform_Admin_Structure.res.mjs +4 -2
- package/src/admin/Platform_BakedManifest.res.mjs +3 -1
- package/src/admin/Platform_ComponentDefinitionsApi.res +31 -1
- package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +29 -1
- package/src/admin/Platform_PluginStructuresApi.res +80 -8
- package/src/admin/Platform_PluginStructuresApi.res.mjs +76 -2
- package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res.mjs +8 -0
- package/src/components/Api/ApiNoApiHelpers.res +37 -3
- package/src/components/Api/ApiNoApiHelpers.res.mjs +29 -2
- package/src/components/AutomationSlice/AutomationSlice_Callback.res +4 -4
- package/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs +3 -3
- package/src/plugin/api/PluginBaseFragment.res +3 -3
- package/src/plugin/component/Plugin_Structure.res +787 -384
- package/src/plugin/component/Plugin_Structure.res.mjs +430 -20
- package/src/plugin/connect/PluginConnectExtension_Mapping.res +3 -6
- package/src/plugin/connect/PluginConnectExtension_Mapping.res.mjs +14 -2
- package/src/plugin/connect/PluginExtensionPoint_Plugin.res +0 -1
- package/src/plugin/connect/PluginExtensionPoint_Plugin.res.mjs +63 -3
- package/src/plugin/connect/PluginExtensionPoint_UiFragment.res.mjs +22 -3
- package/src/plugin/lifecycle/PluginBehavior.res +11 -11
- package/src/plugin/lifecycle/PluginBehavior.res.mjs +11 -11
- package/src/plugin/lifecycle/PluginSpec.res +11 -3
- package/src/plugin/lifecycle/PluginSpec.res.mjs +11 -3
- package/tests/admin/AdminApiSchemaDriftTest.res +486 -0
- package/tests/admin/AdminApiSchemaDriftTest.res.mjs +619 -0
- package/tests/admin/Platform_Admin_StructureTest.res +8 -3
- package/tests/admin/Platform_Admin_StructureTest.res.mjs +8 -2
- package/tests/admin/Platform_BakedManifestTest.res +2 -0
- package/tests/admin/Platform_BakedManifestTest.res.mjs +3 -1
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +10 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +30 -8
- package/tests/admin/Platform_PluginStructuresApiTest.res +28 -1
- package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +27 -5
- package/tests/aggregate/AggregateCacheTest.res.mjs +15 -3
- package/tests/aggregate/AggregateConflictTest.res.mjs +15 -3
- package/tests/aggregate/AggregateFixtures.res.mjs +15 -3
- package/tests/aggregate/AggregateSnapshotTest.res.mjs +21 -5
- package/tests/api/ApiNoApiTest.res +46 -0
- package/tests/api/ApiNoApiTest.res.mjs +49 -1
- package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +27 -3
- package/tests/commandgenerator/CommandGeneratorFixtures.res.mjs +12 -2
- package/tests/commandtopic/CommandOutcomeHookTest.res.mjs +2 -0
- package/tests/commandtopic/CommandTopicCallbackFixtures.res.mjs +12 -2
- package/tests/dcb/DcbFixtures.res.mjs +8 -0
- package/tests/dcb/DcbStateChangeSliceTest.res.mjs +18 -0
- package/tests/eventmapper/EventMapperFixtures.res.mjs +15 -3
- package/tests/extensionpoint/ExtensionPointFixtures.res.mjs +9 -1
- package/tests/extensionpoint/ExtensionPointOperationsTest.res.mjs +9 -1
- package/tests/message/MessageTest.res +2 -0
- package/tests/message/MessageTest.res.mjs +3 -1
- package/tests/plugin/PluginStructureAccessTest.res.mjs +2 -0
- package/tests/plugin/PluginStructureTest.res +294 -1
- package/tests/plugin/PluginStructureTest.res.mjs +373 -3
- package/tests/plugin/StateChangeSlice/PsAttachInvoice.res.mjs +8 -0
- package/tests/plugin/StateChangeSlice/PsChangePhoto.res.mjs +8 -0
- package/tests/plugin/StateChangeSlice/PsDispatchShipment.res.mjs +8 -0
- package/tests/plugin/StateChangeSlice/PsGatedCommands.res.mjs +8 -0
- package/tests/plugin/StateChangeSlice/PsPlaceOrder.res.mjs +8 -0
- package/tests/plugin/StateChangeSlice/PsReserveStock.res.mjs +8 -0
- package/tests/plugin/StateChangeSlice/PsShipOrder.res.mjs +8 -0
- package/tests/plugin/StateChangeSlice/PsTransitionSwitch.res +59 -0
- package/tests/plugin/StateChangeSlice/PsTransitionSwitch.res.mjs +115 -0
- package/tests/plugin/StateChangeSlice/PsTypoStore.res.mjs +8 -0
- package/tests/plugin/StateChangeSlice/PsUploadAvatar.res.mjs +8 -0
- package/tests/plugin/StateChangeSlice/PsUploadImages.res.mjs +8 -0
- package/tests/plugin/pluginDefinitionRequiredScalars.txt +26 -0
- package/tests/util/CommandPublisherTest.res.mjs +9 -1
|
@@ -65,6 +65,8 @@ let SliceHandler = StateChangeSlice_Callback$ReventlessCore.Make({
|
|
|
65
65
|
eventSchema: DcbFixtures$ReventlessCore.TestCommandSpec.eventSchema,
|
|
66
66
|
commandSchema: DcbFixtures$ReventlessCore.TestCommandSpec.commandSchema,
|
|
67
67
|
commandAuthorization: DcbFixtures$ReventlessCore.TestCommandSpec.commandAuthorization,
|
|
68
|
+
commandTransition: DcbFixtures$ReventlessCore.TestCommandSpec.commandTransition,
|
|
69
|
+
traits: DcbFixtures$ReventlessCore.TestCommandSpec.traits,
|
|
68
70
|
readConsistency: DcbFixtures$ReventlessCore.TestCommandSpec.readConsistency
|
|
69
71
|
})(SliceBehavior);
|
|
70
72
|
|
|
@@ -25,11 +25,19 @@ function commandAuthorization(param) {
|
|
|
25
25
|
return "AllowAuthenticated";
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function commandTransition(param) {
|
|
29
|
+
return "Unrestricted";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let traits = [];
|
|
33
|
+
|
|
28
34
|
let TestSpec = {
|
|
29
35
|
Id: undefined,
|
|
30
36
|
name: name,
|
|
31
37
|
commandSchema: commandSchema,
|
|
32
|
-
commandAuthorization: commandAuthorization
|
|
38
|
+
commandAuthorization: commandAuthorization,
|
|
39
|
+
commandTransition: commandTransition,
|
|
40
|
+
traits: traits
|
|
33
41
|
};
|
|
34
42
|
|
|
35
43
|
let capturedItems = {
|
|
@@ -64,7 +72,9 @@ let TestOps_Spec = {
|
|
|
64
72
|
Id: Id$Reventless.StringPure,
|
|
65
73
|
name: name,
|
|
66
74
|
commandSchema: commandSchema,
|
|
67
|
-
commandAuthorization: commandAuthorization
|
|
75
|
+
commandAuthorization: commandAuthorization,
|
|
76
|
+
commandTransition: commandTransition,
|
|
77
|
+
traits: traits
|
|
68
78
|
};
|
|
69
79
|
|
|
70
80
|
let TestOps = {
|
|
@@ -156,6 +156,12 @@ function commandAuthorization(param) {
|
|
|
156
156
|
return "AllowAuthenticated";
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
function commandTransition(param) {
|
|
160
|
+
return "Unrestricted";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let traits = [];
|
|
164
|
+
|
|
159
165
|
let TestCommandSpec_initialState = {
|
|
160
166
|
exists: false,
|
|
161
167
|
currentName: undefined
|
|
@@ -173,6 +179,8 @@ let TestCommandSpec = {
|
|
|
173
179
|
evolve: evolve,
|
|
174
180
|
decide: decide,
|
|
175
181
|
commandAuthorization: commandAuthorization,
|
|
182
|
+
commandTransition: commandTransition,
|
|
183
|
+
traits: traits,
|
|
176
184
|
readConsistency: "EscalateOnRetry"
|
|
177
185
|
};
|
|
178
186
|
|
|
@@ -61,6 +61,8 @@ let TestHandler = StateChangeSlice_Callback$ReventlessCore.Make({
|
|
|
61
61
|
eventSchema: DcbFixtures$ReventlessCore.TestCommandSpec.eventSchema,
|
|
62
62
|
commandSchema: DcbFixtures$ReventlessCore.TestCommandSpec.commandSchema,
|
|
63
63
|
commandAuthorization: DcbFixtures$ReventlessCore.TestCommandSpec.commandAuthorization,
|
|
64
|
+
commandTransition: DcbFixtures$ReventlessCore.TestCommandSpec.commandTransition,
|
|
65
|
+
traits: DcbFixtures$ReventlessCore.TestCommandSpec.traits,
|
|
64
66
|
readConsistency: DcbFixtures$ReventlessCore.TestCommandSpec.readConsistency
|
|
65
67
|
})(TestCommandBehavior);
|
|
66
68
|
|
|
@@ -78,6 +80,10 @@ let errorSchema = DcbFixtures$ReventlessCore.TestCommandSpec.errorSchema;
|
|
|
78
80
|
|
|
79
81
|
let commandAuthorization = DcbFixtures$ReventlessCore.TestCommandSpec.commandAuthorization;
|
|
80
82
|
|
|
83
|
+
let commandTransition = DcbFixtures$ReventlessCore.TestCommandSpec.commandTransition;
|
|
84
|
+
|
|
85
|
+
let traits = DcbFixtures$ReventlessCore.TestCommandSpec.traits;
|
|
86
|
+
|
|
81
87
|
let StrongSpec_Id = DcbFixtures$ReventlessCore.TestCommandSpec.Id;
|
|
82
88
|
|
|
83
89
|
let StrongSpec_initialState = DcbFixtures$ReventlessCore.TestCommandSpec.initialState;
|
|
@@ -98,6 +104,8 @@ let StrongSpec = {
|
|
|
98
104
|
evolve: StrongSpec_evolve,
|
|
99
105
|
decide: StrongSpec_decide,
|
|
100
106
|
commandAuthorization: commandAuthorization,
|
|
107
|
+
commandTransition: commandTransition,
|
|
108
|
+
traits: traits,
|
|
101
109
|
readConsistency: "AlwaysStrong"
|
|
102
110
|
};
|
|
103
111
|
|
|
@@ -115,6 +123,10 @@ let errorSchema$1 = DcbFixtures$ReventlessCore.TestCommandSpec.errorSchema;
|
|
|
115
123
|
|
|
116
124
|
let commandAuthorization$1 = DcbFixtures$ReventlessCore.TestCommandSpec.commandAuthorization;
|
|
117
125
|
|
|
126
|
+
let commandTransition$1 = DcbFixtures$ReventlessCore.TestCommandSpec.commandTransition;
|
|
127
|
+
|
|
128
|
+
let traits$1 = DcbFixtures$ReventlessCore.TestCommandSpec.traits;
|
|
129
|
+
|
|
118
130
|
let EventualSpec_Id = DcbFixtures$ReventlessCore.TestCommandSpec.Id;
|
|
119
131
|
|
|
120
132
|
let EventualSpec_initialState = DcbFixtures$ReventlessCore.TestCommandSpec.initialState;
|
|
@@ -135,6 +147,8 @@ let EventualSpec = {
|
|
|
135
147
|
evolve: EventualSpec_evolve,
|
|
136
148
|
decide: EventualSpec_decide,
|
|
137
149
|
commandAuthorization: commandAuthorization$1,
|
|
150
|
+
commandTransition: commandTransition$1,
|
|
151
|
+
traits: traits$1,
|
|
138
152
|
readConsistency: "AlwaysEventual"
|
|
139
153
|
};
|
|
140
154
|
|
|
@@ -147,6 +161,8 @@ let StrongHandler = StateChangeSlice_Callback$ReventlessCore.Make({
|
|
|
147
161
|
eventSchema: eventSchema,
|
|
148
162
|
commandSchema: commandSchema,
|
|
149
163
|
commandAuthorization: commandAuthorization,
|
|
164
|
+
commandTransition: commandTransition,
|
|
165
|
+
traits: traits,
|
|
150
166
|
readConsistency: "AlwaysStrong"
|
|
151
167
|
})(TestCommandBehavior);
|
|
152
168
|
|
|
@@ -159,6 +175,8 @@ let EventualHandler = StateChangeSlice_Callback$ReventlessCore.Make({
|
|
|
159
175
|
eventSchema: eventSchema$1,
|
|
160
176
|
commandSchema: commandSchema$1,
|
|
161
177
|
commandAuthorization: commandAuthorization$1,
|
|
178
|
+
commandTransition: commandTransition$1,
|
|
179
|
+
traits: traits$1,
|
|
162
180
|
readConsistency: "AlwaysEventual"
|
|
163
181
|
})(TestCommandBehavior);
|
|
164
182
|
|
|
@@ -153,12 +153,20 @@ function commandAuthorization(param) {
|
|
|
153
153
|
return "AllowAuthenticated";
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
function commandTransition(param) {
|
|
157
|
+
return "Unrestricted";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let traits = [];
|
|
161
|
+
|
|
156
162
|
let CmdTargetSpec = {
|
|
157
163
|
Id: undefined,
|
|
158
164
|
name: name$3,
|
|
159
165
|
commandSchema: commandSchema,
|
|
160
166
|
moduleUrl: moduleUrl$2,
|
|
161
|
-
commandAuthorization: commandAuthorization
|
|
167
|
+
commandAuthorization: commandAuthorization,
|
|
168
|
+
commandTransition: commandTransition,
|
|
169
|
+
traits: traits
|
|
162
170
|
};
|
|
163
171
|
|
|
164
172
|
function map(id, event, _queryEngine) {
|
|
@@ -233,7 +241,9 @@ let OrderMappings_Target = {
|
|
|
233
241
|
name: name$3,
|
|
234
242
|
commandSchema: commandSchema,
|
|
235
243
|
moduleUrl: moduleUrl$2,
|
|
236
|
-
commandAuthorization: commandAuthorization
|
|
244
|
+
commandAuthorization: commandAuthorization,
|
|
245
|
+
commandTransition: commandTransition,
|
|
246
|
+
traits: traits
|
|
237
247
|
};
|
|
238
248
|
|
|
239
249
|
let OrderMappings = {
|
|
@@ -262,7 +272,9 @@ let CountOrderMappings_Target = {
|
|
|
262
272
|
name: name$3,
|
|
263
273
|
commandSchema: commandSchema,
|
|
264
274
|
moduleUrl: moduleUrl$2,
|
|
265
|
-
commandAuthorization: commandAuthorization
|
|
275
|
+
commandAuthorization: commandAuthorization,
|
|
276
|
+
commandTransition: commandTransition,
|
|
277
|
+
traits: traits
|
|
266
278
|
};
|
|
267
279
|
|
|
268
280
|
let CountOrderMappings = {
|
|
@@ -30,13 +30,21 @@ function commandAuthorization(param) {
|
|
|
30
30
|
return "AllowAuthenticated";
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function commandTransition(param) {
|
|
34
|
+
return "Unrestricted";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let traits = [];
|
|
38
|
+
|
|
33
39
|
let TestEPSpec = {
|
|
34
40
|
name: name,
|
|
35
41
|
commandSchema: commandSchema,
|
|
36
42
|
eventSchema: eventSchema,
|
|
37
43
|
directiveSchema: directiveSchema,
|
|
38
44
|
moduleUrl: moduleUrl,
|
|
39
|
-
commandAuthorization: commandAuthorization
|
|
45
|
+
commandAuthorization: commandAuthorization,
|
|
46
|
+
commandTransition: commandTransition,
|
|
47
|
+
traits: traits
|
|
40
48
|
};
|
|
41
49
|
|
|
42
50
|
let capturedPublishedCmds = {
|
|
@@ -25,13 +25,21 @@ function commandAuthorization(param) {
|
|
|
25
25
|
return "AllowAuthenticated";
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function commandTransition(param) {
|
|
29
|
+
return "Unrestricted";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let traits = [];
|
|
33
|
+
|
|
28
34
|
let OpsEPSpec = {
|
|
29
35
|
name: name,
|
|
30
36
|
commandSchema: commandSchema,
|
|
31
37
|
eventSchema: eventSchema,
|
|
32
38
|
directiveSchema: directiveSchema,
|
|
33
39
|
moduleUrl: moduleUrl,
|
|
34
|
-
commandAuthorization: commandAuthorization
|
|
40
|
+
commandAuthorization: commandAuthorization,
|
|
41
|
+
commandTransition: commandTransition,
|
|
42
|
+
traits: traits
|
|
35
43
|
};
|
|
36
44
|
|
|
37
45
|
let capturedPublished = {
|
|
@@ -124,7 +124,9 @@ globalThis.describe("Message should", () => {
|
|
|
124
124
|
extensions: [],
|
|
125
125
|
extensionPoints: undefined,
|
|
126
126
|
requiredStores: undefined,
|
|
127
|
-
requiredStoreDeclarations: undefined
|
|
127
|
+
requiredStoreDeclarations: undefined,
|
|
128
|
+
requiredCapabilities: undefined,
|
|
129
|
+
traitDeclarations: undefined
|
|
128
130
|
}
|
|
129
131
|
},
|
|
130
132
|
dcbEventLog: undefined,
|
|
@@ -28,6 +28,8 @@ let PsGatedCommandsSlice_Spec = {
|
|
|
28
28
|
eventSchema: PsGatedCommands$ReventlessCore.eventSchema,
|
|
29
29
|
commandSchema: PsGatedCommands$ReventlessCore.commandSchema,
|
|
30
30
|
commandAuthorization: PsGatedCommands$ReventlessCore.commandAuthorization,
|
|
31
|
+
commandTransition: PsGatedCommands$ReventlessCore.commandTransition,
|
|
32
|
+
traits: PsGatedCommands$ReventlessCore.traits,
|
|
31
33
|
readConsistency: PsGatedCommands$ReventlessCore.readConsistency
|
|
32
34
|
};
|
|
33
35
|
|
|
@@ -185,6 +185,19 @@ module PsDispatchShipmentSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
|
185
185
|
type component = scsComponent
|
|
186
186
|
let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
|
|
187
187
|
}
|
|
188
|
+
module PsTransitionSwitchSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
189
|
+
module Spec = PsTransitionSwitch
|
|
190
|
+
module Behavior = {
|
|
191
|
+
type state = PsTransitionSwitch.state
|
|
192
|
+
let initialState = PsTransitionSwitch.initialState
|
|
193
|
+
let evolve = PsTransitionSwitch.evolve
|
|
194
|
+
let decide = PsTransitionSwitch.decide
|
|
195
|
+
let moduleUrl = PsTransitionSwitch.moduleUrl
|
|
196
|
+
}
|
|
197
|
+
let isAsync = false
|
|
198
|
+
type component = scsComponent
|
|
199
|
+
let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
|
|
200
|
+
}
|
|
188
201
|
module PsShipmentsViewSlice: ReventlessInfra.StateViewSlice.T = {
|
|
189
202
|
module Spec = PsShipmentsView
|
|
190
203
|
module Projection = {
|
|
@@ -290,6 +303,87 @@ describe("lifecycle topology", () => {
|
|
|
290
303
|
})
|
|
291
304
|
})
|
|
292
305
|
|
|
306
|
+
describe("commandTransition — the edge as a switch", () => {
|
|
307
|
+
// The seam a variant spread needs. `@transition` lowers to a dict on the
|
|
308
|
+
// parent union keyed by variant name, and a spread splices members, so a
|
|
309
|
+
// command a host did not declare cannot be annotated at all. A switch over the
|
|
310
|
+
// command reaches every constructor, spliced or not, and the compiler makes
|
|
311
|
+
// it exhaustive.
|
|
312
|
+
let structure = Plugin_Structure.make(
|
|
313
|
+
~name="TransitionSwitchPlugin",
|
|
314
|
+
~stateChangeSlices=[module(PsTransitionSwitchSlice)],
|
|
315
|
+
~stateViewSlices=[],
|
|
316
|
+
)
|
|
317
|
+
let slice = structure.stateChangeSlices->Array.getUnsafe(0)
|
|
318
|
+
let byName = name => slice.commands->Array.find(c => c.name == name)
|
|
319
|
+
|
|
320
|
+
testSync("the switch fills allowedStates and targetState", () => {
|
|
321
|
+
expect((
|
|
322
|
+
byName("Book")->Option.flatMap(c => c.allowedStates),
|
|
323
|
+
byName("Book")->Option.flatMap(c => c.targetState),
|
|
324
|
+
))->toEqual((Some(["Draft"]), Some("Booked")))
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
testSync("a guard declares its from-set and no target", () => {
|
|
328
|
+
// The same positive claim the one-sided `@transition` makes: legal in these
|
|
329
|
+
// states, and it moves the row nowhere.
|
|
330
|
+
expect((
|
|
331
|
+
byName("Rebook")->Option.flatMap(c => c.allowedStates),
|
|
332
|
+
byName("Rebook")->Option.flatMap(c => c.targetState),
|
|
333
|
+
))->toEqual((Some(["Booked"]), None))
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
testSync("the switch wins over an @transition on the same command", () => {
|
|
337
|
+
// `Rebook` carries both, and they disagree on purpose: the attribute says
|
|
338
|
+
// Draft. Two sources of truth need a stated winner, and it is the one the
|
|
339
|
+
// compiler checked.
|
|
340
|
+
expect(byName("Rebook")->Option.flatMap(c => c.allowedStates))->toEqual(Some(["Booked"]))
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
testSync("Unrestricted declares nothing, leaving the command unconstrained", () => {
|
|
344
|
+
// Distinct from a from-set naming every state, which says the same thing to
|
|
345
|
+
// a menu and something different to a reader.
|
|
346
|
+
expect((
|
|
347
|
+
byName("Abandon")->Option.flatMap(c => c.allowedStates),
|
|
348
|
+
byName("Abandon")->Option.flatMap(c => c.targetState),
|
|
349
|
+
))->toEqual((None, None))
|
|
350
|
+
})
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
describe("traitDeclarations — a graft leaves a trace", () => {
|
|
354
|
+
// The one signal a graft leaves that survives into a deployed plugin. Every
|
|
355
|
+
// other one — the package dependency, the variant spread, the rules alias, the
|
|
356
|
+
// conformance binding — is source-side and invisible to a running estate.
|
|
357
|
+
let structure = Plugin_Structure.make(
|
|
358
|
+
~name="TraitPlugin",
|
|
359
|
+
~stateChangeSlices=[module(PsTransitionSwitchSlice), module(PsShipOrderSlice)],
|
|
360
|
+
~stateViewSlices=[],
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
testSync("a declaring component's trait reaches the structure", () => {
|
|
364
|
+
expect(
|
|
365
|
+
structure.traitDeclarations->Option.getOr([])->Array.map(d => (d.trait, d.component)),
|
|
366
|
+
)->toEqual([("@reventlessdev/trait-booking", "TransitionSwitch")])
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
testSync("the component name is filled in by the structure, not declared", () => {
|
|
370
|
+
// The trait exports its identity and posture; it cannot know which component a
|
|
371
|
+
// host grafted it onto, and a host writing that name down would be the one
|
|
372
|
+
// hand-typed string this mechanism exists to avoid.
|
|
373
|
+
expect(
|
|
374
|
+
structure.traitDeclarations
|
|
375
|
+
->Option.getOr([])
|
|
376
|
+
->Array.map(d => (d.version, d.posture, d.component)),
|
|
377
|
+
)->toEqual([("9.9.9", "SelfContained", "TransitionSwitch")])
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
testSync("a component that is nobody's graft contributes nothing", () => {
|
|
381
|
+
// `PsShipOrder` declares no traits, and the injected default is empty — so the
|
|
382
|
+
// absence of a graft is silence rather than a row.
|
|
383
|
+
expect(structure.traitDeclarations->Option.getOr([])->Array.length)->toBe(1)
|
|
384
|
+
})
|
|
385
|
+
})
|
|
386
|
+
|
|
293
387
|
describe("@transition cross-check", () => {
|
|
294
388
|
// The exit criterion: a state no linked view declares stops the build, and the
|
|
295
389
|
// message says which command, which state, and what it was checked against.
|
|
@@ -1366,6 +1460,45 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1366
1460
|
expect(Reventless.CapabilityManifest.fromStructure(structure).capabilities)->toEqual([])
|
|
1367
1461
|
)
|
|
1368
1462
|
|
|
1463
|
+
// A capability a slice declares rides the same manifest as the stores, so
|
|
1464
|
+
// the platform generator reads one file. It carries no `field`: nothing
|
|
1465
|
+
// annotated it, and naming one would be a fiction.
|
|
1466
|
+
describe("declared capabilities", () => {
|
|
1467
|
+
let withCapability = {
|
|
1468
|
+
...structure,
|
|
1469
|
+
requiredCapabilities: Some([
|
|
1470
|
+
({capability: "Geocoding", component: "GeocodeAddress"}: Reventless.Plugin.requiredCapabilityDeclaration),
|
|
1471
|
+
{capability: "Geocoding", component: "GeocodeDropOff"},
|
|
1472
|
+
]),
|
|
1473
|
+
traitDeclarations: None,
|
|
1474
|
+
}
|
|
1475
|
+
let entries = Reventless.CapabilityManifest.fromStructure(withCapability).capabilities
|
|
1476
|
+
|
|
1477
|
+
testSync("two components needing one capability yield one entry", () =>
|
|
1478
|
+
expect(entries->Array.map(e => e.key))->toEqual(["Geocoding"])
|
|
1479
|
+
)
|
|
1480
|
+
|
|
1481
|
+
testSync("both declaring components are kept, with no field", () =>
|
|
1482
|
+
expect((entries->Array.getUnsafe(0)).declaredBy)->toEqual([
|
|
1483
|
+
({component: "GeocodeAddress"}: Reventless.CapabilityManifest.provenance),
|
|
1484
|
+
{component: "GeocodeDropOff"},
|
|
1485
|
+
])
|
|
1486
|
+
)
|
|
1487
|
+
|
|
1488
|
+
// The generator downstream renders a real `Platform.capability` arm, and
|
|
1489
|
+
// a name this build cannot map has no arm to render — so it is dropped
|
|
1490
|
+
// here rather than passed through as an unrenderable key.
|
|
1491
|
+
testSync("a capability this build does not know is dropped", () =>
|
|
1492
|
+
expect(
|
|
1493
|
+
Reventless.CapabilityManifest.fromStructure({
|
|
1494
|
+
...structure,
|
|
1495
|
+
requiredCapabilities: Some([{capability: "Telepathy", component: "Guess"}]),
|
|
1496
|
+
traitDeclarations: None,
|
|
1497
|
+
}).capabilities,
|
|
1498
|
+
)->toEqual([])
|
|
1499
|
+
)
|
|
1500
|
+
})
|
|
1501
|
+
|
|
1369
1502
|
testSync("rendering is deterministic and newline-terminated", () => {
|
|
1370
1503
|
let rendered = Reventless.CapabilityManifest.renderForStructure(withStores)
|
|
1371
1504
|
expect(rendered)->toBe(
|
|
@@ -1671,7 +1804,7 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1671
1804
|
let manifest = Reventless.CapabilityManifest.fromStructure(withHeuristicField)
|
|
1672
1805
|
expect(
|
|
1673
1806
|
manifest.capabilities->Array.every(entry =>
|
|
1674
|
-
entry.declaredBy->Array.every(site => site.field != "photoUrl")
|
|
1807
|
+
entry.declaredBy->Array.every(site => site.field != Some("photoUrl"))
|
|
1675
1808
|
),
|
|
1676
1809
|
)->toBe(true)
|
|
1677
1810
|
})
|
|
@@ -1747,3 +1880,163 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1747
1880
|
})
|
|
1748
1881
|
})
|
|
1749
1882
|
})
|
|
1883
|
+
|
|
1884
|
+
// The port's translation table — the pure halves of the check. The probe itself
|
|
1885
|
+
// needs a mapping module and is exercised by the examples' own build.
|
|
1886
|
+
describe("the port's translation table", () => {
|
|
1887
|
+
let published = (name, fromEventTypes): ReventlessInfra.ExtensionPointMapping.publishedEvent => {
|
|
1888
|
+
name,
|
|
1889
|
+
fromEventTypes,
|
|
1890
|
+
}
|
|
1891
|
+
let failures = (~declared) =>
|
|
1892
|
+
Plugin_Structure.translationTableFailures(
|
|
1893
|
+
~label="Catalog.Products ← Product",
|
|
1894
|
+
~declared,
|
|
1895
|
+
~publishedNames=["ProductBecameAvailable", "ProductWithdrawn"],
|
|
1896
|
+
~sourceNames=["Added", "Archived", "Discontinued", "Renamed"],
|
|
1897
|
+
)
|
|
1898
|
+
|
|
1899
|
+
testSync("accepts a many-to-one declaration", () =>
|
|
1900
|
+
expect(
|
|
1901
|
+
failures(~declared=[published("ProductWithdrawn", ["Archived", "Discontinued"])])->Array.length,
|
|
1902
|
+
)->toBe(0)
|
|
1903
|
+
)
|
|
1904
|
+
|
|
1905
|
+
testSync("rejects a published name the extension point does not declare", () =>
|
|
1906
|
+
expect(
|
|
1907
|
+
failures(~declared=[published("ProductRetired", ["Archived"])])->Array.length,
|
|
1908
|
+
)->toBe(1)
|
|
1909
|
+
)
|
|
1910
|
+
|
|
1911
|
+
testSync("rejects a source name the delegate does not declare", () =>
|
|
1912
|
+
expect(
|
|
1913
|
+
failures(~declared=[published("ProductWithdrawn", ["Archivd"])])->Array.length,
|
|
1914
|
+
)->toBe(1)
|
|
1915
|
+
)
|
|
1916
|
+
|
|
1917
|
+
})
|
|
1918
|
+
|
|
1919
|
+
describe("the translation table against the probe", () => {
|
|
1920
|
+
let declared = [
|
|
1921
|
+
({
|
|
1922
|
+
ReventlessInfra.ExtensionPointMapping.name: "ProductWithdrawn",
|
|
1923
|
+
fromEventTypes: ["Archived", "Discontinued"],
|
|
1924
|
+
}: ReventlessInfra.ExtensionPointMapping.publishedEvent),
|
|
1925
|
+
]
|
|
1926
|
+
let drift = (~observed, ~followed) =>
|
|
1927
|
+
Plugin_Structure.translationTableDrift(~label="EP", ~declared, ~observed, ~followed)
|
|
1928
|
+
|
|
1929
|
+
testSync("says nothing when the arms produce what was declared", () => {
|
|
1930
|
+
let (failures, warnings) = drift(
|
|
1931
|
+
~observed=[("Archived", "ProductWithdrawn"), ("Discontinued", "ProductWithdrawn")],
|
|
1932
|
+
~followed=["Archived", "Discontinued"],
|
|
1933
|
+
)
|
|
1934
|
+
expect((failures->Array.length, warnings->Array.length))->toEqual((0, 0))
|
|
1935
|
+
})
|
|
1936
|
+
|
|
1937
|
+
// The probe watched the arm publish it, so the declaration is stale.
|
|
1938
|
+
testSync("fails on an edge the probe saw and the declaration omits", () => {
|
|
1939
|
+
let (failures, _) = drift(
|
|
1940
|
+
~observed=[("Archived", "ProductRelisted")],
|
|
1941
|
+
~followed=["Archived"],
|
|
1942
|
+
)
|
|
1943
|
+
expect(failures->Array.length)->toBe(1)
|
|
1944
|
+
})
|
|
1945
|
+
|
|
1946
|
+
// The arm may branch on a payload the probe cannot synthesise.
|
|
1947
|
+
testSync("only warns on a declared edge the probe did not see", () => {
|
|
1948
|
+
let (failures, warnings) = drift(
|
|
1949
|
+
~observed=[("Archived", "ProductWithdrawn")],
|
|
1950
|
+
~followed=["Archived", "Discontinued"],
|
|
1951
|
+
)
|
|
1952
|
+
expect((failures->Array.length, warnings->Array.length))->toEqual((0, 1))
|
|
1953
|
+
})
|
|
1954
|
+
|
|
1955
|
+
// "Did not look" must not read as "no edge".
|
|
1956
|
+
testSync("judges nothing about a source the probe could not follow", () => {
|
|
1957
|
+
let (failures, warnings) = drift(~observed=[], ~followed=[])
|
|
1958
|
+
expect((failures->Array.length, warnings->Array.length))->toEqual((0, 0))
|
|
1959
|
+
})
|
|
1960
|
+
})
|
|
1961
|
+
|
|
1962
|
+
describe("the subscriber's half of the table", () => {
|
|
1963
|
+
let handled = (name, toCommandTypes): ReventlessInfra.ExtensionMapping.handledEvent => {
|
|
1964
|
+
name,
|
|
1965
|
+
toCommandTypes,
|
|
1966
|
+
}
|
|
1967
|
+
let failures = declared =>
|
|
1968
|
+
Plugin_Structure.handledTableFailures(
|
|
1969
|
+
~label="Catalog.Products → SyncCatalogProduct",
|
|
1970
|
+
~declared,
|
|
1971
|
+
~eventNames=["ProductBecameAvailable"],
|
|
1972
|
+
// The delegate's commands and the extension point's own, resolved as one set.
|
|
1973
|
+
~commandNames=["SyncNewProduct", "AckProduct"],
|
|
1974
|
+
)
|
|
1975
|
+
|
|
1976
|
+
testSync("accepts a delegate command and an extension point command alike", () =>
|
|
1977
|
+
expect(
|
|
1978
|
+
failures([handled("ProductBecameAvailable", ["SyncNewProduct", "AckProduct"])])->Array.length,
|
|
1979
|
+
)->toBe(0)
|
|
1980
|
+
)
|
|
1981
|
+
|
|
1982
|
+
testSync("rejects a handled event the extension point does not publish", () =>
|
|
1983
|
+
expect(failures([handled("ProductAdded", ["SyncNewProduct"])])->Array.length)->toBe(1)
|
|
1984
|
+
)
|
|
1985
|
+
|
|
1986
|
+
testSync("rejects a command neither side declares", () =>
|
|
1987
|
+
expect(
|
|
1988
|
+
failures([handled("ProductBecameAvailable", ["SyncNewProdct"])])->Array.length,
|
|
1989
|
+
)->toBe(1)
|
|
1990
|
+
)
|
|
1991
|
+
})
|
|
1992
|
+
|
|
1993
|
+
// ── One command name, one handler ────────────────────────────────────────────
|
|
1994
|
+
//
|
|
1995
|
+
// A DCB plugin routes a command by its bare type name, so two slices declaring
|
|
1996
|
+
// one name have one handler between them. The runtime notices only when it goes
|
|
1997
|
+
// to stamp owner fields — by which point the deploy has succeeded and the losing
|
|
1998
|
+
// slice is silently unreachable — so the structure refuses it instead.
|
|
1999
|
+
|
|
2000
|
+
describe("duplicate DCB command names", () => {
|
|
2001
|
+
// The same slice twice is the smallest possible collision, and it needs no
|
|
2002
|
+
// fixture that exists only to collide.
|
|
2003
|
+
testSync("two slices declaring one command are refused", () =>
|
|
2004
|
+
expect(
|
|
2005
|
+
switch Plugin_Structure.make(
|
|
2006
|
+
~name="TestPlugin",
|
|
2007
|
+
~stateChangeSlices=[module(PsShipOrderSlice), module(PsShipOrderSlice)],
|
|
2008
|
+
) {
|
|
2009
|
+
| _ => false
|
|
2010
|
+
| exception _ => true
|
|
2011
|
+
},
|
|
2012
|
+
)->toBe(true)
|
|
2013
|
+
)
|
|
2014
|
+
|
|
2015
|
+
testSync("the refusal names the command and both slices", () =>
|
|
2016
|
+
switch Plugin_Structure.make(
|
|
2017
|
+
~name="TestPlugin",
|
|
2018
|
+
~stateChangeSlices=[module(PsShipOrderSlice), module(PsShipOrderSlice)],
|
|
2019
|
+
) {
|
|
2020
|
+
| _ => expect("no refusal")->toBe("a refusal naming the command")
|
|
2021
|
+
| exception JsExn(e) =>
|
|
2022
|
+
let message = e->JsExn.message->Option.getOr("")
|
|
2023
|
+
expect((
|
|
2024
|
+
message->String.includes("ShipOrder"),
|
|
2025
|
+
message->String.includes("routes a command by its bare name"),
|
|
2026
|
+
))->toEqual((true, true))
|
|
2027
|
+
}
|
|
2028
|
+
)
|
|
2029
|
+
|
|
2030
|
+
// Distinct names are the ordinary case and must stay silent.
|
|
2031
|
+
testSync("distinct command names are fine", () =>
|
|
2032
|
+
expect(
|
|
2033
|
+
switch Plugin_Structure.make(
|
|
2034
|
+
~name="TestPlugin",
|
|
2035
|
+
~stateChangeSlices=[module(PsPlaceOrderSlice), module(PsShipOrderSlice)],
|
|
2036
|
+
) {
|
|
2037
|
+
| _ => true
|
|
2038
|
+
| exception _ => false
|
|
2039
|
+
},
|
|
2040
|
+
)->toBe(true)
|
|
2041
|
+
)
|
|
2042
|
+
})
|