@reventlessdev/reventless-gwt 1.0.0-alpha.242 → 1.0.0-alpha.244

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.244 (2026-10-16)
7
+
8
+ * feat!(automation): schedules set by events ([53bc436](https://github.com/ReventlessDev/reventless-core/commit/53bc4363dffce81ef36475728bf0dae704716021))
9
+ * feat!(automation): declared schedules tick an automation ([420acd2](https://github.com/ReventlessDev/reventless-core/commit/420acd2233aadf700141a31d9d3e0fdc8074bef7))
10
+
11
+ ### BREAKING CHANGES
12
+
13
+ * a hand-written AutomationSlice.Mapping.Make the PPX does
14
+ not see must define `let reschedule = _ => []`.
15
+ * a hand-written AutomationSlice spec the PPX does not see must
16
+ declare `let schedule = None`.
17
+
18
+
19
+
20
+ # 1.0.0-alpha.243 (2026-10-15)
21
+
22
+ * feat!: an automation's process takes ~now, and collect the event's recorded time ([8c8f3f5](https://github.com/ReventlessDev/reventless-core/commit/8c8f3f570474b1658b605b726077debc7d6a1dc7))
23
+
24
+ ### BREAKING CHANGES
25
+
26
+ * a process the PPX does not see — in an Automation module
27
+ written by hand, or a SliceSpec given to the deprecated Automation_GWT.Make
28
+ or Flow_GWT.AutomationStep — must take ~now.
29
+
30
+
31
+
6
32
  # 1.0.0-alpha.242 (2026-10-14)
7
33
 
8
34
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-gwt",
3
- "version": "1.0.0-alpha.242",
3
+ "version": "1.0.0-alpha.244",
4
4
  "description": "Given-When-Then DSLs and test harness for Reventless slice testing",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -14,17 +14,17 @@
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
18
  "@reventlessdev/rescript-node": "2.0.0-alpha.22",
19
- "@reventlessdev/reventless-core": "3.0.0-alpha.294",
20
- "@reventlessdev/reventless-infra": "3.0.0-alpha.191",
19
+ "@reventlessdev/reventless-core": "3.0.0-alpha.296",
20
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.193",
21
21
  "@reventlessdev/rescript-jest": "1.0.0-alpha.12",
22
- "@reventlessdev/reventless-spec": "3.0.0-alpha.163"
22
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.165"
23
23
  },
24
24
  "devDependencies": {
25
25
  "rescript": "12.3.1",
26
26
  "sury-ppx": "11.0.0",
27
- "@reventlessdev/reventless-ppx": "1.0.0-alpha.100"
27
+ "@reventlessdev/reventless-ppx": "1.0.0-alpha.101"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "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,13 +226,19 @@ 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)>,
226
239
  commands: array<(string, Spec.command)>,
240
+ /** The slice's schedule rows after the events, by key. */
241
+ schedules: dict<AutomationSlice_Schedules.row>,
227
242
  }
228
243
 
229
244
  let encTodos = (arr: array<(string, Spec.todoItem)>) =>
@@ -248,19 +263,21 @@ module FromSlice = (
248
263
  }
249
264
 
250
265
  module Mapping = (M: Automation.Mapping) => {
251
- let event = (e: M.sourceEvent, ~sourceId="") => {
266
+ let event = (e: M.sourceEvent, ~sourceId="", ~recordedAt=?) => {
252
267
  sourceName: M.sourceName,
253
268
  sourceId,
254
269
  payload: e->Message.encode(M.sourceEventSchema),
270
+ recordedAt,
255
271
  }
256
272
  let givenEvent = (e: M.sourceEvent) => e
257
- let whenCollect = (e, ~sourceId="", ~context=testContext) => M.collect(e, ~sourceId, context)
273
+ let whenCollect = (e, ~sourceId="", ~context=testContext, ~recordedAt=?) =>
274
+ M.collect(e, ~sourceId, context->withRecordedAt(recordedAt))
258
275
  let whenResolve = M.resolve
259
276
  }
260
277
 
261
278
  // Unit: process and abandonment
262
279
  let givenTodo = (id: string, item: Spec.todoItem) => (id, item)
263
- let whenProcess = ((id, item)) => Automation.process(id, item)
280
+ let whenProcess = ((id, item), ~now=?) => Automation.process(id, item, ~now=nowOr(now))
264
281
  let whenExhausted = ((id, item)) => Automation.onExhausted(id, item)
265
282
  let thenCommand = (actual, expectedId, expectedCmd) =>
266
283
  switch actual {
@@ -282,13 +299,66 @@ module FromSlice = (
282
299
  // Scenario: a sweep routes each event in turn, as phase 1 does, so to-dos keep
283
300
  // the order the events produced them.
284
301
  let givenEvents = (es: array<sourced>) => es
285
- let routeAll = (events: array<sourced>, context) =>
286
- events->Array.flatMap(e =>
287
- Route.route(e.payload, ~sourceName=e.sourceName, ~sourceId=e.sourceId, context)
302
+ /** The events happened at `time`: each one the scenario gave no time of its
303
+ own is recorded then. A sweep given no `~now` runs at the latest of them. */
304
+ let atTime = (events: array<sourced>, time) =>
305
+ events->Array.map(e => e.recordedAt->Option.isSome ? e : {...e, recordedAt: Some(time)})
306
+ let latestRecorded = (events: array<sourced>) =>
307
+ events->Array.reduce(None, (latest, e) =>
308
+ switch (latest, e.recordedAt) {
309
+ | (Some(l), Some(r)) => Some(Math.max(l, r))
310
+ | (None, r) => r
311
+ | (l, None) => l
312
+ }
288
313
  )
314
+ let routeEach = (events: array<sourced>, context) =>
315
+ events->Array.map(e => (
316
+ e,
317
+ Route.route(
318
+ e.payload,
319
+ ~sourceName=e.sourceName,
320
+ ~sourceId=e.sourceId,
321
+ context->withRecordedAt(e.recordedAt),
322
+ ),
323
+ ))
324
+ let routeAll = (events: array<sourced>, context) =>
325
+ routeEach(events, context)->Array.flatMap(((_, routed)) => routed)
326
+
327
+ /** Folds the events' `reschedule` into the rows as the runtime does; a tick
328
+ removes the once-only schedule it fired. A time that cannot fire changes
329
+ nothing, as at run time. */
330
+ let foldSchedules = (
331
+ rows,
332
+ routedEvents: array<(sourced, array<AutomationSlice_Route.routed<Spec.todoItem>>)>,
333
+ ~now,
334
+ ) =>
335
+ routedEvents->Array.forEach(((e, routed)) => {
336
+ let recordedAt = e.recordedAt->Option.getOr(now)
337
+ routed->Array.forEach(r =>
338
+ r.rescheduled->Array.forEach(
339
+ change => {
340
+ let _ = rows->AutomationSlice_Schedules.apply(change, ~setBy="scenario", ~recordedAt)
341
+ },
342
+ )
343
+ )
344
+ if e.sourceName == Reventless.Clock.name {
345
+ rows->AutomationSlice_Schedules.fired(~key=e.sourceId)
346
+ }
347
+ })
348
+
349
+ // The declared time is in the rows before any event, as a deploy put it there.
350
+ let declaredRows = (~now) => {
351
+ let rows = Dict.make()
352
+ rows->AutomationSlice_Schedules.declare(Spec.schedule, ~now)
353
+ rows
354
+ }
289
355
  /** Applies routed events as the runtime's to-do list does: the first writer of
290
356
  an id wins, and a resolve completes only a row that already exists. */
291
- let sweep = (routed: array<AutomationSlice_Route.routed<Spec.todoItem>>) => {
357
+ let sweep = (
358
+ routed: array<AutomationSlice_Route.routed<Spec.todoItem>>,
359
+ ~now,
360
+ ~schedules=declaredRows(~now),
361
+ ) => {
292
362
  let rows: array<(string, Spec.todoItem, ref<bool>)> = []
293
363
  routed->Array.forEach(r => {
294
364
  r.collected->Array.forEach(((id, item)) =>
@@ -306,9 +376,34 @@ module FromSlice = (
306
376
  )
307
377
  })
308
378
  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))}
379
+ {
380
+ todos,
381
+ commands: todos->Array.filterMap(((id, item)) => Automation.process(id, item, ~now)),
382
+ schedules,
383
+ }
384
+ }
385
+ let whenSweep = (events, ~context=testContext, ~now=?) => {
386
+ let now = switch (now, latestRecorded(events)) {
387
+ | (Some(now), _) | (None, Some(now)) => now
388
+ | (None, None) => clockNow()
389
+ }
390
+ let routedEvents = routeEach(events, context)
391
+ let schedules = declaredRows(~now)
392
+ schedules->foldSchedules(routedEvents, ~now)
393
+ sweep(routedEvents->Array.flatMap(((_, routed)) => routed), ~now, ~schedules)
310
394
  }
311
- let whenSweep = (events, ~context=testContext) => sweep(routeAll(events, context))
395
+ /** A tick from one of the slice's schedules, as the `Clock` source delivers it:
396
+ `key` defaults to the declared schedule's. */
397
+ let tick = (~key=Reventless.Clock.defaultKey, at) => {
398
+ sourceName: Reventless.Clock.name,
399
+ sourceId: key,
400
+ payload: Reventless.Clock.Tick({key, at})->Message.encode(Reventless.Clock.eventSchema),
401
+ recordedAt: Some(at),
402
+ }
403
+ /** The given events, then a tick at `at`, swept at `at`: what the slice does when its
404
+ schedule comes due. A tick delivered twice makes one item. */
405
+ let whenTick = (events, ~key=?, ~at, ~context=testContext) =>
406
+ whenSweep(events->Array.concat([tick(~key?, at)]), ~context, ~now=at)
312
407
  let thenCommands = (scenario, expected) =>
313
408
  scenario.commands == expected
314
409
  ? Outcome.pass
@@ -319,8 +414,37 @@ module FromSlice = (
319
414
  }),
320
415
  )
321
416
  let andThenEvents = (scenario, events, ~context=testContext) => {
322
- let resolved = routeAll(events, context)->Array.filterMap(r => r.resolved)
323
- {...scenario, todos: scenario.todos->Array.filter(((id, _)) => !(resolved->Array.includes(id)))}
417
+ let routedEvents = routeEach(events, context)
418
+ let resolved =
419
+ routedEvents->Array.flatMap(((_, routed)) => routed)->Array.filterMap(r => r.resolved)
420
+ let schedules = scenario.schedules->Dict.copy
421
+ schedules->foldSchedules(routedEvents, ~now=latestRecorded(events)->Option.getOr(clockNow()))
422
+ {
423
+ ...scenario,
424
+ todos: scenario.todos->Array.filter(((id, _)) => !(resolved->Array.includes(id))),
425
+ schedules,
426
+ }
427
+ }
428
+ /** The slice's schedules after the scenario, by key: the declared time unless an
429
+ event replaced or cleared it, and every time an event set. An `After` an event
430
+ set is counted from when the event was recorded. */
431
+ let thenSchedules = (scenario, expected: array<(string, Reventless.Schedule.time)>) => {
432
+ let enc = ((key, time)) =>
433
+ JSON.Encode.object(
434
+ Dict.fromArray([
435
+ ("key", key->JSON.Encode.string),
436
+ ("time", time->Message.encode(Reventless.Schedule.timeSchema)),
437
+ ]),
438
+ )
439
+ let byKey = rows => rows->Array.toSorted(((a, _), (b, _)) => String.compare(a, b))
440
+ let actual =
441
+ scenario.schedules
442
+ ->Dict.valuesToArray
443
+ ->Array.filterMap(row => row->AutomationSlice_Schedules.timeOf->Option.map(t => (row.key, t)))
444
+ ->byKey
445
+ ->Array.map(enc)
446
+ let expected = expected->byKey->Array.map(enc)
447
+ actual == expected ? Outcome.pass : Outcome.fail(QueryRowsMismatch({expected, actual}))
324
448
  }
325
449
  let thenScenarioTodos = (scenario, expected) => thenTodos(scenario.todos, expected)
326
450
  }
@@ -1,13 +1,31 @@
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";
7
+ import * as Clock$Reventless from "@reventlessdev/reventless-spec/src/components/Clock.res.mjs";
5
8
  import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
6
9
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
10
+ import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
11
+ import * as Schedule$Reventless from "@reventlessdev/reventless-spec/src/types/Schedule.res.mjs";
7
12
  import * as Outcome$ReventlessGwt from "./Outcome.res.mjs";
8
13
  import * as JestBind$ReventlessGwt from "./JestBind.res.mjs";
9
14
  import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
10
15
  import * as AutomationSlice_Route$ReventlessCore from "@reventlessdev/reventless-core/src/components/AutomationSlice/AutomationSlice_Route.res.mjs";
16
+ import * as AutomationSlice_Schedules$ReventlessCore from "@reventlessdev/reventless-core/src/components/AutomationSlice/AutomationSlice_Schedules.res.mjs";
17
+
18
+ function clockNow() {
19
+ return Effect.runSync(Clock.currentTimeMillis);
20
+ }
21
+
22
+ function nowOr(now) {
23
+ if (now !== undefined) {
24
+ return now;
25
+ } else {
26
+ return Effect.runSync(Clock.currentTimeMillis);
27
+ }
28
+ }
11
29
 
12
30
  function Make(Spec) {
13
31
  let test = (name, body) => JestBind$ReventlessGwt.test(Spec.name, name, body);
@@ -53,7 +71,7 @@ function Make(Spec) {
53
71
  id,
54
72
  todo
55
73
  ];
56
- let whenProcess = param => Spec.process(param[0], param[1]);
74
+ let whenProcess = (param, now) => Spec.process(param[0], param[1], nowOr(now));
57
75
  let thenCommand = (actual, expectedId, expectedCmd) => {
58
76
  if (actual !== undefined) {
59
77
  let cmd = actual[1];
@@ -98,11 +116,12 @@ function Make(Spec) {
98
116
  });
99
117
  };
100
118
  let givenEvents = es => es;
101
- let whenSweep = events => {
119
+ let whenSweep = (events, now) => {
120
+ let now$1 = nowOr(now);
102
121
  let collected = events.map(e => Spec.collect(e)).flat();
103
122
  let resolvedIds = Stdlib_Array.filterMap(events, e => Spec.resolve(e));
104
123
  let pending = collected.filter(param => !resolvedIds.includes(param[0]));
105
- let commands = Stdlib_Array.filterMap(pending, param => Spec.process(param[0], param[1]));
124
+ let commands = Stdlib_Array.filterMap(pending, param => Spec.process(param[0], param[1], now$1));
106
125
  return {
107
126
  todos: pending,
108
127
  commands: commands
@@ -165,6 +184,11 @@ function FromSlice(Spec) {
165
184
  pluginName: "test",
166
185
  sliceName: testContext_sliceName
167
186
  };
187
+ let withRecordedAt = (context, recordedAt) => Stdlib_Option.mapOr(recordedAt, context, recordedAt => {
188
+ let newrecord = {...context};
189
+ newrecord.recordedAt = recordedAt;
190
+ return newrecord;
191
+ });
168
192
  let encTodos = arr => arr.map(param => [
169
193
  param[0],
170
194
  Message$ReventlessCore.encode(param[1], Spec.todoItemSchema)
@@ -202,19 +226,20 @@ function FromSlice(Spec) {
202
226
  }
203
227
  };
204
228
  let Mapping = M => {
205
- let event = (e, sourceIdOpt) => {
229
+ let event = (e, sourceIdOpt, recordedAt) => {
206
230
  let sourceId = sourceIdOpt !== undefined ? sourceIdOpt : "";
207
231
  return {
208
232
  sourceName: M.sourceName,
209
233
  sourceId: sourceId,
210
- payload: Message$ReventlessCore.encode(e, M.sourceEventSchema)
234
+ payload: Message$ReventlessCore.encode(e, M.sourceEventSchema),
235
+ recordedAt: recordedAt
211
236
  };
212
237
  };
213
238
  let givenEvent = e => e;
214
- let whenCollect = (e, sourceIdOpt, contextOpt) => {
239
+ let whenCollect = (e, sourceIdOpt, contextOpt, recordedAt) => {
215
240
  let sourceId = sourceIdOpt !== undefined ? sourceIdOpt : "";
216
241
  let context = contextOpt !== undefined ? contextOpt : testContext;
217
- return M.collect(e, sourceId, context);
242
+ return M.collect(e, sourceId, withRecordedAt(context, recordedAt));
218
243
  };
219
244
  return {
220
245
  event: event,
@@ -227,7 +252,7 @@ function FromSlice(Spec) {
227
252
  id,
228
253
  item
229
254
  ];
230
- let whenProcess = param => Automation.process(param[0], param[1]);
255
+ let whenProcess = (param, now) => Automation.process(param[0], param[1], nowOr(now));
231
256
  let whenExhausted = param => Automation.onExhausted(param[0], param[1]);
232
257
  let thenCommand = (actual, expectedId, expectedCmd) => {
233
258
  if (actual !== undefined && actual[0] === expectedId && Primitive_object.equal(actual[1], expectedCmd)) {
@@ -253,8 +278,56 @@ function FromSlice(Spec) {
253
278
  }
254
279
  };
255
280
  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 => {
281
+ let atTime = (events, time) => events.map(e => {
282
+ if (Stdlib_Option.isSome(e.recordedAt)) {
283
+ return e;
284
+ } else {
285
+ return {
286
+ sourceName: e.sourceName,
287
+ sourceId: e.sourceId,
288
+ payload: e.payload,
289
+ recordedAt: time
290
+ };
291
+ }
292
+ });
293
+ let latestRecorded = events => Stdlib_Array.reduce(events, undefined, (latest, e) => {
294
+ let match = e.recordedAt;
295
+ if (latest !== undefined) {
296
+ if (match !== undefined) {
297
+ return Math.max(latest, match);
298
+ } else {
299
+ return latest;
300
+ }
301
+ } else {
302
+ return match;
303
+ }
304
+ });
305
+ let routeEach = (events, context) => events.map(e => [
306
+ e,
307
+ Route.route(e.payload, e.sourceName, e.sourceId, withRecordedAt(context, e.recordedAt))
308
+ ]);
309
+ let routeAll = (events, context) => routeEach(events, context).flatMap(param => param[1]);
310
+ let foldSchedules = (rows, routedEvents, now) => {
311
+ routedEvents.forEach(param => {
312
+ let e = param[0];
313
+ let recordedAt = Stdlib_Option.getOr(e.recordedAt, now);
314
+ param[1].forEach(r => {
315
+ r.rescheduled.forEach(change => {
316
+ AutomationSlice_Schedules$ReventlessCore.apply(rows, change, "scenario", recordedAt);
317
+ });
318
+ });
319
+ if (e.sourceName === Clock$Reventless.name) {
320
+ return AutomationSlice_Schedules$ReventlessCore.fired(rows, e.sourceId);
321
+ }
322
+ });
323
+ };
324
+ let declaredRows = now => {
325
+ let rows = {};
326
+ AutomationSlice_Schedules$ReventlessCore.declare(rows, Spec.schedule, now);
327
+ return rows;
328
+ };
329
+ let sweep = (routed, now, schedulesOpt) => {
330
+ let schedules = schedulesOpt !== undefined ? schedulesOpt : declaredRows(now);
258
331
  let rows = [];
259
332
  routed.forEach(r => {
260
333
  r.collected.forEach(param => {
@@ -291,12 +364,37 @@ function FromSlice(Spec) {
291
364
  });
292
365
  return {
293
366
  todos: todos,
294
- commands: Stdlib_Array.filterMap(todos, param => Automation.process(param[0], param[1]))
367
+ commands: Stdlib_Array.filterMap(todos, param => Automation.process(param[0], param[1], now)),
368
+ schedules: schedules
369
+ };
370
+ };
371
+ let whenSweep = (events, contextOpt, now) => {
372
+ let context = contextOpt !== undefined ? contextOpt : testContext;
373
+ let match = latestRecorded(events);
374
+ let now$1 = now !== undefined ? now : (
375
+ match !== undefined ? match : Effect.runSync(Clock.currentTimeMillis)
376
+ );
377
+ let routedEvents = routeEach(events, context);
378
+ let schedules = declaredRows(now$1);
379
+ foldSchedules(schedules, routedEvents, now$1);
380
+ return sweep(routedEvents.flatMap(param => param[1]), now$1, schedules);
381
+ };
382
+ let tick = (keyOpt, at) => {
383
+ let key = keyOpt !== undefined ? keyOpt : Clock$Reventless.defaultKey;
384
+ return {
385
+ sourceName: Clock$Reventless.name,
386
+ sourceId: key,
387
+ payload: Message$ReventlessCore.encode({
388
+ TAG: "Tick",
389
+ key: key,
390
+ at: at
391
+ }, Clock$Reventless.eventSchema),
392
+ recordedAt: at
295
393
  };
296
394
  };
297
- let whenSweep = (events, contextOpt) => {
395
+ let whenTick = (events, key, at, contextOpt) => {
298
396
  let context = contextOpt !== undefined ? contextOpt : testContext;
299
- return sweep(routeAll(events, context));
397
+ return whenSweep(events.concat([tick(key, at)]), context, at);
300
398
  };
301
399
  let thenCommands = (scenario, expected) => {
302
400
  if (Primitive_object.equal(scenario.commands, expected)) {
@@ -311,18 +409,50 @@ function FromSlice(Spec) {
311
409
  };
312
410
  let andThenEvents = (scenario, events, contextOpt) => {
313
411
  let context = contextOpt !== undefined ? contextOpt : testContext;
314
- let resolved = Stdlib_Array.filterMap(routeAll(events, context), r => r.resolved);
412
+ let routedEvents = routeEach(events, context);
413
+ let resolved = Stdlib_Array.filterMap(routedEvents.flatMap(param => param[1]), r => r.resolved);
414
+ let schedules = Object.assign({}, scenario.schedules);
415
+ foldSchedules(schedules, routedEvents, Stdlib_Option.getOr(latestRecorded(events), Effect.runSync(Clock.currentTimeMillis)));
315
416
  return {
316
417
  todos: scenario.todos.filter(param => !resolved.includes(param[0])),
317
- commands: scenario.commands
418
+ commands: scenario.commands,
419
+ schedules: schedules
318
420
  };
319
421
  };
422
+ let thenSchedules = (scenario, expected) => {
423
+ let enc = param => Object.fromEntries([
424
+ [
425
+ "key",
426
+ param[0]
427
+ ],
428
+ [
429
+ "time",
430
+ Message$ReventlessCore.encode(param[1], Schedule$Reventless.timeSchema)
431
+ ]
432
+ ]);
433
+ let byKey = rows => rows.toSorted((param, param$1) => Primitive_string.compare(param[0], param$1[0]));
434
+ let actual = byKey(Stdlib_Array.filterMap(Object.values(scenario.schedules), row => Stdlib_Option.map(AutomationSlice_Schedules$ReventlessCore.timeOf(row), t => [
435
+ row.key,
436
+ t
437
+ ]))).map(enc);
438
+ let expected$1 = byKey(expected).map(enc);
439
+ if (Primitive_object.equal(actual, expected$1)) {
440
+ return Outcome$ReventlessGwt.pass;
441
+ } else {
442
+ return Outcome$ReventlessGwt.fail({
443
+ TAG: "QueryRowsMismatch",
444
+ expected: expected$1,
445
+ actual: actual
446
+ });
447
+ }
448
+ };
320
449
  let thenScenarioTodos = (scenario, expected) => thenTodos(scenario.todos, expected);
321
450
  return {
322
451
  Route: Route,
323
452
  describe: JestBind$ReventlessGwt.describe,
324
453
  test: test,
325
454
  testContext: testContext,
455
+ withRecordedAt: withRecordedAt,
326
456
  encTodos: encTodos,
327
457
  encPair: encPair,
328
458
  thenTodos: thenTodos,
@@ -334,18 +464,28 @@ function FromSlice(Spec) {
334
464
  thenCommand: thenCommand,
335
465
  thenNoCommand: thenNoCommand,
336
466
  givenEvents: givenEvents,
467
+ atTime: atTime,
468
+ latestRecorded: latestRecorded,
469
+ routeEach: routeEach,
337
470
  routeAll: routeAll,
471
+ foldSchedules: foldSchedules,
472
+ declaredRows: declaredRows,
338
473
  sweep: sweep,
339
474
  whenSweep: whenSweep,
475
+ tick: tick,
476
+ whenTick: whenTick,
340
477
  thenCommands: thenCommands,
341
478
  andThenEvents: andThenEvents,
479
+ thenSchedules: thenSchedules,
342
480
  thenScenarioTodos: thenScenarioTodos
343
481
  };
344
482
  };
345
483
  }
346
484
 
347
485
  export {
486
+ clockNow,
487
+ nowOr,
348
488
  Make,
349
489
  FromSlice,
350
490
  }
351
- /* Message-ReventlessCore Not a pure module */
491
+ /* 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(), undefined);
528
529
  return {
529
530
  log: s.log,
530
531
  outcome: s.outcome,