@reventlessdev/reventless-gwt 1.0.0-alpha.199 → 1.0.0-alpha.201
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 +15 -0
- package/package.json +6 -6
- package/src/Flow_GWT.res +39 -0
- package/src/Flow_GWT.res.mjs +13 -0
- package/src/Hint.res +7 -0
- package/src/Hint.res.mjs +6 -0
- package/src/MismatchRender.res +4 -0
- package/src/MismatchRender.res.mjs +12 -0
- package/src/Outcome.res +10 -0
- package/src/Outcome.res.mjs +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 1.0.0-alpha.201 (2026-09-02)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **dcb:** a boundary that cannot derive its scope says so ([db79969](https://github.com/ReventlessDev/reventless-core/commit/db79969df36bd90607425f6a22b4624227cfc4a0))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 1.0.0-alpha.200 (2026-09-01)
|
|
14
|
+
|
|
15
|
+
**Note:** Version bump only for package @reventlessdev/reventless-gwt
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
6
21
|
# 1.0.0-alpha.199 (2026-09-01)
|
|
7
22
|
|
|
8
23
|
### Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-gwt",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.201",
|
|
4
4
|
"description": "Given-When-Then DSLs and test harness for Reventless slice testing",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -15,16 +15,16 @@
|
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"sury": "11.0.0-rc.2",
|
|
17
17
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
18
|
-
"@reventlessdev/rescript-node": "2.0.0-alpha.8",
|
|
19
18
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
20
|
-
"@reventlessdev/
|
|
21
|
-
"@reventlessdev/reventless-
|
|
22
|
-
"@reventlessdev/reventless-
|
|
19
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.8",
|
|
20
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.254",
|
|
21
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.155",
|
|
22
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.127"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"rescript": "12.3.0",
|
|
26
26
|
"sury-ppx": "11.0.0-rc.2",
|
|
27
|
-
"@reventlessdev/reventless-ppx": "1.0.0-alpha.
|
|
27
|
+
"@reventlessdev/reventless-ppx": "1.0.0-alpha.76"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"rescript": "12.3.0"
|
package/src/Flow_GWT.res
CHANGED
|
@@ -218,6 +218,45 @@ let test = (name, ~timeout=?, body: unit => flow) =>
|
|
|
218
218
|
s.outcome
|
|
219
219
|
})
|
|
220
220
|
|
|
221
|
+
// -- Boundary scope ----------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
// Assert that a DCB consistency boundary derives its tag scope.
|
|
224
|
+
//
|
|
225
|
+
// Every other step here tests slices; this one tests the set of them, and it is
|
|
226
|
+
// in the flow DSL because a flow is the only test kind that already reasons
|
|
227
|
+
// about more than one slice at a time. The rest of the harness derives scope
|
|
228
|
+
// per slice (`DcbScopeInference.crossPartitionForSlice`), which cannot fail the
|
|
229
|
+
// way a boundary fails: a single slice always resolves, so the all-or-nothing
|
|
230
|
+
// fallback in `deriveEffectiveScope` — one unresolvable slice discarding the
|
|
231
|
+
// derived scope for every slice beside it — has no per-slice symptom at all. The
|
|
232
|
+
// slices keep passing their own tests and the platform rejects valid commands.
|
|
233
|
+
//
|
|
234
|
+
// Takes the generated `<Plugin>.Plugin.dcbSliceSchemas`, so it asserts over the
|
|
235
|
+
// boundary as deployed rather than over the steps a given flow happens to name.
|
|
236
|
+
let thenBoundaryScopeResolves = async (
|
|
237
|
+
flowP: flow,
|
|
238
|
+
~name: string,
|
|
239
|
+
slices: array<Reventless.DcbTag.sliceSchemas>,
|
|
240
|
+
) => {
|
|
241
|
+
let s = await flowP
|
|
242
|
+
let scope = Reventless.DcbTag.deriveEffectiveScope(slices)
|
|
243
|
+
// An ambiguity that costs nothing is not a failure: a boundary whose fallback
|
|
244
|
+
// carries every derived key reads exactly as the derivation would. What fails
|
|
245
|
+
// is losing a key, because that is the case where the runtime answers wrongly.
|
|
246
|
+
let outcome = if scope.droppedCrossPartitionTagKeys->Array.length > 0 {
|
|
247
|
+
Outcome.fail(
|
|
248
|
+
ScopeDegraded({
|
|
249
|
+
boundary: name,
|
|
250
|
+
dropped: scope.droppedCrossPartitionTagKeys,
|
|
251
|
+
ambiguities: scope.ambiguities->Array.map(((slice, reason)) => `${slice}: ${reason}`),
|
|
252
|
+
}),
|
|
253
|
+
)
|
|
254
|
+
} else {
|
|
255
|
+
Outcome.pass
|
|
256
|
+
}
|
|
257
|
+
s->recordOutcome(outcome)
|
|
258
|
+
}
|
|
259
|
+
|
|
221
260
|
// -- CommandStep: a StateChangeSlice / Aggregate decider ---------------------
|
|
222
261
|
|
|
223
262
|
module CommandStep = (
|
package/src/Flow_GWT.res.mjs
CHANGED
|
@@ -131,6 +131,18 @@ function test(name, timeout, body) {
|
|
|
131
131
|
JestBind$ReventlessGwt.testPromise("Flow", name, timeout, async () => (await body()).outcome);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
async function thenBoundaryScopeResolves(flowP, name, slices) {
|
|
135
|
+
let s = await flowP;
|
|
136
|
+
let scope = DcbTag$Reventless.deriveEffectiveScope(slices);
|
|
137
|
+
let outcome = scope.droppedCrossPartitionTagKeys.length !== 0 ? Outcome$ReventlessGwt.fail({
|
|
138
|
+
TAG: "ScopeDegraded",
|
|
139
|
+
boundary: name,
|
|
140
|
+
dropped: scope.droppedCrossPartitionTagKeys,
|
|
141
|
+
ambiguities: scope.ambiguities.map(param => param[0] + `: ` + param[1])
|
|
142
|
+
}) : Outcome$ReventlessGwt.pass;
|
|
143
|
+
return recordOutcome(s, outcome);
|
|
144
|
+
}
|
|
145
|
+
|
|
134
146
|
function CommandStep(Spec) {
|
|
135
147
|
return Behavior => {
|
|
136
148
|
let consumedDecoder = DcbDecode$Reventless.makeDecoder(Spec.consumedEventSchema);
|
|
@@ -589,6 +601,7 @@ export {
|
|
|
589
601
|
decodeAggregateMatching,
|
|
590
602
|
describe,
|
|
591
603
|
test,
|
|
604
|
+
thenBoundaryScopeResolves,
|
|
592
605
|
CommandStep,
|
|
593
606
|
AggregateCommandStep,
|
|
594
607
|
AutomationStep,
|
package/src/Hint.res
CHANGED
|
@@ -93,6 +93,13 @@ let forMismatch = (~slice="<slice>", m: Outcome.mismatch): t =>
|
|
|
93
93
|
branch: None,
|
|
94
94
|
message: "The ExtensionPoint mapping / Extension delegate published a different set of events or commands than expected. Check the variant match arms and the one-to-many fan-out.",
|
|
95
95
|
}
|
|
96
|
+
| ScopeDegraded({dropped}) => {
|
|
97
|
+
locus: `${slice} (DCB boundary)`,
|
|
98
|
+
branch: None,
|
|
99
|
+
message: `A slice in this boundary has no derivable partition key, so the derived DCB scope was discarded for all of them and the cross-partition reads of [${dropped->Array.join(
|
|
100
|
+
", ",
|
|
101
|
+
)}] were lost. A slice referencing another entity by one of these keys now decides against an empty history and rejects valid commands. Fix the named slice — usually a consumed arm declaring the id the slice is already partitioned by.`,
|
|
102
|
+
}
|
|
96
103
|
| Throw({error}) => {
|
|
97
104
|
locus: `${slice}`,
|
|
98
105
|
branch: None,
|
package/src/Hint.res.mjs
CHANGED
|
@@ -89,6 +89,12 @@ function forMismatch(sliceOpt, m) {
|
|
|
89
89
|
branch: undefined,
|
|
90
90
|
message: "The ExtensionPoint mapping / Extension delegate published a different set of events or commands than expected. Check the variant match arms and the one-to-many fan-out."
|
|
91
91
|
};
|
|
92
|
+
case "ScopeDegraded" :
|
|
93
|
+
return {
|
|
94
|
+
locus: slice + ` (DCB boundary)`,
|
|
95
|
+
branch: undefined,
|
|
96
|
+
message: `A slice in this boundary has no derivable partition key, so the derived DCB scope was discarded for all of them and the cross-partition reads of [` + m.dropped.join(", ") + `] were lost. A slice referencing another entity by one of these keys now decides against an empty history and rejects valid commands. Fix the named slice — usually a consumed arm declaring the id the slice is already partitioned by.`
|
|
97
|
+
};
|
|
92
98
|
case "Throw" :
|
|
93
99
|
return {
|
|
94
100
|
locus: slice,
|
package/src/MismatchRender.res
CHANGED
|
@@ -66,6 +66,10 @@ let normalize = (m: Outcome.mismatch): normalized => {
|
|
|
66
66
|
Expected(renderMany(expected)),
|
|
67
67
|
Actual(renderMany(actual)),
|
|
68
68
|
]
|
|
69
|
+
| ScopeDegraded({boundary, dropped, ambiguities}) => [
|
|
70
|
+
Expected(`${boundary}: cross-partition reads [${dropped->Array.join(", ")}] derived`),
|
|
71
|
+
Actual(ambiguities->Array.join(" | ")),
|
|
72
|
+
]
|
|
69
73
|
| Throw({error, stack}) => [Error(error), Stack(stack)]
|
|
70
74
|
}
|
|
71
75
|
{kind: Outcome.kindName(m), fields}
|
|
@@ -82,6 +82,18 @@ function normalize(m) {
|
|
|
82
82
|
}
|
|
83
83
|
];
|
|
84
84
|
break;
|
|
85
|
+
case "ScopeDegraded" :
|
|
86
|
+
fields = [
|
|
87
|
+
{
|
|
88
|
+
TAG: "Expected",
|
|
89
|
+
_0: m.boundary + `: cross-partition reads [` + m.dropped.join(", ") + `] derived`
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
TAG: "Actual",
|
|
93
|
+
_0: m.ambiguities.join(" | ")
|
|
94
|
+
}
|
|
95
|
+
];
|
|
96
|
+
break;
|
|
85
97
|
case "Throw" :
|
|
86
98
|
fields = [
|
|
87
99
|
{
|
package/src/Outcome.res
CHANGED
|
@@ -26,6 +26,13 @@ type mismatch =
|
|
|
26
26
|
// one-to-many fan-out (one inbound message → N published actions). Used by
|
|
27
27
|
// `Delegate_GWT` and the cross-plugin `Flow_GWT` boundary steps.
|
|
28
28
|
| PublishedActionsMismatch({expected: array<JSON.t>, actual: array<JSON.t>})
|
|
29
|
+
// A DCB consistency boundary could not derive its tag scope, so the runtime
|
|
30
|
+
// falls back to `@crossPartition` annotations and loses the cross-partition
|
|
31
|
+
// reference reads named in `dropped`. Not a mismatch between an expectation
|
|
32
|
+
// and a value — every slice in it still behaves correctly in isolation, which
|
|
33
|
+
// is exactly why it needs its own outcome: the defect is in the composition,
|
|
34
|
+
// and nothing that tests one slice can see it.
|
|
35
|
+
| ScopeDegraded({boundary: string, dropped: array<string>, ambiguities: array<string>})
|
|
29
36
|
| Throw({error: string, stack: string})
|
|
30
37
|
|
|
31
38
|
type outcome = result<unit, mismatch>
|
|
@@ -44,6 +51,7 @@ let kindName = (m: mismatch) =>
|
|
|
44
51
|
| TranslateError(_) => "TranslateError"
|
|
45
52
|
| QueryRowsMismatch(_) => "QueryRowsMismatch"
|
|
46
53
|
| PublishedActionsMismatch(_) => "PublishedActionsMismatch"
|
|
54
|
+
| ScopeDegraded(_) => "ScopeDegraded"
|
|
47
55
|
| Throw(_) => "Throw"
|
|
48
56
|
}
|
|
49
57
|
|
|
@@ -93,5 +101,7 @@ let format = (m: mismatch) =>
|
|
|
93
101
|
`PublishedActionsMismatch:\n expected: ${stringifyJsonArray(expected)}\n actual: ${stringifyJsonArray(
|
|
94
102
|
actual,
|
|
95
103
|
)}`
|
|
104
|
+
| ScopeDegraded({boundary, dropped, ambiguities}) =>
|
|
105
|
+
`ScopeDegraded:\n boundary: ${boundary}\n dropped: ${dropped->Array.join(", ")}\n cause: ${ambiguities->Array.join(" | ")}`
|
|
96
106
|
| Throw({error, stack}) => `Throw: ${error}\n${stack}`
|
|
97
107
|
}
|
package/src/Outcome.res.mjs
CHANGED
|
@@ -29,6 +29,8 @@ function kindName(m) {
|
|
|
29
29
|
return "QueryRowsMismatch";
|
|
30
30
|
case "PublishedActionsMismatch" :
|
|
31
31
|
return "PublishedActionsMismatch";
|
|
32
|
+
case "ScopeDegraded" :
|
|
33
|
+
return "ScopeDegraded";
|
|
32
34
|
case "Throw" :
|
|
33
35
|
return "Throw";
|
|
34
36
|
}
|
|
@@ -71,6 +73,8 @@ function format(m) {
|
|
|
71
73
|
return `QueryRowsMismatch:\n expected: ` + stringifyJsonArray(m.expected) + `\n actual: ` + stringifyJsonArray(m.actual);
|
|
72
74
|
case "PublishedActionsMismatch" :
|
|
73
75
|
return `PublishedActionsMismatch:\n expected: ` + stringifyJsonArray(m.expected) + `\n actual: ` + stringifyJsonArray(m.actual);
|
|
76
|
+
case "ScopeDegraded" :
|
|
77
|
+
return `ScopeDegraded:\n boundary: ` + m.boundary + `\n dropped: ` + m.dropped.join(", ") + `\n cause: ` + m.ambiguities.join(" | ");
|
|
74
78
|
case "Throw" :
|
|
75
79
|
return `Throw: ` + m.error + `\n` + m.stack;
|
|
76
80
|
}
|