@reventlessdev/reventless-gwt 1.0.0-alpha.238 → 1.0.0-alpha.240

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/package.json +6 -6
  3. package/src/Automation_GWT.res +130 -0
  4. package/src/Automation_GWT.res.mjs +193 -0
  5. package/src/Behavior_GWT.res +6 -6
  6. package/src/Behavior_GWT.res.mjs +5 -5
  7. package/src/Capabilities_Fake.res +157 -0
  8. package/src/Capabilities_Fake.res.mjs +254 -0
  9. package/src/Flow_GWT.res +112 -1
  10. package/src/Flow_GWT.res.mjs +165 -11
  11. package/src/InboundTranslation_GWT.res +112 -0
  12. package/src/InboundTranslation_GWT.res.mjs +190 -1
  13. package/src/OutboundTranslation_GWT.res +207 -4
  14. package/src/OutboundTranslation_GWT.res.mjs +257 -1
  15. package/src/StateChange/ExternalAddCategorySlice.res +1 -1
  16. package/src/StateChange/ExternalAddCategorySlice.res.mjs +2 -2
  17. package/src/StateChange/WithFixtures.res +1 -1
  18. package/src/StateChange/WithFixtures.res.mjs +2 -2
  19. package/src/StateChange/WithManualOpen.res +1 -1
  20. package/src/StateChange/WithManualOpen.res.mjs +2 -2
  21. package/tests/AutomationFromSliceGwtTest.res +145 -0
  22. package/tests/AutomationFromSliceGwtTest.res.mjs +353 -0
  23. package/tests/AutomationGwtTest.res.mjs +2 -2
  24. package/tests/DelegateGwtTest.res +5 -5
  25. package/tests/DelegateGwtTest.res.mjs +27 -27
  26. package/tests/FlowAggregateGwtTest.res.mjs +4 -4
  27. package/tests/FlowCrossPluginGwtTest.res +4 -4
  28. package/tests/FlowCrossPluginGwtTest.res.mjs +22 -22
  29. package/tests/FlowGwtTest.res.mjs +6 -6
  30. package/tests/InboundFromSliceGwtTest.res +63 -0
  31. package/tests/InboundFromSliceGwtTest.res.mjs +164 -0
  32. package/tests/InboundTranslationGwtTest.res.mjs +2 -7
  33. package/tests/MappingGwtTest.res.mjs +12 -12
  34. package/tests/OutboundFromSliceGwtTest.res +124 -0
  35. package/tests/OutboundFromSliceGwtTest.res.mjs +268 -0
  36. package/tests/StateChangeSliceAuthorizeGwtTest.res.mjs +20 -16
  37. package/tests/StateChangeSliceGwtTest.res.mjs +6 -6
@@ -30,6 +30,8 @@ module type T = {
30
30
  let thenTranslateError: (translateResult, string) => Outcome.outcome
31
31
  }
32
32
 
33
+ /** Deprecated: takes an adapter module with `translate` on the spec. Use
34
+ `FromSlice`; this goes one release after the examples have moved. */
33
35
  module Make = (Spec: SliceSpec): (T with module Spec = Spec) => {
34
36
  module Spec = Spec
35
37
 
@@ -74,3 +76,113 @@ module Make = (Spec: SliceSpec): (T with module Spec = Spec) => {
74
76
  | Ok(_) => Outcome.fail(TranslateError({expected: expectedMsg, actual: None}))
75
77
  }
76
78
  }
79
+
80
+ // The slice as written: its `Spec` and its `_Translation` body.
81
+ module FromSlice = (
82
+ Spec: Reventless.InboundTranslationSlice.Spec,
83
+ Translation: Reventless.InboundTranslationSlice.Translation with module Spec := Spec,
84
+ ) => {
85
+ let describe = JestBind.describe
86
+ let test = (name, body) => JestBind.test(~slice=Spec.name, name, body)
87
+
88
+ /** What became of one input. `InputRefused` is input that did not decode, so
89
+ `translate` never saw it; `NotUnderstood` is input it declined. */
90
+ type received =
91
+ | Translated(array<(string, Spec.command)>)
92
+ | NotUnderstood(string)
93
+ | InputRefused(string)
94
+
95
+ let encPairs = (pairs: array<(string, Spec.command)>) =>
96
+ pairs->Array.map(((id, cmd)) => {
97
+ let d = Dict.make()
98
+ d->Dict.set("id", JSON.Encode.string(id))
99
+ d->Dict.set("command", cmd->Message.encode(Spec.commandSchema))
100
+ JSON.Encode.object(d)
101
+ })
102
+
103
+ let whenInput = (input: Spec.externalInput) =>
104
+ switch Translation.translate(input) {
105
+ | Ok(pairs) => Translated(pairs)
106
+ | Error(msg) => NotUnderstood(msg)
107
+ }
108
+
109
+ /** Input as it arrives: decoded through `externalInputSchema` first. */
110
+ let whenReceived = (json: JSON.t) =>
111
+ switch json->Reventless.Util_Sury.fromJson(Spec.externalInputSchema) {
112
+ | input => whenInput(input)
113
+ // Sury's message names the field that failed.
114
+ | exception S.Exn(e) => InputRefused(e.message)
115
+ | exception _ => InputRefused("invalid input")
116
+ }
117
+
118
+ let describeOther = received =>
119
+ switch received {
120
+ | Translated(_) => None
121
+ | NotUnderstood(msg) => Some(`not understood: ${msg}`)
122
+ | InputRefused(msg) => Some(`input refused: ${msg}`)
123
+ }
124
+
125
+ // Each command must survive its own schema both ways, as it will on the topic.
126
+ let roundTrips = (cmd: Spec.command) =>
127
+ switch cmd
128
+ ->Message.encode(Spec.commandSchema)
129
+ ->Reventless.Util_Sury.fromJson(Spec.commandSchema) {
130
+ | back => back == cmd
131
+ | exception _ => false
132
+ }
133
+
134
+ let thenCommands = (received, expected) =>
135
+ switch received {
136
+ | Translated(actual) if actual == expected =>
137
+ switch actual->Array.find(((_, cmd)) => !roundTrips(cmd)) {
138
+ | None => Outcome.pass
139
+ | Some(pair) =>
140
+ Outcome.fail(
141
+ TranslateError({
142
+ expected: "commands that encode and decode through commandSchema",
143
+ actual: Some(`${JSON.stringifyAny(encPairs([pair]))->Option.getOr("")} does not`),
144
+ }),
145
+ )
146
+ }
147
+ | Translated(actual) =>
148
+ Outcome.fail(EventsMismatch({expected: encPairs(expected), actual: encPairs(actual)}))
149
+ | other => Outcome.fail(TranslateError({expected: "(commands)", actual: describeOther(other)}))
150
+ }
151
+
152
+ let thenCommand = (received, expectedId, expectedCmd) =>
153
+ thenCommands(received, [(expectedId, expectedCmd)])
154
+
155
+ let thenNoCommand = received =>
156
+ switch received {
157
+ | Translated([]) => Outcome.pass
158
+ | Translated(actual) => Outcome.fail(NoEventExpected({actual: encPairs(actual)}))
159
+ | other =>
160
+ Outcome.fail(TranslateError({expected: "(no commands)", actual: describeOther(other)}))
161
+ }
162
+
163
+ let thenNotUnderstood = (received, expectedMsg) =>
164
+ switch received {
165
+ | NotUnderstood(actual) if actual == expectedMsg => Outcome.pass
166
+ | NotUnderstood(actual) =>
167
+ Outcome.fail(TranslateError({expected: expectedMsg, actual: Some(actual)}))
168
+ | other => Outcome.fail(TranslateError({expected: expectedMsg, actual: describeOther(other)}))
169
+ }
170
+ let thenTranslateError = thenNotUnderstood
171
+
172
+ /** Passes when the input was refused and the reason contains `reason`: the
173
+ decoder words its own messages, so a test pins the part that matters. */
174
+ let thenRefusedInput = (received, reason) =>
175
+ switch received {
176
+ | InputRefused(actual) if actual->String.includes(reason) => Outcome.pass
177
+ | other =>
178
+ Outcome.fail(
179
+ TranslateError({
180
+ expected: `input refused: …${reason}…`,
181
+ actual: switch other {
182
+ | Translated(_) => Some("translated")
183
+ | other => describeOther(other)
184
+ },
185
+ }),
186
+ )
187
+ }
188
+ }
@@ -1,6 +1,10 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
3
5
  import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
6
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
7
+ import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
4
8
  import * as Outcome$ReventlessGwt from "./Outcome.res.mjs";
5
9
  import * as JestBind$ReventlessGwt from "./JestBind.res.mjs";
6
10
  import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
@@ -86,7 +90,192 @@ function Make(Spec) {
86
90
  };
87
91
  }
88
92
 
93
+ function FromSlice(Spec) {
94
+ return Translation => {
95
+ let test = (name, body) => JestBind$ReventlessGwt.test(Spec.name, name, body);
96
+ let encPairs = pairs => pairs.map(param => {
97
+ let d = {};
98
+ d["id"] = param[0];
99
+ d["command"] = Message$ReventlessCore.encode(param[1], Spec.commandSchema);
100
+ return d;
101
+ });
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
+ };
109
+ } else {
110
+ return {
111
+ TAG: "NotUnderstood",
112
+ _0: pairs._0
113
+ };
114
+ }
115
+ };
116
+ let whenReceived = json => {
117
+ let input;
118
+ try {
119
+ input = Util_Sury$Reventless.fromJson(json, Spec.externalInputSchema);
120
+ } catch (raw_e) {
121
+ let e = Primitive_exceptions.internalToException(raw_e);
122
+ if (e.RE_EXN_ID === S.Exn) {
123
+ return {
124
+ TAG: "InputRefused",
125
+ _0: e._1.message
126
+ };
127
+ } else {
128
+ return {
129
+ TAG: "InputRefused",
130
+ _0: "invalid input"
131
+ };
132
+ }
133
+ }
134
+ return whenInput(input);
135
+ };
136
+ let describeOther = received => {
137
+ switch (received.TAG) {
138
+ case "Translated" :
139
+ return;
140
+ case "NotUnderstood" :
141
+ return `not understood: ` + received._0;
142
+ case "InputRefused" :
143
+ return `input refused: ` + received._0;
144
+ }
145
+ };
146
+ let roundTrips = cmd => {
147
+ let back;
148
+ try {
149
+ back = Util_Sury$Reventless.fromJson(Message$ReventlessCore.encode(cmd, Spec.commandSchema), Spec.commandSchema);
150
+ } catch (exn) {
151
+ return false;
152
+ }
153
+ return Primitive_object.equal(back, cmd);
154
+ };
155
+ let thenCommands = (received, expected) => {
156
+ switch (received.TAG) {
157
+ case "Translated" :
158
+ let actual = received._0;
159
+ if (!Primitive_object.equal(actual, expected)) {
160
+ return Outcome$ReventlessGwt.fail({
161
+ TAG: "EventsMismatch",
162
+ expected: encPairs(expected),
163
+ actual: encPairs(actual)
164
+ });
165
+ }
166
+ let pair = actual.find(param => !roundTrips(param[1]));
167
+ if (pair !== undefined) {
168
+ return Outcome$ReventlessGwt.fail({
169
+ TAG: "TranslateError",
170
+ expected: "commands that encode and decode through commandSchema",
171
+ actual: Stdlib_Option.getOr(JSON.stringify(encPairs([pair])), "") + ` does not`
172
+ });
173
+ } else {
174
+ return Outcome$ReventlessGwt.pass;
175
+ }
176
+ case "NotUnderstood" :
177
+ case "InputRefused" :
178
+ return Outcome$ReventlessGwt.fail({
179
+ TAG: "TranslateError",
180
+ expected: "(commands)",
181
+ actual: describeOther(received)
182
+ });
183
+ }
184
+ };
185
+ let thenCommand = (received, expectedId, expectedCmd) => thenCommands(received, [[
186
+ expectedId,
187
+ expectedCmd
188
+ ]]);
189
+ let thenNoCommand = received => {
190
+ switch (received.TAG) {
191
+ case "Translated" :
192
+ let actual = received._0;
193
+ if (actual.length !== 0) {
194
+ return Outcome$ReventlessGwt.fail({
195
+ TAG: "NoEventExpected",
196
+ actual: encPairs(actual)
197
+ });
198
+ } else {
199
+ return Outcome$ReventlessGwt.pass;
200
+ }
201
+ case "NotUnderstood" :
202
+ case "InputRefused" :
203
+ return Outcome$ReventlessGwt.fail({
204
+ TAG: "TranslateError",
205
+ expected: "(no commands)",
206
+ actual: describeOther(received)
207
+ });
208
+ }
209
+ };
210
+ let thenNotUnderstood = (received, expectedMsg) => {
211
+ switch (received.TAG) {
212
+ case "NotUnderstood" :
213
+ let actual = received._0;
214
+ if (actual === expectedMsg) {
215
+ return Outcome$ReventlessGwt.pass;
216
+ } else {
217
+ return Outcome$ReventlessGwt.fail({
218
+ TAG: "TranslateError",
219
+ expected: expectedMsg,
220
+ actual: actual
221
+ });
222
+ }
223
+ case "Translated" :
224
+ case "InputRefused" :
225
+ return Outcome$ReventlessGwt.fail({
226
+ TAG: "TranslateError",
227
+ expected: expectedMsg,
228
+ actual: describeOther(received)
229
+ });
230
+ }
231
+ };
232
+ let thenRefusedInput = (received, reason) => {
233
+ switch (received.TAG) {
234
+ case "Translated" :
235
+ case "NotUnderstood" :
236
+ break;
237
+ case "InputRefused" :
238
+ if (received._0.includes(reason)) {
239
+ return Outcome$ReventlessGwt.pass;
240
+ }
241
+ break;
242
+ }
243
+ let tmp;
244
+ switch (received.TAG) {
245
+ case "Translated" :
246
+ tmp = "translated";
247
+ break;
248
+ case "NotUnderstood" :
249
+ case "InputRefused" :
250
+ tmp = describeOther(received);
251
+ break;
252
+ }
253
+ return Outcome$ReventlessGwt.fail({
254
+ TAG: "TranslateError",
255
+ expected: `input refused: …` + reason + `…`,
256
+ actual: tmp
257
+ });
258
+ };
259
+ return {
260
+ describe: JestBind$ReventlessGwt.describe,
261
+ test: test,
262
+ encPairs: encPairs,
263
+ whenInput: whenInput,
264
+ whenReceived: whenReceived,
265
+ describeOther: describeOther,
266
+ roundTrips: roundTrips,
267
+ thenCommands: thenCommands,
268
+ thenCommand: thenCommand,
269
+ thenNoCommand: thenNoCommand,
270
+ thenNotUnderstood: thenNotUnderstood,
271
+ thenTranslateError: thenNotUnderstood,
272
+ thenRefusedInput: thenRefusedInput
273
+ };
274
+ };
275
+ }
276
+
89
277
  export {
90
278
  Make,
279
+ FromSlice,
91
280
  }
92
- /* Message-ReventlessCore Not a pure module */
281
+ /* S Not a pure module */
@@ -1,9 +1,7 @@
1
1
  open ReventlessCore
2
2
 
3
- // Minimal inline spec for OutboundTranslationSlice. `translate` is async and
4
- // supplied at test-time via `whenTranslateMocked` so the external service is
5
- // never hit. The spec's own `translate` is not part of the GWT — it's
6
- // exercised only through the runtime tests.
3
+ // Minimal inline spec for the flat `Make`, which only mocks `translate`.
4
+ // `FromSlice` below runs the slice's own `translate` against capability fakes.
7
5
  module type SliceSpec = {
8
6
  let name: string
9
7
 
@@ -73,6 +71,9 @@ module type T = {
73
71
  let thenTodoStatus: (promise<attempt>, string, todoStatus) => promise<Outcome.outcome>
74
72
  }
75
73
 
74
+ /** Deprecated: takes an adapter module with `collect` on the spec and never
75
+ runs `translate`. Use `FromSlice`; this goes one release after the examples
76
+ have moved. */
76
77
  module Make = (Spec: SliceSpec): (T with module Spec = Spec) => {
77
78
  module Spec = Spec
78
79
 
@@ -223,3 +224,205 @@ module Make = (Spec: SliceSpec): (T with module Spec = Spec) => {
223
224
  }
224
225
  }
225
226
  }
227
+
228
+ /** A to-do's status as the runtime leaves it after an attempt. `#Pending` is the
229
+ older name for `#Failed`, kept for one release. */
230
+ type status = [#Completed | #Failed | #Abandoned | #Pending]
231
+
232
+ // The slice as written: its `Spec` and its `_Translation` body, run against
233
+ // recording capability fakes.
234
+ module FromSlice = (
235
+ Spec: Reventless.OutboundTranslationSlice.Spec,
236
+ Translation: Reventless.OutboundTranslationSlice.Translation with module Spec := Spec,
237
+ ) => {
238
+ let describe = JestBind.describe
239
+ let test = (name, ~timeout=?, body) =>
240
+ JestBind.testPromise(~slice=Spec.name, name, ~timeout?, body)
241
+ let testSync = (name, body) => JestBind.test(~slice=Spec.name, name, body)
242
+
243
+ type translateResult = result<option<(string, Spec.inboundCommand)>, string>
244
+
245
+ type todo = {id: string, item: Spec.outboundItem, fakes: Capabilities_Fake.t}
246
+
247
+ /** What one or more attempts left behind. `retries` is the row's
248
+ `retryCount`: the attempts that failed. */
249
+ type attempt = {
250
+ id: string,
251
+ item: Spec.outboundItem,
252
+ result: translateResult,
253
+ retries: int,
254
+ status: status,
255
+ sent: array<Capabilities_Fake.call>,
256
+ }
257
+
258
+ let encItems = (arr: array<(string, Spec.outboundItem)>) =>
259
+ arr->Array.map(((id, i)) => (id, i->Message.encode(Spec.outboundItemSchema)))
260
+ let commandPairJson = (id, cmd) => {
261
+ let d = Dict.make()
262
+ d->Dict.set("id", JSON.Encode.string(id))
263
+ d->Dict.set("command", cmd->Message.encode(Spec.inboundCommandSchema))
264
+ JSON.Encode.object(d)
265
+ }
266
+ let errorOf = exn =>
267
+ switch exn {
268
+ | S.Exn(e) => e.message
269
+ | JsExn(e) => JsExn.message(e)->Option.getOr("unknown error")
270
+ | _ => "unknown error"
271
+ }
272
+
273
+ // Unit: collect
274
+ let givenEvent = (e: Spec.consumedEvent) => e
275
+ let whenCollect = (e, ~sourceId="") => Translation.collect(e, ~sourceId)
276
+ let thenTodos = (actual, expected) =>
277
+ actual == expected
278
+ ? Outcome.pass
279
+ : Outcome.fail(TodoMismatch({expected: encItems(expected), actual: encItems(actual)}))
280
+
281
+ // Unit: translate
282
+ let givenTodo = (id, item) => {id, item, fakes: Capabilities_Fake.make()}
283
+ /** Scripted capabilities for `givenCapabilities`; see `Capabilities_Fake.make`. */
284
+ let fakes = Capabilities_Fake.make
285
+ let givenCapabilities = (todo: todo, fakes) => {...todo, fakes}
286
+
287
+ // One attempt, judged as the runtime judges it: a throw is a failure, and so
288
+ // is a command its own schema cannot encode.
289
+ let attemptWith = async (todo: todo, run) => {
290
+ let result = try await run(todo.id, todo.item) catch {
291
+ | exn => Error(errorOf(exn))
292
+ }
293
+ switch result {
294
+ | Ok(Some((_, cmd))) =>
295
+ switch cmd->Message.encode(Spec.inboundCommandSchema) {
296
+ | _ => result
297
+ | exception exn => Error(`failed to encode inbound command: ${errorOf(exn)}`)
298
+ }
299
+ | other => other
300
+ }
301
+ }
302
+
303
+ // Attempts until one succeeds or `limit` have failed. The status is judged
304
+ // against `maxRetries`, the runtime's budget: that many failures abandon a row.
305
+ let attempts = async (todo: todo, ~limit, ~maxRetries, run) => {
306
+ let result = ref(await attemptWith(todo, run))
307
+ let failures = ref(Result.isError(result.contents) ? 1 : 0)
308
+ while Result.isError(result.contents) && failures.contents < limit {
309
+ result := (await attemptWith(todo, run))
310
+ if Result.isError(result.contents) {
311
+ failures := failures.contents + 1
312
+ }
313
+ }
314
+ {
315
+ id: todo.id,
316
+ item: todo.item,
317
+ result: result.contents,
318
+ retries: failures.contents,
319
+ status: switch result.contents {
320
+ | Ok(_) => #Completed
321
+ | Error(_) => failures.contents >= maxRetries ? #Abandoned : #Failed
322
+ },
323
+ sent: todo.fakes.calls(),
324
+ }
325
+ }
326
+
327
+ /** One attempt of the slice's own `translate`, against the given capabilities. */
328
+ let whenTranslated = (todo: todo) =>
329
+ attempts(todo, ~limit=1, ~maxRetries=Spec.maxRetries, (id, item) =>
330
+ Translation.translate(id, item, ~capabilities=todo.fakes.capabilities)
331
+ )
332
+ /** One attempt, answered by `mock` instead of `translate`. */
333
+ let whenTranslateMocked = (todo, mock) =>
334
+ attempts(todo, ~limit=1, ~maxRetries=Spec.maxRetries, mock)
335
+ /** Attempts as the sweeps would make them: until one succeeds or `maxRetries`
336
+ (the Spec's, unless given) have failed. */
337
+ let whenTranslateRetrying = (todo, ~maxRetries=Spec.maxRetries, mock) =>
338
+ attempts(todo, ~limit=maxRetries, ~maxRetries, mock)
339
+
340
+ /** The budget is spent: what `onExhausted` publishes, if anything. */
341
+ let whenExhausted = async (todo: todo, ~lastError=?) => {
342
+ id: todo.id,
343
+ item: todo.item,
344
+ result: Ok(Translation.onExhausted(todo.id, todo.item, ~lastError)),
345
+ retries: Spec.maxRetries,
346
+ status: #Abandoned,
347
+ sent: [],
348
+ }
349
+
350
+ let thenCommand = async (pending, expectedId, expectedCmd) => {
351
+ let {result, _} = await pending
352
+ switch result {
353
+ | Ok(Some((id, cmd))) if id == expectedId && cmd == expectedCmd => Outcome.pass
354
+ | Ok(actual) =>
355
+ Outcome.fail(
356
+ EventsMismatch({
357
+ expected: [commandPairJson(expectedId, expectedCmd)],
358
+ actual: actual->Option.mapOr([], ((id, cmd)) => [commandPairJson(id, cmd)]),
359
+ }),
360
+ )
361
+ | Error(msg) => Outcome.fail(TranslateError({expected: "(command)", actual: Some(msg)}))
362
+ }
363
+ }
364
+
365
+ let thenNoCommand = async pending => {
366
+ let {result, _} = await pending
367
+ switch result {
368
+ | Ok(None) => Outcome.pass
369
+ | Ok(Some((id, cmd))) => Outcome.fail(NoEventExpected({actual: [commandPairJson(id, cmd)]}))
370
+ | Error(msg) => Outcome.fail(TranslateError({expected: "(no command)", actual: Some(msg)}))
371
+ }
372
+ }
373
+
374
+ let thenSent = async (pending, expected: array<Capabilities_Fake.call>) => {
375
+ let {sent, _} = await pending
376
+ sent == expected
377
+ ? Outcome.pass
378
+ : Outcome.fail(
379
+ EventsMismatch({
380
+ expected: expected->Array.map(Capabilities_Fake.toJson),
381
+ actual: sent->Array.map(Capabilities_Fake.toJson),
382
+ }),
383
+ )
384
+ }
385
+ let thenNothingSent = pending => thenSent(pending, [])
386
+
387
+ let thenRetryRecorded = async (pending, expectedRetries) => {
388
+ let {retries, _} = await pending
389
+ retries == expectedRetries
390
+ ? Outcome.pass
391
+ : Outcome.fail(
392
+ StateMismatch({
393
+ key: "retries",
394
+ expected: Some(JSON.Encode.int(expectedRetries)),
395
+ actual: Some(JSON.Encode.int(retries)),
396
+ }),
397
+ )
398
+ }
399
+
400
+ let statusName = (s: status) =>
401
+ switch s {
402
+ | #Completed => "#Completed"
403
+ | #Failed | #Pending => "#Failed"
404
+ | #Abandoned => "#Abandoned"
405
+ }
406
+
407
+ let thenTodoStatus = async (pending, expectedId, expectedStatus: status) => {
408
+ let {id, status, _} = await pending
409
+ if id == expectedId && statusName(status) == statusName(expectedStatus) {
410
+ Outcome.pass
411
+ } else {
412
+ let encOne = (rid, s) =>
413
+ JSON.Encode.object(
414
+ Dict.fromArray([
415
+ ("id", JSON.Encode.string(rid)),
416
+ ("status", JSON.Encode.string(statusName(s))),
417
+ ]),
418
+ )
419
+ Outcome.fail(
420
+ StateMismatch({
421
+ key: id,
422
+ expected: Some(encOne(expectedId, expectedStatus)),
423
+ actual: Some(encOne(id, status)),
424
+ }),
425
+ )
426
+ }
427
+ }
428
+ }