@reventlessdev/online-shop-hybrid-ordering 1.0.0-alpha.268 → 1.0.0-alpha.270

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 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.270 (2026-10-10)
7
+
8
+ ### Features
9
+
10
+ * **gwt:** automation and translation tests take the slice as written, and the sidecars read them ([93a0919](https://github.com/ReventlessDev/reventless-core/commit/93a0919a0d66730cf9bf89144ad8fba3cb025332))
11
+
12
+
13
+ # 1.0.0-alpha.269 (2026-10-08)
14
+
15
+ **Note:** Version bump only for package @reventlessdev/online-shop-hybrid-ordering
16
+
17
+
18
+
19
+
20
+
6
21
  # 1.0.0-alpha.268 (2026-10-08)
7
22
 
8
23
  **Note:** Version bump only for package @reventlessdev/online-shop-hybrid-ordering
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/online-shop-hybrid-ordering",
3
- "version": "1.0.0-alpha.268",
3
+ "version": "1.0.0-alpha.270",
4
4
  "description": "Online Shop Hybrid example — Ordering plugin (Customer aggregate + Order/CatalogProduct DCB)",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -21,15 +21,15 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "sury": "11.0.0",
24
- "@reventlessdev/online-shop-hybrid-catalog-spec": "1.0.0-alpha.156",
25
- "@reventlessdev/online-shop-hybrid-ordering-spec": "1.0.0-alpha.155",
26
- "@reventlessdev/reventless-infra": "3.0.0-alpha.187",
24
+ "@reventlessdev/online-shop-hybrid-catalog-spec": "1.0.0-alpha.158",
25
+ "@reventlessdev/online-shop-hybrid-ordering-spec": "1.0.0-alpha.157",
27
26
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.23",
28
- "@reventlessdev/reventless-gwt": "1.0.0-alpha.237",
29
- "@reventlessdev/reventless-local": "3.0.0-alpha.280",
30
- "@reventlessdev/trait-notification": "1.0.0-alpha.39",
31
- "@reventlessdev/trait-address-geocoding": "1.0.0-alpha.39",
32
- "@reventlessdev/reventless-spec": "3.0.0-alpha.159"
27
+ "@reventlessdev/reventless-gwt": "1.0.0-alpha.239",
28
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.189",
29
+ "@reventlessdev/reventless-local": "3.0.0-alpha.282",
30
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.161",
31
+ "@reventlessdev/trait-address-geocoding": "1.0.0-alpha.41",
32
+ "@reventlessdev/trait-notification": "1.0.0-alpha.41"
33
33
  },
34
34
  "devDependencies": {
35
35
  "rescript": "12.3.1",
@@ -2,15 +2,9 @@
2
2
  // log, which is what makes `~sourceId` load-bearing: an aggregate's event payload
3
3
  // does not name its own subject.
4
4
  //
5
- // `OutboundTranslation_GWT.Make` expects a single SliceSpec with `collect` at the
6
- // top level, so compose it on locally. The graft rules — keying, outage-vs-verdict —
7
- // are asserted by the trait's suite in `AddressGeocodingConformance_GWT.res`; what
8
- // stays here is the host-specific wording of the answers.
9
-
10
- module GeocodeCustomerAddressSlice = {
11
- include GeocodeCustomerAddress
12
- let collect = GeocodeCustomerAddress_Translation.collect
13
- }
5
+ // The graft rules — keying, outage-vs-verdict — are asserted by the trait's suite
6
+ // in `AddressGeocodingConformance_GWT.res`; what stays here is the host-specific
7
+ // wording of the answers.
14
8
 
15
9
  @@reventless.gwt
16
10
 
@@ -18,20 +12,8 @@ open Ordering_Examples
18
12
 
19
13
  let vienna: Reventless.GeoPoint.t = {lat: 48.2082, lng: 16.3738}
20
14
 
21
- // The real `translate`, driven by a stub geocoder. `whenTranslateMocked` takes any
22
- // (id, item) => promise<translateResult>, so no DSL verb is needed to reach it.
23
- let withGeocoder = (
24
- answer: result<array<Reventless.Geocoding.candidate>, Reventless.Geocoding.failure>,
25
- ) => {
26
- // Spread `none` and override the one capability under test: a literal record
27
- // would have to name every other capability the framework grows, and this test
28
- // has nothing to say about them.
29
- let capabilities: Reventless.Capabilities.t = {
30
- ...Reventless.Capabilities.none,
31
- geocode: (~text as _) => Promise.resolve(answer),
32
- }
33
- (id, item) => GeocodeCustomerAddress_Translation.translate(id, item, ~capabilities)
34
- }
15
+ // The real `translate`, against a geocoder that answers `answer`.
16
+ let geocoder = answer => fakes(~geocode=async (~text as _) => answer, ())
35
17
 
36
18
  describe("GeocodeCustomerAddress OutboundTranslationSlice", () => {
37
19
  // scenario-id: 8b319e75-7826-4d3d-bd88-a43a197fb014
@@ -63,8 +45,8 @@ describe("GeocodeCustomerAddress OutboundTranslationSlice", () => {
63
45
  address: viennaAddress,
64
46
  },
65
47
  )
66
- ->whenTranslateMocked(
67
- withGeocoder(
48
+ ->givenCapabilities(
49
+ geocoder(
68
50
  Ok([
69
51
  {
70
52
  label: "Stephansplatz 1, Vienna",
@@ -74,6 +56,7 @@ describe("GeocodeCustomerAddress OutboundTranslationSlice", () => {
74
56
  ]),
75
57
  ),
76
58
  )
59
+ ->whenTranslated
77
60
  ->thenCommand("cust-1", SetLocation({location: vienna, resolvedFrom: viennaAddress}))
78
61
  )
79
62
 
@@ -82,8 +65,8 @@ describe("GeocodeCustomerAddress OutboundTranslationSlice", () => {
82
65
  // scenario-id: 93103dd6-1304-42b1-83f9-fd4ac4a90160
83
66
  test("translate: an ambiguous answer produces a reason naming the candidates", () =>
84
67
  givenTodo("cust-1:Springfield", {customerId: cust1, address: "Springfield"})
85
- ->whenTranslateMocked(
86
- withGeocoder(
68
+ ->givenCapabilities(
69
+ geocoder(
87
70
  Ok([
88
71
  {
89
72
  label: "Springfield, IL",
@@ -98,6 +81,7 @@ describe("GeocodeCustomerAddress OutboundTranslationSlice", () => {
98
81
  ]),
99
82
  ),
100
83
  )
84
+ ->whenTranslated
101
85
  ->thenCommand(
102
86
  "cust-1",
103
87
  MarkAddressUnresolvable({
@@ -106,4 +90,34 @@ describe("GeocodeCustomerAddress OutboundTranslationSlice", () => {
106
90
  }),
107
91
  )
108
92
  )
93
+
94
+ // The address is what is asked, as written.
95
+ test("translate: the geocoder is asked for the address as written", () =>
96
+ givenTodo("cust-1:Stephansplatz 1, Vienna", {customerId: cust1, address: viennaAddress})
97
+ ->givenCapabilities(geocoder(Ok([])))
98
+ ->whenTranslated
99
+ ->thenSent([Geocoded({text: viennaAddress})])
100
+ )
101
+
102
+ // An outage is not a verdict: the row stays open for the next sweep.
103
+ test("translate: a geocoder that is down leaves the row to be retried", () =>
104
+ givenTodo("cust-1:Stephansplatz 1, Vienna", {customerId: cust1, address: viennaAddress})
105
+ ->givenCapabilities(geocoder(Error(Unavailable("timeout"))))
106
+ ->whenTranslated
107
+ ->thenTodoStatus("cust-1:Stephansplatz 1, Vienna", #Failed)
108
+ )
109
+
110
+ // The budget ran out with the geocoder never answering: the verdict is recorded
111
+ // rather than leaving the row pending forever.
112
+ test("exhausted: the address is recorded as unresolvable", () =>
113
+ givenTodo("cust-1:Stephansplatz 1, Vienna", {customerId: cust1, address: viennaAddress})
114
+ ->whenExhausted(~lastError="timeout")
115
+ ->thenCommand(
116
+ "cust-1",
117
+ MarkAddressUnresolvable({
118
+ address: viennaAddress,
119
+ reason: GeocodeCustomerAddress_Translation.Geocode.exhaustedReason(Some("timeout")),
120
+ }),
121
+ )
122
+ )
109
123
  })
@@ -1,14 +1,14 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
- import * as Capabilities$Reventless from "@reventlessdev/reventless-spec/src/semantic/Capabilities.res.mjs";
4
3
  import * as Ordering_Examples$OrderingPlugin from "../../Ordering_Examples.res.mjs";
5
4
  import * as GeocodeCustomerAddress$OrderingPlugin from "../../../src/Customer/OutboundTranslation/GeocodeCustomerAddress.res.mjs";
6
5
  import * as OutboundTranslation_GWT$ReventlessGwt from "@reventlessdev/reventless-gwt/src/OutboundTranslation_GWT.res.mjs";
6
+ import * as AddressGeocoding_Translate$TraitAddressGeocoding from "@reventlessdev/trait-address-geocoding/src/AddressGeocoding_Translate.res.mjs";
7
7
  import * as GeocodeCustomerAddress_Translation$OrderingPlugin from "../../../src/Customer/OutboundTranslation/GeocodeCustomerAddress_Translation.res.mjs";
8
8
 
9
- let GeocodeCustomerAddressSlice = {
9
+ let include = OutboundTranslation_GWT$ReventlessGwt.FromSlice({
10
10
  name: GeocodeCustomerAddress$OrderingPlugin.name,
11
- Id: GeocodeCustomerAddress$OrderingPlugin.Id,
11
+ moduleUrl: GeocodeCustomerAddress$OrderingPlugin.moduleUrl,
12
12
  consumedEventSchema: GeocodeCustomerAddress$OrderingPlugin.consumedEventSchema,
13
13
  outboundItemSchema: GeocodeCustomerAddress$OrderingPlugin.outboundItemSchema,
14
14
  inboundCommandSchema: GeocodeCustomerAddress$OrderingPlugin.inboundCommandSchema,
@@ -18,17 +18,12 @@ let GeocodeCustomerAddressSlice = {
18
18
  sourceNames: GeocodeCustomerAddress$OrderingPlugin.sourceNames,
19
19
  externalSystem: GeocodeCustomerAddress$OrderingPlugin.externalSystem,
20
20
  capabilityNeeds: GeocodeCustomerAddress$OrderingPlugin.capabilityNeeds,
21
- traits: GeocodeCustomerAddress$OrderingPlugin.traits,
22
- moduleUrl: GeocodeCustomerAddress$OrderingPlugin.moduleUrl,
23
- collect: GeocodeCustomerAddress_Translation$OrderingPlugin.collect
24
- };
25
-
26
- let include = OutboundTranslation_GWT$ReventlessGwt.Make({
27
- name: GeocodeCustomerAddress$OrderingPlugin.name,
28
- consumedEventSchema: GeocodeCustomerAddress$OrderingPlugin.consumedEventSchema,
29
- outboundItemSchema: GeocodeCustomerAddress$OrderingPlugin.outboundItemSchema,
30
- inboundCommandSchema: GeocodeCustomerAddress$OrderingPlugin.inboundCommandSchema,
31
- collect: GeocodeCustomerAddress_Translation$OrderingPlugin.collect
21
+ traits: GeocodeCustomerAddress$OrderingPlugin.traits
22
+ })({
23
+ collect: GeocodeCustomerAddress_Translation$OrderingPlugin.collect,
24
+ translate: GeocodeCustomerAddress_Translation$OrderingPlugin.translate,
25
+ onExhausted: GeocodeCustomerAddress_Translation$OrderingPlugin.onExhausted,
26
+ moduleUrl: GeocodeCustomerAddress_Translation$OrderingPlugin.moduleUrl
32
27
  });
33
28
 
34
29
  let describe = include.describe;
@@ -37,10 +32,20 @@ let test = include.test;
37
32
 
38
33
  let givenTodo = include.givenTodo;
39
34
 
35
+ let fakes = include.fakes;
36
+
37
+ let givenCapabilities = include.givenCapabilities;
38
+
39
+ let whenTranslated = include.whenTranslated;
40
+
40
41
  let whenTranslateMocked = include.whenTranslateMocked;
41
42
 
43
+ let whenExhausted = include.whenExhausted;
44
+
42
45
  let thenCommand = include.thenCommand;
43
46
 
47
+ let thenSent = include.thenSent;
48
+
44
49
  let thenTodoStatus = include.thenTodoStatus;
45
50
 
46
51
  let vienna = {
@@ -48,18 +53,8 @@ let vienna = {
48
53
  lng: 16.3738
49
54
  };
50
55
 
51
- function withGeocoder(answer) {
52
- let capabilities_geocode = param => Promise.resolve(answer);
53
- let capabilities_messaging = Capabilities$Reventless.none.messaging;
54
- let capabilities_identityProvider = Capabilities$Reventless.none.identityProvider;
55
- let capabilities_secrets = Capabilities$Reventless.none.secrets;
56
- let capabilities = {
57
- geocode: capabilities_geocode,
58
- messaging: capabilities_messaging,
59
- identityProvider: capabilities_identityProvider,
60
- secrets: capabilities_secrets
61
- };
62
- return (id, item) => GeocodeCustomerAddress_Translation$OrderingPlugin.translate(id, item, capabilities);
56
+ function geocoder(answer) {
57
+ return fakes(async param => answer, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined);
63
58
  }
64
59
 
65
60
  describe("GeocodeCustomerAddress OutboundTranslationSlice", () => {
@@ -77,25 +72,25 @@ describe("GeocodeCustomerAddress OutboundTranslationSlice", () => {
77
72
  }
78
73
  ]
79
74
  })), "cust-1:Stephansplatz 1, Vienna", "Completed"));
80
- test("translate: a confident answer produces SetLocation", undefined, () => thenCommand(whenTranslateMocked(givenTodo("cust-1:Stephansplatz 1, Vienna", {
75
+ test("translate: a confident answer produces SetLocation", undefined, () => thenCommand(whenTranslated(givenCapabilities(givenTodo("cust-1:Stephansplatz 1, Vienna", {
81
76
  customerId: Ordering_Examples$OrderingPlugin.cust1,
82
77
  address: Ordering_Examples$OrderingPlugin.viennaAddress
83
- }), withGeocoder({
78
+ }), geocoder({
84
79
  TAG: "Ok",
85
80
  _0: [{
86
81
  label: "Stephansplatz 1, Vienna",
87
82
  point: vienna,
88
83
  relevance: 0.995
89
84
  }]
90
- })), "cust-1", {
85
+ }))), "cust-1", {
91
86
  TAG: "SetLocation",
92
87
  location: vienna,
93
88
  resolvedFrom: Ordering_Examples$OrderingPlugin.viennaAddress
94
89
  }));
95
- test("translate: an ambiguous answer produces a reason naming the candidates", undefined, () => thenCommand(whenTranslateMocked(givenTodo("cust-1:Springfield", {
90
+ test("translate: an ambiguous answer produces a reason naming the candidates", undefined, () => thenCommand(whenTranslated(givenCapabilities(givenTodo("cust-1:Springfield", {
96
91
  customerId: Ordering_Examples$OrderingPlugin.cust1,
97
92
  address: "Springfield"
98
- }), withGeocoder({
93
+ }), geocoder({
99
94
  TAG: "Ok",
100
95
  _0: [
101
96
  {
@@ -109,46 +104,96 @@ describe("GeocodeCustomerAddress OutboundTranslationSlice", () => {
109
104
  relevance: 0.985
110
105
  }
111
106
  ]
112
- })), "cust-1", {
107
+ }))), "cust-1", {
113
108
  TAG: "MarkAddressUnresolvable",
114
109
  address: "Springfield",
115
110
  reason: `"Springfield" matched "Springfield, IL" and "Springfield, MA" about equally well`
116
111
  }));
112
+ test("translate: the geocoder is asked for the address as written", undefined, () => thenSent(whenTranslated(givenCapabilities(givenTodo("cust-1:Stephansplatz 1, Vienna", {
113
+ customerId: Ordering_Examples$OrderingPlugin.cust1,
114
+ address: Ordering_Examples$OrderingPlugin.viennaAddress
115
+ }), geocoder({
116
+ TAG: "Ok",
117
+ _0: []
118
+ }))), [{
119
+ TAG: "Geocoded",
120
+ text: Ordering_Examples$OrderingPlugin.viennaAddress
121
+ }]));
122
+ test("translate: a geocoder that is down leaves the row to be retried", undefined, () => thenTodoStatus(whenTranslated(givenCapabilities(givenTodo("cust-1:Stephansplatz 1, Vienna", {
123
+ customerId: Ordering_Examples$OrderingPlugin.cust1,
124
+ address: Ordering_Examples$OrderingPlugin.viennaAddress
125
+ }), geocoder({
126
+ TAG: "Error",
127
+ _0: {
128
+ TAG: "Unavailable",
129
+ _0: "timeout"
130
+ }
131
+ }))), "cust-1:Stephansplatz 1, Vienna", "Failed"));
132
+ test("exhausted: the address is recorded as unresolvable", undefined, () => thenCommand(whenExhausted(givenTodo("cust-1:Stephansplatz 1, Vienna", {
133
+ customerId: Ordering_Examples$OrderingPlugin.cust1,
134
+ address: Ordering_Examples$OrderingPlugin.viennaAddress
135
+ }), "timeout"), "cust-1", {
136
+ TAG: "MarkAddressUnresolvable",
137
+ address: Ordering_Examples$OrderingPlugin.viennaAddress,
138
+ reason: AddressGeocoding_Translate$TraitAddressGeocoding.exhaustedReason("timeout")
139
+ }));
117
140
  });
118
141
 
119
- let Spec = include.Spec;
120
-
121
142
  let testSync = include.testSync;
122
143
 
144
+ let encItems = include.encItems;
145
+
146
+ let commandPairJson = include.commandPairJson;
147
+
148
+ let errorOf = include.errorOf;
149
+
123
150
  let givenEvent = include.givenEvent;
124
151
 
125
152
  let whenCollect = include.whenCollect;
126
153
 
127
154
  let thenTodos = include.thenTodos;
128
155
 
156
+ let attemptWith = include.attemptWith;
157
+
158
+ let attempts = include.attempts;
159
+
129
160
  let whenTranslateRetrying = include.whenTranslateRetrying;
130
161
 
131
162
  let thenNoCommand = include.thenNoCommand;
132
163
 
164
+ let thenNothingSent = include.thenNothingSent;
165
+
133
166
  let thenRetryRecorded = include.thenRetryRecorded;
134
167
 
168
+ let statusName = include.statusName;
169
+
135
170
  export {
136
- GeocodeCustomerAddressSlice,
137
- Spec,
138
171
  describe,
139
172
  test,
140
173
  testSync,
174
+ encItems,
175
+ commandPairJson,
176
+ errorOf,
141
177
  givenEvent,
142
178
  whenCollect,
143
179
  thenTodos,
144
180
  givenTodo,
181
+ fakes,
182
+ givenCapabilities,
183
+ attemptWith,
184
+ attempts,
185
+ whenTranslated,
145
186
  whenTranslateMocked,
146
187
  whenTranslateRetrying,
188
+ whenExhausted,
147
189
  thenCommand,
148
190
  thenNoCommand,
191
+ thenSent,
192
+ thenNothingSent,
149
193
  thenRetryRecorded,
194
+ statusName,
150
195
  thenTodoStatus,
151
196
  vienna,
152
- withGeocoder,
197
+ geocoder,
153
198
  }
154
199
  /* include Not a pure module */
@@ -6,66 +6,41 @@
6
6
  // predict, and this file would otherwise be able to check only that *something*
7
7
  // was sent.
8
8
 
9
- module VerifyCustomerEmailSlice = {
10
- include VerifyCustomerEmail
11
- let collect = VerifyCustomerEmail_Translation.collect
12
- }
13
-
14
9
  @@reventless.gwt
15
10
 
16
11
  module Outcome = ReventlessGwt.Outcome
17
12
 
18
13
  let item: VerifyCustomerEmail.outboundItem = {customerId: "c1", email: "alice@x.y"}
19
14
 
20
- // What the run put on the wire, in both directions: the bodies that went to the
21
- // address, and the commands that reached the ledger.
22
- let bodies: ref<array<string>> = ref([])
23
- let issued: ref<array<string>> = ref([])
24
-
25
15
  // A known secret and a hash obviously derived from it, so an assertion can name
26
16
  // both. `hashed:` is not what the real backend computes — what matters is that
27
17
  // whatever the source produced is what the ledger was told about.
28
- let withSecrets = (~send): Reventless.Capabilities.t => {
29
- ...Reventless.Capabilities.none,
30
- secrets: Reventless.Secrets.fixed(~token="known-secret", ~hash=t => `hashed:${t}`),
31
- messaging: Reventless.Messaging.makeProvider(~emailAndSms=[Email], ~pushServices=[], ~send),
32
- }
33
-
34
- // No secret source at all: `Capabilities.none` refuses, and messaging is wired
35
- // so the refusal cannot be mistaken for a mail problem.
36
- let withoutSecrets = (~send): Reventless.Capabilities.t => {
37
- ...Reventless.Capabilities.none,
38
- messaging: Reventless.Messaging.makeProvider(~emailAndSms=[Email], ~pushServices=[], ~send),
39
- }
40
-
41
- let accepts = (~recipient as _, ~message: Reventless.Messaging.message) => {
42
- bodies := bodies.contents->Array.concat([message.body])
43
- Promise.resolve(Ok({Reventless.Messaging.ref: "ses-1"}))
44
- }
45
-
46
- let refuses = failure => (~recipient as _, ~message as _) => Promise.resolve(Error(failure))
47
-
48
- // Records what reached the ledger, because the command carries an instant read
49
- // from the clock and so cannot be compared whole.
50
- let run = capabilities => {
51
- bodies := []
52
- issued := []
53
- async (id, item) => {
54
- let attempt = await VerifyCustomerEmail_Translation.translate(id, item, ~capabilities)
55
- switch attempt {
56
- | Ok(Some((_, IssueEmailChallenge({proofHash, email, purpose, _})))) =>
57
- issued := issued.contents->Array.concat([`${email}|${purpose}|${proofHash}`])
58
- | _ => ()
59
- }
60
- attempt
61
- }
62
- }
18
+ let withSecrets = (~send=?, ()) =>
19
+ fakes(~token="known-secret", ~hash=t => `hashed:${t}`, ~channels=[Email], ~send?, ())
20
+
21
+ // No secret source at all, and messaging wired so the refusal cannot be
22
+ // mistaken for a mail problem.
23
+ let withoutSecrets = () =>
24
+ fakes(~secrets=Reventless.Secrets.unavailable(~reason="none provisioned"), ~channels=[Email], ())
25
+
26
+ let refuses = failure => async (~recipient as _, ~message as _) => Error(failure)
63
27
 
64
- let alsoThat = async (pending, ~expected, holds) =>
65
- switch await pending {
66
- | Error(_) as failed => failed
67
- | Ok() =>
68
- holds() ? Outcome.pass : Outcome.fail(TranslateError({expected, actual: Some("it did not")}))
28
+ // The challenge mail, as the slice words it for this secret.
29
+ let challengeMail: ReventlessGwt.Capabilities_Fake.call = Sent({
30
+ recipient: ContactVerification.ByEmail("alice@x.y")->ContactVerification.toRecipient,
31
+ message: {subject: "Confirm your email address", body: body(~token="known-secret")},
32
+ })
33
+
34
+ // What reached the ledger. The command carries an instant read from the clock,
35
+ // so it cannot be compared whole.
36
+ let thenLedgerTold = async (pending, expected) =>
37
+ switch (await pending).result {
38
+ | Ok(Some((_, IssueEmailChallenge({proofHash, email, purpose, _})))) =>
39
+ let actual = `${email}|${purpose}|${proofHash}`
40
+ actual == expected
41
+ ? Outcome.pass
42
+ : Outcome.fail(TranslateError({expected, actual: Some(actual)}))
43
+ | _ => Outcome.fail(TranslateError({expected, actual: None}))
69
44
  }
70
45
 
71
46
  describe("VerifyCustomerEmail OutboundTranslationSlice", () => {
@@ -74,23 +49,21 @@ describe("VerifyCustomerEmail OutboundTranslationSlice", () => {
74
49
  // scenario-id: 866cd8c0-c30b-4049-85e1-98359a252c49
75
50
  test("a sent challenge is opened against the hash, never the secret", () =>
76
51
  givenTodo("c1:alice@x.y", item)
77
- ->whenTranslateMocked(run(withSecrets(~send=accepts)))
78
- ->thenTodoStatus("c1:alice@x.y", #Completed)
79
- ->alsoThat(
80
- ~expected="the ledger is told alice@x.y|ContactChange|hashed:known-secret",
81
- () => issued.contents == ["alice@x.y|ContactChange|hashed:known-secret"],
82
- )
52
+ ->givenCapabilities(withSecrets())
53
+ ->whenTranslated
54
+ ->thenLedgerTold("alice@x.y|ContactChange|hashed:known-secret")
83
55
  )
84
56
 
85
57
  // scenario-id: dea4775c-9c1f-41e2-bd54-751b308c65cd
86
58
  test("the secret itself is what goes to the address", () =>
87
59
  givenTodo("c1:alice@x.y", item)
88
- ->whenTranslateMocked(run(withSecrets(~send=accepts)))
89
- ->thenTodoStatus("c1:alice@x.y", #Completed)
90
- ->alsoThat(
91
- ~expected="the message carries known-secret",
92
- () => bodies.contents->Array.some(body => body->String.includes("known-secret")),
93
- )
60
+ ->givenCapabilities(withSecrets())
61
+ ->whenTranslated
62
+ ->thenSent([
63
+ TokenDrawn({length: policy.proofLength, alphabet: policy.proofAlphabet}),
64
+ Hashed({input: "known-secret"}),
65
+ challengeMail,
66
+ ])
94
67
  )
95
68
 
96
69
  // 🚨 Send first, then record. A challenge opened against a message that never
@@ -99,19 +72,21 @@ describe("VerifyCustomerEmail OutboundTranslationSlice", () => {
99
72
  // scenario-id: 7de712d8-9930-41a2-977d-137639e38812
100
73
  test("a refused send opens no challenge", () =>
101
74
  givenTodo("c1:alice@x.y", item)
102
- ->whenTranslateMocked(
103
- run(withSecrets(~send=refuses(Reventless.Messaging.Refused("no such mailbox")))),
75
+ ->givenCapabilities(
76
+ withSecrets(~send=refuses(Reventless.Messaging.Refused("no such mailbox")), ()),
104
77
  )
105
- ->thenTodoStatus("c1:alice@x.y", #Pending)
78
+ ->whenTranslated
79
+ ->thenTodoStatus("c1:alice@x.y", #Failed)
106
80
  )
107
81
 
108
82
  // scenario-id: a04b17e9-d691-40d3-a431-894a1c75e9f2
109
83
  test("an unreachable provider opens no challenge either", () =>
110
84
  givenTodo("c1:alice@x.y", item)
111
- ->whenTranslateMocked(
112
- run(withSecrets(~send=refuses(Reventless.Messaging.Unavailable("smtp timeout")))),
85
+ ->givenCapabilities(
86
+ withSecrets(~send=refuses(Reventless.Messaging.Unavailable("smtp timeout")), ()),
113
87
  )
114
- ->thenTodoStatus("c1:alice@x.y", #Pending)
88
+ ->whenTranslated
89
+ ->thenTodoStatus("c1:alice@x.y", #Failed)
115
90
  )
116
91
 
117
92
  // A deployment with no secret source cannot prove anything. That is a gap in
@@ -120,16 +95,17 @@ describe("VerifyCustomerEmail OutboundTranslationSlice", () => {
120
95
  // scenario-id: 70d4d062-1ba5-4d77-8e27-8af0f98d4bd8
121
96
  test("no secret source opens no challenge", () =>
122
97
  givenTodo("c1:alice@x.y", item)
123
- ->whenTranslateMocked(run(withoutSecrets(~send=accepts)))
124
- ->thenTodoStatus("c1:alice@x.y", #Pending)
98
+ ->givenCapabilities(withoutSecrets())
99
+ ->whenTranslated
100
+ ->thenTodoStatus("c1:alice@x.y", #Failed)
125
101
  )
126
102
 
127
103
  // scenario-id: 29f4868d-b099-401f-9609-913f6c55bddf
128
104
  test("no secret source sends nothing either", () =>
129
105
  givenTodo("c1:alice@x.y", item)
130
- ->whenTranslateMocked(run(withoutSecrets(~send=accepts)))
131
- ->thenTodoStatus("c1:alice@x.y", #Pending)
132
- ->alsoThat(~expected="no message was sent", () => bodies.contents == [])
106
+ ->givenCapabilities(withoutSecrets())
107
+ ->whenTranslated
108
+ ->thenSent([TokenDrawn({length: policy.proofLength, alphabet: policy.proofAlphabet})])
133
109
  )
134
110
  })
135
111