@reventlessdev/reventless-gwt 1.0.0-alpha.254 → 1.0.0-alpha.256

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,32 @@
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.256 (2026-10-25)
7
+
8
+ * refactor(spec)!: the plugin structure's optional parts are optional fields ([130bad5](https://github.com/ReventlessDev/reventless-core/commit/130bad581caf725a9d61b5a3df3d012e4f8ba402))
9
+ * feat!: an inbound translation is async and handed the platform's capabilities, and its audit row withholds sensitive fields ([dae6049](https://github.com/ReventlessDev/reventless-core/commit/dae6049e084f81408a494a2cd2011ec564dda65c))
10
+ ### Features
11
+
12
+ * a stash capability holds a value for minutes outside the log and hands it over once ([627f285](https://github.com/ReventlessDev/reventless-core/commit/627f28529ead2d2d31bdd091c9f42e7faca40f6f))
13
+
14
+ ### BREAKING CHANGES
15
+
16
+ * code that builds these records drops `name: None`, writes
17
+ `name: x` for `name: Some(x)`, and `name: ?x` for a value already of type
18
+ option.
19
+ * an inbound translation's translate takes ~capabilities and
20
+ returns a promise, and its spec must declare capabilityNeeds ([] for one that
21
+ only maps input). FromSlice steps return promises.
22
+
23
+
24
+
25
+ # 1.0.0-alpha.255 (2026-10-23)
26
+
27
+ ### Bug Fixes
28
+
29
+ * **gwt:** a recorded sidecar is written only when it says something new ([a8f3cb9](https://github.com/ReventlessDev/reventless-core/commit/a8f3cb926d779f6a0c763196f73da5df102ca31e))
30
+
31
+
6
32
  # 1.0.0-alpha.254 (2026-10-22)
7
33
 
8
34
  **Note:** Version bump only for package @reventlessdev/reventless-gwt
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-gwt",
3
- "version": "1.0.0-alpha.254",
3
+ "version": "1.0.0-alpha.256",
4
4
  "description": "Given-When-Then DSLs and test harness for Reventless slice testing",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -14,12 +14,12 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "sury": "11.0.0",
17
+ "@reventlessdev/rescript-effect": "1.0.0-alpha.1",
17
18
  "@reventlessdev/rescript-node": "2.0.0-alpha.23",
18
19
  "@reventlessdev/rescript-jest": "1.0.0-alpha.13",
19
- "@reventlessdev/reventless-core": "3.0.0-alpha.306",
20
- "@reventlessdev/reventless-infra": "3.0.0-alpha.203",
21
- "@reventlessdev/rescript-effect": "1.0.0-alpha.1",
22
- "@reventlessdev/reventless-spec": "3.0.0-alpha.175"
20
+ "@reventlessdev/reventless-core": "3.0.0-alpha.308",
21
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.205",
22
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.176"
23
23
  },
24
24
  "devDependencies": {
25
25
  "rescript": "12.3.1",
@@ -7,6 +7,7 @@ module Geocoding = Reventless.Geocoding
7
7
  module Messaging = Reventless.Messaging
8
8
  module IdentityProvider = Reventless.IdentityProvider
9
9
  module Secrets = Reventless.Secrets
10
+ module Stash = Reventless.Stash
10
11
 
11
12
  /** One call a slice made through its capabilities. */
12
13
  type call =
@@ -22,6 +23,10 @@ type call =
22
23
  | PrincipalDeleted({principal: IdentityProvider.principal})
23
24
  | TokenDrawn({length: int, alphabet: Secrets.alphabet})
24
25
  | Hashed({input: string})
26
+ /** A value put in the stash. The value itself is recorded, so a test can
27
+ assert which secret was handed over — a fake has no log to keep it out of. */
28
+ | Stashed({key: string, value: string, ttl: Reventless.Duration.t})
29
+ | Taken({key: string})
25
30
 
26
31
  type t = {
27
32
  capabilities: Capabilities.t,
@@ -34,7 +39,9 @@ Fakes whose answers are given per capability. An answer left out is a plain
34
39
  success: a geocoder with no candidates, a send with a numbered receipt, a
35
40
  principal with a numbered id, `token` (or `t` repeated to length), and a hash
36
41
  that prefixes `"hash:"`. `~secrets` replaces the secret source whole, e.g. with
37
- `Secrets.unavailable`; its calls are still recorded.
42
+ `Secrets.unavailable`; its calls are still recorded. The stash holds what is put
43
+ in it and hands each value over once, starting from `~stashed`; `~stash`
44
+ replaces it whole, e.g. with `Stash.unavailable`.
38
45
  */
39
46
  let make = (
40
47
  ~geocode: option<Geocoding.search>=?,
@@ -51,6 +58,8 @@ let make = (
51
58
  ~token: option<string>=?,
52
59
  ~hash: option<string => string>=?,
53
60
  ~secrets: option<Secrets.t>=?,
61
+ ~stashed: array<(string, string)>=[],
62
+ ~stash: option<Stash.t>=?,
54
63
  (),
55
64
  ): t => {
56
65
  let log: array<call> = []
@@ -76,6 +85,7 @@ let make = (
76
85
  | None => Ok({IdentityProvider.providerId: `principal-${Int.toString(log->Array.length)}`})
77
86
  }
78
87
  }
88
+ let held = Dict.fromArray(stashed)
79
89
  let capabilities: Capabilities.t = {
80
90
  geocode,
81
91
  messaging: Messaging.makeProvider(~emailAndSms=channels, ~pushServices, ~send),
@@ -111,6 +121,27 @@ let make = (
111
121
  }
112
122
  },
113
123
  },
124
+ stash: {
125
+ put: async (~key, ~value, ~ttl) => {
126
+ record(Stashed({key, value, ttl}))
127
+ switch stash {
128
+ | Some(s) => await s.put(~key, ~value, ~ttl)
129
+ | None =>
130
+ held->Dict.set(key, value)
131
+ Ok()
132
+ }
133
+ },
134
+ take: async (~key) => {
135
+ record(Taken({key: key}))
136
+ switch stash {
137
+ | Some(s) => await s.take(~key)
138
+ | None =>
139
+ let found = held->Dict.get(key)
140
+ held->Dict.delete(key)
141
+ Ok(found)
142
+ }
143
+ },
144
+ },
114
145
  }
115
146
  {capabilities, calls: () => log->Array.copy}
116
147
  }
@@ -153,5 +184,7 @@ let toJson = (c: call): JSON.t => {
153
184
  obj("PrincipalDeleted", [("principal", str(principal.providerId))])
154
185
  | TokenDrawn({length, _}) => obj("TokenDrawn", [("length", JSON.Encode.int(length))])
155
186
  | Hashed({input}) => obj("Hashed", [("input", str(input))])
187
+ | Stashed({key, ttl, _}) => obj("Stashed", [("key", str(key)), ("ttl", JSON.Encode.int(ttl))])
188
+ | Taken({key}) => obj("Taken", [("key", str(key))])
156
189
  }
157
190
  }
@@ -1,16 +1,18 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Belt_Array from "@rescript/runtime/lib/es6/Belt_Array.js";
4
+ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
4
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
6
  import * as Messaging$Reventless from "@reventlessdev/reventless-spec/src/semantic/Messaging.res.mjs";
6
7
  import * as IdentityProvider$Reventless from "@reventlessdev/reventless-spec/src/semantic/IdentityProvider.res.mjs";
7
8
 
8
- function make(geocode, send, channelsOpt, pushServicesOpt, createPrincipal, token, hash, secrets, param) {
9
+ function make(geocode, send, channelsOpt, pushServicesOpt, createPrincipal, token, hash, secrets, stashedOpt, stash, param) {
9
10
  let channels = channelsOpt !== undefined ? channelsOpt : [
10
11
  "Email",
11
12
  "Sms"
12
13
  ];
13
14
  let pushServices = pushServicesOpt !== undefined ? pushServicesOpt : [];
15
+ let stashed = stashedOpt !== undefined ? stashedOpt : [];
14
16
  let log = [];
15
17
  let geocode$1 = async text => {
16
18
  log.push({
@@ -61,6 +63,7 @@ function make(geocode, send, channelsOpt, pushServicesOpt, createPrincipal, toke
61
63
  };
62
64
  }
63
65
  };
66
+ let held = Object.fromEntries(stashed);
64
67
  let capabilities_messaging = Messaging$Reventless.makeProvider(channels, pushServices, send$1);
65
68
  let capabilities_identityProvider = IdentityProvider$Reventless.make(createPrincipal$1, async (principal, group) => {
66
69
  log.push({
@@ -128,11 +131,46 @@ function make(geocode, send, channelsOpt, pushServicesOpt, createPrincipal, toke
128
131
  }
129
132
  }
130
133
  };
134
+ let capabilities_stash = {
135
+ put: async (key, value, ttl) => {
136
+ log.push({
137
+ TAG: "Stashed",
138
+ key: key,
139
+ value: value,
140
+ ttl: ttl
141
+ });
142
+ if (stash !== undefined) {
143
+ return await stash.put(key, value, ttl);
144
+ } else {
145
+ held[key] = value;
146
+ return {
147
+ TAG: "Ok",
148
+ _0: undefined
149
+ };
150
+ }
151
+ },
152
+ take: async key => {
153
+ log.push({
154
+ TAG: "Taken",
155
+ key: key
156
+ });
157
+ if (stash !== undefined) {
158
+ return await stash.take(key);
159
+ }
160
+ let found = held[key];
161
+ Stdlib_Dict.$$delete(held, key);
162
+ return {
163
+ TAG: "Ok",
164
+ _0: found
165
+ };
166
+ }
167
+ };
131
168
  let capabilities = {
132
169
  geocode: geocode$1,
133
170
  messaging: capabilities_messaging,
134
171
  identityProvider: capabilities_identityProvider,
135
- secrets: capabilities_secrets
172
+ secrets: capabilities_secrets,
173
+ stash: capabilities_stash
136
174
  };
137
175
  return {
138
176
  capabilities: capabilities,
@@ -229,6 +267,22 @@ function toJson(c) {
229
267
  "input",
230
268
  c.input
231
269
  ]]);
270
+ case "Stashed" :
271
+ return obj("Stashed", [
272
+ [
273
+ "key",
274
+ c.key
275
+ ],
276
+ [
277
+ "ttl",
278
+ c.ttl
279
+ ]
280
+ ]);
281
+ case "Taken" :
282
+ return obj("Taken", [[
283
+ "key",
284
+ c.key
285
+ ]]);
232
286
  }
233
287
  }
234
288
 
@@ -242,12 +296,15 @@ let IdentityProvider;
242
296
 
243
297
  let Secrets;
244
298
 
299
+ let Stash;
300
+
245
301
  export {
246
302
  Capabilities,
247
303
  Geocoding,
248
304
  Messaging,
249
305
  IdentityProvider,
250
306
  Secrets,
307
+ Stash,
251
308
  make,
252
309
  toJson,
253
310
  }
package/src/Flow_GWT.res CHANGED
@@ -680,9 +680,9 @@ module InboundSlice = (
680
680
  module I = InboundTranslation_GWT.FromSlice(Spec, Translation)
681
681
 
682
682
  // Received input becomes the flow's next commands, as an automation's do.
683
- let whenReceived = async (flowP: flow, json: JSON.t) => {
683
+ let whenReceived = async (flowP: flow, json: JSON.t, ~fakes=?) => {
684
684
  let s = await flowP
685
- switch I.whenReceived(json) {
685
+ switch (await I.whenReceived(json, ~fakes?)).outcome {
686
686
  | Translated(pairs) => {
687
687
  ...s,
688
688
  lastCommands: pairs->Array.map(((_id, cmd)) => cmd->Message.encode(Spec.commandSchema)),
@@ -582,9 +582,9 @@ function OutboundSlice(Spec) {
582
582
  function InboundSlice(Spec) {
583
583
  return Translation => {
584
584
  let I = InboundTranslation_GWT$ReventlessGwt.FromSlice(Spec)(Translation);
585
- let whenReceived = async (flowP, json) => {
585
+ let whenReceived = async (flowP, json, fakes) => {
586
586
  let s = await flowP;
587
- let pairs = I.whenReceived(json);
587
+ let pairs = (await I.whenReceived(json, fakes, undefined)).outcome;
588
588
  switch (pairs.TAG) {
589
589
  case "Translated" :
590
590
  return {
@@ -77,21 +77,29 @@ module Make = (Spec: SliceSpec): (T with module Spec = Spec) => {
77
77
  }
78
78
  }
79
79
 
80
- // The slice as written: its `Spec` and its `_Translation` body.
80
+ // The slice as written: its `Spec` and its `_Translation` body, run against
81
+ // recording capability fakes as the outbound DSL's `FromSlice` is.
81
82
  module FromSlice = (
82
83
  Spec: Reventless.InboundTranslationSlice.Spec,
83
84
  Translation: Reventless.InboundTranslationSlice.Translation with module Spec := Spec,
84
85
  ) => {
85
86
  let describe = JestBind.describe
86
- let test = (name, body) => JestBind.test(~slice=Spec.name, name, body)
87
+ let test = (name, ~timeout=?, body) =>
88
+ JestBind.testPromise(~slice=Spec.name, name, ~timeout?, body)
87
89
 
88
90
  /** What became of one input. `InputRefused` is input that did not decode, so
89
91
  `translate` never saw it; `NotUnderstood` is input it declined. */
90
- type received =
92
+ type outcome =
91
93
  | Translated(array<(string, Spec.command)>)
92
94
  | NotUnderstood(string)
93
95
  | InputRefused(string)
94
96
 
97
+ /** An outcome, and the capability calls `translate` made reaching it. */
98
+ type received = {outcome: outcome, sent: array<Capabilities_Fake.call>}
99
+
100
+ /** Scripted capabilities for `whenInput(~fakes)`; see `Capabilities_Fake.make`. */
101
+ let fakes = Capabilities_Fake.make
102
+
95
103
  let encPairs = (pairs: array<(string, Spec.command)>) =>
96
104
  pairs->Array.map(((id, cmd)) => {
97
105
  let d = Dict.make()
@@ -100,23 +108,49 @@ module FromSlice = (
100
108
  JSON.Encode.object(d)
101
109
  })
102
110
 
103
- let whenInput = (input: Spec.externalInput) =>
104
- switch Translation.translate(input) {
111
+ let errorOf = exn =>
112
+ switch exn {
113
+ | S.Exn(e) => e.message
114
+ | JsExn(e) => JsExn.message(e)->Option.getOr("unknown error")
115
+ | _ => "unknown error"
116
+ }
117
+
118
+ /** One input through the slice's own `translate`. A throw is input the slice
119
+ declined, as the runtime records it. `~caller` is who called the door, as
120
+ the transport would hand it on; left out, the platform is acting for
121
+ itself. */
122
+ let whenInput = async (
123
+ input: Spec.externalInput,
124
+ ~fakes=Capabilities_Fake.make(),
125
+ ~caller: option<Reventless.Identity.t>=?,
126
+ ) => {
127
+ let outcome = switch await Translation.translate(
128
+ input,
129
+ ~capabilities=fakes.capabilities,
130
+ ~caller,
131
+ ) {
105
132
  | Ok(pairs) => Translated(pairs)
106
133
  | Error(msg) => NotUnderstood(msg)
134
+ | exception exn => NotUnderstood(errorOf(exn))
107
135
  }
136
+ {outcome, sent: fakes.calls()}
137
+ }
108
138
 
109
139
  /** Input as it arrives: decoded through `externalInputSchema` first. */
110
- let whenReceived = (json: JSON.t) =>
140
+ let whenReceived = async (
141
+ json: JSON.t,
142
+ ~fakes=Capabilities_Fake.make(),
143
+ ~caller: option<Reventless.Identity.t>=?,
144
+ ) =>
111
145
  switch json->Reventless.Util_Sury.fromJson(Spec.externalInputSchema) {
112
- | input => whenInput(input)
146
+ | input => await whenInput(input, ~fakes, ~caller?)
113
147
  // Sury's message names the field that failed.
114
- | exception S.Exn(e) => InputRefused(e.message)
115
- | exception _ => InputRefused("invalid input")
148
+ | exception S.Exn(e) => {outcome: InputRefused(e.message), sent: fakes.calls()}
149
+ | exception _ => {outcome: InputRefused("invalid input"), sent: fakes.calls()}
116
150
  }
117
151
 
118
- let describeOther = received =>
119
- switch received {
152
+ let describeOther = outcome =>
153
+ switch outcome {
120
154
  | Translated(_) => None
121
155
  | NotUnderstood(msg) => Some(`not understood: ${msg}`)
122
156
  | InputRefused(msg) => Some(`input refused: ${msg}`)
@@ -131,8 +165,8 @@ module FromSlice = (
131
165
  | exception _ => false
132
166
  }
133
167
 
134
- let thenCommands = (received, expected) =>
135
- switch received {
168
+ let thenCommands = async (pending: promise<received>, expected) =>
169
+ switch (await pending).outcome {
136
170
  | Translated(actual) if actual == expected =>
137
171
  switch actual->Array.find(((_, cmd)) => !roundTrips(cmd)) {
138
172
  | None => Outcome.pass
@@ -149,19 +183,19 @@ module FromSlice = (
149
183
  | other => Outcome.fail(TranslateError({expected: "(commands)", actual: describeOther(other)}))
150
184
  }
151
185
 
152
- let thenCommand = (received, expectedId, expectedCmd) =>
153
- thenCommands(received, [(expectedId, expectedCmd)])
186
+ let thenCommand = (pending, expectedId, expectedCmd) =>
187
+ thenCommands(pending, [(expectedId, expectedCmd)])
154
188
 
155
- let thenNoCommand = received =>
156
- switch received {
189
+ let thenNoCommand = async (pending: promise<received>) =>
190
+ switch (await pending).outcome {
157
191
  | Translated([]) => Outcome.pass
158
192
  | Translated(actual) => Outcome.fail(NoEventExpected({actual: encPairs(actual)}))
159
193
  | other =>
160
194
  Outcome.fail(TranslateError({expected: "(no commands)", actual: describeOther(other)}))
161
195
  }
162
196
 
163
- let thenNotUnderstood = (received, expectedMsg) =>
164
- switch received {
197
+ let thenNotUnderstood = async (pending: promise<received>, expectedMsg) =>
198
+ switch (await pending).outcome {
165
199
  | NotUnderstood(actual) if actual == expectedMsg => Outcome.pass
166
200
  | NotUnderstood(actual) =>
167
201
  Outcome.fail(TranslateError({expected: expectedMsg, actual: Some(actual)}))
@@ -171,8 +205,8 @@ module FromSlice = (
171
205
 
172
206
  /** Passes when the input was refused and the reason contains `reason`: the
173
207
  decoder words its own messages, so a test pins the part that matters. */
174
- let thenRefusedInput = (received, reason) =>
175
- switch received {
208
+ let thenRefusedInput = async (pending: promise<received>, reason) =>
209
+ switch (await pending).outcome {
176
210
  | InputRefused(actual) if actual->String.includes(reason) => Outcome.pass
177
211
  | other =>
178
212
  Outcome.fail(
@@ -185,4 +219,18 @@ module FromSlice = (
185
219
  }),
186
220
  )
187
221
  }
222
+
223
+ /** The capability calls `translate` made, in order. */
224
+ let thenSent = async (pending: promise<received>, expected: array<Capabilities_Fake.call>) => {
225
+ let {sent, _} = await pending
226
+ sent == expected
227
+ ? Outcome.pass
228
+ : Outcome.fail(
229
+ EventsMismatch({
230
+ expected: expected->Array.map(Capabilities_Fake.toJson),
231
+ actual: sent->Array.map(Capabilities_Fake.toJson),
232
+ }),
233
+ )
234
+ }
235
+ let thenNothingSent = pending => thenSent(pending, [])
188
236
  }
@@ -1,6 +1,7 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as S from "sury/src/S.res.mjs";
4
+ import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
4
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
6
  import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
6
7
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
@@ -8,6 +9,7 @@ import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/U
8
9
  import * as Outcome$ReventlessGwt from "./Outcome.res.mjs";
9
10
  import * as JestBind$ReventlessGwt from "./JestBind.res.mjs";
10
11
  import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
12
+ import * as Capabilities_Fake$ReventlessGwt from "./Capabilities_Fake.res.mjs";
11
13
 
12
14
  function Make(Spec) {
13
15
  let test = (name, body) => JestBind$ReventlessGwt.test(Spec.name, name, body);
@@ -92,28 +94,53 @@ function Make(Spec) {
92
94
 
93
95
  function FromSlice(Spec) {
94
96
  return Translation => {
95
- let test = (name, body) => JestBind$ReventlessGwt.test(Spec.name, name, body);
97
+ let test = (name, timeout, body) => JestBind$ReventlessGwt.testPromise(Spec.name, name, timeout, body);
96
98
  let encPairs = pairs => pairs.map(param => {
97
99
  let d = {};
98
100
  d["id"] = param[0];
99
101
  d["command"] = Message$ReventlessCore.encode(param[1], Spec.commandSchema);
100
102
  return d;
101
103
  });
102
- let whenInput = input => {
103
- let pairs = Translation.translate(input);
104
- if (pairs.TAG === "Ok") {
105
- return {
106
- TAG: "Translated",
107
- _0: pairs._0
108
- };
104
+ let errorOf = exn => {
105
+ if (exn.RE_EXN_ID === S.Exn) {
106
+ return exn._1.message;
107
+ } else if (exn.RE_EXN_ID === "JsExn") {
108
+ return Stdlib_Option.getOr(Stdlib_JsExn.message(exn._1), "unknown error");
109
109
  } else {
110
- return {
110
+ return "unknown error";
111
+ }
112
+ };
113
+ let whenInput = async (input, fakesOpt, caller) => {
114
+ let fakes = fakesOpt !== undefined ? fakesOpt : Capabilities_Fake$ReventlessGwt.make(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined);
115
+ let outcome;
116
+ let exit = 0;
117
+ let pairs;
118
+ try {
119
+ pairs = await Translation.translate(input, fakes.capabilities, caller);
120
+ exit = 1;
121
+ } catch (raw_exn) {
122
+ let exn = Primitive_exceptions.internalToException(raw_exn);
123
+ outcome = {
111
124
  TAG: "NotUnderstood",
112
- _0: pairs._0
125
+ _0: errorOf(exn)
113
126
  };
114
127
  }
128
+ if (exit === 1) {
129
+ outcome = pairs.TAG === "Ok" ? ({
130
+ TAG: "Translated",
131
+ _0: pairs._0
132
+ }) : ({
133
+ TAG: "NotUnderstood",
134
+ _0: pairs._0
135
+ });
136
+ }
137
+ return {
138
+ outcome: outcome,
139
+ sent: fakes.calls()
140
+ };
115
141
  };
116
- let whenReceived = json => {
142
+ let whenReceived = async (json, fakesOpt, caller) => {
143
+ let fakes = fakesOpt !== undefined ? fakesOpt : Capabilities_Fake$ReventlessGwt.make(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined);
117
144
  let input;
118
145
  try {
119
146
  input = Util_Sury$Reventless.fromJson(json, Spec.externalInputSchema);
@@ -121,26 +148,32 @@ function FromSlice(Spec) {
121
148
  let e = Primitive_exceptions.internalToException(raw_e);
122
149
  if (e.RE_EXN_ID === S.Exn) {
123
150
  return {
124
- TAG: "InputRefused",
125
- _0: e._1.message
151
+ outcome: {
152
+ TAG: "InputRefused",
153
+ _0: e._1.message
154
+ },
155
+ sent: fakes.calls()
126
156
  };
127
157
  } else {
128
158
  return {
129
- TAG: "InputRefused",
130
- _0: "invalid input"
159
+ outcome: {
160
+ TAG: "InputRefused",
161
+ _0: "invalid input"
162
+ },
163
+ sent: fakes.calls()
131
164
  };
132
165
  }
133
166
  }
134
- return whenInput(input);
167
+ return await whenInput(input, fakes, caller);
135
168
  };
136
- let describeOther = received => {
137
- switch (received.TAG) {
169
+ let describeOther = outcome => {
170
+ switch (outcome.TAG) {
138
171
  case "Translated" :
139
172
  return;
140
173
  case "NotUnderstood" :
141
- return `not understood: ` + received._0;
174
+ return `not understood: ` + outcome._0;
142
175
  case "InputRefused" :
143
- return `input refused: ` + received._0;
176
+ return `input refused: ` + outcome._0;
144
177
  }
145
178
  };
146
179
  let roundTrips = cmd => {
@@ -152,18 +185,19 @@ function FromSlice(Spec) {
152
185
  }
153
186
  return Primitive_object.equal(back, cmd);
154
187
  };
155
- let thenCommands = (received, expected) => {
156
- switch (received.TAG) {
188
+ let thenCommands = async (pending, expected) => {
189
+ let actual = (await pending).outcome;
190
+ switch (actual.TAG) {
157
191
  case "Translated" :
158
- let actual = received._0;
159
- if (!Primitive_object.equal(actual, expected)) {
192
+ let actual$1 = actual._0;
193
+ if (!Primitive_object.equal(actual$1, expected)) {
160
194
  return Outcome$ReventlessGwt.fail({
161
195
  TAG: "EventsMismatch",
162
196
  expected: encPairs(expected),
163
- actual: encPairs(actual)
197
+ actual: encPairs(actual$1)
164
198
  });
165
199
  }
166
- let pair = actual.find(param => !roundTrips(param[1]));
200
+ let pair = actual$1.find(param => !roundTrips(param[1]));
167
201
  if (pair !== undefined) {
168
202
  return Outcome$ReventlessGwt.fail({
169
203
  TAG: "TranslateError",
@@ -178,22 +212,23 @@ function FromSlice(Spec) {
178
212
  return Outcome$ReventlessGwt.fail({
179
213
  TAG: "TranslateError",
180
214
  expected: "(commands)",
181
- actual: describeOther(received)
215
+ actual: describeOther(actual)
182
216
  });
183
217
  }
184
218
  };
185
- let thenCommand = (received, expectedId, expectedCmd) => thenCommands(received, [[
219
+ let thenCommand = (pending, expectedId, expectedCmd) => thenCommands(pending, [[
186
220
  expectedId,
187
221
  expectedCmd
188
222
  ]]);
189
- let thenNoCommand = received => {
190
- switch (received.TAG) {
223
+ let thenNoCommand = async pending => {
224
+ let actual = (await pending).outcome;
225
+ switch (actual.TAG) {
191
226
  case "Translated" :
192
- let actual = received._0;
193
- if (actual.length !== 0) {
227
+ let actual$1 = actual._0;
228
+ if (actual$1.length !== 0) {
194
229
  return Outcome$ReventlessGwt.fail({
195
230
  TAG: "NoEventExpected",
196
- actual: encPairs(actual)
231
+ actual: encPairs(actual$1)
197
232
  });
198
233
  } else {
199
234
  return Outcome$ReventlessGwt.pass;
@@ -203,21 +238,22 @@ function FromSlice(Spec) {
203
238
  return Outcome$ReventlessGwt.fail({
204
239
  TAG: "TranslateError",
205
240
  expected: "(no commands)",
206
- actual: describeOther(received)
241
+ actual: describeOther(actual)
207
242
  });
208
243
  }
209
244
  };
210
- let thenNotUnderstood = (received, expectedMsg) => {
211
- switch (received.TAG) {
245
+ let thenNotUnderstood = async (pending, expectedMsg) => {
246
+ let actual = (await pending).outcome;
247
+ switch (actual.TAG) {
212
248
  case "NotUnderstood" :
213
- let actual = received._0;
214
- if (actual === expectedMsg) {
249
+ let actual$1 = actual._0;
250
+ if (actual$1 === expectedMsg) {
215
251
  return Outcome$ReventlessGwt.pass;
216
252
  } else {
217
253
  return Outcome$ReventlessGwt.fail({
218
254
  TAG: "TranslateError",
219
255
  expected: expectedMsg,
220
- actual: actual
256
+ actual: actual$1
221
257
  });
222
258
  }
223
259
  case "Translated" :
@@ -225,29 +261,30 @@ function FromSlice(Spec) {
225
261
  return Outcome$ReventlessGwt.fail({
226
262
  TAG: "TranslateError",
227
263
  expected: expectedMsg,
228
- actual: describeOther(received)
264
+ actual: describeOther(actual)
229
265
  });
230
266
  }
231
267
  };
232
- let thenRefusedInput = (received, reason) => {
233
- switch (received.TAG) {
268
+ let thenRefusedInput = async (pending, reason) => {
269
+ let actual = (await pending).outcome;
270
+ switch (actual.TAG) {
234
271
  case "Translated" :
235
272
  case "NotUnderstood" :
236
273
  break;
237
274
  case "InputRefused" :
238
- if (received._0.includes(reason)) {
275
+ if (actual._0.includes(reason)) {
239
276
  return Outcome$ReventlessGwt.pass;
240
277
  }
241
278
  break;
242
279
  }
243
280
  let tmp;
244
- switch (received.TAG) {
281
+ switch (actual.TAG) {
245
282
  case "Translated" :
246
283
  tmp = "translated";
247
284
  break;
248
285
  case "NotUnderstood" :
249
286
  case "InputRefused" :
250
- tmp = describeOther(received);
287
+ tmp = describeOther(actual);
251
288
  break;
252
289
  }
253
290
  return Outcome$ReventlessGwt.fail({
@@ -256,10 +293,26 @@ function FromSlice(Spec) {
256
293
  actual: tmp
257
294
  });
258
295
  };
296
+ let thenSent = async (pending, expected) => {
297
+ let match = await pending;
298
+ let sent = match.sent;
299
+ if (Primitive_object.equal(sent, expected)) {
300
+ return Outcome$ReventlessGwt.pass;
301
+ } else {
302
+ return Outcome$ReventlessGwt.fail({
303
+ TAG: "EventsMismatch",
304
+ expected: expected.map(Capabilities_Fake$ReventlessGwt.toJson),
305
+ actual: sent.map(Capabilities_Fake$ReventlessGwt.toJson)
306
+ });
307
+ }
308
+ };
309
+ let thenNothingSent = pending => thenSent(pending, []);
259
310
  return {
260
311
  describe: JestBind$ReventlessGwt.describe,
261
312
  test: test,
313
+ fakes: Capabilities_Fake$ReventlessGwt.make,
262
314
  encPairs: encPairs,
315
+ errorOf: errorOf,
263
316
  whenInput: whenInput,
264
317
  whenReceived: whenReceived,
265
318
  describeOther: describeOther,
@@ -269,7 +322,9 @@ function FromSlice(Spec) {
269
322
  thenNoCommand: thenNoCommand,
270
323
  thenNotUnderstood: thenNotUnderstood,
271
324
  thenTranslateError: thenNotUnderstood,
272
- thenRefusedInput: thenRefusedInput
325
+ thenRefusedInput: thenRefusedInput,
326
+ thenSent: thenSent,
327
+ thenNothingSent: thenNothingSent
273
328
  };
274
329
  };
275
330
  }
@@ -229,7 +229,7 @@ function FromSlice(Spec) {
229
229
  let givenTodo = (id, item) => ({
230
230
  id: id,
231
231
  item: item,
232
- fakes: Capabilities_Fake$ReventlessGwt.make(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined)
232
+ fakes: Capabilities_Fake$ReventlessGwt.make(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined)
233
233
  });
234
234
  let givenCapabilities = (todo, fakes) => ({
235
235
  id: todo.id,
@@ -120,7 +120,16 @@ let write = (~path: string, ~file: string, ~spec: string, scenarios: array<scena
120
120
  ),
121
121
  ]),
122
122
  )
123
- NodeFs.writeFileSync(path, JSON.stringifyAny(json, ~space=2)->Option.getOr("{}") ++ "\n")
123
+ let text = JSON.stringifyAny(json, ~space=2)->Option.getOr("{}") ++ "\n"
124
+ // Written only when it says something new: a watch reacts to the write, and the same run
125
+ // records the same scenarios each time.
126
+ let same = switch NodeFs.readFileSync(path) {
127
+ | old => old == text
128
+ | exception _ => false
129
+ }
130
+ if !same {
131
+ NodeFs.writeFileSync(path, text)
132
+ }
124
133
  }
125
134
 
126
135
  /** Runs one test's `body`, recording its steps when recording is on, and adds the scenario to
@@ -161,7 +161,23 @@ function write(path, file, spec, scenarios) {
161
161
  ])))
162
162
  ]
163
163
  ]);
164
- Nodefs.writeFileSync(path, Stdlib_Option.getOr(JSON.stringify(json, undefined, 2), "{}") + "\n", "utf8");
164
+ let text = Stdlib_Option.getOr(JSON.stringify(json, undefined, 2), "{}") + "\n";
165
+ let same;
166
+ let exit = 0;
167
+ let old;
168
+ try {
169
+ old = Nodefs.readFileSync(path, "utf8");
170
+ exit = 1;
171
+ } catch (exn) {
172
+ same = false;
173
+ }
174
+ if (exit === 1) {
175
+ same = old === text;
176
+ }
177
+ if (!same) {
178
+ Nodefs.writeFileSync(path, text, "utf8");
179
+ return;
180
+ }
165
181
  }
166
182
 
167
183
  function around(spec, title, body) {
@@ -46,8 +46,6 @@ let pluginDefinition: Reventless.Plugin.pluginDefinition = {
46
46
  eventCollector: "NOT-SET",
47
47
  extensionProtocols: [],
48
48
  apiSchemaFragment: None,
49
- apiTarget: None,
50
49
  structure: None,
51
- dcbEventLog: None,
52
50
  kind: Domain,
53
51
  }
@@ -31,9 +31,7 @@ let pluginDefinition = {
31
31
  eventCollector: "NOT-SET",
32
32
  extensionProtocols: [],
33
33
  apiSchemaFragment: undefined,
34
- apiTarget: undefined,
35
34
  structure: undefined,
36
- dcbEventLog: undefined,
37
35
  kind: "Domain"
38
36
  };
39
37
 
@@ -16,14 +16,37 @@ module PaymentHook = {
16
16
  let endpoint = None
17
17
  type role = Reventless.Role.name
18
18
  let authorizationOf = _ => Reventless.Authorization.AllowAuthenticated
19
+ let capabilityNeeds = [Reventless.CapabilityNeed.Messaging]
19
20
  }
20
21
 
22
+ // Confirms a paid order and, through the capability `translate` is handed, tells
23
+ // the buyer — which is what makes the fakes worth asserting on. A payment marked
24
+ // paid by hand is confirmed only for a caller in Finance, which is what makes the
25
+ // caller worth handing in.
21
26
  module PaymentHook_Translation = {
22
27
  let moduleUrl = ""
23
- let translate = (input: PaymentHook.externalInput) =>
28
+ let translate = async (
29
+ input: PaymentHook.externalInput,
30
+ ~capabilities: Reventless.Capabilities.t,
31
+ ~caller: option<Reventless.Identity.t>,
32
+ ) =>
24
33
  switch input.status {
25
- | "paid" => Ok([(input.orderId, PaymentHook.ConfirmPayment({orderId: input.orderId}))])
34
+ | "paidByHand" =>
35
+ switch caller {
36
+ | Some(identity) if identity->Reventless.Identity.hasGroup("Finance") =>
37
+ Ok([(input.orderId, PaymentHook.ConfirmPayment({orderId: input.orderId}))])
38
+ | _ => Error("only Finance marks a payment paid by hand")
39
+ }
40
+ | "paid" =>
41
+ switch await capabilities.messaging.send(
42
+ ~recipient=ToEmail(Reventless.Email.unsafe("buyer@example.com")),
43
+ ~message={subject: "Paid", body: input.orderId},
44
+ ) {
45
+ | Ok(_) => Ok([(input.orderId, PaymentHook.ConfirmPayment({orderId: input.orderId}))])
46
+ | Error(failure) => Error(Reventless.Messaging.failureReason(failure))
47
+ }
26
48
  | "pending" => Ok([])
49
+ | "broken" => JsError.throwWithMessage("the translation threw")
27
50
  | other => Error(`Unknown status: ${other}`)
28
51
  }
29
52
  }
@@ -34,7 +57,41 @@ include InboundTranslation_GWT.FromSlice(PaymentHook, PaymentHook_Translation)
34
57
  let json = (pairs: array<(string, string)>) =>
35
58
  pairs->Array.map(((k, v)) => (k, JSON.Encode.string(v)))->Dict.fromArray->JSON.Encode.object
36
59
 
60
+ let financeClerk: Reventless.Identity.t = {
61
+ ...Reventless.Identity.anonymous,
62
+ userId: "u_clerk",
63
+ groups: ["Finance"],
64
+ }
65
+ let visitor = Reventless.Identity.anonymous
66
+
37
67
  describe("InboundTranslation_GWT.FromSlice", () => {
68
+ test("a door that reads its caller translates for the caller it admits", () =>
69
+ whenInput({orderId: "o1", status: "paidByHand"}, ~caller=financeClerk)->thenCommand(
70
+ "o1",
71
+ ConfirmPayment({orderId: "o1"}),
72
+ )
73
+ )
74
+
75
+ test("…and declines a caller it does not", () =>
76
+ whenInput({orderId: "o1", status: "paidByHand"}, ~caller=visitor)->thenNotUnderstood(
77
+ "only Finance marks a payment paid by hand",
78
+ )
79
+ )
80
+
81
+ // No caller is the platform acting for itself, which this door does not admit.
82
+ test("…and an input with no caller at all", () =>
83
+ whenInput({orderId: "o1", status: "paidByHand"})->thenNotUnderstood(
84
+ "only Finance marks a payment paid by hand",
85
+ )
86
+ )
87
+
88
+ test("received JSON carries its caller through the decode", () =>
89
+ whenReceived(
90
+ json([("orderId", "o1"), ("status", "paidByHand")]),
91
+ ~caller=financeClerk,
92
+ )->thenCommand("o1", ConfirmPayment({orderId: "o1"}))
93
+ )
94
+
38
95
  test("input translates to a command", () =>
39
96
  whenInput({orderId: "o1", status: "paid"})->thenCommand("o1", ConfirmPayment({orderId: "o1"}))
40
97
  )
@@ -61,4 +118,28 @@ describe("InboundTranslation_GWT.FromSlice", () => {
61
118
  test("no commands is an answer", () =>
62
119
  whenInput({orderId: "o1", status: "pending"})->thenNoCommand
63
120
  )
121
+
122
+ test("translate reaches the capabilities it is handed", () =>
123
+ whenInput({orderId: "o1", status: "paid"})->thenSent([
124
+ Sent({
125
+ recipient: ToEmail(Reventless.Email.unsafe("buyer@example.com")),
126
+ message: {subject: "Paid", body: "o1"},
127
+ }),
128
+ ])
129
+ )
130
+
131
+ test("a capability failure is the translation declining the input", () =>
132
+ whenInput(
133
+ {orderId: "o1", status: "paid"},
134
+ ~fakes=fakes(~send=async (~recipient as _, ~message as _) => Error(Unavailable("down")), ()),
135
+ )->thenNotUnderstood("down")
136
+ )
137
+
138
+ test("a translation that throws declined the input", () =>
139
+ whenInput({orderId: "o1", status: "broken"})->thenNotUnderstood("the translation threw")
140
+ )
141
+
142
+ test("input that never translated sent nothing", () =>
143
+ whenReceived(json([("status", "paid")]))->thenNothingSent
144
+ )
64
145
  })
@@ -1,6 +1,9 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Sury from "sury";
4
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
5
+ import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
6
+ import * as Messaging$Reventless from "@reventlessdev/reventless-spec/src/semantic/Messaging.res.mjs";
4
7
  import * as InboundTranslation_GWT$ReventlessGwt from "../src/InboundTranslation_GWT.res.mjs";
5
8
 
6
9
  let name = "PaymentHook";
@@ -25,6 +28,8 @@ function authorizationOf(param) {
25
28
  return "AllowAuthenticated";
26
29
  }
27
30
 
31
+ let capabilityNeeds = ["Messaging"];
32
+
28
33
  let traits = [];
29
34
 
30
35
  let PaymentHook = {
@@ -36,25 +41,60 @@ let PaymentHook = {
36
41
  externalSystem: externalSystem,
37
42
  endpoint: undefined,
38
43
  authorizationOf: authorizationOf,
44
+ capabilityNeeds: capabilityNeeds,
39
45
  traits: traits
40
46
  };
41
47
 
42
48
  let moduleUrl$1 = "";
43
49
 
44
- function translate(input) {
50
+ async function translate(input, capabilities, caller) {
45
51
  let other = input.status;
46
52
  switch (other) {
53
+ case "broken" :
54
+ return Stdlib_JsError.throwWithMessage("the translation threw");
47
55
  case "paid" :
48
- return {
49
- TAG: "Ok",
50
- _0: [[
51
- input.orderId,
52
- {
53
- TAG: "ConfirmPayment",
54
- orderId: input.orderId
55
- }
56
- ]]
57
- };
56
+ let failure = await capabilities.messaging.send({
57
+ TAG: "ToEmail",
58
+ _0: "buyer@example.com"
59
+ }, {
60
+ subject: "Paid",
61
+ body: input.orderId
62
+ });
63
+ if (failure.TAG === "Ok") {
64
+ return {
65
+ TAG: "Ok",
66
+ _0: [[
67
+ input.orderId,
68
+ {
69
+ TAG: "ConfirmPayment",
70
+ orderId: input.orderId
71
+ }
72
+ ]]
73
+ };
74
+ } else {
75
+ return {
76
+ TAG: "Error",
77
+ _0: Messaging$Reventless.failureReason(failure._0)
78
+ };
79
+ }
80
+ case "paidByHand" :
81
+ if (caller !== undefined && Identity$Reventless.hasGroup(caller, "Finance")) {
82
+ return {
83
+ TAG: "Ok",
84
+ _0: [[
85
+ input.orderId,
86
+ {
87
+ TAG: "ConfirmPayment",
88
+ orderId: input.orderId
89
+ }
90
+ ]]
91
+ };
92
+ } else {
93
+ return {
94
+ TAG: "Error",
95
+ _0: "only Finance marks a payment paid by hand"
96
+ };
97
+ }
58
98
  case "pending" :
59
99
  return {
60
100
  TAG: "Ok",
@@ -81,7 +121,8 @@ let include = InboundTranslation_GWT$ReventlessGwt.FromSlice({
81
121
  targetName: targetName,
82
122
  externalSystem: externalSystem,
83
123
  authorizationOf: authorizationOf,
84
- endpoint: undefined
124
+ endpoint: undefined,
125
+ capabilityNeeds: capabilityNeeds
85
126
  })({
86
127
  translate: translate,
87
128
  moduleUrl: moduleUrl$1
@@ -91,6 +132,8 @@ let describe = include.describe;
91
132
 
92
133
  let test = include.test;
93
134
 
135
+ let fakes = include.fakes;
136
+
94
137
  let whenInput = include.whenInput;
95
138
 
96
139
  let whenReceived = include.whenReceived;
@@ -105,6 +148,10 @@ let thenTranslateError = include.thenTranslateError;
105
148
 
106
149
  let thenRefusedInput = include.thenRefusedInput;
107
150
 
151
+ let thenSent = include.thenSent;
152
+
153
+ let thenNothingSent = include.thenNothingSent;
154
+
108
155
  function json(pairs) {
109
156
  return Object.fromEntries(pairs.map(param => [
110
157
  param[0],
@@ -112,15 +159,49 @@ function json(pairs) {
112
159
  ]));
113
160
  }
114
161
 
162
+ let newrecord = {...Identity$Reventless.anonymous};
163
+
164
+ newrecord.groups = ["Finance"];
165
+
166
+ newrecord.userId = "u_clerk";
167
+
115
168
  describe("InboundTranslation_GWT.FromSlice", () => {
116
- test("input translates to a command", () => thenCommand(whenInput({
169
+ test("a door that reads its caller translates for the caller it admits", undefined, () => thenCommand(whenInput({
170
+ orderId: "o1",
171
+ status: "paidByHand"
172
+ }, undefined, newrecord), "o1", {
173
+ TAG: "ConfirmPayment",
174
+ orderId: "o1"
175
+ }));
176
+ test("…and declines a caller it does not", undefined, () => thenNotUnderstood(whenInput({
177
+ orderId: "o1",
178
+ status: "paidByHand"
179
+ }, undefined, Identity$Reventless.anonymous), "only Finance marks a payment paid by hand"));
180
+ test("…and an input with no caller at all", undefined, () => thenNotUnderstood(whenInput({
181
+ orderId: "o1",
182
+ status: "paidByHand"
183
+ }, undefined, undefined), "only Finance marks a payment paid by hand"));
184
+ test("received JSON carries its caller through the decode", undefined, () => thenCommand(whenReceived(json([
185
+ [
186
+ "orderId",
187
+ "o1"
188
+ ],
189
+ [
190
+ "status",
191
+ "paidByHand"
192
+ ]
193
+ ]), undefined, newrecord), "o1", {
194
+ TAG: "ConfirmPayment",
195
+ orderId: "o1"
196
+ }));
197
+ test("input translates to a command", undefined, () => thenCommand(whenInput({
117
198
  orderId: "o1",
118
199
  status: "paid"
119
- }), "o1", {
200
+ }, undefined, undefined), "o1", {
120
201
  TAG: "ConfirmPayment",
121
202
  orderId: "o1"
122
203
  }));
123
- test("received JSON decodes before it translates", () => thenCommand(whenReceived(json([
204
+ test("received JSON decodes before it translates", undefined, () => thenCommand(whenReceived(json([
124
205
  [
125
206
  "orderId",
126
207
  "o1"
@@ -129,42 +210,82 @@ describe("InboundTranslation_GWT.FromSlice", () => {
129
210
  "status",
130
211
  "paid"
131
212
  ]
132
- ])), "o1", {
213
+ ]), undefined, undefined), "o1", {
133
214
  TAG: "ConfirmPayment",
134
215
  orderId: "o1"
135
216
  }));
136
- test("input that does not decode is refused before translate", () => thenRefusedInput(whenReceived(json([[
217
+ test("input that does not decode is refused before translate", undefined, () => thenRefusedInput(whenReceived(json([[
137
218
  "status",
138
219
  "paid"
139
- ]])), "orderId"));
140
- test("input translate declines is not understood", () => thenNotUnderstood(whenInput({
220
+ ]]), undefined, undefined), "orderId"));
221
+ test("input translate declines is not understood", undefined, () => thenNotUnderstood(whenInput({
141
222
  orderId: "o1",
142
223
  status: "lost"
143
- }), "Unknown status: lost"));
144
- test("thenTranslateError is the older name", () => thenTranslateError(whenInput({
224
+ }, undefined, undefined), "Unknown status: lost"));
225
+ test("thenTranslateError is the older name", undefined, () => thenTranslateError(whenInput({
145
226
  orderId: "o1",
146
227
  status: "lost"
147
- }), "Unknown status: lost"));
148
- test("no commands is an answer", () => thenNoCommand(whenInput({
228
+ }, undefined, undefined), "Unknown status: lost"));
229
+ test("no commands is an answer", undefined, () => thenNoCommand(whenInput({
149
230
  orderId: "o1",
150
231
  status: "pending"
151
- })));
232
+ }, undefined, undefined)));
233
+ test("translate reaches the capabilities it is handed", undefined, () => thenSent(whenInput({
234
+ orderId: "o1",
235
+ status: "paid"
236
+ }, undefined, undefined), [{
237
+ TAG: "Sent",
238
+ recipient: {
239
+ TAG: "ToEmail",
240
+ _0: "buyer@example.com"
241
+ },
242
+ message: {
243
+ subject: "Paid",
244
+ body: "o1"
245
+ }
246
+ }]));
247
+ test("a capability failure is the translation declining the input", undefined, () => thenNotUnderstood(whenInput({
248
+ orderId: "o1",
249
+ status: "paid"
250
+ }, fakes(undefined, async (param, param$1) => ({
251
+ TAG: "Error",
252
+ _0: {
253
+ TAG: "Unavailable",
254
+ _0: "down"
255
+ }
256
+ }), undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined), "down"));
257
+ test("a translation that throws declined the input", undefined, () => thenNotUnderstood(whenInput({
258
+ orderId: "o1",
259
+ status: "broken"
260
+ }, undefined, undefined), "the translation threw"));
261
+ test("input that never translated sent nothing", undefined, () => thenNothingSent(whenReceived(json([[
262
+ "status",
263
+ "paid"
264
+ ]]), undefined, undefined)));
152
265
  });
153
266
 
154
267
  let encPairs = include.encPairs;
155
268
 
269
+ let errorOf = include.errorOf;
270
+
156
271
  let describeOther = include.describeOther;
157
272
 
158
273
  let roundTrips = include.roundTrips;
159
274
 
160
275
  let thenCommands = include.thenCommands;
161
276
 
277
+ let financeClerk = newrecord;
278
+
279
+ let visitor = Identity$Reventless.anonymous;
280
+
162
281
  export {
163
282
  PaymentHook,
164
283
  PaymentHook_Translation,
165
284
  describe,
166
285
  test,
286
+ fakes,
167
287
  encPairs,
288
+ errorOf,
168
289
  whenInput,
169
290
  whenReceived,
170
291
  describeOther,
@@ -175,6 +296,10 @@ export {
175
296
  thenNotUnderstood,
176
297
  thenTranslateError,
177
298
  thenRefusedInput,
299
+ thenSent,
300
+ thenNothingSent,
178
301
  json,
302
+ financeClerk,
303
+ visitor,
179
304
  }
180
305
  /* externalInputSchema Not a pure module */
@@ -151,7 +151,7 @@ let down = Capabilities_Fake$ReventlessGwt.make(undefined, async (param, param$1
151
151
  TAG: "Unavailable",
152
152
  _0: "smtp down"
153
153
  }
154
- }), undefined, undefined, undefined, undefined, undefined, undefined, undefined);
154
+ }), undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined);
155
155
 
156
156
  let welcome = {
157
157
  TAG: "Sent",