@reventlessdev/reventless-local 3.0.0-alpha.312 → 3.0.0-alpha.314

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,21 @@
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
+ # 3.0.0-alpha.314 (2026-10-30)
7
+
8
+ **Note:** Version bump only for package @reventlessdev/reventless-local
9
+
10
+
11
+
12
+
13
+
14
+ # 3.0.0-alpha.313 (2026-10-30)
15
+
16
+ ### Features
17
+
18
+ * **automation:** a work item with personal data is stored like an event ([f9e173f](https://github.com/ReventlessDev/reventless-core/commit/f9e173fb6f1dc671900052f2c4b6590ee99b5bdf))
19
+
20
+
6
21
  # 3.0.0-alpha.312 (2026-10-30)
7
22
 
8
23
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-local",
3
- "version": "3.0.0-alpha.312",
3
+ "version": "3.0.0-alpha.314",
4
4
  "description": "Local platform for Reventless (in-memory or SQLite backend, for development and testing without AWS)",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -35,23 +35,23 @@
35
35
  "sury": "11.0.0",
36
36
  "ws": "^8.18.0",
37
37
  "@reventlessdev/rescript-effect": "1.0.0-alpha.1",
38
- "@reventlessdev/rescript-jest": "1.0.0-alpha.13",
39
38
  "@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.29",
40
39
  "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.24",
41
40
  "@reventlessdev/rescript-node": "2.0.0-alpha.24",
42
- "@reventlessdev/reventless-core": "3.0.0-alpha.321",
41
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.13",
43
42
  "@reventlessdev/rescript-pulumi-pulumi": "3.0.0-alpha.0",
44
- "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.169",
45
- "@reventlessdev/reventless-infra": "3.0.0-alpha.218",
46
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.185",
43
+ "@reventlessdev/reventless-core": "3.0.0-alpha.323",
44
+ "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.171",
45
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.219",
46
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.187",
47
+ "@reventlessdev/reventless-gwt": "1.0.0-alpha.271",
47
48
  "@reventlessdev/reventless-seed": "1.0.0-alpha.39",
48
- "@reventlessdev/reventless-gwt": "1.0.0-alpha.269",
49
- "@reventlessdev/reventless-spec": "3.0.0-alpha.189"
49
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.190"
50
50
  },
51
51
  "devDependencies": {
52
52
  "rescript": "12.3.1",
53
53
  "sury-ppx": "11.0.0",
54
- "@reventlessdev/reventless-ppx": "1.0.0-alpha.109"
54
+ "@reventlessdev/reventless-ppx": "1.0.0-alpha.111"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "rescript": "^12.3.1"
@@ -26,6 +26,7 @@ module Make = (Bus: LocalBus.T) => {
26
26
  EventCollectorRuntimeBuilder,
27
27
  Api,
28
28
  LocalAutomationSchedules,
29
+ LocalCapabilities,
29
30
  )
30
31
 
31
32
  module Make = (
@@ -7,6 +7,7 @@ import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
7
7
  import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
8
8
  import * as TestRunner$ReventlessLocal from "../test/TestRunner.res.mjs";
9
9
  import * as EffectLogger$ReventlessCore from "@reventlessdev/reventless-core/src/util/EffectLogger.res.mjs";
10
+ import * as LocalCapabilities$ReventlessLocal from "../adapter/LocalCapabilities.res.mjs";
10
11
  import * as LocalQueryDbStorage$ReventlessLocal from "../adapter/QueryDb/LocalQueryDbStorage.res.mjs";
11
12
  import * as LocalAutomationSweep$ReventlessLocal from "./LocalAutomationSweep.res.mjs";
12
13
  import * as AutomationSlice_Builder$ReventlessCore from "@reventlessdev/reventless-core/src/components/AutomationSlice/AutomationSlice_Builder.res.mjs";
@@ -42,6 +43,8 @@ function Make(Bus) {
42
43
  make: QueryDbResolvers.make
43
44
  })(EventCollectorChannel)(EventCollectorRuntimeBuilder)(Api)({
44
45
  platformFor: LocalAutomationSchedules$ReventlessLocal.platformFor
46
+ })({
47
+ capabilities: LocalCapabilities$ReventlessLocal.capabilities
45
48
  });
46
49
  let Make$1 = Spec => (Automation => {
47
50
  let Inner = CoreMaker.Make(Spec)(Automation);
@@ -29,7 +29,7 @@ describe("AutomationSlice Callback (mixed-source)", () => {
29
29
  "OrderPlaced JSON creates a pending TODO item",
30
30
  async () => {
31
31
  let json = encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))
32
- Callback.phase1([json], testContext)
32
+ await Callback.phase1([json], testContext, ~capabilities=Reventless.Capabilities.none)
33
33
  let items = Callback.todoItems
34
34
  expect(items->Dict.toArray->Array.length)->toBe(1)
35
35
  let row = items->Dict.get("ord-1")->Option.getOrThrow
@@ -43,8 +43,8 @@ describe("AutomationSlice Callback (mixed-source)", () => {
43
43
  async () => {
44
44
  let j1 = encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))
45
45
  let j2 = encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "456 Oak Ave"}))
46
- Callback.phase1([j1], testContext)
47
- Callback.phase1([j2], testContext)
46
+ await Callback.phase1([j1], testContext, ~capabilities=Reventless.Capabilities.none)
47
+ await Callback.phase1([j2], testContext, ~capabilities=Reventless.Capabilities.none)
48
48
  expect(Callback.todoItems->Dict.toArray->Array.length)->toBe(1)
49
49
  },
50
50
  )
@@ -54,7 +54,7 @@ describe("AutomationSlice Callback (mixed-source)", () => {
54
54
  async () => {
55
55
  let j1 = encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))
56
56
  let j2 = encodeShipOrderEvent(OrderPlaced({orderId: "ord-2", address: "456 Oak Ave"}))
57
- Callback.phase1([j1, j2], testContext)
57
+ await Callback.phase1([j1, j2], testContext, ~capabilities=Reventless.Capabilities.none)
58
58
  expect(Callback.todoItems->Dict.toArray->Array.length)->toBe(2)
59
59
  },
60
60
  )
@@ -63,7 +63,7 @@ describe("AutomationSlice Callback (mixed-source)", () => {
63
63
  "ShipmentCreated does not create a TODO item",
64
64
  async () => {
65
65
  let json = encodeShipOrderEvent(ShipmentCreated({orderId: "ord-1"}))
66
- Callback.phase1([json], testContext)
66
+ await Callback.phase1([json], testContext, ~capabilities=Reventless.Capabilities.none)
67
67
  expect(Callback.todoItems->Dict.toArray->Array.length)->toBe(0)
68
68
  },
69
69
  )
@@ -73,11 +73,16 @@ describe("AutomationSlice Callback (mixed-source)", () => {
73
73
  testPromise(
74
74
  "ShipmentCreated marks pending TODO item as completed",
75
75
  async () => {
76
- Callback.phase1(
76
+ await Callback.phase1(
77
77
  [encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))],
78
78
  testContext,
79
+ ~capabilities=Reventless.Capabilities.none,
80
+ )
81
+ await Callback.phase1(
82
+ [encodeShipOrderEvent(ShipmentCreated({orderId: "ord-1"}))],
83
+ testContext,
84
+ ~capabilities=Reventless.Capabilities.none,
79
85
  )
80
- Callback.phase1([encodeShipOrderEvent(ShipmentCreated({orderId: "ord-1"}))], testContext)
81
86
  let row = Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow
82
87
  expect(row.status)->toBe(Completed)
83
88
  },
@@ -86,7 +91,11 @@ describe("AutomationSlice Callback (mixed-source)", () => {
86
91
  testPromise(
87
92
  "ShipmentCreated for unknown id is a no-op",
88
93
  async () => {
89
- Callback.phase1([encodeShipOrderEvent(ShipmentCreated({orderId: "unknown"}))], testContext)
94
+ await Callback.phase1(
95
+ [encodeShipOrderEvent(ShipmentCreated({orderId: "unknown"}))],
96
+ testContext,
97
+ ~capabilities=Reventless.Capabilities.none,
98
+ )
90
99
  expect(Callback.todoItems->Dict.toArray->Array.length)->toBe(0)
91
100
  },
92
101
  )
@@ -96,9 +105,10 @@ describe("AutomationSlice Callback (mixed-source)", () => {
96
105
  testPromise(
97
106
  "pending items produce commands via publishJsons",
98
107
  async () => {
99
- Callback.phase1(
108
+ await Callback.phase1(
100
109
  [encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))],
101
110
  testContext,
111
+ ~capabilities=Reventless.Capabilities.none,
102
112
  )
103
113
  let publishedCommands = ref([])
104
114
  let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
@@ -118,11 +128,16 @@ describe("AutomationSlice Callback (mixed-source)", () => {
118
128
  testPromise(
119
129
  "completed items are not processed",
120
130
  async () => {
121
- Callback.phase1(
131
+ await Callback.phase1(
122
132
  [encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))],
123
133
  testContext,
134
+ ~capabilities=Reventless.Capabilities.none,
135
+ )
136
+ await Callback.phase1(
137
+ [encodeShipOrderEvent(ShipmentCreated({orderId: "ord-1"}))],
138
+ testContext,
139
+ ~capabilities=Reventless.Capabilities.none,
124
140
  )
125
- Callback.phase1([encodeShipOrderEvent(ShipmentCreated({orderId: "ord-1"}))], testContext)
126
141
  let publishedCommands = ref([])
127
142
  let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
128
143
  publishedCommands := cmds
@@ -136,7 +151,7 @@ describe("AutomationSlice Callback (mixed-source)", () => {
136
151
  "items where process returns None are skipped",
137
152
  async () => {
138
153
  let json = encodeSkipProcessEvent(OrderPlaced({orderId: "ord-1"}))
139
- SkipCallback.phase1([json], testContext)
154
+ await SkipCallback.phase1([json], testContext, ~capabilities=Reventless.Capabilities.none)
140
155
  let publishedCommands = ref([])
141
156
  let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
142
157
  publishedCommands := cmds
@@ -163,9 +178,10 @@ describe("AutomationSlice Callback (mixed-source)", () => {
163
178
  testPromise(
164
179
  "publishJsons failure marks items as Failed",
165
180
  async () => {
166
- Callback.phase1(
181
+ await Callback.phase1(
167
182
  [encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))],
168
183
  testContext,
184
+ ~capabilities=Reventless.Capabilities.none,
169
185
  )
170
186
  let failingPublish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => {
171
187
  JsError.throwWithMessage("publish failed")
@@ -180,9 +196,10 @@ describe("AutomationSlice Callback (mixed-source)", () => {
180
196
  testPromise(
181
197
  "failed items with retryCount < maxRetries are retried",
182
198
  async () => {
183
- Callback.phase1(
199
+ await Callback.phase1(
184
200
  [encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))],
185
201
  testContext,
202
+ ~capabilities=Reventless.Capabilities.none,
186
203
  )
187
204
  let failingPublish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => {
188
205
  JsError.throwWithMessage("publish failed")
@@ -210,10 +227,11 @@ describe("AutomationSlice Callback (mixed-source)", () => {
210
227
  let failingPublish: ReventlessInfra.CommandTopic.publishJsons = async _cmds =>
211
228
  JsError.throwWithMessage("publish failed")
212
229
 
213
- let collectOne = () =>
214
- Callback.phase1(
230
+ let collectOne = async () =>
231
+ await Callback.phase1(
215
232
  [encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))],
216
233
  testContext,
234
+ ~capabilities=Reventless.Capabilities.none,
217
235
  )
218
236
 
219
237
  let statusOf = id => (Callback.todoItems->Dict.get(id)->Option.getOrThrow).status
@@ -221,7 +239,7 @@ describe("AutomationSlice Callback (mixed-source)", () => {
221
239
  testPromise(
222
240
  "the attempt that spends the budget writes Abandoned, not Failed",
223
241
  async () => {
224
- collectOne()
242
+ await collectOne()
225
243
  // ShipOrderSpec.maxRetries is 3: attempts at retryCount 0, 1 and 2 leave the
226
244
  // row retriable, and the third failure is the one that ends it.
227
245
  await Callback.phase2(failingPublish)
@@ -237,7 +255,7 @@ describe("AutomationSlice Callback (mixed-source)", () => {
237
255
  testPromise(
238
256
  "an abandoned row is not picked up again",
239
257
  async () => {
240
- collectOne()
258
+ await collectOne()
241
259
  await Callback.phase2(failingPublish)
242
260
  await Callback.phase2(failingPublish)
243
261
  await Callback.phase2(failingPublish)
@@ -256,7 +274,7 @@ describe("AutomationSlice Callback (mixed-source)", () => {
256
274
  testPromise(
257
275
  "a row left stranded at the ceiling is normalised on the next pass",
258
276
  async () => {
259
- collectOne()
277
+ await collectOne()
260
278
  // What an older build wrote, and what a Spec that lowered maxRetries leaves
261
279
  // behind: Failed at the ceiling, which no pass would ever pick up or mark.
262
280
  let row = Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow
@@ -272,7 +290,7 @@ describe("AutomationSlice Callback (mixed-source)", () => {
272
290
  testPromise(
273
291
  "the row carries the ceiling its count is racing",
274
292
  async () => {
275
- collectOne()
293
+ await collectOne()
276
294
  expect((Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow).maxRetries)->toEqual(
277
295
  Some(ShipOrderSpec.maxRetries),
278
296
  )
@@ -284,9 +302,10 @@ describe("AutomationSlice Callback (mixed-source)", () => {
284
302
  testPromise(
285
303
  "collect → process → resolve completes the TODO item",
286
304
  async () => {
287
- Callback.phase1(
305
+ await Callback.phase1(
288
306
  [encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))],
289
307
  testContext,
308
+ ~capabilities=Reventless.Capabilities.none,
290
309
  )
291
310
  expect((Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)->toBe(Pending)
292
311
 
@@ -298,7 +317,11 @@ describe("AutomationSlice Callback (mixed-source)", () => {
298
317
  expect(publishedCommands.contents->Array.length)->toBe(1)
299
318
  expect((Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)->toBe(Processing)
300
319
 
301
- Callback.phase1([encodeShipOrderEvent(ShipmentCreated({orderId: "ord-1"}))], testContext)
320
+ await Callback.phase1(
321
+ [encodeShipOrderEvent(ShipmentCreated({orderId: "ord-1"}))],
322
+ testContext,
323
+ ~capabilities=Reventless.Capabilities.none,
324
+ )
302
325
  expect((Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)->toBe(Completed)
303
326
 
304
327
  let publishedCommands2 = ref([])
@@ -3,6 +3,7 @@
3
3
  import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
4
4
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
5
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
6
+ import * as Capabilities$Reventless from "@reventlessdev/reventless-spec/src/semantic/Capabilities.res.mjs";
6
7
  import * as AutomationSliceFixtures$ReventlessLocal from "./AutomationSliceFixtures.res.mjs";
7
8
  import * as AutomationSlice_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs";
8
9
 
@@ -22,7 +23,7 @@ globalThis.describe("AutomationSlice Callback (mixed-source)", () => {
22
23
  orderId: "ord-1",
23
24
  address: "123 Main St"
24
25
  });
25
- Callback.phase1([json], AutomationSliceFixtures$ReventlessLocal.testContext);
26
+ await Callback.phase1([json], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
26
27
  let items = Callback.todoItems;
27
28
  globalThis.expect(Object.entries(items).length).toBe(1);
28
29
  let row = Stdlib_Option.getOrThrow(items["ord-1"], undefined);
@@ -40,8 +41,8 @@ globalThis.describe("AutomationSlice Callback (mixed-source)", () => {
40
41
  orderId: "ord-1",
41
42
  address: "456 Oak Ave"
42
43
  });
43
- Callback.phase1([j1], AutomationSliceFixtures$ReventlessLocal.testContext);
44
- Callback.phase1([j2], AutomationSliceFixtures$ReventlessLocal.testContext);
44
+ await Callback.phase1([j1], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
45
+ await Callback.phase1([j2], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
45
46
  globalThis.expect(Object.entries(Callback.todoItems).length).toBe(1);
46
47
  });
47
48
  globalThis.test("multiple OrderPlaced events create multiple TODO items", async () => {
@@ -55,10 +56,10 @@ globalThis.describe("AutomationSlice Callback (mixed-source)", () => {
55
56
  orderId: "ord-2",
56
57
  address: "456 Oak Ave"
57
58
  });
58
- Callback.phase1([
59
+ await Callback.phase1([
59
60
  j1,
60
61
  j2
61
- ], AutomationSliceFixtures$ReventlessLocal.testContext);
62
+ ], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
62
63
  globalThis.expect(Object.entries(Callback.todoItems).length).toBe(2);
63
64
  });
64
65
  globalThis.test("ShipmentCreated does not create a TODO item", async () => {
@@ -66,39 +67,39 @@ globalThis.describe("AutomationSlice Callback (mixed-source)", () => {
66
67
  TAG: "ShipmentCreated",
67
68
  orderId: "ord-1"
68
69
  });
69
- Callback.phase1([json], AutomationSliceFixtures$ReventlessLocal.testContext);
70
+ await Callback.phase1([json], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
70
71
  globalThis.expect(Object.entries(Callback.todoItems).length).toBe(0);
71
72
  });
72
73
  });
73
74
  globalThis.describe("Phase 1 — resolve", () => {
74
75
  globalThis.test("ShipmentCreated marks pending TODO item as completed", async () => {
75
- Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
76
+ await Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
76
77
  TAG: "OrderPlaced",
77
78
  orderId: "ord-1",
78
79
  address: "123 Main St"
79
- })], AutomationSliceFixtures$ReventlessLocal.testContext);
80
- Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
80
+ })], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
81
+ await Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
81
82
  TAG: "ShipmentCreated",
82
83
  orderId: "ord-1"
83
- })], AutomationSliceFixtures$ReventlessLocal.testContext);
84
+ })], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
84
85
  let row = Stdlib_Option.getOrThrow(Callback.todoItems["ord-1"], undefined);
85
86
  globalThis.expect(row.status).toBe("Completed");
86
87
  });
87
88
  globalThis.test("ShipmentCreated for unknown id is a no-op", async () => {
88
- Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
89
+ await Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
89
90
  TAG: "ShipmentCreated",
90
91
  orderId: "unknown"
91
- })], AutomationSliceFixtures$ReventlessLocal.testContext);
92
+ })], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
92
93
  globalThis.expect(Object.entries(Callback.todoItems).length).toBe(0);
93
94
  });
94
95
  });
95
96
  globalThis.describe("Phase 2 — process", () => {
96
97
  globalThis.test("pending items produce commands via publishJsons", async () => {
97
- Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
98
+ await Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
98
99
  TAG: "OrderPlaced",
99
100
  orderId: "ord-1",
100
101
  address: "123 Main St"
101
- })], AutomationSliceFixtures$ReventlessLocal.testContext);
102
+ })], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
102
103
  let publishedCommands = {
103
104
  contents: []
104
105
  };
@@ -113,15 +114,15 @@ globalThis.describe("AutomationSlice Callback (mixed-source)", () => {
113
114
  globalThis.expect(row.status).toBe("Processing");
114
115
  });
115
116
  globalThis.test("completed items are not processed", async () => {
116
- Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
117
+ await Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
117
118
  TAG: "OrderPlaced",
118
119
  orderId: "ord-1",
119
120
  address: "123 Main St"
120
- })], AutomationSliceFixtures$ReventlessLocal.testContext);
121
- Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
121
+ })], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
122
+ await Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
122
123
  TAG: "ShipmentCreated",
123
124
  orderId: "ord-1"
124
- })], AutomationSliceFixtures$ReventlessLocal.testContext);
125
+ })], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
125
126
  let publishedCommands = {
126
127
  contents: []
127
128
  };
@@ -136,7 +137,7 @@ globalThis.describe("AutomationSlice Callback (mixed-source)", () => {
136
137
  TAG: "OrderPlaced",
137
138
  orderId: "ord-1"
138
139
  });
139
- SkipCallback.phase1([json], AutomationSliceFixtures$ReventlessLocal.testContext);
140
+ await SkipCallback.phase1([json], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
140
141
  let publishedCommands = {
141
142
  contents: []
142
143
  };
@@ -159,11 +160,11 @@ globalThis.describe("AutomationSlice Callback (mixed-source)", () => {
159
160
  globalThis.expect(publishedCommands.contents.length).toBe(0);
160
161
  });
161
162
  globalThis.test("publishJsons failure marks items as Failed", async () => {
162
- Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
163
+ await Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
163
164
  TAG: "OrderPlaced",
164
165
  orderId: "ord-1",
165
166
  address: "123 Main St"
166
- })], AutomationSliceFixtures$ReventlessLocal.testContext);
167
+ })], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
167
168
  let failingPublish = async _cmds => Stdlib_JsError.throwWithMessage("publish failed");
168
169
  await Callback.phase2(failingPublish);
169
170
  let row = Stdlib_Option.getOrThrow(Callback.todoItems["ord-1"], undefined);
@@ -171,11 +172,11 @@ globalThis.describe("AutomationSlice Callback (mixed-source)", () => {
171
172
  globalThis.expect(row.retryCount).toBe(1);
172
173
  });
173
174
  globalThis.test("failed items with retryCount < maxRetries are retried", async () => {
174
- Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
175
+ await Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
175
176
  TAG: "OrderPlaced",
176
177
  orderId: "ord-1",
177
178
  address: "123 Main St"
178
- })], AutomationSliceFixtures$ReventlessLocal.testContext);
179
+ })], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
179
180
  let failingPublish = async _cmds => Stdlib_JsError.throwWithMessage("publish failed");
180
181
  await Callback.phase2(failingPublish);
181
182
  let row1 = Stdlib_Option.getOrThrow(Callback.todoItems["ord-1"], undefined);
@@ -194,14 +195,14 @@ globalThis.describe("AutomationSlice Callback (mixed-source)", () => {
194
195
  });
195
196
  globalThis.describe("retry exhaustion", () => {
196
197
  let failingPublish = async _cmds => Stdlib_JsError.throwWithMessage("publish failed");
197
- let collectOne = () => Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
198
+ let collectOne = async () => await Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
198
199
  TAG: "OrderPlaced",
199
200
  orderId: "ord-1",
200
201
  address: "123 Main St"
201
- })], AutomationSliceFixtures$ReventlessLocal.testContext);
202
+ })], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
202
203
  let statusOf = id => Stdlib_Option.getOrThrow(Callback.todoItems[id], undefined).status;
203
204
  globalThis.test("the attempt that spends the budget writes Abandoned, not Failed", async () => {
204
- collectOne();
205
+ await collectOne();
205
206
  await Callback.phase2(failingPublish);
206
207
  globalThis.expect(statusOf("ord-1")).toBe("Failed");
207
208
  await Callback.phase2(failingPublish);
@@ -211,7 +212,7 @@ globalThis.describe("AutomationSlice Callback (mixed-source)", () => {
211
212
  globalThis.expect(Stdlib_Option.getOrThrow(Callback.todoItems["ord-1"], undefined).retryCount).toBe(3);
212
213
  });
213
214
  globalThis.test("an abandoned row is not picked up again", async () => {
214
- collectOne();
215
+ await collectOne();
215
216
  await Callback.phase2(failingPublish);
216
217
  await Callback.phase2(failingPublish);
217
218
  await Callback.phase2(failingPublish);
@@ -227,7 +228,7 @@ globalThis.describe("AutomationSlice Callback (mixed-source)", () => {
227
228
  globalThis.expect(Stdlib_Option.getOrThrow(Callback.todoItems["ord-1"], undefined).retryCount).toBe(3);
228
229
  });
229
230
  globalThis.test("a row left stranded at the ceiling is normalised on the next pass", async () => {
230
- collectOne();
231
+ await collectOne();
231
232
  let row = Stdlib_Option.getOrThrow(Callback.todoItems["ord-1"], undefined);
232
233
  let newrecord = {...row};
233
234
  newrecord.retryCount = 3;
@@ -243,17 +244,17 @@ globalThis.describe("AutomationSlice Callback (mixed-source)", () => {
243
244
  globalThis.expect(statusOf("ord-1")).toBe("Abandoned");
244
245
  });
245
246
  globalThis.test("the row carries the ceiling its count is racing", async () => {
246
- collectOne();
247
+ await collectOne();
247
248
  globalThis.expect(Stdlib_Option.getOrThrow(Callback.todoItems["ord-1"], undefined).maxRetries).toEqual(AutomationSliceFixtures$ReventlessLocal.ShipOrderSpec.maxRetries);
248
249
  });
249
250
  });
250
251
  globalThis.describe("full lifecycle", () => {
251
252
  globalThis.test("collect → process → resolve completes the TODO item", async () => {
252
- Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
253
+ await Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
253
254
  TAG: "OrderPlaced",
254
255
  orderId: "ord-1",
255
256
  address: "123 Main St"
256
- })], AutomationSliceFixtures$ReventlessLocal.testContext);
257
+ })], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
257
258
  globalThis.expect(Stdlib_Option.getOrThrow(Callback.todoItems["ord-1"], undefined).status).toBe("Pending");
258
259
  let publishedCommands = {
259
260
  contents: []
@@ -264,10 +265,10 @@ globalThis.describe("AutomationSlice Callback (mixed-source)", () => {
264
265
  await Callback.phase2(mockPublish);
265
266
  globalThis.expect(publishedCommands.contents.length).toBe(1);
266
267
  globalThis.expect(Stdlib_Option.getOrThrow(Callback.todoItems["ord-1"], undefined).status).toBe("Processing");
267
- Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
268
+ await Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
268
269
  TAG: "ShipmentCreated",
269
270
  orderId: "ord-1"
270
- })], AutomationSliceFixtures$ReventlessLocal.testContext);
271
+ })], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
271
272
  globalThis.expect(Stdlib_Option.getOrThrow(Callback.todoItems["ord-1"], undefined).status).toBe("Completed");
272
273
  let publishedCommands2 = {
273
274
  contents: []
@@ -0,0 +1,178 @@
1
+ // An automation's work item that carries personal data is stored as an event is:
2
+ // its marked fields sealed, its row keyed by a fingerprint of the key the mapping
3
+ // chose, that key sealed inside the item for `process` and `resolve`, and an item
4
+ // whose subject was erased completed without processing.
5
+
6
+ open JestGlobals
7
+
8
+ module Source = {
9
+ module Id = Reventless.Id.String
10
+ let name = "GreetDcbEventLog"
11
+ @schema
12
+ type event =
13
+ | MemberJoined({memberId: @s.matches(Reventless.DcbTag.string) string, email: string})
14
+ | MemberGreeted({memberId: @s.matches(Reventless.DcbTag.string) string, email: string})
15
+ }
16
+
17
+ module Spec = {
18
+ let name = "GreetMember"
19
+ let moduleUrl: string = %raw(`import.meta.url`)
20
+
21
+ @schema
22
+ type todoItem = {
23
+ memberId: string,
24
+ email: @s.matches(Reventless.Pii.mark(~subject="memberId", S.string)) string,
25
+ }
26
+
27
+ @schema
28
+ type command = SendGreeting({memberId: @s.matches(Reventless.DcbTag.string) string, key: string})
29
+
30
+ let maxRetries = 3
31
+ let targetName = "SendGreeting"
32
+ let schedule = None
33
+ }
34
+
35
+ // Keyed by the address, as a translation keyed by contact would be.
36
+ module Mapping = Reventless.AutomationSlice.Mapping.Make(
37
+ Source,
38
+ Spec,
39
+ {
40
+ let collect = (event: Source.event, ~sourceId as _, _ctx) =>
41
+ switch event {
42
+ | MemberJoined({memberId, email}) => [
43
+ (`${memberId}:${email}`, ({memberId, email}: Spec.todoItem)),
44
+ ]
45
+ | MemberGreeted(_) => []
46
+ }
47
+ let resolve = (event: Source.event) =>
48
+ switch event {
49
+ | MemberGreeted({memberId, email}) => Some(`${memberId}:${email}`)
50
+ | MemberJoined(_) => None
51
+ }
52
+ let reschedule = _ => []
53
+ },
54
+ )
55
+
56
+ let processed: ref<array<(string, string)>> = ref([])
57
+
58
+ module Automation: Reventless.AutomationSlice.Automation with module Spec := Spec = {
59
+ let process = (key, item: Spec.todoItem, ~now as _) => {
60
+ processed := processed.contents->Array.concat([(key, item.email)])
61
+ Some((item.memberId, Spec.SendGreeting({memberId: item.memberId, key})))
62
+ }
63
+ let onExhausted = (_id, _item: Spec.todoItem) => None
64
+ let moduleUrl: string = %raw(`import.meta.url`)
65
+ module M = Reventless.AutomationSlice.Mappings.Make(Spec)
66
+ module type Mapping = M.Mapping
67
+ let mappings: array<module(Mapping)> = [module(Mapping)]
68
+ }
69
+
70
+ module Callback = ReventlessCore.AutomationSlice_Callback.Make(Spec, Automation)
71
+
72
+ let context: Reventless.AutomationSlice.context = {
73
+ environment: "test",
74
+ platformName: "local",
75
+ pluginName: "TestPlugin",
76
+ sliceName: "GreetMember",
77
+ }
78
+
79
+ let envelope = (event: Source.event) =>
80
+ JSON.Encode.object(
81
+ Dict.fromArray([
82
+ ("id", JSON.Encode.string("m-1")),
83
+ (
84
+ "meta",
85
+ ReventlessCore.Message.generateMeta(~service=Source.name)->Reventless.Util_Sury.toJson(
86
+ Reventless.Message.metaSchema,
87
+ ),
88
+ ),
89
+ ("event", event->Reventless.Util_Sury.toJson(Source.eventSchema)),
90
+ ]),
91
+ )
92
+
93
+ let toBase64 = (s: string) =>
94
+ NodeCrypto.bufferFromString(s, "utf8")->NodeCrypto.bufferToString("base64")
95
+ let fromBase64 = (s: string) =>
96
+ NodeCrypto.bufferFromString(s, "base64")->NodeCrypto.bufferToString("utf8")
97
+
98
+ let erased = ref(false)
99
+ let provider: Reventless.Pii.provider = {
100
+ name: "fake",
101
+ encrypts: true,
102
+ seal: async (~subject, ~field as _, ~forgettable as _, json) => Ok(
103
+ Reventless.Pii.wrap(`enc:v1:${subject}:fake:${json->JSON.stringify->toBase64}`),
104
+ ),
105
+ prefetch: async _ => Ok(),
106
+ openNow: (~field, json) =>
107
+ switch json->Reventless.Pii.unwrap->Option.flatMap(Reventless.Pii.parseStored) {
108
+ | Some(Cipher(_)) if erased.contents => Ok(Reventless.Pii.wrap(Reventless.Pii.erasedMarker))
109
+ | Some(Cipher({bytes})) => Ok(bytes->fromBase64->JSON.parseOrThrow)
110
+ | _ => Error(Reventless.Pii.UnknownForm({field: field}))
111
+ },
112
+ release: async (~subject as _, ~ref as _) => Ok(),
113
+ }
114
+
115
+ let keyed: Reventless.Capabilities.t = {
116
+ ...Reventless.Capabilities.none,
117
+ secrets: Reventless.Secrets.fixed(
118
+ ~token="t",
119
+ ~hash=s => s,
120
+ ~fingerprint=(~purpose, input) =>
121
+ `fp(${purpose})#${input->toBase64->String.length->Int.toString}`,
122
+ ),
123
+ }
124
+
125
+ let joined = Source.MemberJoined({memberId: "m-1", email: "ada@example.org"})
126
+ let greeted = Source.MemberGreeted({memberId: "m-1", email: "ada@example.org"})
127
+ let rows = () => Callback.todoItems->Dict.toArray
128
+ let published: ref<array<Reventless.Message.commandJson>> = ref([])
129
+ let publish: ReventlessInfra.CommandTopic.publishJsons = async msgs => {
130
+ published := published.contents->Array.concat(msgs)
131
+ }
132
+
133
+ describe("an automation's work item with personal data", () => {
134
+ beforeEach(() => {
135
+ Callback.todoItems->Dict.keysToArray->Array.forEach(k => Callback.todoItems->Dict.delete(k))
136
+ processed := []
137
+ published := []
138
+ erased := false
139
+ Reventless.Pii.Provider.install(provider)
140
+ })
141
+ afterEach(() => Reventless.Pii.Provider.install(Reventless.Pii.plain))
142
+
143
+ testPromise("is stored sealed, under a key that holds no address", async () => {
144
+ await Callback.phase1([envelope(joined)], context, ~capabilities=keyed)
145
+ expect(rows()->JSON.stringifyAny->Option.getOr("")->String.includes("ada@example.org"))->toBe(
146
+ false,
147
+ )
148
+ expect(
149
+ rows()->Array.every(((id, _)) => id->String.startsWith("fp(automationWorkItem:GreetMember)")),
150
+ )->toBe(true)
151
+ expect(rows()->Array.length)->toBe(1)
152
+ })
153
+
154
+ testPromise("is processed with the key and the value it was collected with", async () => {
155
+ await Callback.phase1([envelope(joined)], context, ~capabilities=keyed)
156
+ await Callback.phase2(publish)
157
+ expect(processed.contents)->toEqual([("m-1:ada@example.org", "ada@example.org")])
158
+ expect(published.contents->Array.length)->toBe(1)
159
+ })
160
+
161
+ testPromise("is resolved by the key the mapping chose", async () => {
162
+ await Callback.phase1([envelope(joined)], context, ~capabilities=keyed)
163
+ await Callback.phase1([envelope(greeted)], context, ~capabilities=keyed)
164
+ expect(rows()->Array.map(((_, row)) => row.status))->toEqual([
165
+ ReventlessCore.AutomationSlice_Callback.Completed,
166
+ ])
167
+ })
168
+
169
+ testPromise("is completed without processing once its subject is erased", async () => {
170
+ await Callback.phase1([envelope(joined)], context, ~capabilities=keyed)
171
+ erased := true
172
+ await Callback.phase2(publish)
173
+ expect(processed.contents)->toEqual([])
174
+ expect(rows()->Array.map(((_, row)) => row.status))->toEqual([
175
+ ReventlessCore.AutomationSlice_Callback.Completed,
176
+ ])
177
+ })
178
+ })
@@ -0,0 +1,352 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Sury from "sury";
4
+ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
5
+ import * as Nodebuffer from "node:buffer";
6
+ import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
7
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
8
+ import * as Pii$Reventless from "@reventlessdev/reventless-spec/src/components/Pii.res.mjs";
9
+ import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
10
+ import * as Message$Reventless from "@reventlessdev/reventless-spec/src/types/Message.res.mjs";
11
+ import * as Secrets$Reventless from "@reventlessdev/reventless-spec/src/semantic/Secrets.res.mjs";
12
+ import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
13
+ import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
14
+ import * as Capabilities$Reventless from "@reventlessdev/reventless-spec/src/semantic/Capabilities.res.mjs";
15
+ import * as AutomationSlice$Reventless from "@reventlessdev/reventless-spec/src/components/AutomationSlice.res.mjs";
16
+ import * as AutomationSlice_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs";
17
+
18
+ let name = "GreetDcbEventLog";
19
+
20
+ let eventSchema = Sury.union([
21
+ Sury.$schema(s => ({
22
+ TAG: "MemberJoined",
23
+ memberId: s.m(DcbTag$Reventless.string),
24
+ email: s.m(Sury.string)
25
+ })),
26
+ Sury.$schema(s => ({
27
+ TAG: "MemberGreeted",
28
+ memberId: s.m(DcbTag$Reventless.string),
29
+ email: s.m(Sury.string)
30
+ }))
31
+ ]);
32
+
33
+ let Source = {
34
+ Id: undefined,
35
+ name: name,
36
+ eventSchema: eventSchema
37
+ };
38
+
39
+ let moduleUrl = import.meta.url;
40
+
41
+ let todoItemSchema = Sury.$schema(s => ({
42
+ memberId: s.m(Sury.string),
43
+ email: s.m(Pii$Reventless.mark("memberId", undefined, Sury.string))
44
+ }));
45
+
46
+ let commandSchema = Sury.$schema(s => ({
47
+ TAG: "SendGreeting",
48
+ memberId: s.m(DcbTag$Reventless.string),
49
+ key: s.m(Sury.string)
50
+ }));
51
+
52
+ function authorizationOf(param) {
53
+ return "AllowAuthenticated";
54
+ }
55
+
56
+ function commandTransition(param) {
57
+ return "Undeclared";
58
+ }
59
+
60
+ let traits = [];
61
+
62
+ let Spec = {
63
+ name: "GreetMember",
64
+ moduleUrl: moduleUrl,
65
+ todoItemSchema: todoItemSchema,
66
+ commandSchema: commandSchema,
67
+ maxRetries: 3,
68
+ targetName: "SendGreeting",
69
+ schedule: undefined,
70
+ authorizationOf: authorizationOf,
71
+ commandTransition: commandTransition,
72
+ traits: traits
73
+ };
74
+
75
+ function collect(event, param, _ctx) {
76
+ if (event.TAG !== "MemberJoined") {
77
+ return [];
78
+ }
79
+ let email = event.email;
80
+ let memberId = event.memberId;
81
+ return [[
82
+ memberId + `:` + email,
83
+ {
84
+ memberId: memberId,
85
+ email: email
86
+ }
87
+ ]];
88
+ }
89
+
90
+ function resolve(event) {
91
+ if (event.TAG === "MemberJoined") {
92
+ return;
93
+ } else {
94
+ return event.memberId + `:` + event.email;
95
+ }
96
+ }
97
+
98
+ function reschedule(param) {
99
+ return [];
100
+ }
101
+
102
+ let Mapping = AutomationSlice$Reventless.Mapping.Make({
103
+ Id: Id$Reventless.$$String,
104
+ name: name,
105
+ eventSchema: eventSchema
106
+ })(Spec)({
107
+ collect: collect,
108
+ resolve: resolve,
109
+ reschedule: reschedule
110
+ });
111
+
112
+ let processed = {
113
+ contents: []
114
+ };
115
+
116
+ function process(key, item, param) {
117
+ processed.contents = processed.contents.concat([[
118
+ key,
119
+ item.email
120
+ ]]);
121
+ return [
122
+ item.memberId,
123
+ {
124
+ TAG: "SendGreeting",
125
+ memberId: item.memberId,
126
+ key: key
127
+ }
128
+ ];
129
+ }
130
+
131
+ function onExhausted(_id, _item) {
132
+
133
+ }
134
+
135
+ let moduleUrl$1 = import.meta.url;
136
+
137
+ AutomationSlice$Reventless.Mappings.Make(Spec);
138
+
139
+ let mappings = [Mapping];
140
+
141
+ let Automation = {
142
+ process: process,
143
+ onExhausted: onExhausted,
144
+ moduleUrl: moduleUrl$1,
145
+ mappings: mappings
146
+ };
147
+
148
+ let Callback = AutomationSlice_Callback$ReventlessCore.Make(Spec)(Automation);
149
+
150
+ let context = {
151
+ environment: "test",
152
+ platformName: "local",
153
+ pluginName: "TestPlugin",
154
+ sliceName: "GreetMember"
155
+ };
156
+
157
+ function envelope(event) {
158
+ return Object.fromEntries([
159
+ [
160
+ "id",
161
+ "m-1"
162
+ ],
163
+ [
164
+ "meta",
165
+ Util_Sury$Reventless.toJson(Message$ReventlessCore.generateMeta(name, undefined, undefined, undefined, undefined, undefined, undefined), Message$Reventless.metaSchema)
166
+ ],
167
+ [
168
+ "event",
169
+ Util_Sury$Reventless.toJson(event, eventSchema)
170
+ ]
171
+ ]);
172
+ }
173
+
174
+ function toBase64(s) {
175
+ return Nodebuffer.Buffer.from(s, "utf8").toString("base64");
176
+ }
177
+
178
+ function fromBase64(s) {
179
+ return Nodebuffer.Buffer.from(s, "base64").toString("utf8");
180
+ }
181
+
182
+ let erased = {
183
+ contents: false
184
+ };
185
+
186
+ async function provider_seal(subject, param, param$1, json) {
187
+ return {
188
+ TAG: "Ok",
189
+ _0: Pii$Reventless.wrap(`enc:v1:` + subject + `:fake:` + toBase64(JSON.stringify(json)))
190
+ };
191
+ }
192
+
193
+ async function provider_prefetch(param) {
194
+ return {
195
+ TAG: "Ok",
196
+ _0: undefined
197
+ };
198
+ }
199
+
200
+ function provider_openNow(field, json) {
201
+ let match = Stdlib_Option.flatMap(Pii$Reventless.unwrap(json), Pii$Reventless.parseStored);
202
+ if (match === undefined) {
203
+ return {
204
+ TAG: "Error",
205
+ _0: {
206
+ TAG: "UnknownForm",
207
+ field: field
208
+ }
209
+ };
210
+ }
211
+ switch (match.TAG) {
212
+ case "Cipher" :
213
+ if (erased.contents) {
214
+ return {
215
+ TAG: "Ok",
216
+ _0: Pii$Reventless.wrap(Pii$Reventless.erasedMarker)
217
+ };
218
+ } else {
219
+ return {
220
+ TAG: "Ok",
221
+ _0: JSON.parse(fromBase64(match.bytes))
222
+ };
223
+ }
224
+ case "ErasedForm" :
225
+ case "ForgottenForm" :
226
+ return {
227
+ TAG: "Error",
228
+ _0: {
229
+ TAG: "UnknownForm",
230
+ field: field
231
+ }
232
+ };
233
+ }
234
+ }
235
+
236
+ async function provider_release(param, param$1) {
237
+ return {
238
+ TAG: "Ok",
239
+ _0: undefined
240
+ };
241
+ }
242
+
243
+ let provider = {
244
+ name: "fake",
245
+ encrypts: true,
246
+ seal: provider_seal,
247
+ prefetch: provider_prefetch,
248
+ openNow: provider_openNow,
249
+ release: provider_release
250
+ };
251
+
252
+ let keyed_geocode = Capabilities$Reventless.none.geocode;
253
+
254
+ let keyed_messaging = Capabilities$Reventless.none.messaging;
255
+
256
+ let keyed_identityProvider = Capabilities$Reventless.none.identityProvider;
257
+
258
+ let keyed_secrets = Secrets$Reventless.fixed("t", s => s, (purpose, input) => `fp(` + purpose + `)#` + toBase64(input).length.toString());
259
+
260
+ let keyed_stash = Capabilities$Reventless.none.stash;
261
+
262
+ let keyed = {
263
+ geocode: keyed_geocode,
264
+ messaging: keyed_messaging,
265
+ identityProvider: keyed_identityProvider,
266
+ secrets: keyed_secrets,
267
+ stash: keyed_stash
268
+ };
269
+
270
+ let joined = {
271
+ TAG: "MemberJoined",
272
+ memberId: "m-1",
273
+ email: "ada@example.org"
274
+ };
275
+
276
+ let greeted = {
277
+ TAG: "MemberGreeted",
278
+ memberId: "m-1",
279
+ email: "ada@example.org"
280
+ };
281
+
282
+ function rows() {
283
+ return Object.entries(Callback.todoItems);
284
+ }
285
+
286
+ let published = {
287
+ contents: []
288
+ };
289
+
290
+ async function publish(msgs) {
291
+ published.contents = published.contents.concat(msgs);
292
+ }
293
+
294
+ globalThis.describe("an automation's work item with personal data", () => {
295
+ globalThis.beforeEach(() => {
296
+ Object.keys(Callback.todoItems).forEach(k => Stdlib_Dict.$$delete(Callback.todoItems, k));
297
+ processed.contents = [];
298
+ published.contents = [];
299
+ erased.contents = false;
300
+ Pii$Reventless.Provider.install(provider);
301
+ });
302
+ globalThis.afterEach(() => Pii$Reventless.Provider.install(Pii$Reventless.plain));
303
+ globalThis.test("is stored sealed, under a key that holds no address", async () => {
304
+ await Callback.phase1([envelope(joined)], context, keyed);
305
+ globalThis.expect(Stdlib_Option.getOr(JSON.stringify(Object.entries(Callback.todoItems)), "").includes("ada@example.org")).toBe(false);
306
+ globalThis.expect(Object.entries(Callback.todoItems).every(param => param[0].startsWith("fp(automationWorkItem:GreetMember)"))).toBe(true);
307
+ globalThis.expect(Object.entries(Callback.todoItems).length).toBe(1);
308
+ });
309
+ globalThis.test("is processed with the key and the value it was collected with", async () => {
310
+ await Callback.phase1([envelope(joined)], context, keyed);
311
+ await Callback.phase2(publish);
312
+ globalThis.expect(processed.contents).toEqual([[
313
+ "m-1:ada@example.org",
314
+ "ada@example.org"
315
+ ]]);
316
+ globalThis.expect(published.contents.length).toBe(1);
317
+ });
318
+ globalThis.test("is resolved by the key the mapping chose", async () => {
319
+ await Callback.phase1([envelope(joined)], context, keyed);
320
+ await Callback.phase1([envelope(greeted)], context, keyed);
321
+ globalThis.expect(Object.entries(Callback.todoItems).map(param => param[1].status)).toEqual(["Completed"]);
322
+ });
323
+ globalThis.test("is completed without processing once its subject is erased", async () => {
324
+ await Callback.phase1([envelope(joined)], context, keyed);
325
+ erased.contents = true;
326
+ await Callback.phase2(publish);
327
+ globalThis.expect(processed.contents).toEqual([]);
328
+ globalThis.expect(Object.entries(Callback.todoItems).map(param => param[1].status)).toEqual(["Completed"]);
329
+ });
330
+ });
331
+
332
+ export {
333
+ Source,
334
+ Spec,
335
+ Mapping,
336
+ processed,
337
+ Automation,
338
+ Callback,
339
+ context,
340
+ envelope,
341
+ toBase64,
342
+ fromBase64,
343
+ erased,
344
+ provider,
345
+ keyed,
346
+ joined,
347
+ greeted,
348
+ rows,
349
+ published,
350
+ publish,
351
+ }
352
+ /* eventSchema Not a pure module */