@reventlessdev/reventless-gwt 1.0.0-alpha.241 → 1.0.0-alpha.243

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,25 @@
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.243 (2026-10-15)
7
+
8
+ * feat!: an automation's process takes ~now, and collect the event's recorded time ([8c8f3f5](https://github.com/ReventlessDev/reventless-core/commit/8c8f3f570474b1658b605b726077debc7d6a1dc7))
9
+
10
+ ### BREAKING CHANGES
11
+
12
+ * a process the PPX does not see — in an Automation module
13
+ written by hand, or a SliceSpec given to the deprecated Automation_GWT.Make
14
+ or Flow_GWT.AutomationStep — must take ~now.
15
+
16
+
17
+
18
+ # 1.0.0-alpha.242 (2026-10-14)
19
+
20
+ ### Bug Fixes
21
+
22
+ * **gwt:** thenStateWithId checks the rows under its id, not that they are the only ones ([279d298](https://github.com/ReventlessDev/reventless-core/commit/279d298866fec60d976f0f8c8a247f684e600ba2))
23
+
24
+
6
25
  # 1.0.0-alpha.241 (2026-10-12)
7
26
 
8
27
  **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.241",
3
+ "version": "1.0.0-alpha.243",
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": "0.1.0-alpha.35",
17
+ "@reventlessdev/rescript-effect": "1.0.0-alpha.0",
18
+ "@reventlessdev/reventless-core": "3.0.0-alpha.295",
18
19
  "@reventlessdev/rescript-jest": "1.0.0-alpha.12",
19
- "@reventlessdev/reventless-infra": "3.0.0-alpha.191",
20
20
  "@reventlessdev/rescript-node": "2.0.0-alpha.22",
21
- "@reventlessdev/reventless-spec": "3.0.0-alpha.163",
22
- "@reventlessdev/reventless-core": "3.0.0-alpha.294"
21
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.192",
22
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.164"
23
23
  },
24
24
  "devDependencies": {
25
25
  "rescript": "12.3.1",
@@ -1,5 +1,13 @@
1
1
  open ReventlessCore
2
2
 
3
+ // The current time when a scenario fixes none, from Effect's Clock.
4
+ let clockNow = () => Clock.currentTimeMillis->Effect.runSync
5
+ let nowOr = now =>
6
+ switch now {
7
+ | Some(now) => now
8
+ | None => clockNow()
9
+ }
10
+
3
11
  // Minimal inline spec for AutomationSlice — mirrors the pattern used by
4
12
  // the other GWT DSLs so sury-ppx processes the @schema attributes in this
5
13
  // compilation unit.
@@ -17,7 +25,7 @@ module type SliceSpec = {
17
25
 
18
26
  let collect: consumedEvent => array<(string, todoItem)>
19
27
  let resolve: consumedEvent => option<string>
20
- let process: (string, todoItem) => option<(string, command)>
28
+ let process: (string, todoItem, ~now: float) => option<(string, command)>
21
29
  }
22
30
 
23
31
  module type T = {
@@ -44,13 +52,13 @@ module type T = {
44
52
 
45
53
  // Unit combinators — process
46
54
  let givenTodo: (string, Spec.todoItem) => (string, Spec.todoItem)
47
- let whenProcess: ((string, Spec.todoItem)) => option<(string, Spec.command)>
55
+ let whenProcess: ((string, Spec.todoItem), ~now: float=?) => option<(string, Spec.command)>
48
56
  let thenCommand: (option<(string, Spec.command)>, string, Spec.command) => Outcome.outcome
49
57
  let thenNoCommand: option<(string, Spec.command)> => Outcome.outcome
50
58
 
51
59
  // Scenario combinators — full sweep
52
60
  let givenEvents: array<Spec.consumedEvent> => array<Spec.consumedEvent>
53
- let whenSweep: array<Spec.consumedEvent> => scenario
61
+ let whenSweep: (array<Spec.consumedEvent>, ~now: float=?) => scenario
54
62
  let thenCommands: (scenario, array<(string, Spec.command)>) => Outcome.outcome
55
63
  let andThenEvents: (scenario, array<Spec.consumedEvent>) => scenario
56
64
  let thenScenarioTodos: (scenario, array<(string, Spec.todoItem)>) => Outcome.outcome
@@ -111,7 +119,7 @@ module Make = (Spec: SliceSpec): (T with module Spec = Spec) => {
111
119
 
112
120
  // Unit: process
113
121
  let givenTodo = (id, todo) => (id, todo)
114
- let whenProcess = ((id, todo)) => Spec.process(id, todo)
122
+ let whenProcess = ((id, todo), ~now=?) => Spec.process(id, todo, ~now=nowOr(now))
115
123
  let thenCommand = (actual, expectedId, expectedCmd) =>
116
124
  switch actual {
117
125
  | Some((id, cmd)) if id == expectedId && cmd == expectedCmd => Outcome.pass
@@ -157,7 +165,8 @@ module Make = (Spec: SliceSpec): (T with module Spec = Spec) => {
157
165
 
158
166
  // Scenario: full sweep
159
167
  let givenEvents = es => es
160
- let whenSweep = events => {
168
+ let whenSweep = (events, ~now=?) => {
169
+ let now = nowOr(now)
161
170
  // Build the todo list by running collect, then drain any items that are
162
171
  // completed by resolve in the same input stream.
163
172
  let collected = events->Array.map(e => e->Spec.collect)->Array.flat
@@ -165,7 +174,7 @@ module Make = (Spec: SliceSpec): (T with module Spec = Spec) => {
165
174
  let pending = collected->Array.filter(((id, _)) => !Array.includes(resolvedIds, id))
166
175
 
167
176
  // Process each pending todo to produce commands.
168
- let commands = pending->Array.filterMap(((id, todo)) => Spec.process(id, todo))
177
+ let commands = pending->Array.filterMap(((id, todo)) => Spec.process(id, todo, ~now))
169
178
 
170
179
  {todos: pending, commands}
171
180
  }
@@ -217,9 +226,13 @@ module FromSlice = (
217
226
  sliceName: Spec.name,
218
227
  }
219
228
 
220
- /** A source event as the runtime receives it: encoded, and addressed by its
221
- source's name. Built with `Mapping(M).event`. */
222
- type sourced = {sourceName: string, sourceId: string, payload: JSON.t}
229
+ /** A source event as the runtime receives it: encoded, addressed by its
230
+ source's name, and recorded at a time when the scenario gives one. Built
231
+ with `Mapping(M).event`. */
232
+ type sourced = {sourceName: string, sourceId: string, payload: JSON.t, recordedAt: option<float>}
233
+
234
+ let withRecordedAt = (context: Reventless.AutomationSlice.context, recordedAt) =>
235
+ recordedAt->Option.mapOr(context, recordedAt => {...context, recordedAt})
223
236
 
224
237
  type scenario = {
225
238
  todos: array<(string, Spec.todoItem)>,
@@ -248,19 +261,21 @@ module FromSlice = (
248
261
  }
249
262
 
250
263
  module Mapping = (M: Automation.Mapping) => {
251
- let event = (e: M.sourceEvent, ~sourceId="") => {
264
+ let event = (e: M.sourceEvent, ~sourceId="", ~recordedAt=?) => {
252
265
  sourceName: M.sourceName,
253
266
  sourceId,
254
267
  payload: e->Message.encode(M.sourceEventSchema),
268
+ recordedAt,
255
269
  }
256
270
  let givenEvent = (e: M.sourceEvent) => e
257
- let whenCollect = (e, ~sourceId="", ~context=testContext) => M.collect(e, ~sourceId, context)
271
+ let whenCollect = (e, ~sourceId="", ~context=testContext, ~recordedAt=?) =>
272
+ M.collect(e, ~sourceId, context->withRecordedAt(recordedAt))
258
273
  let whenResolve = M.resolve
259
274
  }
260
275
 
261
276
  // Unit: process and abandonment
262
277
  let givenTodo = (id: string, item: Spec.todoItem) => (id, item)
263
- let whenProcess = ((id, item)) => Automation.process(id, item)
278
+ let whenProcess = ((id, item), ~now=?) => Automation.process(id, item, ~now=nowOr(now))
264
279
  let whenExhausted = ((id, item)) => Automation.onExhausted(id, item)
265
280
  let thenCommand = (actual, expectedId, expectedCmd) =>
266
281
  switch actual {
@@ -282,13 +297,30 @@ module FromSlice = (
282
297
  // Scenario: a sweep routes each event in turn, as phase 1 does, so to-dos keep
283
298
  // the order the events produced them.
284
299
  let givenEvents = (es: array<sourced>) => es
300
+ /** The events happened at `time`: each one the scenario gave no time of its
301
+ own is recorded then. A sweep given no `~now` runs at the latest of them. */
302
+ let atTime = (events: array<sourced>, time) =>
303
+ events->Array.map(e => e.recordedAt->Option.isSome ? e : {...e, recordedAt: Some(time)})
304
+ let latestRecorded = (events: array<sourced>) =>
305
+ events->Array.reduce(None, (latest, e) =>
306
+ switch (latest, e.recordedAt) {
307
+ | (Some(l), Some(r)) => Some(Math.max(l, r))
308
+ | (None, r) => r
309
+ | (l, None) => l
310
+ }
311
+ )
285
312
  let routeAll = (events: array<sourced>, context) =>
286
313
  events->Array.flatMap(e =>
287
- Route.route(e.payload, ~sourceName=e.sourceName, ~sourceId=e.sourceId, context)
314
+ Route.route(
315
+ e.payload,
316
+ ~sourceName=e.sourceName,
317
+ ~sourceId=e.sourceId,
318
+ context->withRecordedAt(e.recordedAt),
319
+ )
288
320
  )
289
321
  /** Applies routed events as the runtime's to-do list does: the first writer of
290
322
  an id wins, and a resolve completes only a row that already exists. */
291
- let sweep = (routed: array<AutomationSlice_Route.routed<Spec.todoItem>>) => {
323
+ let sweep = (routed: array<AutomationSlice_Route.routed<Spec.todoItem>>, ~now) => {
292
324
  let rows: array<(string, Spec.todoItem, ref<bool>)> = []
293
325
  routed->Array.forEach(r => {
294
326
  r.collected->Array.forEach(((id, item)) =>
@@ -306,9 +338,16 @@ module FromSlice = (
306
338
  )
307
339
  })
308
340
  let todos = rows->Array.filterMap(((id, item, done)) => done.contents ? None : Some((id, item)))
309
- {todos, commands: todos->Array.filterMap(((id, item)) => Automation.process(id, item))}
341
+ {todos, commands: todos->Array.filterMap(((id, item)) => Automation.process(id, item, ~now))}
310
342
  }
311
- let whenSweep = (events, ~context=testContext) => sweep(routeAll(events, context))
343
+ let whenSweep = (events, ~context=testContext, ~now=?) =>
344
+ sweep(
345
+ routeAll(events, context),
346
+ ~now=switch (now, latestRecorded(events)) {
347
+ | (Some(now), _) | (None, Some(now)) => now
348
+ | (None, None) => clockNow()
349
+ },
350
+ )
312
351
  let thenCommands = (scenario, expected) =>
313
352
  scenario.commands == expected
314
353
  ? Outcome.pass
@@ -1,7 +1,9 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
+ import * as Clock from "effect/Clock";
4
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as Effect from "effect/Effect";
5
7
  import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
6
8
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
7
9
  import * as Outcome$ReventlessGwt from "./Outcome.res.mjs";
@@ -9,6 +11,18 @@ import * as JestBind$ReventlessGwt from "./JestBind.res.mjs";
9
11
  import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
10
12
  import * as AutomationSlice_Route$ReventlessCore from "@reventlessdev/reventless-core/src/components/AutomationSlice/AutomationSlice_Route.res.mjs";
11
13
 
14
+ function clockNow() {
15
+ return Effect.runSync(Clock.currentTimeMillis);
16
+ }
17
+
18
+ function nowOr(now) {
19
+ if (now !== undefined) {
20
+ return now;
21
+ } else {
22
+ return Effect.runSync(Clock.currentTimeMillis);
23
+ }
24
+ }
25
+
12
26
  function Make(Spec) {
13
27
  let test = (name, body) => JestBind$ReventlessGwt.test(Spec.name, name, body);
14
28
  let encTodos = arr => arr.map(param => [
@@ -53,7 +67,7 @@ function Make(Spec) {
53
67
  id,
54
68
  todo
55
69
  ];
56
- let whenProcess = param => Spec.process(param[0], param[1]);
70
+ let whenProcess = (param, now) => Spec.process(param[0], param[1], nowOr(now));
57
71
  let thenCommand = (actual, expectedId, expectedCmd) => {
58
72
  if (actual !== undefined) {
59
73
  let cmd = actual[1];
@@ -98,11 +112,12 @@ function Make(Spec) {
98
112
  });
99
113
  };
100
114
  let givenEvents = es => es;
101
- let whenSweep = events => {
115
+ let whenSweep = (events, now) => {
116
+ let now$1 = nowOr(now);
102
117
  let collected = events.map(e => Spec.collect(e)).flat();
103
118
  let resolvedIds = Stdlib_Array.filterMap(events, e => Spec.resolve(e));
104
119
  let pending = collected.filter(param => !resolvedIds.includes(param[0]));
105
- let commands = Stdlib_Array.filterMap(pending, param => Spec.process(param[0], param[1]));
120
+ let commands = Stdlib_Array.filterMap(pending, param => Spec.process(param[0], param[1], now$1));
106
121
  return {
107
122
  todos: pending,
108
123
  commands: commands
@@ -165,6 +180,11 @@ function FromSlice(Spec) {
165
180
  pluginName: "test",
166
181
  sliceName: testContext_sliceName
167
182
  };
183
+ let withRecordedAt = (context, recordedAt) => Stdlib_Option.mapOr(recordedAt, context, recordedAt => {
184
+ let newrecord = {...context};
185
+ newrecord.recordedAt = recordedAt;
186
+ return newrecord;
187
+ });
168
188
  let encTodos = arr => arr.map(param => [
169
189
  param[0],
170
190
  Message$ReventlessCore.encode(param[1], Spec.todoItemSchema)
@@ -202,19 +222,20 @@ function FromSlice(Spec) {
202
222
  }
203
223
  };
204
224
  let Mapping = M => {
205
- let event = (e, sourceIdOpt) => {
225
+ let event = (e, sourceIdOpt, recordedAt) => {
206
226
  let sourceId = sourceIdOpt !== undefined ? sourceIdOpt : "";
207
227
  return {
208
228
  sourceName: M.sourceName,
209
229
  sourceId: sourceId,
210
- payload: Message$ReventlessCore.encode(e, M.sourceEventSchema)
230
+ payload: Message$ReventlessCore.encode(e, M.sourceEventSchema),
231
+ recordedAt: recordedAt
211
232
  };
212
233
  };
213
234
  let givenEvent = e => e;
214
- let whenCollect = (e, sourceIdOpt, contextOpt) => {
235
+ let whenCollect = (e, sourceIdOpt, contextOpt, recordedAt) => {
215
236
  let sourceId = sourceIdOpt !== undefined ? sourceIdOpt : "";
216
237
  let context = contextOpt !== undefined ? contextOpt : testContext;
217
- return M.collect(e, sourceId, context);
238
+ return M.collect(e, sourceId, withRecordedAt(context, recordedAt));
218
239
  };
219
240
  return {
220
241
  event: event,
@@ -227,7 +248,7 @@ function FromSlice(Spec) {
227
248
  id,
228
249
  item
229
250
  ];
230
- let whenProcess = param => Automation.process(param[0], param[1]);
251
+ let whenProcess = (param, now) => Automation.process(param[0], param[1], nowOr(now));
231
252
  let whenExhausted = param => Automation.onExhausted(param[0], param[1]);
232
253
  let thenCommand = (actual, expectedId, expectedCmd) => {
233
254
  if (actual !== undefined && actual[0] === expectedId && Primitive_object.equal(actual[1], expectedCmd)) {
@@ -253,8 +274,32 @@ function FromSlice(Spec) {
253
274
  }
254
275
  };
255
276
  let givenEvents = es => es;
256
- let routeAll = (events, context) => events.flatMap(e => Route.route(e.payload, e.sourceName, e.sourceId, context));
257
- let sweep = routed => {
277
+ let atTime = (events, time) => events.map(e => {
278
+ if (Stdlib_Option.isSome(e.recordedAt)) {
279
+ return e;
280
+ } else {
281
+ return {
282
+ sourceName: e.sourceName,
283
+ sourceId: e.sourceId,
284
+ payload: e.payload,
285
+ recordedAt: time
286
+ };
287
+ }
288
+ });
289
+ let latestRecorded = events => Stdlib_Array.reduce(events, undefined, (latest, e) => {
290
+ let match = e.recordedAt;
291
+ if (latest !== undefined) {
292
+ if (match !== undefined) {
293
+ return Math.max(latest, match);
294
+ } else {
295
+ return latest;
296
+ }
297
+ } else {
298
+ return match;
299
+ }
300
+ });
301
+ let routeAll = (events, context) => events.flatMap(e => Route.route(e.payload, e.sourceName, e.sourceId, withRecordedAt(context, e.recordedAt)));
302
+ let sweep = (routed, now) => {
258
303
  let rows = [];
259
304
  routed.forEach(r => {
260
305
  r.collected.forEach(param => {
@@ -291,12 +336,15 @@ function FromSlice(Spec) {
291
336
  });
292
337
  return {
293
338
  todos: todos,
294
- commands: Stdlib_Array.filterMap(todos, param => Automation.process(param[0], param[1]))
339
+ commands: Stdlib_Array.filterMap(todos, param => Automation.process(param[0], param[1], now))
295
340
  };
296
341
  };
297
- let whenSweep = (events, contextOpt) => {
342
+ let whenSweep = (events, contextOpt, now) => {
298
343
  let context = contextOpt !== undefined ? contextOpt : testContext;
299
- return sweep(routeAll(events, context));
344
+ let match = latestRecorded(events);
345
+ return sweep(routeAll(events, context), now !== undefined ? now : (
346
+ match !== undefined ? match : Effect.runSync(Clock.currentTimeMillis)
347
+ ));
300
348
  };
301
349
  let thenCommands = (scenario, expected) => {
302
350
  if (Primitive_object.equal(scenario.commands, expected)) {
@@ -323,6 +371,7 @@ function FromSlice(Spec) {
323
371
  describe: JestBind$ReventlessGwt.describe,
324
372
  test: test,
325
373
  testContext: testContext,
374
+ withRecordedAt: withRecordedAt,
326
375
  encTodos: encTodos,
327
376
  encPair: encPair,
328
377
  thenTodos: thenTodos,
@@ -334,6 +383,8 @@ function FromSlice(Spec) {
334
383
  thenCommand: thenCommand,
335
384
  thenNoCommand: thenNoCommand,
336
385
  givenEvents: givenEvents,
386
+ atTime: atTime,
387
+ latestRecorded: latestRecorded,
337
388
  routeAll: routeAll,
338
389
  sweep: sweep,
339
390
  whenSweep: whenSweep,
@@ -345,7 +396,9 @@ function FromSlice(Spec) {
345
396
  }
346
397
 
347
398
  export {
399
+ clockNow,
400
+ nowOr,
348
401
  Make,
349
402
  FromSlice,
350
403
  }
351
- /* Message-ReventlessCore Not a pure module */
404
+ /* effect/Clock Not a pure module */
package/src/Flow_GWT.res CHANGED
@@ -505,7 +505,8 @@ module AutomationStep = (Spec: Automation_GWT.SliceSpec) => {
505
505
  let collected = events->Array.map(e => e->Spec.collect)->Array.flat
506
506
  let resolvedIds = events->Array.filterMap(e => e->Spec.resolve)
507
507
  let pending = collected->Array.filter(((id, _)) => !(resolvedIds->Array.includes(id)))
508
- let commands = pending->Array.filterMap(((id, todo)) => Spec.process(id, todo))
508
+ let now = Automation_GWT.clockNow()
509
+ let commands = pending->Array.filterMap(((id, todo)) => Spec.process(id, todo, ~now))
509
510
  {
510
511
  ...s,
511
512
  lastCommands: commands->Array.map(((_id, cmd)) => cmd->Message.encode(Spec.commandSchema)),
@@ -628,7 +629,7 @@ module AutomationSlice = (
628
629
  d.handle(entry.json, ~sourceId=entry->sourceIdOf, A.testContext)
629
630
  )
630
631
  )
631
- let {commands, _} = A.sweep(routed)
632
+ let {commands, _} = A.sweep(routed, ~now=Automation_GWT.clockNow())
632
633
  {
633
634
  ...s,
634
635
  lastCommands: commands->Array.map(((_id, cmd)) => cmd->Message.encode(Spec.commandSchema)),
@@ -406,7 +406,8 @@ function AutomationStep(Spec) {
406
406
  let collected = events.map(e => Spec.collect(e)).flat();
407
407
  let resolvedIds = Stdlib_Array.filterMap(events, e => Spec.resolve(e));
408
408
  let pending = collected.filter(param => !resolvedIds.includes(param[0]));
409
- let commands = Stdlib_Array.filterMap(pending, param => Spec.process(param[0], param[1]));
409
+ let now = Automation_GWT$ReventlessGwt.clockNow();
410
+ let commands = Stdlib_Array.filterMap(pending, param => Spec.process(param[0], param[1], now));
410
411
  return {
411
412
  log: s.log,
412
413
  outcome: s.outcome,
@@ -524,7 +525,7 @@ function AutomationSlice(Spec) {
524
525
  let whenReacts = async flowP => {
525
526
  let s = await flowP;
526
527
  let routed = s.log.flatMap(entry => Stdlib_Array.filterMap(A.Route.dispatches, d => d.handle(entry.json, Stdlib_Option.getOr(entry.aggregateId, ""), A.testContext)));
527
- let match = A.sweep(routed);
528
+ let match = A.sweep(routed, Automation_GWT$ReventlessGwt.clockNow());
528
529
  return {
529
530
  log: s.log,
530
531
  outcome: s.outcome,
@@ -250,10 +250,10 @@ module Make = (Projection: Reventless.Projection.Mapping): (
250
250
 
251
251
  let thenStatesWithId = async (thunk, id, expectedStates) => {
252
252
  let store = await thunk()
253
- let keys = store->Dict.keysToArray
254
- let actualId = keys->Array.get(0)
255
- let actualStates = store->Dict.valuesToArray->Array.get(0)->Option.getOr([])
256
- if keys->Array.length == 1 && actualId == Some(id) && statesEq(actualStates, expectedStates) {
253
+
254
+ // The rows under that id: rows the given events filed under other ids are another
255
+ // expectation's (`thenAllStates` checks them all).
256
+ if store->Dict.get(id)->Option.mapOr(false, actual => statesEq(actual, expectedStates)) {
257
257
  Outcome.pass
258
258
  } else {
259
259
  Outcome.fail(
@@ -255,10 +255,7 @@ function Make(Projection) {
255
255
  };
256
256
  let thenStatesWithId = async (thunk, id, expectedStates) => {
257
257
  let store = await thunk();
258
- let keys = Object.keys(store);
259
- let actualId = keys[0];
260
- let actualStates = Stdlib_Option.getOr(Object.values(store)[0], []);
261
- if (keys.length === 1 && Primitive_object.equal(actualId, id) && statesEq(actualStates, expectedStates)) {
258
+ if (Stdlib_Option.mapOr(store[id], false, actual => statesEq(actual, expectedStates))) {
262
259
  return Outcome$ReventlessGwt.pass;
263
260
  } else {
264
261
  return Outcome$ReventlessGwt.fail({
@@ -254,10 +254,10 @@ module Make = (Spec: Spec, Projection: Projection with module Spec := Spec): (
254
254
 
255
255
  let thenStatesWithId = async (thunk, id, expectedStates) => {
256
256
  let store = await thunk()
257
- let keys = store->Dict.keysToArray
258
- let actualId = keys->Array.get(0)
259
- let actualStates = store->Dict.valuesToArray->Array.get(0)->Option.getOr([])
260
- if keys->Array.length == 1 && actualId == Some(id) && statesEq(actualStates, expectedStates) {
257
+
258
+ // The rows under that id: rows the given events filed under other ids are another
259
+ // expectation's (`thenAllStates` checks them all).
260
+ if store->Dict.get(id)->Option.mapOr(false, actual => statesEq(actual, expectedStates)) {
261
261
  Outcome.pass
262
262
  } else {
263
263
  Outcome.fail(
@@ -238,10 +238,7 @@ function Make(Spec) {
238
238
  };
239
239
  let thenStatesWithId = async (thunk, id, expectedStates) => {
240
240
  let store = await thunk();
241
- let keys = Object.keys(store);
242
- let actualId = keys[0];
243
- let actualStates = Stdlib_Option.getOr(Object.values(store)[0], []);
244
- if (keys.length === 1 && Primitive_object.equal(actualId, id) && statesEq(actualStates, expectedStates)) {
241
+ if (Stdlib_Option.mapOr(store[id], false, actual => statesEq(actual, expectedStates))) {
245
242
  return Outcome$ReventlessGwt.pass;
246
243
  } else {
247
244
  return Outcome$ReventlessGwt.fail({
@@ -0,0 +1,111 @@
1
+ // A deadline tested at a fixed time: an order not shipped within three days of
2
+ // being placed is cancelled. `collect` counts the due time from the event's
3
+ // recorded time; `process` compares it with `~now`. `atTime` and `~recordedAt`
4
+ // say when the events happened, `~now` when the sweep runs.
5
+
6
+ module CancelUnshipped = {
7
+ let name = "CancelUnshipped"
8
+ let moduleUrl = ""
9
+
10
+ @schema
11
+ type todoItem = {orderId: string, dueAt: float}
12
+
13
+ @schema
14
+ type command = CancelOrder({orderId: string})
15
+
16
+ let maxRetries = 3
17
+ let heartbeatInterval = 60
18
+ let targetName = "Order"
19
+ }
20
+
21
+ let day = 24. *. 60. *. 60. *. 1000.
22
+ let day0 = Date.fromString("2026-10-01T09:00:00Z")->Date.getTime
23
+
24
+ module CancelUnshipped_Automation = {
25
+ module M = Reventless.AutomationSlice.Mappings.Make(CancelUnshipped)
26
+ module type Mapping = M.Mapping
27
+ let moduleUrl = ""
28
+
29
+ module Orders = {
30
+ module Id = Reventless.Id.String
31
+ let name = "Orders"
32
+ @schema
33
+ type event =
34
+ | OrderPlaced({orderId: string})
35
+ | OrderShipped({orderId: string})
36
+ }
37
+
38
+ module FromOrders = Reventless.AutomationSlice.Mapping.Make(
39
+ Orders,
40
+ CancelUnshipped,
41
+ {
42
+ let collect = (event, ~sourceId as _, ctx: Reventless.AutomationSlice.context) =>
43
+ switch (event, ctx.recordedAt) {
44
+ | (Orders.OrderPlaced({orderId}), Some(placedAt)) => [
45
+ (orderId, ({orderId, dueAt: placedAt +. 3. *. day}: CancelUnshipped.todoItem)),
46
+ ]
47
+ | _ => []
48
+ }
49
+ let resolve = event =>
50
+ switch event {
51
+ | Orders.OrderShipped({orderId}) => Some(orderId)
52
+ | OrderPlaced(_) => None
53
+ }
54
+ },
55
+ )
56
+
57
+ let mappings: array<module(Mapping)> = [module(FromOrders)]
58
+
59
+ let process = (id, item: CancelUnshipped.todoItem, ~now) =>
60
+ now >= item.dueAt ? Some((id, CancelUnshipped.CancelOrder({orderId: item.orderId}))) : None
61
+ let onExhausted = (_id, _item: CancelUnshipped.todoItem) => None
62
+ }
63
+
64
+ open CancelUnshipped
65
+ open CancelUnshipped_Automation
66
+ include Automation_GWT.FromSlice(CancelUnshipped, CancelUnshipped_Automation)
67
+ module O = Mapping(FromOrders)
68
+
69
+ describe("a deadline at a fixed time", () => {
70
+ test("collect counts the due time from the event's recorded time", () =>
71
+ O.givenEvent(OrderPlaced({orderId: "o1"}))
72
+ ->O.whenCollect(~recordedAt=day0)
73
+ ->thenTodos([("o1", {orderId: "o1", dueAt: day0 +. 3. *. day})])
74
+ )
75
+
76
+ test("not yet due on day 2: no command", () =>
77
+ givenEvents([O.event(OrderPlaced({orderId: "o1"}))])
78
+ ->atTime(day0)
79
+ ->whenSweep(~now=day0 +. 2. *. day)
80
+ ->thenCommands([])
81
+ )
82
+
83
+ test("due on day 3: CancelOrder", () =>
84
+ givenEvents([O.event(OrderPlaced({orderId: "o1"}))])
85
+ ->atTime(day0)
86
+ ->whenSweep(~now=day0 +. 3. *. day)
87
+ ->thenCommands([("o1", CancelOrder({orderId: "o1"}))])
88
+ )
89
+
90
+ test("shipped on day 2: nothing on day 3", () =>
91
+ givenEvents([
92
+ O.event(OrderPlaced({orderId: "o1"}), ~recordedAt=day0),
93
+ O.event(OrderShipped({orderId: "o1"}), ~recordedAt=day0 +. 2. *. day),
94
+ ])
95
+ ->whenSweep(~now=day0 +. 3. *. day)
96
+ ->thenCommands([])
97
+ )
98
+
99
+ test("a sweep given no time runs when the events happened", () =>
100
+ givenEvents([O.event(OrderPlaced({orderId: "o1"}))])
101
+ ->atTime(day0)
102
+ ->whenSweep
103
+ ->thenCommands([])
104
+ )
105
+
106
+ test("process is given the time a scenario fixes", () =>
107
+ givenTodo("o1", {orderId: "o1", dueAt: day0})
108
+ ->whenProcess(~now=day0)
109
+ ->thenCommand("o1", CancelOrder({orderId: "o1"}))
110
+ )
111
+ })
@@ -0,0 +1,267 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Sury from "sury";
4
+ import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
5
+ import * as AutomationSlice$Reventless from "@reventlessdev/reventless-spec/src/components/AutomationSlice.res.mjs";
6
+ import * as Automation_GWT$ReventlessGwt from "../src/Automation_GWT.res.mjs";
7
+
8
+ let todoItemSchema = Sury.$schema(s => ({
9
+ orderId: s.m(Sury.string),
10
+ dueAt: s.m(Sury.number)
11
+ }));
12
+
13
+ let commandSchema = Sury.$schema(s => ({
14
+ TAG: "CancelOrder",
15
+ orderId: s.m(Sury.string)
16
+ }));
17
+
18
+ function authorizationOf(param) {
19
+ return "AllowAuthenticated";
20
+ }
21
+
22
+ function commandTransition(param) {
23
+ return "Undeclared";
24
+ }
25
+
26
+ let traits = [];
27
+
28
+ let CancelUnshipped = {
29
+ name: "CancelUnshipped",
30
+ moduleUrl: "",
31
+ todoItemSchema: todoItemSchema,
32
+ commandSchema: commandSchema,
33
+ maxRetries: 3,
34
+ heartbeatInterval: 60,
35
+ targetName: "Order",
36
+ authorizationOf: authorizationOf,
37
+ commandTransition: commandTransition,
38
+ traits: traits
39
+ };
40
+
41
+ let day = 24 * 60 * 60 * 1000;
42
+
43
+ let day0 = new Date("2026-10-01T09:00:00Z").getTime();
44
+
45
+ let M = AutomationSlice$Reventless.Mappings.Make(CancelUnshipped);
46
+
47
+ let moduleUrl = "";
48
+
49
+ let name = "Orders";
50
+
51
+ let eventSchema = Sury.union([
52
+ Sury.$schema(s => ({
53
+ TAG: "OrderPlaced",
54
+ orderId: s.m(Sury.string)
55
+ })),
56
+ Sury.$schema(s => ({
57
+ TAG: "OrderShipped",
58
+ orderId: s.m(Sury.string)
59
+ }))
60
+ ]);
61
+
62
+ let Orders = {
63
+ Id: undefined,
64
+ name: name,
65
+ eventSchema: eventSchema
66
+ };
67
+
68
+ function collect(event, param, ctx) {
69
+ let match = ctx.recordedAt;
70
+ if (event.TAG !== "OrderPlaced") {
71
+ return [];
72
+ }
73
+ if (match === undefined) {
74
+ return [];
75
+ }
76
+ let orderId = event.orderId;
77
+ return [[
78
+ orderId,
79
+ {
80
+ orderId: orderId,
81
+ dueAt: match + 3 * day
82
+ }
83
+ ]];
84
+ }
85
+
86
+ function resolve(event) {
87
+ if (event.TAG === "OrderPlaced") {
88
+ return;
89
+ } else {
90
+ return event.orderId;
91
+ }
92
+ }
93
+
94
+ let FromOrders = AutomationSlice$Reventless.Mapping.Make({
95
+ Id: Id$Reventless.$$String,
96
+ name: name,
97
+ eventSchema: eventSchema
98
+ })(CancelUnshipped)({
99
+ collect: collect,
100
+ resolve: resolve
101
+ });
102
+
103
+ let mappings = [FromOrders];
104
+
105
+ function process(id, item, now) {
106
+ if (now >= item.dueAt) {
107
+ return [
108
+ id,
109
+ {
110
+ TAG: "CancelOrder",
111
+ orderId: item.orderId
112
+ }
113
+ ];
114
+ }
115
+ }
116
+
117
+ function onExhausted(_id, _item) {
118
+
119
+ }
120
+
121
+ let CancelUnshipped_Automation = {
122
+ M: M,
123
+ moduleUrl: moduleUrl,
124
+ Orders: Orders,
125
+ FromOrders: FromOrders,
126
+ mappings: mappings,
127
+ process: process,
128
+ onExhausted: onExhausted
129
+ };
130
+
131
+ let include = Automation_GWT$ReventlessGwt.FromSlice(CancelUnshipped)({
132
+ process: process,
133
+ onExhausted: onExhausted,
134
+ moduleUrl: moduleUrl,
135
+ mappings: mappings
136
+ });
137
+
138
+ let describe = include.describe;
139
+
140
+ let test = include.test;
141
+
142
+ let thenTodos = include.thenTodos;
143
+
144
+ let Mapping = include.Mapping;
145
+
146
+ let givenTodo = include.givenTodo;
147
+
148
+ let whenProcess = include.whenProcess;
149
+
150
+ let thenCommand = include.thenCommand;
151
+
152
+ let givenEvents = include.givenEvents;
153
+
154
+ let atTime = include.atTime;
155
+
156
+ let whenSweep = include.whenSweep;
157
+
158
+ let thenCommands = include.thenCommands;
159
+
160
+ let O = Mapping(FromOrders);
161
+
162
+ describe("a deadline at a fixed time", () => {
163
+ test("collect counts the due time from the event's recorded time", () => thenTodos(O.whenCollect(O.givenEvent({
164
+ TAG: "OrderPlaced",
165
+ orderId: "o1"
166
+ }), undefined, undefined, day0), [[
167
+ "o1",
168
+ {
169
+ orderId: "o1",
170
+ dueAt: day0 + 3 * day
171
+ }
172
+ ]]));
173
+ test("not yet due on day 2: no command", () => thenCommands(whenSweep(atTime(givenEvents([O.event({
174
+ TAG: "OrderPlaced",
175
+ orderId: "o1"
176
+ }, undefined, undefined)]), day0), undefined, day0 + 2 * day), []));
177
+ test("due on day 3: CancelOrder", () => thenCommands(whenSweep(atTime(givenEvents([O.event({
178
+ TAG: "OrderPlaced",
179
+ orderId: "o1"
180
+ }, undefined, undefined)]), day0), undefined, day0 + 3 * day), [[
181
+ "o1",
182
+ {
183
+ TAG: "CancelOrder",
184
+ orderId: "o1"
185
+ }
186
+ ]]));
187
+ test("shipped on day 2: nothing on day 3", () => thenCommands(whenSweep(givenEvents([
188
+ O.event({
189
+ TAG: "OrderPlaced",
190
+ orderId: "o1"
191
+ }, undefined, day0),
192
+ O.event({
193
+ TAG: "OrderShipped",
194
+ orderId: "o1"
195
+ }, undefined, day0 + 2 * day)
196
+ ]), undefined, day0 + 3 * day), []));
197
+ test("a sweep given no time runs when the events happened", () => thenCommands(whenSweep(atTime(givenEvents([O.event({
198
+ TAG: "OrderPlaced",
199
+ orderId: "o1"
200
+ }, undefined, undefined)]), day0), undefined, undefined), []));
201
+ test("process is given the time a scenario fixes", () => thenCommand(whenProcess(givenTodo("o1", {
202
+ orderId: "o1",
203
+ dueAt: day0
204
+ }), day0), "o1", {
205
+ TAG: "CancelOrder",
206
+ orderId: "o1"
207
+ }));
208
+ });
209
+
210
+ let Route = include.Route;
211
+
212
+ let testContext = include.testContext;
213
+
214
+ let withRecordedAt = include.withRecordedAt;
215
+
216
+ let encTodos = include.encTodos;
217
+
218
+ let encPair = include.encPair;
219
+
220
+ let thenResolved = include.thenResolved;
221
+
222
+ let whenExhausted = include.whenExhausted;
223
+
224
+ let thenNoCommand = include.thenNoCommand;
225
+
226
+ let latestRecorded = include.latestRecorded;
227
+
228
+ let routeAll = include.routeAll;
229
+
230
+ let sweep = include.sweep;
231
+
232
+ let andThenEvents = include.andThenEvents;
233
+
234
+ let thenScenarioTodos = include.thenScenarioTodos;
235
+
236
+ export {
237
+ CancelUnshipped,
238
+ day,
239
+ day0,
240
+ CancelUnshipped_Automation,
241
+ Route,
242
+ describe,
243
+ test,
244
+ testContext,
245
+ withRecordedAt,
246
+ encTodos,
247
+ encPair,
248
+ thenTodos,
249
+ thenResolved,
250
+ Mapping,
251
+ givenTodo,
252
+ whenProcess,
253
+ whenExhausted,
254
+ thenCommand,
255
+ thenNoCommand,
256
+ givenEvents,
257
+ atTime,
258
+ latestRecorded,
259
+ routeAll,
260
+ sweep,
261
+ whenSweep,
262
+ thenCommands,
263
+ andThenEvents,
264
+ thenScenarioTodos,
265
+ O,
266
+ }
267
+ /* todoItemSchema Not a pure module */
@@ -79,7 +79,10 @@ module ShipPaid_Automation = {
79
79
 
80
80
  let mappings: array<module(Mapping)> = [module(FromOrders), module(FromPayments)]
81
81
 
82
- let process = (id, _item: ShipPaid.todoItem) => Some((id, ShipPaid.Ship({orderId: id})))
82
+ let process = (id, _item: ShipPaid.todoItem, ~now as _) => Some((
83
+ id,
84
+ ShipPaid.Ship({orderId: id}),
85
+ ))
83
86
  let onExhausted = (id, _item: ShipPaid.todoItem) => Some((id, ShipPaid.GiveUp({orderId: id})))
84
87
  }
85
88
 
@@ -146,7 +146,7 @@ let mappings = [
146
146
  FromPayments
147
147
  ];
148
148
 
149
- function process(id, _item) {
149
+ function process(id, _item, param) {
150
150
  return [
151
151
  id,
152
152
  {
@@ -221,7 +221,7 @@ describe("Automation_GWT.FromSlice", () => {
221
221
  test("collect through one mapping", () => thenTodos(O.whenCollect(O.givenEvent({
222
222
  TAG: "OrderPlaced",
223
223
  orderId: "o1"
224
- }), undefined, undefined), [[
224
+ }), undefined, undefined, undefined), [[
225
225
  "o1",
226
226
  {
227
227
  orderId: "o1"
@@ -230,7 +230,7 @@ describe("Automation_GWT.FromSlice", () => {
230
230
  test("collect keys by the source id where the event does not name it", () => thenTodos(P.whenCollect(P.givenEvent({
231
231
  TAG: "Captured",
232
232
  amount: 5
233
- }), "o2", undefined), [[
233
+ }), "o2", undefined, undefined), [[
234
234
  "o2",
235
235
  {
236
236
  orderId: "o2"
@@ -242,7 +242,7 @@ describe("Automation_GWT.FromSlice", () => {
242
242
  })), "o1"));
243
243
  test("process", () => thenCommand(whenProcess(givenTodo("o1", {
244
244
  orderId: "o1"
245
- })), "o1", {
245
+ }), undefined), "o1", {
246
246
  TAG: "Ship",
247
247
  orderId: "o1"
248
248
  }));
@@ -256,12 +256,12 @@ describe("Automation_GWT.FromSlice", () => {
256
256
  P.event({
257
257
  TAG: "Captured",
258
258
  amount: 5
259
- }, "o2"),
259
+ }, "o2", undefined),
260
260
  O.event({
261
261
  TAG: "OrderPlaced",
262
262
  orderId: "o1"
263
- }, undefined)
264
- ]), undefined), [
263
+ }, undefined, undefined)
264
+ ]), undefined, undefined), [
265
265
  [
266
266
  "o2",
267
267
  {
@@ -281,20 +281,20 @@ describe("Automation_GWT.FromSlice", () => {
281
281
  O.event({
282
282
  TAG: "OrderCancelled",
283
283
  orderId: "o1"
284
- }, undefined),
284
+ }, undefined, undefined),
285
285
  O.event({
286
286
  TAG: "OrderPlaced",
287
287
  orderId: "o1"
288
- }, undefined),
288
+ }, undefined, undefined),
289
289
  O.event({
290
290
  TAG: "OrderPlaced",
291
291
  orderId: "o3"
292
- }, undefined),
292
+ }, undefined, undefined),
293
293
  O.event({
294
294
  TAG: "OrderCancelled",
295
295
  orderId: "o3"
296
- }, undefined)
297
- ]), undefined), [[
296
+ }, undefined, undefined)
297
+ ]), undefined, undefined), [[
298
298
  "o1",
299
299
  {
300
300
  orderId: "o1"
@@ -303,22 +303,28 @@ describe("Automation_GWT.FromSlice", () => {
303
303
  test("andThenEvents drains what later events resolve", () => thenScenarioTodos(andThenEvents(whenSweep(givenEvents([O.event({
304
304
  TAG: "OrderPlaced",
305
305
  orderId: "o1"
306
- }, undefined)]), undefined), [O.event({
306
+ }, undefined, undefined)]), undefined, undefined), [O.event({
307
307
  TAG: "OrderCancelled",
308
308
  orderId: "o1"
309
- }, undefined)], undefined), []));
309
+ }, undefined, undefined)], undefined), []));
310
310
  });
311
311
 
312
312
  let Route = include.Route;
313
313
 
314
314
  let testContext = include.testContext;
315
315
 
316
+ let withRecordedAt = include.withRecordedAt;
317
+
316
318
  let encTodos = include.encTodos;
317
319
 
318
320
  let encPair = include.encPair;
319
321
 
320
322
  let thenNoCommand = include.thenNoCommand;
321
323
 
324
+ let atTime = include.atTime;
325
+
326
+ let latestRecorded = include.latestRecorded;
327
+
322
328
  let routeAll = include.routeAll;
323
329
 
324
330
  let sweep = include.sweep;
@@ -330,6 +336,7 @@ export {
330
336
  describe,
331
337
  test,
332
338
  testContext,
339
+ withRecordedAt,
333
340
  encTodos,
334
341
  encPair,
335
342
  thenTodos,
@@ -341,6 +348,8 @@ export {
341
348
  thenCommand,
342
349
  thenNoCommand,
343
350
  givenEvents,
351
+ atTime,
352
+ latestRecorded,
344
353
  routeAll,
345
354
  sweep,
346
355
  whenSweep,
@@ -31,7 +31,7 @@ module ShipOrderSlice = {
31
31
  | _ => None
32
32
  }
33
33
 
34
- let process = (_id, item) => Some((
34
+ let process = (_id, item, ~now as _) => Some((
35
35
  item.orderId,
36
36
  CreateShipment({orderId: item.orderId, address: item.shippingAddress}),
37
37
  ))
@@ -49,7 +49,7 @@ function resolve(event) {
49
49
  }
50
50
  }
51
51
 
52
- function process(_id, item) {
52
+ function process(_id, item, param) {
53
53
  return [
54
54
  item.orderId,
55
55
  {
@@ -139,7 +139,7 @@ describe("ShipOrder AutomationSlice", () => {
139
139
  test("process: pending TODO emits CreateShipment", () => thenCommand(whenProcess(givenTodo("o1", {
140
140
  orderId: "o1",
141
141
  shippingAddress: "1 Main St"
142
- })), "o1", {
142
+ }), undefined), "o1", {
143
143
  TAG: "CreateShipment",
144
144
  orderId: "o1",
145
145
  address: "1 Main St"
@@ -149,7 +149,7 @@ describe("ShipOrder AutomationSlice", () => {
149
149
  TAG: "OrderPlaced",
150
150
  orderId: "o1",
151
151
  shippingAddress: "1 Main St"
152
- }]));
152
+ }]), undefined);
153
153
  let commandsOk = thenCommands(s, [[
154
154
  "o1",
155
155
  {
@@ -73,7 +73,7 @@ module AutoShipSlice = {
73
73
  | OrderPlaced(_) => None
74
74
  }
75
75
 
76
- let process = (id, _todo) => Some((id, ShipOrder({orderId: id})))
76
+ let process = (id, _todo, ~now as _) => Some((id, ShipOrder({orderId: id})))
77
77
  let onExhausted = (_id, _todo) => None
78
78
  }
79
79
 
@@ -122,7 +122,7 @@ function resolve(event) {
122
122
  }
123
123
  }
124
124
 
125
- function process(id, _todo) {
125
+ function process(id, _todo, param) {
126
126
  return [
127
127
  id,
128
128
  {
@@ -50,4 +50,11 @@ describe("CategoriesView StateViewSlice", () => {
50
50
  ->whenEvent(CategoryRenamed({categoryId: "c1", name: "Consumer Electronics"}))
51
51
  ->thenStateWithId("c1", {categoryId: "c1", name: "Consumer Electronics", archived: false})
52
52
  )
53
+
54
+ // The rows under that id: another category's row, from a given event, is not this one's.
55
+ test("a second category gets its own row, the first one's left as it is", () =>
56
+ givenEvents([CategoryAdded({categoryId: "c1", name: "Electronics"})])
57
+ ->whenEvent(CategoryAdded({categoryId: "c2", name: "Garden"}))
58
+ ->thenStateWithId("c2", {categoryId: "c2", name: "Garden", archived: false})
59
+ )
53
60
  })
@@ -131,6 +131,19 @@ describe("CategoriesView StateViewSlice", () => {
131
131
  name: "Consumer Electronics",
132
132
  archived: false
133
133
  }));
134
+ test("a second category gets its own row, the first one's left as it is", undefined, () => thenStateWithId(whenEvent(givenEvents([{
135
+ TAG: "CategoryAdded",
136
+ categoryId: "c1",
137
+ name: "Electronics"
138
+ }]), {
139
+ TAG: "CategoryAdded",
140
+ categoryId: "c2",
141
+ name: "Garden"
142
+ }), "c2", {
143
+ categoryId: "c2",
144
+ name: "Garden",
145
+ archived: false
146
+ }));
134
147
  });
135
148
 
136
149
  let Spec = include.Spec;