@reventlessdev/reventless-core 3.0.0-alpha.260 → 3.0.0-alpha.261
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 +9 -9
- package/src/Behavior.res +1 -4
- package/src/Message.res +17 -14
- package/src/Projection.res +51 -25
- package/src/RequestContext.res +8 -9
- package/src/ResourceAttribution.res +28 -14
- package/src/adapter/Adapter.res +48 -4
- package/src/adapter/EventLogProvisioning/EventLogProvisioning.res +5 -5
- package/src/adapter/Messaging/Messaging_Log_Backend.res +10 -7
- package/src/adapter/Messaging/Messaging_Log_Backend.res.mjs +2 -4
- package/src/adapter/Monitoring/Monitoring.res +16 -17
- package/src/adapter/QueryInterception/QueryInterception.res +0 -1
- package/src/adapter/Runtime/AggregateRuntime_Builder_Common.res +12 -5
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +11 -7
- package/src/adapter/Runtime/PluginRuntime_Builder_Micro.res +1 -7
- package/src/adapter/Runtime/PluginRuntime_Builder_Single.res +1 -7
- package/src/adapter/RuntimeExtension/RuntimeExtension.res +0 -1
- package/src/admin/Platform_Admin.res +17 -6
- package/src/admin/Platform_AdminApi.res +14 -9
- package/src/admin/Platform_Admin_Callback.res +1 -1
- package/src/admin/Platform_BakedManifest.res +34 -34
- package/src/admin/Platform_ComponentDefinitionsApi.res +9 -21
- package/src/admin/Platform_PluginStructuresApi.res +15 -12
- package/src/admin/UiFragmentRegistry/StateChange/UiFragmentRegistry.res +1 -1
- package/src/components/Aggregate/Aggregate.res +13 -12
- package/src/components/Aggregate/Aggregate_Builder.res +22 -21
- package/src/components/Aggregate/Aggregate_Callback.res +91 -86
- package/src/components/Api/ApiNoApiHelpers.res +1 -3
- package/src/components/Api/Api_Ids.res +0 -1
- package/src/components/Api/Api_Naming.res +41 -18
- package/src/components/Api/GraphQL_FragmentGenerator.res +38 -51
- package/src/components/Api/GraphQL_SchemaInspector.res +5 -3
- package/src/components/Api/GraphQL_Stitcher.res +9 -10
- package/src/components/Api/MCP_SchemaGenerator.res +8 -12
- package/src/components/Api/OwnerScopeDiagnostics.res +1 -2
- package/src/components/Api/QueryDbListQuery.res +13 -6
- package/src/components/Api/SchemaType.res +28 -23
- package/src/components/Api/SuryToJsonSchema.res +9 -10
- package/src/components/AutomationSlice/AutomationSlice.res +4 -4
- package/src/components/AutomationSlice/AutomationSlice_Builder.res +15 -14
- package/src/components/AutomationSlice/AutomationSlice_Callback.res +3 -3
- package/src/components/Builder_Helpers.res +12 -23
- package/src/components/CommandGenerator/CommandGenerator.res +6 -1
- package/src/components/CommandGenerator/CommandGenerator_Builder.res +29 -21
- package/src/components/CommandGenerator/CommandGenerator_Callback.res +17 -21
- package/src/components/CommandTopic/CommandTopic.res +3 -3
- package/src/components/CommandTopic/CommandTopic_Builder.res +10 -10
- package/src/components/CommandTopic/CommandTopic_Callback.res +1 -4
- package/src/components/CommandTopic/CommandTopic_Helpers.res +1 -2
- package/src/components/Counter/Counter_Builder.res +1 -6
- package/src/components/Counter/Counter_Callback.res +3 -3
- package/src/components/Dcb/Dcb_Builder.res +970 -956
- package/src/components/DcbEventLog/DcbEventLog.res +4 -4
- package/src/components/DcbEventLog/DcbEventLog_Builder.res +0 -1
- package/src/components/DcbEventLog/DcbEventLog_Operations.res +17 -18
- package/src/components/EventCollector/EventCollector_Builder.res +2 -1
- package/src/components/EventLog/EventLog.res +5 -1
- package/src/components/EventLog/EventLog_Operations.res +12 -8
- package/src/components/EventMapper/EventMapper.res +8 -10
- package/src/components/EventMapper/EventMapper_Builder.res +4 -1
- package/src/components/EventMapper/EventMapper_Callback.res +46 -31
- package/src/components/EventTopic/EventTopic.res +3 -3
- package/src/components/EventTopic/EventTopic_Builder.res +1 -1
- package/src/components/Extension/Extension.res +4 -1
- package/src/components/Extension/Extension_Operations.res +11 -3
- package/src/components/ExtensionPoint/ExtensionPoint.res +3 -3
- package/src/components/ExtensionPoint/ExtensionPoint_Builder.res +9 -10
- package/src/components/ExtensionPoint/ExtensionPoint_Callback.res +2 -6
- package/src/components/ExtensionPoint/ExtensionPoint_Operations.res +13 -5
- package/src/components/Heartbeat/Heartbeat_Callback.res +3 -3
- package/src/components/InboundTranslationSlice/InboundTranslationSlice.res +4 -4
- package/src/components/InboundTranslationSlice/InboundTranslationSlice_Builder.res +7 -10
- package/src/components/OutboundTranslationSlice/OutboundTranslationSlice.res +4 -4
- package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Builder.res +15 -14
- package/src/components/QueryDb/QueryDb_Adapter.res +1 -3
- package/src/components/QueryDb/QueryDb_Operations.res +10 -10
- package/src/components/ReadModel/ReadModel.res +3 -3
- package/src/components/ReadModel/ReadModel_Builder.res +1 -2
- package/src/components/ReadModel/ReadModel_Callback.res +2 -1
- package/src/components/SideEffectHandler/SideEffectHandler_Callback.res +5 -2
- package/src/components/StateChangeSlice/StateChangeSlice.res +3 -3
- package/src/components/StateChangeSlice/StateChangeSlice_Builder.res +23 -4
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +22 -6
- package/src/components/StateTopic.res +1 -3
- package/src/components/StateViewSlice/StateViewSlice.res +4 -4
- package/src/components/StateViewSlice/StateViewSlice_Builder.res +79 -68
- package/src/components/StateViewSlice/StateViewSlice_Callback.res +3 -1
- package/src/components/Task/Task.res +3 -3
- package/src/components/Task/Task_Adapter.res +4 -1
- package/src/components/Task/Task_Builder.res +24 -21
- package/src/plugin/api/PluginBaseFragment.res +9 -3
- package/src/plugin/component/Capability_Inference.res +8 -10
- package/src/plugin/component/Plugin.res +6 -6
- package/src/plugin/component/Plugin_Builder.res +54 -50
- package/src/plugin/component/Plugin_Callback.res +10 -6
- package/src/plugin/component/Plugin_Helpers.res +264 -230
- package/src/plugin/component/Plugin_Structure.res +370 -324
- package/src/plugin/component/Plugin_SubscriptionSchema.res +8 -6
- package/src/plugin/component/SchemaWalker.res +11 -12
- package/src/plugin/connect/PluginConnectExtension_Mapping.res +45 -50
- package/src/plugin/lifecycle/PluginBehavior.res +3 -4
- package/src/plugin/lifecycle/PluginSpec.res +1 -2
- package/src/plugin/lifecycle/PluginsProjection.res +16 -5
- package/src/plugin/lifecycle/PluginsReadModelSpec.res +0 -2
- package/src/util/CommandPublisher.res +9 -2
- package/src/util/CsvStream.res +3 -3
- package/src/util/Interstack.res +7 -15
- package/src/util/Interstack.resi +3 -3
- package/src/util/LogFormat.res +5 -9
- package/src/util/Logger.res +1 -2
- package/src/util/Lru.res +2 -1
- package/src/util/Lru.resi +2 -3
- package/src/util/ScheduleOps.res +24 -27
- package/src/util/StorePrefixCollision.res +6 -9
- package/src/util/Util_Promise.res +4 -1
- package/src/util/Util_Promise.res.mjs +1 -1
- package/tests/IdentityTest.res +166 -113
- package/tests/ResourceAttributionTest.res +28 -25
- package/tests/StorePrefixCollisionTest.res +2 -1
- package/tests/TaskBucketNamingTest.res +3 -2
- package/tests/adapter/EventLogProvisioningTest.res +51 -48
- package/tests/adapter/MonitoringTest.res +81 -38
- package/tests/adapter/QueryInterceptionTest.res +2 -2
- package/tests/adapter/RefusalVocabularyTest.res +31 -23
- package/tests/adapter/RuntimeExtensionHookReachTest.res +9 -8
- package/tests/adapter/RuntimeExtensionTest.res +5 -3
- package/tests/admin/AdminApiSchemaDriftTest.res +22 -16
- package/tests/admin/Platform_Admin_StructureTest.res +10 -11
- package/tests/admin/Platform_BakedManifestTest.res +31 -35
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +18 -15
- package/tests/admin/Platform_PluginStructuresApiTest.res +5 -3
- package/tests/admin/Platform_UIFragmentsApiTest.res +6 -3
- package/tests/aggregate/AggregateCacheTest.res +10 -12
- package/tests/aggregate/AggregateCallbackTest.res +75 -46
- package/tests/aggregate/AggregateCausationTest.res +19 -20
- package/tests/aggregate/AggregateConflictTest.res +41 -19
- package/tests/aggregate/AggregateFixtures.res +15 -5
- package/tests/aggregate/AggregateRejectionTest.res +97 -80
- package/tests/aggregate/AggregateSnapshotTest.res +44 -16
- package/tests/api/Api_IdsTest.res +10 -4
- package/tests/api/BrandedMarkerCompositionTest.res +48 -36
- package/tests/api/GeolocationSemanticTest.res +3 -3
- package/tests/api/GraphQL_FragmentGeneratorTest.res +66 -54
- package/tests/api/OwnerScopeDiagnosticsTest.res +31 -27
- package/tests/api/StorageRefFieldsTest.res +30 -18
- package/tests/api/SuryToJsonSchemaTest.res +1315 -1014
- package/tests/api/TaggedUnionSchemaTest.res +17 -24
- package/tests/commandgenerator/CommandGeneratorCallbackTest.res +235 -179
- package/tests/commandgenerator/CommandGeneratorFixtures.res +8 -13
- package/tests/commandgenerator/OwnerStampingTest.res +91 -76
- package/tests/commandtopic/CommandOutcomeHookTest.res +198 -155
- package/tests/commandtopic/CommandTopicCallbackFixtures.res +5 -4
- package/tests/commandtopic/CommandTopicCallbackTest.res +61 -43
- package/tests/commandtopic/CommandTopicHelpersRejectionTest.res +70 -55
- package/tests/counter/CounterCallbackTest.res +40 -34
- package/tests/counter/CounterFixtures.res +4 -4
- package/tests/dcb/DcbDecodeTest.res +115 -84
- package/tests/dcb/DcbEventLogOperationsTest.res +194 -143
- package/tests/dcb/DcbEventLogSchemaTest.res +13 -13
- package/tests/dcb/DcbFixtures.res +4 -5
- package/tests/dcb/DcbScopeInferenceTest.res +173 -114
- package/tests/dcb/DcbStateChangeSliceTest.res +391 -262
- package/tests/dcb/DcbTagTest.res +22 -11
- package/tests/dcb/DcbValidationTest.res +288 -218
- package/tests/eventlog/EventLogFixtures.res +4 -4
- package/tests/eventlog/EventLogOperationsTest.res +102 -67
- package/tests/eventlog/EventLogRetryTest.res +104 -65
- package/tests/eventmapper/EventMapperCallbackTest.res +118 -83
- package/tests/eventmapper/EventMapperFixtures.res +15 -23
- package/tests/eventmapper/EventMapperTest.res +54 -38
- package/tests/eventtopic/EventTopicFixtures.res +3 -4
- package/tests/eventtopic/EventTopicOperationsTest.res +41 -29
- package/tests/extensionpoint/ExtensionPointCallbackTest.res +58 -45
- package/tests/extensionpoint/ExtensionPointFixtures.res +16 -20
- package/tests/extensionpoint/ExtensionPointOperationsTest.res +70 -49
- package/tests/extensionpoint/PluginDisconnectScheduleTest.res +2 -1
- package/tests/logger/LogFormatTest.res +104 -82
- package/tests/logger/LoggerLazyTest.res +21 -12
- package/tests/message/MessageTest.res +96 -93
- package/tests/message/MetaEnvelopeTest.res +26 -30
- package/tests/plugin/HeartbeatDisconnectGraceTest.res +31 -25
- package/tests/plugin/ManifestVisibilityTest.res +20 -12
- package/tests/plugin/PluginDefinitionScalars.res +1 -2
- package/tests/plugin/PluginLifecycleCorpusTest.res +15 -12
- package/tests/plugin/PluginSpecExperiment.res +0 -1
- package/tests/plugin/PluginStructureTest.res +1529 -1045
- package/tests/plugin/Plugin_EventQuerySchemaTest.res +21 -10
- package/tests/plugin/SchemaWalkerTest.res +116 -70
- package/tests/plugin/StateChange/PsReserveStock.res +4 -4
- package/tests/plugin/StateView/PsAnnotatedView.res +4 -4
- package/tests/plugin/StateView/PsCustomersView.res +4 -4
- package/tests/plugin/StateView/PsShipmentsView.res +3 -2
- package/tests/projection/LifecycleTrailTest.res +1 -4
- package/tests/projection/ProjectionOptimizeTest.res +13 -9
- package/tests/querydb/QueryDbFixtures.res +0 -1
- package/tests/querydb/QueryDbOperationsTest.res +99 -64
- package/tests/querydb/TaggedUnionStampTest.res +10 -8
- package/tests/sideeffecthandler/SideEffectHandlerCallbackTest.res +50 -35
- package/tests/sideeffecthandler/SideEffectHandlerFixtures.res +4 -10
- package/tests/util/CommandPublisherTest.res +0 -1
- package/tests/util/CsvStreamTest.res +3 -3
- package/tests/util/LruTest.res +84 -56
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
open JestGlobals
|
|
2
2
|
open LogFormat
|
|
3
3
|
|
|
4
|
-
|
|
5
4
|
// Sink detection (Reventless.AnsiStyle) defaults a non-TTY stdout — Jest
|
|
6
5
|
// included — to JSON, which no-ops `bold`/`fmtComp`. The human-format
|
|
7
6
|
// assertions below expect ANSI, so force text mode for this file. The
|
|
@@ -141,34 +140,47 @@ describe("LogFormat", () => {
|
|
|
141
140
|
type command = AddProduct({productId: string, name: string, imageUrl?: string})
|
|
142
141
|
}
|
|
143
142
|
|
|
144
|
-
testSync(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
143
|
+
testSync(
|
|
144
|
+
"omits an absent optional field",
|
|
145
|
+
() => {
|
|
146
|
+
let json =
|
|
147
|
+
OptionalFieldCmd.AddProduct({productId: "prd-1", name: "Cirrus"})->Message.encode(
|
|
148
|
+
OptionalFieldCmd.commandSchema,
|
|
149
|
+
)
|
|
150
|
+
expect(variantFields(json))->toEqual(`, {productId:"prd-1",name:"Cirrus"}`)
|
|
151
|
+
},
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
testSync(
|
|
155
|
+
"keeps a present optional field",
|
|
156
|
+
() => {
|
|
157
|
+
let json = OptionalFieldCmd.AddProduct({
|
|
158
|
+
productId: "prd-1",
|
|
159
|
+
name: "Cirrus",
|
|
160
|
+
imageUrl: "/u/a.png",
|
|
161
|
+
})->Message.encode(OptionalFieldCmd.commandSchema)
|
|
162
|
+
expect(
|
|
163
|
+
variantFields(json),
|
|
164
|
+
)->toEqual(`, {productId:"prd-1",name:"Cirrus",imageUrl:"/u/a.png"}`)
|
|
165
|
+
},
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
testSync(
|
|
169
|
+
"elides a value past the budget rather than dropping it",
|
|
170
|
+
() => {
|
|
171
|
+
let long = String.repeat("x", maxValueChars + 20)
|
|
172
|
+
let json =
|
|
173
|
+
OptionalFieldCmd.AddProduct({productId: "prd-1", name: long})->Message.encode(
|
|
174
|
+
OptionalFieldCmd.commandSchema,
|
|
175
|
+
)
|
|
176
|
+
let rendered = variantFields(json)
|
|
177
|
+
expect(rendered->String.includes("…(+22 chars)"))->toBe(true)
|
|
178
|
+
// The oversized value is capped, not the line: the neighbouring field that
|
|
179
|
+
// says which product this is stays intact.
|
|
180
|
+
expect(rendered->String.includes(long))->toBe(false)
|
|
181
|
+
expect(rendered->String.includes(`productId:"prd-1"`))->toBe(true)
|
|
182
|
+
},
|
|
183
|
+
)
|
|
172
184
|
})
|
|
173
185
|
})
|
|
174
186
|
|
|
@@ -207,8 +219,7 @@ let validLevels = ["DEBUG", "INFO", "WARN", "ERROR"]
|
|
|
207
219
|
// True when a captured line is a clean JSON log record: parses as JSON, carries
|
|
208
220
|
// no ANSI escape anywhere, has a string `message` and a known `level`.
|
|
209
221
|
let isCleanRecord = (line: string): bool =>
|
|
210
|
-
!hasAnsi(line) &&
|
|
211
|
-
(try {
|
|
222
|
+
!hasAnsi(line) && try {
|
|
212
223
|
switch line->JSON.parseOrThrow->JSON.Decode.object {
|
|
213
224
|
| Some(obj) =>
|
|
214
225
|
let levelOk =
|
|
@@ -222,7 +233,7 @@ let isCleanRecord = (line: string): bool =>
|
|
|
222
233
|
}
|
|
223
234
|
} catch {
|
|
224
235
|
| _ => false
|
|
225
|
-
}
|
|
236
|
+
}
|
|
226
237
|
|
|
227
238
|
// Read a top-level string field off a captured JSON record (None if absent).
|
|
228
239
|
let fieldOf = (line: string, key: string): option<string> =>
|
|
@@ -269,8 +280,8 @@ describe("JSON sink", () => {
|
|
|
269
280
|
// Event-collector comps carry the component *resource* name (`<Spec><Kind>`),
|
|
270
281
|
// while the registry holds the bare spec name — the prefix candidate bridges them.
|
|
271
282
|
Logger.registerComponentPlugin(~componentName="Products", ~pluginName="Catalog")
|
|
272
|
-
let lines = captureLogs(
|
|
273
|
-
log.info(~comp="EventCollector(ProductsReadModel)", "projecting event")
|
|
283
|
+
let lines = captureLogs(
|
|
284
|
+
() => log.info(~comp="EventCollector(ProductsReadModel)", "projecting event"),
|
|
274
285
|
)
|
|
275
286
|
let line = lines->Array.length == 1 ? lines->Array.getUnsafe(0) : ""
|
|
276
287
|
expect(lines->Array.length == 1 && line->fieldOf("plugin") == Some("Catalog"))->toBe(true)
|
|
@@ -280,8 +291,8 @@ describe("JSON sink", () => {
|
|
|
280
291
|
// `Order` (Ordering) must not win over `OrderLines` (Billing) for `OrderLinesReadModel`.
|
|
281
292
|
Logger.registerComponentPlugin(~componentName="Order", ~pluginName="Ordering")
|
|
282
293
|
Logger.registerComponentPlugin(~componentName="OrderLines", ~pluginName="Billing")
|
|
283
|
-
let lines = captureLogs(
|
|
284
|
-
log.info(~comp="EventCollector(OrderLinesReadModel)", "projecting event")
|
|
294
|
+
let lines = captureLogs(
|
|
295
|
+
() => log.info(~comp="EventCollector(OrderLinesReadModel)", "projecting event"),
|
|
285
296
|
)
|
|
286
297
|
let line = lines->Array.length == 1 ? lines->Array.getUnsafe(0) : ""
|
|
287
298
|
expect(lines->Array.length == 1 && line->fieldOf("plugin") == Some("Billing"))->toBe(true)
|
|
@@ -290,14 +301,15 @@ describe("JSON sink", () => {
|
|
|
290
301
|
testSync("dispatch annotation carries comp onto a handler line that passes none", () => {
|
|
291
302
|
// What makes two collectors in one runtime process separable: the element's comp
|
|
292
303
|
// comes from the dispatch boundary, not from the application's own log call.
|
|
293
|
-
let lines = captureLogs(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
304
|
+
let lines = captureLogs(
|
|
305
|
+
() =>
|
|
306
|
+
EffectLogger.logInfo("projected")
|
|
307
|
+
->Runtime.annotateInvocation(
|
|
308
|
+
~correlationId="cid-1",
|
|
309
|
+
~causationId="parent-1",
|
|
310
|
+
~comp="EventCollector(ProductsReadModel)",
|
|
311
|
+
)
|
|
312
|
+
->Effect.runSync,
|
|
301
313
|
)
|
|
302
314
|
let line = lines->Array.length >= 1 ? lines->Array.getUnsafe(0) : ""
|
|
303
315
|
let ok =
|
|
@@ -314,52 +326,56 @@ describe("JSON sink", () => {
|
|
|
314
326
|
})
|
|
315
327
|
|
|
316
328
|
testSync("Logger.error with ~data emits one clean JSON record", () => {
|
|
317
|
-
let lines = captureLogs(
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
329
|
+
let lines = captureLogs(
|
|
330
|
+
() =>
|
|
331
|
+
log.error(
|
|
332
|
+
~comp="Util_AppSync_Caller",
|
|
333
|
+
~data=JSON.parseOrThrow(`{"errors":["boom"]}`),
|
|
334
|
+
"query errors",
|
|
335
|
+
),
|
|
323
336
|
)
|
|
324
337
|
expect(lines->Array.length == 1 && isCleanRecord(lines->Array.getUnsafe(0)))->toBe(true)
|
|
325
338
|
})
|
|
326
339
|
|
|
327
340
|
testSync("EffectLogger.logInfo emits clean JSON through install()", () => {
|
|
328
|
-
let lines = captureLogs(
|
|
329
|
-
EffectLogger.logInfo(~comp="Aggregate(Product)", "replay done")->Effect.runSync
|
|
341
|
+
let lines = captureLogs(
|
|
342
|
+
() => EffectLogger.logInfo(~comp="Aggregate(Product)", "replay done")->Effect.runSync,
|
|
330
343
|
)
|
|
331
344
|
let allClean = lines->Array.filter(l => !isCleanRecord(l))->Array.length == 0
|
|
332
345
|
expect(lines->Array.length >= 1 && allClean)->toBe(true)
|
|
333
346
|
})
|
|
334
347
|
|
|
335
348
|
testSync("EffectLogger.logInfo carries ~comp + ~detail as structured fields (no \\x00)", () => {
|
|
336
|
-
let lines = captureLogs(
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
349
|
+
let lines = captureLogs(
|
|
350
|
+
() =>
|
|
351
|
+
EffectLogger.logInfo(
|
|
352
|
+
~comp="Aggregate(Product)",
|
|
353
|
+
~detail=JSON.parseOrThrow(`{"id":"p-1"}`),
|
|
354
|
+
"added",
|
|
355
|
+
)->Effect.runSync,
|
|
342
356
|
)
|
|
343
|
-
let ok = lines->Array.some(
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
357
|
+
let ok = lines->Array.some(
|
|
358
|
+
line =>
|
|
359
|
+
switch line->JSON.parseOrThrow->JSON.Decode.object {
|
|
360
|
+
| Some(obj) =>
|
|
361
|
+
obj->Dict.get("comp")->Option.flatMap(JSON.Decode.string) == Some("Aggregate(Product)") &&
|
|
362
|
+
obj
|
|
363
|
+
->Dict.get("detail")
|
|
364
|
+
->Option.flatMap(JSON.Decode.object)
|
|
365
|
+
->Option.flatMap(d => d->Dict.get("id"))
|
|
366
|
+
->Option.flatMap(JSON.Decode.string) == Some("p-1")
|
|
367
|
+
| None => false
|
|
368
|
+
},
|
|
354
369
|
)
|
|
355
370
|
expect(ok)->toBe(true)
|
|
356
371
|
})
|
|
357
372
|
|
|
358
373
|
testSync("Effect.annotateLogs(correlationId) surfaces as a top-level field", () => {
|
|
359
|
-
let lines = captureLogs(
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
374
|
+
let lines = captureLogs(
|
|
375
|
+
() =>
|
|
376
|
+
EffectLogger.logInfo(~comp="Aggregate(Product)", "handling")
|
|
377
|
+
->Effect.annotateLogs("correlationId", "c-123")
|
|
378
|
+
->Effect.runSync,
|
|
363
379
|
)
|
|
364
380
|
expect(lines->Array.some(line => line->fieldOf("correlationId") == Some("c-123")))->toBe(true)
|
|
365
381
|
})
|
|
@@ -368,10 +384,11 @@ describe("JSON sink", () => {
|
|
|
368
384
|
// The component is NOT registered against any plugin; without an explicit
|
|
369
385
|
// ~plugin annotation, resolvePlugin would fall back through transformations
|
|
370
386
|
// and emit no plugin field. With the annotation, plugin wins.
|
|
371
|
-
let lines = captureLogs(
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
387
|
+
let lines = captureLogs(
|
|
388
|
+
() =>
|
|
389
|
+
EffectLogger.logInfo(~comp="Aggregate(Unregistered)", "handling")
|
|
390
|
+
->Effect.annotateLogs("plugin", "Ordering")
|
|
391
|
+
->Effect.runSync,
|
|
375
392
|
)
|
|
376
393
|
expect(lines->Array.some(line => line->fieldOf("plugin") == Some("Ordering")))->toBe(true)
|
|
377
394
|
})
|
|
@@ -398,14 +415,17 @@ describe("JSON sink", () => {
|
|
|
398
415
|
let detail = Dict.fromArray([("blob", JSON.Encode.string(big))])->JSON.Encode.object
|
|
399
416
|
// ~detail flows through emit (Logger.t's ~data goes into the message instead).
|
|
400
417
|
let direct = captureLogs(() => Logger.emit(~level=Info, ~comp="Platform", ~detail, "big"))
|
|
401
|
-
let ok = switch direct
|
|
418
|
+
let ok = switch direct
|
|
419
|
+
->Array.get(0)
|
|
420
|
+
->Option.flatMap(l => l->JSON.parseOrThrow->JSON.Decode.object) {
|
|
402
421
|
| Some(obj) =>
|
|
403
422
|
switch obj->Dict.get("detail")->Option.flatMap(JSON.Decode.object) {
|
|
404
423
|
| Some(d) =>
|
|
405
424
|
d->Dict.get("truncated")->Option.flatMap(JSON.Decode.bool) == Some(true) &&
|
|
406
|
-
d
|
|
407
|
-
|
|
408
|
-
)
|
|
425
|
+
d
|
|
426
|
+
->Dict.get("preview")
|
|
427
|
+
->Option.flatMap(JSON.Decode.string)
|
|
428
|
+
->Option.mapOr(false, p => p->String.length <= 512)
|
|
409
429
|
| None => false
|
|
410
430
|
}
|
|
411
431
|
| None => false
|
|
@@ -425,7 +445,9 @@ describe("JSON sink", () => {
|
|
|
425
445
|
let cmd: Message.commandJson = {
|
|
426
446
|
Message.id: "0",
|
|
427
447
|
meta,
|
|
428
|
-
commandJson: (Heartbeat("0.0.0"): PluginSpec.command)->Message.encode(
|
|
448
|
+
commandJson: (Heartbeat("0.0.0"): PluginSpec.command)->Message.encode(
|
|
449
|
+
PluginSpec.commandSchema,
|
|
450
|
+
),
|
|
429
451
|
}
|
|
430
452
|
let name = cmd->cmdName
|
|
431
453
|
expect(!hasAnsi(name) && name == "Heartbeat")->toBe(true)
|
|
@@ -8,29 +8,38 @@ describe("Logger.debugLazy", () => {
|
|
|
8
8
|
testSync("does NOT invoke the thunk at the default Info level", () => {
|
|
9
9
|
let called = ref(false)
|
|
10
10
|
let log = Logger.makeLogger(~minLevel=Logger.Info)
|
|
11
|
-
log.debugLazy(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
log.debugLazy(
|
|
12
|
+
~comp="Test",
|
|
13
|
+
() => {
|
|
14
|
+
called := true
|
|
15
|
+
"should never be built"
|
|
16
|
+
},
|
|
17
|
+
)
|
|
15
18
|
expect(called.contents)->toEqual(false)
|
|
16
19
|
})
|
|
17
20
|
|
|
18
21
|
testSync("invokes the thunk when debug output is enabled", () => {
|
|
19
22
|
let called = ref(false)
|
|
20
23
|
let log = Logger.makeLogger(~minLevel=Logger.Debug)
|
|
21
|
-
log.debugLazy(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
log.debugLazy(
|
|
25
|
+
~comp="Test",
|
|
26
|
+
() => {
|
|
27
|
+
called := true
|
|
28
|
+
"built"
|
|
29
|
+
},
|
|
30
|
+
)
|
|
25
31
|
expect(called.contents)->toEqual(true)
|
|
26
32
|
})
|
|
27
33
|
|
|
28
34
|
testSync("the silent logger never invokes the thunk", () => {
|
|
29
35
|
let called = ref(false)
|
|
30
|
-
Logger.silent.debugLazy(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
Logger.silent.debugLazy(
|
|
37
|
+
~comp="Test",
|
|
38
|
+
() => {
|
|
39
|
+
called := true
|
|
40
|
+
"x"
|
|
41
|
+
},
|
|
42
|
+
)
|
|
34
43
|
expect(called.contents)->toEqual(false)
|
|
35
44
|
})
|
|
36
45
|
})
|
|
@@ -97,67 +97,70 @@ describe("Message should", () => {
|
|
|
97
97
|
// before later pluginStructure fields like `events`/`chapter`) existed must still
|
|
98
98
|
// decode — otherwise the one stale event bricks the whole lifecycle aggregate. See
|
|
99
99
|
// docs/plans/done/platform-infrastructure-in-plugin-list.md (durable fix option 2).
|
|
100
|
-
testSync(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
extensionProtocols: [],
|
|
110
|
-
apiSchemaFragment: None,
|
|
111
|
-
apiTarget: None,
|
|
112
|
-
structure: Some(Reventless.Offload.Inline({
|
|
113
|
-
readModels: [],
|
|
114
|
-
stateViewSlices: [],
|
|
115
|
-
stateChangeSlices: [],
|
|
116
|
-
aggregates: [
|
|
117
|
-
{
|
|
118
|
-
name: "Order",
|
|
119
|
-
commands: [],
|
|
120
|
-
producedEventTypes: [],
|
|
121
|
-
consumedEventTypes: [],
|
|
122
|
-
linkedViews: [],
|
|
123
|
-
consistencyRead: None,
|
|
124
|
-
events: [],
|
|
125
|
-
errors: [],
|
|
126
|
-
chapter: None,
|
|
127
|
-
},
|
|
128
|
-
],
|
|
129
|
-
automationSlices: [],
|
|
130
|
-
outboundTranslationSlices: [],
|
|
131
|
-
inboundTranslationSlices: [],
|
|
100
|
+
testSync(
|
|
101
|
+
"tolerantly decode a VersionConnected persisted before kind/structure fields existed",
|
|
102
|
+
() => {
|
|
103
|
+
open PluginSpec
|
|
104
|
+
let def: Reventless.Plugin.pluginDefinition = {
|
|
105
|
+
id: "id",
|
|
106
|
+
name: "Ordering",
|
|
107
|
+
version: "1.0.0",
|
|
108
|
+
extensionPoints: [],
|
|
132
109
|
extensions: [],
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
110
|
+
eventCollector: "arn",
|
|
111
|
+
extensionProtocols: [],
|
|
112
|
+
apiSchemaFragment: None,
|
|
113
|
+
apiTarget: None,
|
|
114
|
+
structure: Some(
|
|
115
|
+
Reventless.Offload.Inline({
|
|
116
|
+
readModels: [],
|
|
117
|
+
stateViewSlices: [],
|
|
118
|
+
stateChangeSlices: [],
|
|
119
|
+
aggregates: [
|
|
120
|
+
{
|
|
121
|
+
name: "Order",
|
|
122
|
+
commands: [],
|
|
123
|
+
producedEventTypes: [],
|
|
124
|
+
consumedEventTypes: [],
|
|
125
|
+
linkedViews: [],
|
|
126
|
+
consistencyRead: None,
|
|
127
|
+
events: [],
|
|
128
|
+
errors: [],
|
|
129
|
+
chapter: None,
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
automationSlices: [],
|
|
133
|
+
outboundTranslationSlices: [],
|
|
134
|
+
inboundTranslationSlices: [],
|
|
135
|
+
extensions: [],
|
|
136
|
+
extensionPoints: None,
|
|
137
|
+
requiredStores: None,
|
|
138
|
+
requiredStoreDeclarations: None,
|
|
139
|
+
requiredCapabilities: None,
|
|
140
|
+
traitDeclarations: None,
|
|
141
|
+
}),
|
|
142
|
+
),
|
|
143
|
+
dcbEventLog: None,
|
|
144
|
+
kind: Domain,
|
|
145
|
+
}
|
|
146
|
+
let event' = {
|
|
147
|
+
id: "id",
|
|
148
|
+
meta: {
|
|
149
|
+
service: "svc",
|
|
150
|
+
time: "t",
|
|
151
|
+
ip: "ip",
|
|
152
|
+
user: "u",
|
|
153
|
+
msgId: "m",
|
|
154
|
+
correlationId: "c",
|
|
155
|
+
},
|
|
156
|
+
event: VersionConnected(def),
|
|
157
|
+
}
|
|
158
|
+
let json = event'->Message.encodeEvent'(S.string, PluginSpec.eventSchema)
|
|
159
|
+
|
|
160
|
+
// Simulate the persisted JSON as written before these fields existed: drop the
|
|
161
|
+
// mandatory enum `kind`, a top-level `T | null` field, and two nested writableDef
|
|
162
|
+
// fields (a mandatory array + a `T | null`).
|
|
163
|
+
let oldJson = %raw(`function(j){
|
|
161
164
|
var c = JSON.parse(JSON.stringify(j));
|
|
162
165
|
var d = c.event._0;
|
|
163
166
|
delete d.kind;
|
|
@@ -168,35 +171,36 @@ describe("Message should", () => {
|
|
|
168
171
|
return c;
|
|
169
172
|
}`)(json)
|
|
170
173
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
174
|
+
// Strict decode must reject the stale JSON — the tolerance is what saves it.
|
|
175
|
+
let strictThrew = switch oldJson->Reventless.Util_Sury.fromJson(
|
|
176
|
+
Message.toEventSchema'(S.string, PluginSpec.eventSchema),
|
|
177
|
+
) {
|
|
178
|
+
| _ => false
|
|
179
|
+
| exception _ => true
|
|
180
|
+
}
|
|
181
|
+
expect(strictThrew)->toBe(true)
|
|
182
|
+
|
|
183
|
+
// Tolerant decode heals: kind -> Domain (first variant), missing array -> [],
|
|
184
|
+
// missing `T | null` fields -> None.
|
|
185
|
+
let decoded = oldJson->Message.decodeEvent'(S.string, PluginSpec.eventSchema)
|
|
186
|
+
switch decoded.event {
|
|
187
|
+
| VersionConnected(d) =>
|
|
188
|
+
expect(
|
|
189
|
+
switch d.kind {
|
|
190
|
+
| Domain => "Domain"
|
|
191
|
+
| _ => "other"
|
|
192
|
+
},
|
|
193
|
+
)->toBe("Domain")
|
|
194
|
+
expect(d.apiSchemaFragment->Option.isNone)->toBe(true)
|
|
195
|
+
let structure: Reventless.Plugin.pluginStructure =
|
|
196
|
+
d.structure->Option.flatMap(Reventless.Offload.getInline)->Option.getOrThrow
|
|
197
|
+
let agg = structure.aggregates->Array.getUnsafe(0)
|
|
198
|
+
expect(agg.events->Array.length)->toBe(0)
|
|
199
|
+
expect(agg.chapter->Option.isNone)->toBe(true)
|
|
200
|
+
| _ => expect("wrong-variant")->toBe("VersionConnected")
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
)
|
|
200
204
|
|
|
201
205
|
// The exact aggregate-replay path: EventLog_Operations.decodeEvent reassembles the
|
|
202
206
|
// event variant and calls `Message.decode(Spec.eventSchema)`. This is what actually
|
|
@@ -218,10 +222,9 @@ describe("Message should", () => {
|
|
|
218
222
|
kind: Domain,
|
|
219
223
|
}
|
|
220
224
|
let variantJson = VersionConnected(def)->Message.encode(PluginSpec.eventSchema)
|
|
221
|
-
let staleJson =
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
)
|
|
225
|
+
let staleJson = %raw(`function(j){ var c = JSON.parse(JSON.stringify(j)); delete c._0.kind; return c; }`)(
|
|
226
|
+
variantJson,
|
|
227
|
+
)
|
|
225
228
|
|
|
226
229
|
// Strict decode of the stale variant throws (reproduces the production brick).
|
|
227
230
|
let strictThrew = switch staleJson->Reventless.Util_Sury.fromJson(PluginSpec.eventSchema) {
|
|
@@ -129,10 +129,7 @@ describe("StoredEvent encode/decode round-trip:", () => {
|
|
|
129
129
|
position: "000000001",
|
|
130
130
|
event: "ItemCreated",
|
|
131
131
|
data: JSON.Object(
|
|
132
|
-
Dict.fromArray([
|
|
133
|
-
("itemId", JSON.String("agg-1")),
|
|
134
|
-
("name", JSON.String("Widget")),
|
|
135
|
-
]),
|
|
132
|
+
Dict.fromArray([("itemId", JSON.String("agg-1")), ("name", JSON.String("Widget"))]),
|
|
136
133
|
),
|
|
137
134
|
meta,
|
|
138
135
|
recordedAt: "2026-05-13T10:00:00.000Z",
|
|
@@ -157,10 +154,7 @@ describe("StoredEvent encode/decode round-trip:", () => {
|
|
|
157
154
|
data: JSON.Object(Dict.fromArray([("productId", JSON.String("p-1"))])),
|
|
158
155
|
meta,
|
|
159
156
|
recordedAt: "2026-05-13T10:00:00.000Z",
|
|
160
|
-
tags: [
|
|
161
|
-
{key: "productId", value: "p-1"},
|
|
162
|
-
{key: "categoryId", value: "c-1"},
|
|
163
|
-
],
|
|
157
|
+
tags: [{key: "productId", value: "p-1"}, {key: "categoryId", value: "c-1"}],
|
|
164
158
|
}
|
|
165
159
|
let flat = stored->storedEventToFlatJson(S.string)
|
|
166
160
|
let roundTrip = flat->flatJsonToStoredEvent(S.string)
|
|
@@ -175,26 +169,28 @@ describe("StoredEvent encode/decode round-trip:", () => {
|
|
|
175
169
|
expect(first.value)->toBe("p-1")
|
|
176
170
|
})
|
|
177
171
|
|
|
178
|
-
testSync(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
172
|
+
testSync(
|
|
173
|
+
"meta is flattened to top-level keys in the on-disk JSON (DynamoDB GSI projectability)",
|
|
174
|
+
() => {
|
|
175
|
+
let meta = generateMeta(~service="catalog")
|
|
176
|
+
let stored: Reventless.StoredEvent.storedEvent<string> = {
|
|
177
|
+
id: "agg-1",
|
|
178
|
+
position: "000000001",
|
|
179
|
+
event: "ItemCreated",
|
|
180
|
+
data: JSON.Object(Dict.make()),
|
|
181
|
+
meta,
|
|
182
|
+
recordedAt: "2026-05-13T10:00:00.000Z",
|
|
183
|
+
}
|
|
184
|
+
let flat = stored->storedEventToFlatJson(S.string)
|
|
185
|
+
let obj = flat->JSON.Decode.object->Option.getOrThrow
|
|
186
|
+
// meta.* should be top-level (not nested under "meta")
|
|
187
|
+
expect(obj->Dict.get("meta"))->toEqual(None)->ignore
|
|
188
|
+
expect(obj->Dict.get("service"))->toEqual(Some(JSON.String("catalog")))->ignore
|
|
189
|
+
expect(obj->Dict.get("msgId")->Option.isSome)->toBe(true)->ignore
|
|
190
|
+
// envelope fields are also top-level
|
|
191
|
+
expect(obj->Dict.get("position"))->toEqual(Some(JSON.String("000000001")))->ignore
|
|
192
|
+
expect(obj->Dict.get("event"))->toEqual(Some(JSON.String("ItemCreated")))->ignore
|
|
193
|
+
expect(obj->Dict.get("recordedAt"))->toEqual(Some(JSON.String("2026-05-13T10:00:00.000Z")))
|
|
194
|
+
},
|
|
195
|
+
)
|
|
200
196
|
})
|