@reventlessdev/reventless-local 3.0.0-alpha.249 → 3.0.0-alpha.250
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 +8 -0
- package/package.json +13 -13
- package/src/BakedManifest.res +5 -6
- package/src/EmitCapabilities.res +3 -1
- package/src/LocalPlatformRegistry.res +3 -1
- package/src/LocalPlatformStart.res +7 -6
- package/src/LocalSeedTarget.res +3 -1
- package/src/Platform.res +291 -215
- package/src/ShellConfig.res +10 -17
- package/src/UiHints.res +8 -14
- package/src/UiSlots.res +10 -17
- package/src/adapter/Api/LocalEvents_Server.res +5 -3
- package/src/adapter/Api/LocalGraphQL_Adapter.res +4 -4
- package/src/adapter/Auth/LocalAuth.res +11 -15
- package/src/adapter/Auth/UserStore.res +5 -7
- package/src/adapter/Backend.res +1 -2
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +42 -26
- package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res +13 -3
- package/src/adapter/CommandTopic/LocalCommandTopicChannel.res +15 -17
- package/src/adapter/CommandTopic/LocalCommandTopicRemoteChannel.res +18 -22
- package/src/adapter/Counter/LocalCounterHandler.res +1 -1
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +33 -22
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +57 -57
- package/src/adapter/DomainGraphQL_Server.res +35 -34
- package/src/adapter/DomainMCP_Server.res +45 -41
- package/src/adapter/EventCollector/LocalEventCollectorChannel.res +47 -36
- package/src/adapter/EventHistory/EventHistoryResolvers_GraphQL.res +15 -12
- package/src/adapter/EventLog/EventLogStorage_InMemory.res +9 -4
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res +36 -24
- package/src/adapter/EventTopic/LocalEventTopicPublisher.res +9 -3
- package/src/adapter/Heartbeat/LocalHeartbeatRunner.res +9 -14
- package/src/adapter/LocalBus.res +12 -12
- package/src/adapter/LocalCapabilities.res.mjs +1 -1
- package/src/adapter/LocalGeocodeResolvers.res +1 -4
- package/src/adapter/LocalSideEffectHandler.res +1 -1
- package/src/adapter/LocalStateChangeDescriptor.res +2 -1
- package/src/adapter/MCP_ServerInstance.res +51 -39
- package/src/adapter/ObjectStore/LocalObjectStore.res +5 -2
- package/src/adapter/ObjectStore/ObjectStoreStorage_FileSystem.res +1 -2
- package/src/adapter/PlatformGraphQL_Server.res +9 -5
- package/src/adapter/ProjectionCheckpoint.res +8 -1
- package/src/adapter/ProjectionPending.res +1 -3
- package/src/adapter/QueryDb/LocalQueryDbStorage.res +2 -1
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +367 -269
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +5 -3
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +63 -40
- package/src/adapter/QueryDb/SortKey_Filter.res +14 -6
- package/src/adapter/Runtime/LocalAggregateRuntime_Builder.res +12 -10
- package/src/adapter/Runtime/LocalEventCollectorRuntime_Builder.res +8 -2
- package/src/adapter/Runtime/LocalPluginRuntime_Builder.res +1 -4
- package/src/adapter/Scheduler/LocalScheduledPublisher.res +12 -12
- package/src/adapter/SqliteDriver.res +16 -4
- package/src/adapter/Task/TaskBucket_Sqlite.res +5 -6
- package/src/components/Aggregate_Builder.res +6 -1
- package/src/components/Counter_Builder.res +1 -5
- package/src/components/DcbEventLog_Builder.res +5 -3
- package/src/components/ExtensionPoint_Builder.res +13 -15
- package/src/components/InboundTranslationSlice_Builder.res +3 -2
- package/src/components/OutboundTranslationSlice_Builder.res +3 -2
- package/src/components/ReadModel_Builder.res +18 -20
- package/src/components/StateViewSlice_Builder.res +3 -2
- package/src/components/Task_Builder.res +12 -13
- package/src/reset/LocalSeedReset.res +2 -5
- package/src/test/Mocks/MockDcbEventLogStorage.res +5 -1
- package/src/test/Mocks/MockEventLogStorage.res +8 -3
- package/src/test/Mocks/MockQueryDbStorage.res +3 -2
- package/src/test/TestRunner.res +1 -1
- package/tests/BakedManifestFilesTest.res +7 -10
- package/tests/LocalEventTapTest.res +26 -21
- package/tests/LocalPlatformStartTest.res +33 -28
- package/tests/PluginEventDecodeTest.res +3 -3
- package/tests/ShellConfigTest.res +30 -21
- package/tests/SplitApiFixtures.res +34 -10
- package/tests/SplitApiTest.res +92 -62
- package/tests/adapter/BackendParityTest.res +180 -130
- package/tests/adapter/CollectNEventsTest.res +6 -4
- package/tests/adapter/CommandAuthorizationTest.res +47 -41
- package/tests/adapter/CommandTopicChannelTest.res +163 -139
- package/tests/adapter/CounterHandlerTest.res +95 -77
- package/tests/adapter/DcbEventLogStorageSqliteTest.res +171 -168
- package/tests/adapter/DcbEventLogStorageTest.res +264 -207
- package/tests/adapter/DomainGraphQL_ServerTest.res +53 -43
- package/tests/adapter/EventCollectorChannelTest.res +153 -124
- package/tests/adapter/EventHistoryResolverTest.res +46 -18
- package/tests/adapter/EventLogSnapshotParityTest.res +57 -45
- package/tests/adapter/EventLogStorageSqliteTest.res +9 -10
- package/tests/adapter/EventTapTest.res +37 -20
- package/tests/adapter/EventTopicPublisherTest.res +74 -50
- package/tests/adapter/GraphQLSubscriptionBridgeTest.res +4 -3
- package/tests/adapter/GraphQL_SchemaInspectorTest.res +666 -568
- package/tests/adapter/GraphQL_SubscriptionResolversTest.res +105 -96
- package/tests/adapter/HeartbeatRunnerTest.res +99 -81
- package/tests/adapter/InboundTranslationMutationTest.res +4 -1
- package/tests/adapter/LocalAuthHttpTest.res +79 -85
- package/tests/adapter/LocalAuthLoginTest.res +42 -50
- package/tests/adapter/LocalAuthTest.res +8 -16
- package/tests/adapter/LocalAuthUserStoreTest.res +2 -7
- package/tests/adapter/LocalBusBoundedTest.res +187 -117
- package/tests/adapter/LocalBusPubSubTest.res +118 -76
- package/tests/adapter/LocalDispatchContextTest.res +1 -2
- package/tests/adapter/LocalEvents_ServerTest.res +242 -187
- package/tests/adapter/PgProjectionCatchupTest.res +44 -10
- package/tests/adapter/ProjectionCheckpointTest.res +261 -206
- package/tests/adapter/QueryDbGsiTtlTest.res +123 -83
- package/tests/adapter/QueryDbListPushdownParityTest.res +107 -67
- package/tests/adapter/QueryDbListResolverTest.res +225 -208
- package/tests/adapter/QueryDbResolvedFieldsTest.res +17 -14
- package/tests/adapter/QueryDbStorageSqliteTest.res +33 -49
- package/tests/adapter/QueryDbStorageTest.res +163 -117
- package/tests/adapter/QueryDbUnionRoundTripTest.res +8 -10
- package/tests/adapter/QueryEngineTest.res +179 -126
- package/tests/adapter/ScheduledPublisherTest.res +171 -135
- package/tests/adapter/ServedBucketHttpTest.res +65 -59
- package/tests/adapter/SortKey_FilterTest.res +19 -3
- package/tests/adapter/SqliteDriverTest.res +13 -10
- package/tests/adapter/TaskBucketTest.res +94 -80
- package/tests/components/aggregate/AggregateTest.res +31 -18
- package/tests/components/automationslice/AutomationSliceCallbackTest.res +259 -222
- package/tests/components/automationslice/AutomationSliceFixtures.res +4 -2
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +38 -38
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +12 -14
- package/tests/components/automationslice/MixedSourceAutomationSliceTest.res +8 -32
- package/tests/components/commandgenerator/CommandGeneratorFixtures.res +7 -5
- package/tests/components/commandgenerator/CommandGeneratorTest.res +40 -31
- package/tests/components/commandtopic/CommandTopicFixtures.res +3 -4
- package/tests/components/commandtopic/CommandTopicStreamFixtures.res +7 -5
- package/tests/components/commandtopic/CommandTopicTest.res +24 -15
- package/tests/components/counter/CounterFixtures.res +6 -8
- package/tests/components/dcb/DcbCrossPartitionFixtures.res +8 -2
- package/tests/components/dcb/DcbCrossPartitionReadTest.res +20 -13
- package/tests/components/dcb/DcbEventLogAppendStreamTest.res +12 -8
- package/tests/components/dcb/DcbEventLogStreamTest.res +88 -61
- package/tests/components/dcb/DcbFixtures.res +4 -1
- package/tests/components/dcb/DcbTest.res +9 -6
- package/tests/components/eventcollector/EventCollectorFixtures.res +0 -1
- package/tests/components/eventcollector/EventCollectorTest.res +6 -3
- package/tests/components/eventlog/EventLogAppendStreamTest.res +88 -68
- package/tests/components/eventlog/EventLogConflictTest.res +8 -4
- package/tests/components/eventlog/EventLogFixtures.res +3 -3
- package/tests/components/eventlog/EventLogProvisioningSeamTest.res +26 -13
- package/tests/components/eventlog/EventLogStreamTest.res +119 -56
- package/tests/components/eventlog/EventLogTest.res +20 -4
- package/tests/components/eventtopic/EventTopicFixtures.res +3 -3
- package/tests/components/eventtopic/EventTopicStreamFixtures.res +1 -2
- package/tests/components/eventtopic/EventTopicStreamTest.res +7 -1
- package/tests/components/extensionpoint/ExtensionPointFixtures.res +14 -9
- package/tests/components/extensionpoint/ExtensionPointTest.res +3 -2
- package/tests/components/heartbeat/HeartbeatFixtures.res +5 -6
- package/tests/components/heartbeat/HeartbeatTest.res +56 -47
- package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res +271 -249
- package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res +3 -2
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +361 -276
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res +8 -10
- package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res +37 -43
- package/tests/components/querydb/QueryDbFixtures.res +1 -5
- package/tests/components/querydb/QueryDbTest.res +2 -4
- package/tests/components/readmodel/DcbReadModelE2EFixtures.res +15 -18
- package/tests/components/readmodel/MixedSourceReadModelFixtures.res +5 -7
- package/tests/components/readmodel/ReadModelFixtures.res +8 -8
- package/tests/components/scheduler/SchedulerTest.res +76 -58
- package/tests/components/sideeffecthandler/SideEffectHandlerFixtures.res +4 -4
- package/tests/components/stateviewslice/StateViewSliceDisplayNameFixtures.res +5 -7
- package/tests/components/stateviewslice/StateViewSliceFixtures.res +8 -6
- package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res +17 -11
- package/tests/components/stateviewslice/StateViewSliceSubIdTest.res +12 -2
- package/tests/components/task/TaskFixtures.res +5 -5
- package/tests/components/task/TaskTest.res +95 -80
- package/tests/plugin/PluginsProjection_GWT.res +3 -1
- package/tests/plugin/UiFragments_GWT.res +6 -1
- package/tests/reset/LocalSeedResetTest.res +1 -0
|
@@ -19,7 +19,9 @@ let bob: Reventless.Identity.t = {
|
|
|
19
19
|
provider: InMemory,
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
let buildContext = (
|
|
22
|
+
let buildContext = (
|
|
23
|
+
headers: array<(string, string)>,
|
|
24
|
+
): ReventlessCore.Auth_Adapter.requestContext => {
|
|
23
25
|
headers: Dict.fromArray(headers),
|
|
24
26
|
}
|
|
25
27
|
|
|
@@ -127,9 +129,7 @@ testPromise("authenticate accepts a valid Bearer token", async () => {
|
|
|
127
129
|
| Ok(t) => t
|
|
128
130
|
| Error(msg) => JsError.throwWithMessage("issue failed: " ++ msg)
|
|
129
131
|
}
|
|
130
|
-
let result = await LocalAuth.authenticate(
|
|
131
|
-
buildContext([("authorization", "Bearer " ++ token)]),
|
|
132
|
-
)
|
|
132
|
+
let result = await LocalAuth.authenticate(buildContext([("authorization", "Bearer " ++ token)]))
|
|
133
133
|
switch result {
|
|
134
134
|
| Authenticated(identity) =>
|
|
135
135
|
expect(identity.username)->toEqual("bob")
|
|
@@ -256,11 +256,7 @@ testPromise("a narrowed token remembers the choice and what it gave up", async (
|
|
|
256
256
|
// the failure that would hand a tampering client everything it asked for.
|
|
257
257
|
testPromise("a login naming a role the user does not hold is REFUSED", async () => {
|
|
258
258
|
carolLoggedIn()
|
|
259
|
-
switch await LocalAuth.Login.issue(
|
|
260
|
-
~username="carol",
|
|
261
|
-
~password="carol-pw",
|
|
262
|
-
~activeRole="Admin",
|
|
263
|
-
) {
|
|
259
|
+
switch await LocalAuth.Login.issue(~username="carol", ~password="carol-pw", ~activeRole="Admin") {
|
|
264
260
|
| Ok(_) => JsError.throwWithMessage("expected a request to widen to be refused")
|
|
265
261
|
| Error(msg) => expect(msg->String.includes("Admin"))->toEqual(true)
|
|
266
262
|
}
|
|
@@ -275,13 +271,8 @@ testPromise("narrowing cannot smuggle a group in through the claims bag", async
|
|
|
275
271
|
claims: Dict.fromArray([("availableRoles", "Admin,Fulfilment,Shopper")]),
|
|
276
272
|
}
|
|
277
273
|
LocalAuth.Login.setCredentials(~username="carol", ~password="carol-pw", ~identity=withClaims)
|
|
278
|
-
switch await LocalAuth.Login.issue(
|
|
279
|
-
|
|
280
|
-
~password="carol-pw",
|
|
281
|
-
~activeRole="Admin",
|
|
282
|
-
) {
|
|
283
|
-
| Ok(_) =>
|
|
284
|
-
JsError.throwWithMessage("expected membership to be judged by groups, not by a claim")
|
|
274
|
+
switch await LocalAuth.Login.issue(~username="carol", ~password="carol-pw", ~activeRole="Admin") {
|
|
275
|
+
| Ok(_) => JsError.throwWithMessage("expected membership to be judged by groups, not by a claim")
|
|
285
276
|
| Error(_) => expect(true)->toEqual(true)
|
|
286
277
|
}
|
|
287
278
|
})
|
|
@@ -298,9 +289,7 @@ testPromise("a narrowed token authenticates as the narrowed identity", async ()
|
|
|
298
289
|
| Ok(t) => t
|
|
299
290
|
| Error(e) => JsError.throwWithMessage(e)
|
|
300
291
|
}
|
|
301
|
-
let result = await LocalAuth.authenticate(
|
|
302
|
-
buildContext([("authorization", "Bearer " ++ token)]),
|
|
303
|
-
)
|
|
292
|
+
let result = await LocalAuth.authenticate(buildContext([("authorization", "Bearer " ++ token)]))
|
|
304
293
|
switch result {
|
|
305
294
|
| Authenticated(identity) => expect(identity.groups)->toEqual(["Shopper"])
|
|
306
295
|
| _ => JsError.throwWithMessage("expected the narrowed token to authenticate")
|
|
@@ -408,32 +397,35 @@ describe("the conformance table, minted locally", () => {
|
|
|
408
397
|
requested,
|
|
409
398
|
expected,
|
|
410
399
|
}) =>
|
|
411
|
-
testPromise(
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
400
|
+
testPromise(
|
|
401
|
+
label,
|
|
402
|
+
async () => {
|
|
403
|
+
resetAll()
|
|
404
|
+
LocalAuth.Login.setCredentials(
|
|
405
|
+
~username="conformance",
|
|
406
|
+
~password="pw",
|
|
407
|
+
~identity={
|
|
408
|
+
userId: "u-conformance",
|
|
409
|
+
username: "conformance",
|
|
410
|
+
groups: membership,
|
|
411
|
+
provider: InMemory,
|
|
412
|
+
},
|
|
413
|
+
)
|
|
414
|
+
let minted = await LocalAuth.Login.issue(
|
|
415
|
+
~username="conformance",
|
|
416
|
+
~password="pw",
|
|
417
|
+
~activeRole=?requested,
|
|
418
|
+
)
|
|
419
|
+
switch (minted, expected) {
|
|
420
|
+
| (Ok(token), Some(groups)) => expect(decodeOrThrow(token).groups)->toEqual(groups)
|
|
421
|
+
// `None` means refused — not honoured, not ignored, and not reduced to the
|
|
422
|
+
// empty set. A token minted here at all would be the vulnerability.
|
|
423
|
+
| (Ok(_), None) => JsError.throwWithMessage("expected the request to be refused")
|
|
424
|
+
| (Error(msg), Some(_)) => JsError.throwWithMessage("expected a token, got: " ++ msg)
|
|
425
|
+
| (Error(_), None) => expect(true)->toEqual(true)
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
)
|
|
437
429
|
)
|
|
438
430
|
})
|
|
439
431
|
|
|
@@ -453,11 +445,11 @@ describe("Login token secret", () => {
|
|
|
453
445
|
let first = LocalAuth.Login._resolveIn(~dir)
|
|
454
446
|
// A second call with no memoised value is what the next process does.
|
|
455
447
|
let second = LocalAuth.Login._resolveIn(~dir)
|
|
456
|
-
expect((
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
))
|
|
448
|
+
expect((
|
|
449
|
+
first == second,
|
|
450
|
+
NodeFs.existsSync(secretPath(dir)),
|
|
451
|
+
String.length(first) >= 16,
|
|
452
|
+
))->toEqual((true, true, true))
|
|
461
453
|
})
|
|
462
454
|
|
|
463
455
|
testSync("reads a secret a previous boot left behind", () => {
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
open JestGlobals
|
|
7
7
|
|
|
8
|
-
let buildContext = (
|
|
8
|
+
let buildContext = (
|
|
9
|
+
headers: array<(string, string)>,
|
|
10
|
+
): ReventlessCore.Auth_Adapter.requestContext => {
|
|
9
11
|
headers: Dict.fromArray(headers),
|
|
10
12
|
}
|
|
11
13
|
|
|
@@ -22,9 +24,7 @@ testPromise("no X-User, no X-Groups → defaultUser (in-memory dev convenience)"
|
|
|
22
24
|
|
|
23
25
|
testPromise("X-User: admin resolves built-in admin identity", async () => {
|
|
24
26
|
LocalAuth.resetUsers()
|
|
25
|
-
let result = await LocalAuth.authenticate(
|
|
26
|
-
buildContext([("x-user", "admin")]),
|
|
27
|
-
)
|
|
27
|
+
let result = await LocalAuth.authenticate(buildContext([("x-user", "admin")]))
|
|
28
28
|
switch result {
|
|
29
29
|
| Authenticated(identity) =>
|
|
30
30
|
expect(identity.username)->toEqual("admin")
|
|
@@ -35,9 +35,7 @@ testPromise("X-User: admin resolves built-in admin identity", async () => {
|
|
|
35
35
|
|
|
36
36
|
testPromise("X-User: user resolves built-in default identity", async () => {
|
|
37
37
|
LocalAuth.resetUsers()
|
|
38
|
-
let result = await LocalAuth.authenticate(
|
|
39
|
-
buildContext([("x-user", "user")]),
|
|
40
|
-
)
|
|
38
|
+
let result = await LocalAuth.authenticate(buildContext([("x-user", "user")]))
|
|
41
39
|
switch result {
|
|
42
40
|
| Authenticated(identity) =>
|
|
43
41
|
expect(identity.username)->toEqual("user")
|
|
@@ -48,9 +46,7 @@ testPromise("X-User: user resolves built-in default identity", async () => {
|
|
|
48
46
|
|
|
49
47
|
testPromise("unknown X-User falls back to Anonymous", async () => {
|
|
50
48
|
LocalAuth.resetUsers()
|
|
51
|
-
let result = await LocalAuth.authenticate(
|
|
52
|
-
buildContext([("x-user", "unknown")]),
|
|
53
|
-
)
|
|
49
|
+
let result = await LocalAuth.authenticate(buildContext([("x-user", "unknown")]))
|
|
54
50
|
switch result {
|
|
55
51
|
| Anonymous => ()
|
|
56
52
|
| _ => JsError.throwWithMessage("expected Anonymous")
|
|
@@ -72,9 +68,7 @@ testPromise("X-Groups overrides the resolved identity's groups", async () => {
|
|
|
72
68
|
|
|
73
69
|
testPromise("X-Groups alone yields defaultUser tagged with the override groups", async () => {
|
|
74
70
|
LocalAuth.resetUsers()
|
|
75
|
-
let result = await LocalAuth.authenticate(
|
|
76
|
-
buildContext([("x-groups", "Tester")]),
|
|
77
|
-
)
|
|
71
|
+
let result = await LocalAuth.authenticate(buildContext([("x-groups", "Tester")]))
|
|
78
72
|
switch result {
|
|
79
73
|
| Authenticated(identity) =>
|
|
80
74
|
expect(identity.userId)->toEqual("local-user")
|
|
@@ -94,9 +88,7 @@ testPromise("registerUser injects a custom identity", async () => {
|
|
|
94
88
|
provider: InMemory,
|
|
95
89
|
},
|
|
96
90
|
)
|
|
97
|
-
let result = await LocalAuth.authenticate(
|
|
98
|
-
buildContext([("x-user", "alice")]),
|
|
99
|
-
)
|
|
91
|
+
let result = await LocalAuth.authenticate(buildContext([("x-user", "alice")]))
|
|
100
92
|
switch result {
|
|
101
93
|
| Authenticated(identity) =>
|
|
102
94
|
expect(identity.userId)->toEqual("alice-id")
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
open JestGlobals
|
|
6
6
|
|
|
7
|
-
|
|
8
7
|
let resetAll = () => {
|
|
9
8
|
LocalAuth.resetUsers()
|
|
10
9
|
LocalAuth.Login.resetStore()
|
|
@@ -115,8 +114,7 @@ testPromise("load() with no args + no default file returns Empty", async () => {
|
|
|
115
114
|
// no such file exists.)
|
|
116
115
|
switch UserStore.load() {
|
|
117
116
|
| Ok(Empty) => ()
|
|
118
|
-
| Ok(DefaultFile(_)) =>
|
|
119
|
-
// If a .reventless/users.yaml happens to exist in CI, accept it.
|
|
117
|
+
| Ok(DefaultFile(_)) => // If a .reventless/users.yaml happens to exist in CI, accept it.
|
|
120
118
|
()
|
|
121
119
|
| Ok(_) => JsError.throwWithMessage("expected Empty or DefaultFile")
|
|
122
120
|
| Error(msg) => JsError.throwWithMessage("load failed: " ++ msg)
|
|
@@ -152,10 +150,7 @@ testPromise("inline users win over a file path", async () => {
|
|
|
152
150
|
|
|
153
151
|
testPromise("autoLoadOnce is a no-op after an explicit load", async () => {
|
|
154
152
|
resetAll()
|
|
155
|
-
let _ = UserStore.load(
|
|
156
|
-
~users=[{username: "x", password: "x", groups: []}],
|
|
157
|
-
(),
|
|
158
|
-
)
|
|
153
|
+
let _ = UserStore.load(~users=[{username: "x", password: "x", groups: []}], ())
|
|
159
154
|
// Second call should not crash even if .reventless/users.yaml doesn't
|
|
160
155
|
// exist; resolved=true prevents the default-discovery branch.
|
|
161
156
|
UserStore.autoLoadOnce()
|
|
@@ -21,137 +21,207 @@ let defaultMeta: Reventless.Message.meta = {
|
|
|
21
21
|
|
|
22
22
|
describe("LocalBus bounded PubSub (Phase G)", () => {
|
|
23
23
|
describe("basic delivery", () => {
|
|
24
|
-
testPromise(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
testPromise(
|
|
25
|
+
"delivers a message to a subscriber in bounded mode",
|
|
26
|
+
async () => {
|
|
27
|
+
module TestBus = LocalBus.MakeBounded({
|
|
28
|
+
let capacity = 10
|
|
29
|
+
})
|
|
30
|
+
let delivered = ref(false)
|
|
31
|
+
TestBus.subscribeToEvents(
|
|
32
|
+
"T",
|
|
33
|
+
async (_, _, _) => {
|
|
34
|
+
delivered := true
|
|
35
|
+
},
|
|
36
|
+
)
|
|
37
|
+
let _ = await TestBus.publishEvent("T", "svc", defaultMeta, JSON.Null)
|
|
38
|
+
expect(delivered.contents)->toBe(true)
|
|
39
|
+
},
|
|
40
|
+
)
|
|
33
41
|
|
|
34
|
-
testPromise(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
testPromise(
|
|
43
|
+
"bounded mode with no subscribers returns immediately",
|
|
44
|
+
async () => {
|
|
45
|
+
module TestBus = LocalBus.MakeBounded({
|
|
46
|
+
let capacity = 5
|
|
47
|
+
})
|
|
48
|
+
// No subscribers — should not hang
|
|
49
|
+
await TestBus.publishEvent("empty-topic", "svc", defaultMeta, JSON.Null)
|
|
50
|
+
},
|
|
51
|
+
)
|
|
39
52
|
|
|
40
|
-
testPromise(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
testPromise(
|
|
54
|
+
"passes service, meta, and json to the subscriber handler",
|
|
55
|
+
async () => {
|
|
56
|
+
module TestBus = LocalBus.MakeBounded({
|
|
57
|
+
let capacity = 10
|
|
58
|
+
})
|
|
59
|
+
let capturedService = ref("")
|
|
60
|
+
let capturedJson = ref(JSON.Null)
|
|
61
|
+
TestBus.subscribeToEvents(
|
|
62
|
+
"T",
|
|
63
|
+
async (svc, _meta, json) => {
|
|
64
|
+
capturedService := svc
|
|
65
|
+
capturedJson := json
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
let payload = JSON.parseOrThrow("{\"x\":42}")
|
|
69
|
+
let _ = await TestBus.publishEvent("T", "my-service", defaultMeta, payload)
|
|
70
|
+
expect(capturedService.contents)->toBe("my-service")
|
|
71
|
+
expect(capturedJson.contents)->toEqual(payload)
|
|
72
|
+
},
|
|
73
|
+
)
|
|
53
74
|
})
|
|
54
75
|
|
|
55
76
|
describe("fan-out", () => {
|
|
56
|
-
testPromise(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
77
|
+
testPromise(
|
|
78
|
+
"delivers to all subscribers on the same topic",
|
|
79
|
+
async () => {
|
|
80
|
+
module TestBus = LocalBus.MakeBounded({
|
|
81
|
+
let capacity = 10
|
|
82
|
+
})
|
|
83
|
+
let count = ref(0)
|
|
84
|
+
TestBus.subscribeToEvents(
|
|
85
|
+
"T",
|
|
86
|
+
async (_, _, _) => {
|
|
87
|
+
count := count.contents + 1
|
|
88
|
+
},
|
|
89
|
+
)
|
|
90
|
+
TestBus.subscribeToEvents(
|
|
91
|
+
"T",
|
|
92
|
+
async (_, _, _) => {
|
|
93
|
+
count := count.contents + 1
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
TestBus.subscribeToEvents(
|
|
97
|
+
"T",
|
|
98
|
+
async (_, _, _) => {
|
|
99
|
+
count := count.contents + 1
|
|
100
|
+
},
|
|
101
|
+
)
|
|
102
|
+
let _ = await TestBus.publishEvent("T", "svc", defaultMeta, JSON.Null)
|
|
103
|
+
expect(count.contents)->toBe(3)
|
|
104
|
+
},
|
|
105
|
+
)
|
|
71
106
|
|
|
72
|
-
testPromise(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
107
|
+
testPromise(
|
|
108
|
+
"topic isolation preserved in bounded mode",
|
|
109
|
+
async () => {
|
|
110
|
+
module TestBus = LocalBus.MakeBounded({
|
|
111
|
+
let capacity = 10
|
|
112
|
+
})
|
|
113
|
+
let countA = ref(0)
|
|
114
|
+
let countB = ref(0)
|
|
115
|
+
TestBus.subscribeToEvents(
|
|
116
|
+
"topic-a",
|
|
117
|
+
async (_, _, _) => {
|
|
118
|
+
countA := countA.contents + 1
|
|
119
|
+
},
|
|
120
|
+
)
|
|
121
|
+
TestBus.subscribeToEvents(
|
|
122
|
+
"topic-b",
|
|
123
|
+
async (_, _, _) => {
|
|
124
|
+
countB := countB.contents + 1
|
|
125
|
+
},
|
|
126
|
+
)
|
|
127
|
+
let _ = await TestBus.publishEvent("topic-a", "svc", defaultMeta, JSON.Null)
|
|
128
|
+
expect(countA.contents)->toBe(1)
|
|
129
|
+
expect(countB.contents)->toBe(0)
|
|
130
|
+
},
|
|
131
|
+
)
|
|
86
132
|
})
|
|
87
133
|
|
|
88
134
|
describe("backpressure", () => {
|
|
89
|
-
testPromise(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
let
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
135
|
+
testPromise(
|
|
136
|
+
"publisher suspends when subscriber queue is full (capacity=1)",
|
|
137
|
+
async () => {
|
|
138
|
+
// capacity=1: subscriber queue can buffer 1 message while the subscriber is processing.
|
|
139
|
+
// Timeline:
|
|
140
|
+
// p1 publish: drain fiber D has a pending Queue.take → msg1 goes to D directly
|
|
141
|
+
// p2 publish: no pending take (D is processing msg1, blocked on gate) → msg2 buffered
|
|
142
|
+
// p3 publish: queue is full (msg2 there, capacity=1) → F3 SUSPENDS
|
|
143
|
+
// After opening gate: msg1 processed → done_1 → D takes msg2 → F3 wakes, delivers msg3
|
|
144
|
+
module TestBus = LocalBus.MakeBounded({
|
|
145
|
+
let capacity = 1
|
|
146
|
+
})
|
|
147
|
+
let processedCount = ref(0)
|
|
148
|
+
// Gate Deferred: subscriber blocks until explicitly opened
|
|
149
|
+
let gate: Deferred.t<unit, unit> = Deferred.make()->Effect.runSync
|
|
150
|
+
TestBus.subscribeToEvents(
|
|
151
|
+
"T",
|
|
152
|
+
async (_, _, _) => {
|
|
153
|
+
let _ = await Deferred.await_(gate)->Effect.runPromise
|
|
154
|
+
processedCount := processedCount.contents + 1
|
|
155
|
+
},
|
|
156
|
+
)
|
|
157
|
+
// Start 3 publishes without awaiting.
|
|
158
|
+
// p1: D's pending Queue.take resolves with msg1; D starts processing (blocks on gate)
|
|
159
|
+
// p2: msg2 buffered in queue (capacity=1, fits since D already took msg1)
|
|
160
|
+
// p3: queue full → F3 fiber suspends inside PubSub.publish
|
|
161
|
+
let p1 = TestBus.publishEvent("T", "svc", defaultMeta, JSON.Null)
|
|
162
|
+
let p2 = TestBus.publishEvent("T", "svc", defaultMeta, JSON.Null)
|
|
163
|
+
let p3 = TestBus.publishEvent("T", "svc", defaultMeta, JSON.Null)
|
|
164
|
+
// Tick 1: drain fiber D runs, takes msg1, calls handler → handler blocks on gate
|
|
165
|
+
let _ = await Promise.resolve()
|
|
166
|
+
// Gate not open yet — no messages fully processed
|
|
167
|
+
expect(processedCount.contents)->toBe(0)
|
|
168
|
+
// Open gate: subscriber can now process all pending messages
|
|
169
|
+
let _ = Deferred.succeed(gate, ())->Effect.runSync
|
|
170
|
+
// Await each publish promise; by the time all resolve, all 3 handlers have run
|
|
171
|
+
let _ = await p1
|
|
172
|
+
let _ = await p2
|
|
173
|
+
let _ = await p3
|
|
174
|
+
expect(processedCount.contents)->toBe(3)
|
|
175
|
+
},
|
|
176
|
+
)
|
|
123
177
|
})
|
|
124
178
|
|
|
125
179
|
describe("timing", () => {
|
|
126
|
-
testPromise(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
180
|
+
testPromise(
|
|
181
|
+
"bounded mode completes within 3 microtask ticks",
|
|
182
|
+
async () => {
|
|
183
|
+
module TestBus = LocalBus.MakeBounded({
|
|
184
|
+
let capacity = 10
|
|
185
|
+
})
|
|
186
|
+
let delivered = ref(false)
|
|
187
|
+
TestBus.subscribeToEvents(
|
|
188
|
+
"T",
|
|
189
|
+
async (_, _, _) => {
|
|
190
|
+
delivered := true
|
|
191
|
+
},
|
|
192
|
+
)
|
|
193
|
+
let pubPromise = TestBus.publishEvent("T", "svc", defaultMeta, JSON.Null)
|
|
194
|
+
// Not delivered synchronously
|
|
195
|
+
expect(delivered.contents)->toBe(false)
|
|
196
|
+
let _ = await Promise.resolve() // tick 1
|
|
197
|
+
let _ = await Promise.resolve() // tick 2
|
|
198
|
+
let _ = await Promise.resolve() // tick 3 (bounded adds 1 vs unbounded's 2)
|
|
199
|
+
let _ = await pubPromise
|
|
200
|
+
expect(delivered.contents)->toBe(true)
|
|
201
|
+
},
|
|
202
|
+
)
|
|
141
203
|
})
|
|
142
204
|
|
|
143
205
|
describe("reset", () => {
|
|
144
|
-
testPromise(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
206
|
+
testPromise(
|
|
207
|
+
"reset shuts down bounded hubs cleanly",
|
|
208
|
+
async () => {
|
|
209
|
+
module TestBus = LocalBus.MakeBounded({
|
|
210
|
+
let capacity = 5
|
|
211
|
+
})
|
|
212
|
+
let count = ref(0)
|
|
213
|
+
TestBus.subscribeToEvents(
|
|
214
|
+
"T",
|
|
215
|
+
async (_, _, _) => {
|
|
216
|
+
count := count.contents + 1
|
|
217
|
+
},
|
|
218
|
+
)
|
|
219
|
+
let _ = await TestBus.publishEvent("T", "svc", defaultMeta, JSON.Null)
|
|
220
|
+
expect(count.contents)->toBe(1)
|
|
221
|
+
// reset should not throw or hang
|
|
222
|
+
TestBus.reset()
|
|
223
|
+
expect(count.contents)->toBe(1) // count unchanged by reset itself
|
|
224
|
+
},
|
|
225
|
+
)
|
|
156
226
|
})
|
|
157
227
|
})
|