@reventlessdev/reventless-aws 3.0.0-alpha.382 → 3.0.0-alpha.384

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.
@@ -1,6 +1,8 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
3
4
  import * as Belt_SetString from "@rescript/runtime/lib/es6/Belt_SetString.js";
5
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
4
6
  import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
5
7
  import * as EventTopic$ReventlessCore from "@reventlessdev/reventless-core/src/components/EventTopic/EventTopic.res.mjs";
6
8
  import * as Util_Bundle$ReventlessAws from "../util/Util_Bundle.res.mjs";
@@ -11,6 +13,20 @@ import * as RuntimeEnvironment_Lambda$ReventlessAws from "../adapter/Runtime/Run
11
13
  import * as EventCollectorChannel_DynamoDbStream$ReventlessAws from "../adapter/EventCollector/EventCollectorChannel_DynamoDbStream.res.mjs";
12
14
  import * as AutomationSliceRuntime_Builder_Single$ReventlessAws from "../adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs";
13
15
 
16
+ function platformFor(slice) {
17
+ let refuse = () => Stdlib_JsError.throwWithMessage(`AutomationSlice(` + slice + `): schedules are set by its Lambda, not at deploy`);
18
+ return {
19
+ put: async (param, param$1) => refuse(),
20
+ removeName: async param => refuse(),
21
+ names: async () => refuse(),
22
+ nameOf: key => key
23
+ };
24
+ }
25
+
26
+ let DeployTimeSchedules = {
27
+ platformFor: platformFor
28
+ };
29
+
14
30
  function Make(Api) {
15
31
  let Inner = AutomationSlice_Builder$ReventlessCore.Make({
16
32
  make: RuntimeEnvironment_Lambda$ReventlessAws.make,
@@ -35,7 +51,7 @@ function Make(Api) {
35
51
  },
36
52
  forEventCollector: AutomationSliceRuntime_Builder_Single$ReventlessAws.forEventCollector,
37
53
  finish: AutomationSliceRuntime_Builder_Single$ReventlessAws.finish
38
- })(Api);
54
+ })(Api)(DeployTimeSchedules);
39
55
  let Make$1 = Spec => (Automation => {
40
56
  let InnerMake = Inner.Make(Spec)(Automation);
41
57
  let sourceNames = InnerMake.sourceNames;
@@ -46,13 +62,14 @@ function Make(Api) {
46
62
  let queryDbTableName = tableResource.name;
47
63
  let sourceTopics = EventTopic$ReventlessCore.filter(allEventTopics, Belt_SetString.fromArray(sourceNames));
48
64
  let consumesDcbLog = sourceNames.length === 0 || sourceNames.some(name => !(name in allEventTopics));
49
- AutomationSliceRuntime_Builder_Single$ReventlessAws.registerAutomationSlice(Spec.name, Util_Bundle$ReventlessAws.getModuleSpecifier(Spec.moduleUrl), Util_Bundle$ReventlessAws.getModuleSpecifier(Automation.moduleUrl), "automation", queryDbTableName, queryDbOutputs.resources, context, sourceTopics, consumesDcbLog, undefined);
65
+ AutomationSliceRuntime_Builder_Single$ReventlessAws.registerAutomationSlice(Spec.name, Util_Bundle$ReventlessAws.getModuleSpecifier(Spec.moduleUrl), Util_Bundle$ReventlessAws.getModuleSpecifier(Automation.moduleUrl), "automation", queryDbTableName, queryDbOutputs.resources, context, sourceTopics, consumesDcbLog, undefined, Spec.schedule, Stdlib_Option.mapOr(Component$ReventlessCore.outputs(as_).schedulesDb, [], db => db.resources));
50
66
  return as_;
51
67
  };
52
68
  return {
53
69
  Spec: Spec,
54
70
  Automation: Automation,
55
71
  queryDbName: InnerMake.queryDbName,
72
+ schedulesDbName: InnerMake.schedulesDbName,
56
73
  sourceNames: sourceNames,
57
74
  make: make
58
75
  };
@@ -75,6 +92,7 @@ export {
75
92
  EventCollectorChannel,
76
93
  RuntimeEnvironment,
77
94
  EventCollectorRuntimeBuilder,
95
+ DeployTimeSchedules,
78
96
  Make,
79
97
  }
80
98
  /* Component-ReventlessCore Not a pure module */
@@ -63,7 +63,7 @@ function Make(Api) {
63
63
  let declaredSources = Spec.sourceNames;
64
64
  let sourceTopics = EventTopic$ReventlessCore.filter(allEventTopics, Belt_SetString.fromArray(declaredSources));
65
65
  let consumesDcbLog = declaredSources.length === 0 || declaredSources.some(name => !(name in allEventTopics));
66
- AutomationSliceRuntime_Builder_Single$ReventlessAws.registerAutomationSlice(Spec.name, Util_Bundle$ReventlessAws.getModuleSpecifier(Spec.moduleUrl), Util_Bundle$ReventlessAws.getModuleSpecifier(Translation.moduleUrl), "outbound", queryDbTableName, queryDbOutputs.resources, undefined, sourceTopics, consumesDcbLog, Spec.targetName);
66
+ AutomationSliceRuntime_Builder_Single$ReventlessAws.registerAutomationSlice(Spec.name, Util_Bundle$ReventlessAws.getModuleSpecifier(Spec.moduleUrl), Util_Bundle$ReventlessAws.getModuleSpecifier(Translation.moduleUrl), "outbound", queryDbTableName, queryDbOutputs.resources, undefined, sourceTopics, consumesDcbLog, Spec.targetName, undefined, undefined);
67
67
  return ots;
68
68
  };
69
69
  return {
@@ -606,6 +606,58 @@ describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth", () => {
606
606
  )
607
607
  },
608
608
  )
609
+
610
+ // The resolvers check no rule, so a door the gate misses reads the gated rows.
611
+ // Run through the real generator, so a door it adds later is covered too.
612
+ testSync("a roles-gated entry's every generated door carries its groups", () => {
613
+ let byCategory: Reventless.ReadModel.indexConfig = {
614
+ index: "byCategory",
615
+ type_: "S",
616
+ idField: "categoryId",
617
+ projectionType: ALL,
618
+ }
619
+ let gated = (
620
+ ~single,
621
+ ~list,
622
+ ~subIdField=?,
623
+ ~indexQueries=?,
624
+ ): ReventlessInfra.Api.querySchemaEntry => {
625
+ singleFieldName: single,
626
+ listFieldName: list,
627
+ returnTypeName: single,
628
+ stateSchema: S.unknown,
629
+ authorization: None,
630
+ permission: AllowRoles([Reventless.Role.make("Admin")]),
631
+ ?subIdField,
632
+ ?indexQueries,
633
+ }
634
+ let entries = [
635
+ gated(~single="p_Item", ~list="p_Items", ~indexQueries=[byCategory]),
636
+ gated(~single="p_Line", ~list="p_Lines", ~subIdField="lineId"),
637
+ ]
638
+ let frag = ReventlessCore.GraphQL_FragmentGenerator.generate(
639
+ ~mutationEntries=[],
640
+ ~queryEntries=entries,
641
+ )
642
+ let queries = (
643
+ AppSync_Adapter.injectAwsAuth(
644
+ frag,
645
+ ~mutationEntries=[],
646
+ ~queryEntries=entries,
647
+ )->decodeFragment
648
+ ).queries
649
+ let names = queries->Array.map(ReventlessCore.GraphQL_Stitcher.extractLeadingName)
650
+ expect(
651
+ ["p_ItemsByIds", "p_ItemsRefs", "p_ItemByCategory", "p_LineItems"]->Array.every(
652
+ door => names->Array.includes(door),
653
+ ),
654
+ )->toBe(true)
655
+ expect(
656
+ queries->Array.filter(
657
+ field => !(field->String.includes(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)),
658
+ ),
659
+ )->toEqual([])
660
+ })
609
661
  })
610
662
 
611
663
  // ── Type-level dual-auth ─────────────────────────────────────────────────────
@@ -12,6 +12,7 @@ import * as GraphQL_Stitcher$ReventlessCore from "@reventlessdev/reventless-core
12
12
  import * as Platform_AdminApi$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_AdminApi.res.mjs";
13
13
  import * as AppSync_SdlDecorate$ReventlessAws from "../src/components/Api/AppSync_SdlDecorate.res.mjs";
14
14
  import * as PluginBaseFragment$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/api/PluginBaseFragment.res.mjs";
15
+ import * as GraphQL_FragmentGenerator$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_FragmentGenerator.res.mjs";
15
16
 
16
17
  globalThis.describe("AppSync_Adapter.sha256Hex", () => {
17
18
  globalThis.test("returns a 64-character hex string", () => {
@@ -596,6 +597,40 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth"
596
597
  }
597
598
  });
598
599
  });
600
+ globalThis.test("a roles-gated entry's every generated door carries its groups", () => {
601
+ let gated = (single, list, subIdField, indexQueries) => ({
602
+ singleFieldName: single,
603
+ listFieldName: list,
604
+ returnTypeName: single,
605
+ stateSchema: Sury.unknown,
606
+ authorization: undefined,
607
+ permission: {
608
+ TAG: "AllowRoles",
609
+ _0: ["Admin"]
610
+ },
611
+ subIdField: subIdField,
612
+ indexQueries: indexQueries
613
+ });
614
+ let entries = [
615
+ gated("p_Item", "p_Items", undefined, [{
616
+ index: "byCategory",
617
+ type_: "S",
618
+ idField: "categoryId",
619
+ projectionType: "ALL"
620
+ }]),
621
+ gated("p_Line", "p_Lines", "lineId", undefined)
622
+ ];
623
+ let frag = GraphQL_FragmentGenerator$ReventlessCore.generate([], entries);
624
+ let queries = GraphQL_Stitcher$ReventlessCore.decode(AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [], entries)).queries;
625
+ let names = queries.map(GraphQL_Stitcher$ReventlessCore.extractLeadingName);
626
+ globalThis.expect([
627
+ "p_ItemsByIds",
628
+ "p_ItemsRefs",
629
+ "p_ItemByCategory",
630
+ "p_LineItems"
631
+ ].every(door => names.includes(door))).toBe(true);
632
+ globalThis.expect(queries.filter(field => !field.includes(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`))).toEqual([]);
633
+ });
599
634
  });
600
635
 
601
636
  globalThis.describe("AppSync_Adapter — type-level dual-auth", () => {
@@ -224,6 +224,9 @@ describe("AutomationSliceEntryPoint_Ops.makeAutomationJsonEventsHandler", () =>
224
224
  steps->Array.push("phase1")
225
225
  },
226
226
  phase2: async _publish => steps->Array.push("phase2"),
227
+ schedules: Dict.make(),
228
+ takeScheduleChanges: () => [],
229
+ rebuildSchedules: _ => (),
227
230
  }
228
231
  let context: Reventless.AutomationSlice.context = {
229
232
  environment: "test",
@@ -250,6 +253,50 @@ describe("AutomationSliceEntryPoint_Ops.makeAutomationJsonEventsHandler", () =>
250
253
  // the reloaded backlog unprocessed until another invocation.
251
254
  expect(steps)->toEqual(["restore", "phase1", "sync", "phase2", "sync"])
252
255
  })
256
+
257
+ // The rows are the truth: a platform told before they are stored could hold a
258
+ // schedule no row knows of, if the store write then failed.
259
+ test("stores a changed schedule's rows before telling the platform", async () => {
260
+ let steps = []
261
+ let daily = Reventless.Schedule.Calendar({calendar: Daily({hour: 6, minute: 0})})
262
+ let callback: AutomationSliceEntryPoint_Ops.automationCallback = {
263
+ todoItems: Dict.make(),
264
+ phase1: (_jsons, _ctx) => steps->Array.push("phase1"),
265
+ phase2: async _publish => steps->Array.push("phase2"),
266
+ schedules: Dict.make(),
267
+ takeScheduleChanges: () => [Put({key: "r1", time: daily})],
268
+ rebuildSchedules: _ => (),
269
+ }
270
+ let schedules: AutomationSliceEntryPoint_Ops.schedulesStep = {
271
+ load: async () => steps->Array.push("load schedules"),
272
+ sync: async () => steps->Array.push("sync schedules"),
273
+ platform: {
274
+ put: async (~key, _) => steps->Array.push(`put ${key}`),
275
+ removeName: async name => steps->Array.push(`remove ${name}`),
276
+ names: async () => [],
277
+ nameOf: key => key,
278
+ },
279
+ }
280
+ let handler = AutomationSliceEntryPoint_Ops.makeAutomationJsonEventsHandler(
281
+ ~context={environment: "test", platformName: "P", pluginName: "Pl", sliceName: "S"},
282
+ ~callback,
283
+ ~publishJsons=noopPublish,
284
+ ~syncTodoItems=async () => steps->Array.push("sync"),
285
+ ~loadTodoItems=async () => steps->Array.push("restore"),
286
+ ~schedules=() => Some(schedules),
287
+ )
288
+ await Stream.fromIterable([str("e")])->handler->Effect.runPromise
289
+ expect(steps)->toEqual([
290
+ "restore",
291
+ "load schedules",
292
+ "phase1",
293
+ "sync",
294
+ "sync schedules",
295
+ "put r1",
296
+ "phase2",
297
+ "sync",
298
+ ])
299
+ })
253
300
  })
254
301
 
255
302
  describe("AutomationSliceEntryPoint_Ops.runSweeps", () => {
@@ -260,6 +307,9 @@ describe("AutomationSliceEntryPoint_Ops.runSweeps", () => {
260
307
  registered: dummyRegistered,
261
308
  sweep: run,
262
309
  comp,
310
+ name: comp,
311
+ tick: None,
312
+ reconcile: None,
263
313
  }
264
314
 
265
315
  test("sweeps every slice", async () => {
@@ -285,6 +335,47 @@ describe("AutomationSliceEntryPoint_Ops.runSweeps", () => {
285
335
  })
286
336
  })
287
337
 
338
+ describe("AutomationSliceEntryPoint_Ops.runTick", () => {
339
+ let dummyRegistered: StreamRoutedEntryPoint_Ops.registeredHandler = {
340
+ handler: (_event, _context) => Effect.succeed(),
341
+ }
342
+ let ticks = []
343
+ let slice = (~name, ~tick): AutomationSliceEntryPoint_Ops.builtSlice => {
344
+ registered: dummyRegistered,
345
+ sweep: async () => (),
346
+ comp: name,
347
+ name,
348
+ tick,
349
+ reconcile: None,
350
+ }
351
+ let slices = [
352
+ slice(~name="Outbound", ~tick=None),
353
+ slice(~name="DemandReport", ~tick=Some(async (~key, ~at) => ticks->Array.push((key, at)))),
354
+ ]
355
+
356
+ test("delivers the scheduled time to the named slice", async () => {
357
+ await AutomationSliceEntryPoint_Ops.runTick(
358
+ slices,
359
+ {slice: "DemandReport", key: "default", at: "2026-10-16T06:00:00Z"},
360
+ )
361
+ expect(ticks)->toEqual([("default", Date.fromString("2026-10-16T06:00:00Z")->Date.getTime)])
362
+ })
363
+
364
+ // A throw makes the Scheduler retry, and leaves a log line to find.
365
+ test("refuses a slice it does not host, or one that maps no Clock", async () => {
366
+ let refused = async invocation =>
367
+ switch await AutomationSliceEntryPoint_Ops.runTick(slices, invocation) {
368
+ | () => false
369
+ | exception _ => true
370
+ }
371
+ expect(await refused({slice: "Nobody", key: "default", at: "2026-10-16T06:00:00Z"}))->toBe(true)
372
+ expect(await refused({slice: "Outbound", key: "default", at: "2026-10-16T06:00:00Z"}))->toBe(
373
+ true,
374
+ )
375
+ expect(await refused({slice: "DemandReport", key: "default", at: "soon"}))->toBe(true)
376
+ })
377
+ })
378
+
288
379
  // Consumed-event fixture for the outbound decode: one consumed variant with a
289
380
  // payload, one payload-less, decoded from a source that also emits types this
290
381
  // slice does not consume.
@@ -328,10 +328,16 @@ globalThis.describe("AutomationSliceEntryPoint_Ops.makeAutomationJsonEventsHandl
328
328
  let callback_phase2 = async _publish => {
329
329
  steps.push("phase2");
330
330
  };
331
+ let callback_schedules = {};
332
+ let callback_takeScheduleChanges = () => [];
333
+ let callback_rebuildSchedules = param => {};
331
334
  let callback = {
332
335
  todoItems: callback_todoItems,
333
336
  phase1: callback_phase1,
334
- phase2: callback_phase2
337
+ phase2: callback_phase2,
338
+ schedules: callback_schedules,
339
+ takeScheduleChanges: callback_takeScheduleChanges,
340
+ rebuildSchedules: callback_rebuildSchedules
335
341
  };
336
342
  let handler = AutomationSliceEntryPoint_Ops$ReventlessAws.makeAutomationJsonEventsHandler({
337
343
  environment: "test",
@@ -342,7 +348,7 @@ globalThis.describe("AutomationSliceEntryPoint_Ops.makeAutomationJsonEventsHandl
342
348
  steps.push("sync");
343
349
  }, async () => {
344
350
  steps.push("restore");
345
- });
351
+ }, undefined);
346
352
  let envelope = Object.fromEntries([
347
353
  [
348
354
  "meta",
@@ -373,6 +379,80 @@ globalThis.describe("AutomationSliceEntryPoint_Ops.makeAutomationJsonEventsHandl
373
379
  "sync"
374
380
  ]);
375
381
  });
382
+ globalThis.test("stores a changed schedule's rows before telling the platform", async () => {
383
+ let steps = [];
384
+ let callback_todoItems = {};
385
+ let callback_phase1 = (_jsons, _ctx) => {
386
+ steps.push("phase1");
387
+ };
388
+ let callback_phase2 = async _publish => {
389
+ steps.push("phase2");
390
+ };
391
+ let callback_schedules = {};
392
+ let callback_takeScheduleChanges = () => [{
393
+ TAG: "Put",
394
+ key: "r1",
395
+ time: {
396
+ TAG: "Calendar",
397
+ calendar: {
398
+ TAG: "Daily",
399
+ hour: 6,
400
+ minute: 0
401
+ }
402
+ }
403
+ }];
404
+ let callback_rebuildSchedules = param => {};
405
+ let callback = {
406
+ todoItems: callback_todoItems,
407
+ phase1: callback_phase1,
408
+ phase2: callback_phase2,
409
+ schedules: callback_schedules,
410
+ takeScheduleChanges: callback_takeScheduleChanges,
411
+ rebuildSchedules: callback_rebuildSchedules
412
+ };
413
+ let schedules_load = async () => {
414
+ steps.push("load schedules");
415
+ };
416
+ let schedules_sync = async () => {
417
+ steps.push("sync schedules");
418
+ };
419
+ let schedules_platform = {
420
+ put: async (key, param) => {
421
+ steps.push(`put ` + key);
422
+ },
423
+ removeName: async name => {
424
+ steps.push(`remove ` + name);
425
+ },
426
+ names: async () => [],
427
+ nameOf: key => key
428
+ };
429
+ let schedules = {
430
+ load: schedules_load,
431
+ sync: schedules_sync,
432
+ platform: schedules_platform
433
+ };
434
+ let handler = AutomationSliceEntryPoint_Ops$ReventlessAws.makeAutomationJsonEventsHandler({
435
+ environment: "test",
436
+ platformName: "P",
437
+ pluginName: "Pl",
438
+ sliceName: "S"
439
+ }, callback, noopPublish, async () => {
440
+ steps.push("sync");
441
+ }, async () => {
442
+ steps.push("restore");
443
+ }, () => schedules);
444
+ await Effect.runPromise(handler(Stream.fromIterable(["e"])));
445
+ globalThis.expect(steps).toEqual([
446
+ "restore",
447
+ "load schedules",
448
+ "phase1",
449
+ "sync",
450
+ "sync schedules",
451
+ "put r1",
452
+ "phase2",
453
+ "sync"
454
+ ]);
455
+ });
376
456
  });
377
457
 
378
458
  globalThis.describe("AutomationSliceEntryPoint_Ops.runSweeps", () => {
@@ -380,23 +460,23 @@ globalThis.describe("AutomationSliceEntryPoint_Ops.runSweeps", () => {
380
460
  let dummyRegistered = {
381
461
  handler: dummyRegistered_handler
382
462
  };
463
+ let sliceThat = (comp, run) => ({
464
+ registered: dummyRegistered,
465
+ sweep: run,
466
+ comp: comp,
467
+ name: comp,
468
+ tick: undefined,
469
+ reconcile: undefined
470
+ });
383
471
  globalThis.test("sweeps every slice", async () => {
384
472
  let swept = [];
385
473
  await AutomationSliceEntryPoint_Ops$ReventlessAws.runSweeps([
386
- {
387
- registered: dummyRegistered,
388
- sweep: async () => {
389
- swept.push("a");
390
- },
391
- comp: "A"
392
- },
393
- {
394
- registered: dummyRegistered,
395
- sweep: async () => {
396
- swept.push("b");
397
- },
398
- comp: "B"
399
- }
474
+ sliceThat("A", async () => {
475
+ swept.push("a");
476
+ }),
477
+ sliceThat("B", async () => {
478
+ swept.push("b");
479
+ })
400
480
  ]);
401
481
  globalThis.expect(swept).toEqual([
402
482
  "a",
@@ -406,25 +486,13 @@ globalThis.describe("AutomationSliceEntryPoint_Ops.runSweeps", () => {
406
486
  globalThis.test("a throwing slice does not stop the others", async () => {
407
487
  let swept = [];
408
488
  await AutomationSliceEntryPoint_Ops$ReventlessAws.runSweeps([
409
- {
410
- registered: dummyRegistered,
411
- sweep: async () => {
412
- swept.push("a");
413
- },
414
- comp: "A"
415
- },
416
- {
417
- registered: dummyRegistered,
418
- sweep: async () => Stdlib_JsError.throwWithMessage("gateway down"),
419
- comp: "Boom"
420
- },
421
- {
422
- registered: dummyRegistered,
423
- sweep: async () => {
424
- swept.push("c");
425
- },
426
- comp: "C"
427
- }
489
+ sliceThat("A", async () => {
490
+ swept.push("a");
491
+ }),
492
+ sliceThat("Boom", async () => Stdlib_JsError.throwWithMessage("gateway down")),
493
+ sliceThat("C", async () => {
494
+ swept.push("c");
495
+ })
428
496
  ]);
429
497
  globalThis.expect(swept).toEqual([
430
498
  "a",
@@ -433,6 +501,74 @@ globalThis.describe("AutomationSliceEntryPoint_Ops.runSweeps", () => {
433
501
  });
434
502
  });
435
503
 
504
+ globalThis.describe("AutomationSliceEntryPoint_Ops.runTick", () => {
505
+ let dummyRegistered_handler = (_event, _context) => Effect.succeed();
506
+ let dummyRegistered = {
507
+ handler: dummyRegistered_handler
508
+ };
509
+ let ticks = [];
510
+ let slice = (name, tick) => ({
511
+ registered: dummyRegistered,
512
+ sweep: async () => {},
513
+ comp: name,
514
+ name: name,
515
+ tick: tick,
516
+ reconcile: undefined
517
+ });
518
+ let slices = [
519
+ {
520
+ registered: dummyRegistered,
521
+ sweep: async () => {},
522
+ comp: "Outbound",
523
+ name: "Outbound",
524
+ tick: undefined,
525
+ reconcile: undefined
526
+ },
527
+ slice("DemandReport", async (key, at) => {
528
+ ticks.push([
529
+ key,
530
+ at
531
+ ]);
532
+ })
533
+ ];
534
+ globalThis.test("delivers the scheduled time to the named slice", async () => {
535
+ await AutomationSliceEntryPoint_Ops$ReventlessAws.runTick(slices, {
536
+ slice: "DemandReport",
537
+ key: "default",
538
+ at: "2026-10-16T06:00:00Z"
539
+ });
540
+ globalThis.expect(ticks).toEqual([[
541
+ "default",
542
+ new Date("2026-10-16T06:00:00Z").getTime()
543
+ ]]);
544
+ });
545
+ globalThis.test("refuses a slice it does not host, or one that maps no Clock", async () => {
546
+ let refused = async invocation => {
547
+ try {
548
+ await AutomationSliceEntryPoint_Ops$ReventlessAws.runTick(slices, invocation);
549
+ return false;
550
+ } catch (exn) {
551
+ return true;
552
+ }
553
+ };
554
+ globalThis.expect(await refused({
555
+ slice: "Nobody",
556
+ key: "default",
557
+ at: "2026-10-16T06:00:00Z"
558
+ })).toBe(true);
559
+ globalThis.expect(await refused({
560
+ slice: "Outbound",
561
+ key: "default",
562
+ at: "2026-10-16T06:00:00Z"
563
+ })).toBe(true);
564
+ globalThis.expect(await refused({
565
+ slice: "DemandReport",
566
+ key: "default",
567
+ at: "soon"
568
+ })).toBe(true);
569
+ });
570
+ });
571
+
436
572
  let outboundEventSchema = Sury.union([
437
573
  Sury.$schema(s => ({
438
574
  TAG: "OrderPlaced",
@@ -62,4 +62,29 @@ describe("ScheduledPublisher_EventBridgeScheduler_Runtime", () => {
62
62
  let tags = Dict.fromArray([("Name", "SchedulerScheduleGroup"), ("reventless:plugin", "")])
63
63
  expect(tags->AWS.Tags.withoutEmpty->Dict.toArray)->toEqual([("Name", "SchedulerScheduleGroup")])
64
64
  })
65
+
66
+ testSync("names an automation's schedule <slice>.<key>, or hashes a key it would refuse", () => {
67
+ expect(Runtime.tickScheduleName(~slice="MarketingDigest", ~key="r1"))->toBe(
68
+ "MarketingDigest.r1",
69
+ )
70
+ let hashed = Runtime.tickScheduleName(~slice="MarketingDigest", ~key="customer:42")
71
+ expect((hashed->String.startsWith("MarketingDigest."), hashed->String.length))->toEqual((
72
+ true,
73
+ 16 + 16,
74
+ ))
75
+ let long = "A"->String.repeat(60)
76
+ let cut = Runtime.tickScheduleName(~slice=long, ~key="default")
77
+ expect((cut->String.length, cut->Runtime.isSliceSchedule(~slice=long)))->toEqual((57, true))
78
+ })
79
+
80
+ testSync("tells a slice's schedules from another slice's sharing its prefix", () => {
81
+ expect("Digest.r1"->Runtime.isSliceSchedule(~slice="Digest"))->toBe(true)
82
+ expect("DigestWeekly.r1"->Runtime.isSliceSchedule(~slice="Digest"))->toBe(false)
83
+ })
84
+
85
+ testSync("invokes the automation Lambda with the slice, the key and the scheduled time", () => {
86
+ expect(
87
+ Runtime.tickInput(~slice="MarketingDigest", ~key="r1"),
88
+ )->toBe(`{"reventlessTick":{"slice":"MarketingDigest","key":"r1","at":"<aws.scheduler.scheduled-time>"}}`)
89
+ })
65
90
  })
@@ -100,6 +100,33 @@ globalThis.describe("ScheduledPublisher_EventBridgeScheduler_Runtime", () => {
100
100
  "SchedulerScheduleGroup"
101
101
  ]]);
102
102
  });
103
+ globalThis.test("names an automation's schedule <slice>.<key>, or hashes a key it would refuse", () => {
104
+ globalThis.expect(ScheduledPublisher_EventBridgeScheduler_Runtime$ReventlessAws.tickScheduleName("MarketingDigest", "r1")).toBe("MarketingDigest.r1");
105
+ let hashed = ScheduledPublisher_EventBridgeScheduler_Runtime$ReventlessAws.tickScheduleName("MarketingDigest", "customer:42");
106
+ globalThis.expect([
107
+ hashed.startsWith("MarketingDigest."),
108
+ hashed.length
109
+ ]).toEqual([
110
+ true,
111
+ 32
112
+ ]);
113
+ let long = "A".repeat(60);
114
+ let cut = ScheduledPublisher_EventBridgeScheduler_Runtime$ReventlessAws.tickScheduleName(long, "default");
115
+ globalThis.expect([
116
+ cut.length,
117
+ ScheduledPublisher_EventBridgeScheduler_Runtime$ReventlessAws.isSliceSchedule(cut, long)
118
+ ]).toEqual([
119
+ 57,
120
+ true
121
+ ]);
122
+ });
123
+ globalThis.test("tells a slice's schedules from another slice's sharing its prefix", () => {
124
+ globalThis.expect(ScheduledPublisher_EventBridgeScheduler_Runtime$ReventlessAws.isSliceSchedule("Digest.r1", "Digest")).toBe(true);
125
+ globalThis.expect(ScheduledPublisher_EventBridgeScheduler_Runtime$ReventlessAws.isSliceSchedule("DigestWeekly.r1", "Digest")).toBe(false);
126
+ });
127
+ globalThis.test("invokes the automation Lambda with the slice, the key and the scheduled time", () => {
128
+ globalThis.expect(ScheduledPublisher_EventBridgeScheduler_Runtime$ReventlessAws.tickInput("MarketingDigest", "r1")).toBe(`{"reventlessTick":{"slice":"MarketingDigest","key":"r1","at":"<aws.scheduler.scheduled-time>"}}`);
129
+ });
103
130
  });
104
131
 
105
132
  let Runtime;