@reventlessdev/reventless-aws 3.0.0-alpha.220 → 3.0.0-alpha.221

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.
Files changed (160) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/package.json +8 -8
  3. package/src/Platform.res.mjs +4 -0
  4. package/src/Platform_Stack.res +6 -0
  5. package/src/Platform_Stack.res.mjs +3 -1
  6. package/src/adapter/AWS_Tags.res +127 -12
  7. package/src/adapter/AWS_Tags.res.mjs +58 -17
  8. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +13 -1
  9. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +3 -4
  10. package/src/adapter/Api/Platform_UIFragments_Lambda.res +13 -1
  11. package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +3 -4
  12. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_AppSync.res +12 -0
  13. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_AppSync.res.mjs +4 -2
  14. package/src/adapter/CommandGenerator/InboundTranslationResolvers_AppSync.res +6 -0
  15. package/src/adapter/CommandGenerator/InboundTranslationResolvers_AppSync.res.mjs +2 -1
  16. package/src/adapter/CommandTopic/CommandTopicChannel_SQS.res +2 -2
  17. package/src/adapter/CommandTopic/CommandTopicChannel_SQS.res.mjs +2 -2
  18. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Async.res +2 -2
  19. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Async.res.mjs +2 -2
  20. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_FIFO.res +2 -2
  21. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_FIFO.res.mjs +2 -2
  22. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Sync.res +2 -2
  23. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Sync.res.mjs +2 -2
  24. package/src/adapter/Counter/CounterHandler_DynamoDbStream.res +1 -0
  25. package/src/adapter/Counter/CounterHandler_DynamoDbStream.res.mjs +1 -1
  26. package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb.res +1 -1
  27. package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb.res.mjs +1 -2
  28. package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream.res +1 -1
  29. package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream.res.mjs +1 -1
  30. package/src/adapter/EventCollector/EventCollectorChannel_SQS.res +2 -2
  31. package/src/adapter/EventCollector/EventCollectorChannel_SQS.res.mjs +2 -2
  32. package/src/adapter/EventCollector/EventCollectorChannel_SQS_FIFO.res +2 -2
  33. package/src/adapter/EventCollector/EventCollectorChannel_SQS_FIFO.res.mjs +2 -2
  34. package/src/adapter/EventLog/EventLogStorage.res +3 -3
  35. package/src/adapter/EventLog/EventLogStorage.res.mjs +3 -3
  36. package/src/adapter/EventLog/EventLogStorage_DynamoDb.res +2 -2
  37. package/src/adapter/EventLog/EventLogStorage_DynamoDb.res.mjs +2 -2
  38. package/src/adapter/EventLog/EventLogStorage_DynamoDbStream.res +2 -2
  39. package/src/adapter/EventLog/EventLogStorage_DynamoDbStream.res.mjs +2 -2
  40. package/src/adapter/EventLog/EventLogStorage_Postgres.res +1 -1
  41. package/src/adapter/EventLog/EventLogStorage_Postgres.res.mjs +1 -1
  42. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res +7 -1
  43. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res.mjs +2 -2
  44. package/src/adapter/EventTopic/EventTopicPublisher_DynamoDbStream.res +1 -1
  45. package/src/adapter/EventTopic/EventTopicPublisher_DynamoDbStream.res.mjs +1 -1
  46. package/src/adapter/EventTopic/EventTopicPublisher_SNS.res +2 -2
  47. package/src/adapter/EventTopic/EventTopicPublisher_SNS.res.mjs +2 -2
  48. package/src/adapter/EventTopic/EventTopicPublisher_SNS_FIFO.res +2 -2
  49. package/src/adapter/EventTopic/EventTopicPublisher_SNS_FIFO.res.mjs +2 -2
  50. package/src/adapter/Mcp/MCP_Lambda.res.mjs +1 -1
  51. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +7 -0
  52. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +4 -2
  53. package/src/adapter/Postgres/PgMigration_Builder.res +1 -0
  54. package/src/adapter/Postgres/PgMigration_Builder.res.mjs +1 -1
  55. package/src/adapter/QueryDb/PgQueryResolver_Builder.res +7 -0
  56. package/src/adapter/QueryDb/PgQueryResolver_Builder.res.mjs +4 -2
  57. package/src/adapter/QueryDb/QueryDbStorage.res +6 -6
  58. package/src/adapter/QueryDb/QueryDbStorage.res.mjs +6 -6
  59. package/src/adapter/QueryDb/QueryDbStorage_DynamoDb.res +2 -2
  60. package/src/adapter/QueryDb/QueryDbStorage_DynamoDb.res.mjs +2 -2
  61. package/src/adapter/QueryDb/QueryDbStorage_DynamoDbStream.res +2 -2
  62. package/src/adapter/QueryDb/QueryDbStorage_DynamoDbStream.res.mjs +2 -2
  63. package/src/adapter/QueryDb/QueryDbStorage_Postgres.res +1 -1
  64. package/src/adapter/QueryDb/QueryDbStorage_Postgres.res.mjs +1 -1
  65. package/src/adapter/Runtime/AdminEventCollectorEntryPoint.mjs +24 -41
  66. package/src/adapter/Runtime/AggregateEntryPoint.mjs +52 -45
  67. package/src/adapter/Runtime/AggregateRuntime_Builder_Micro.res +19 -2
  68. package/src/adapter/Runtime/AggregateRuntime_Builder_Micro.res.mjs +8 -5
  69. package/src/adapter/Runtime/AggregateRuntime_Builder_Micro_Async.res +19 -2
  70. package/src/adapter/Runtime/AggregateRuntime_Builder_Micro_Async.res.mjs +8 -5
  71. package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate.res +9 -1
  72. package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate.res.mjs +4 -2
  73. package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate_Async.res +9 -1
  74. package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate_Async.res.mjs +4 -2
  75. package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res +9 -1
  76. package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res.mjs +4 -2
  77. package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res +9 -1
  78. package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res.mjs +4 -2
  79. package/src/adapter/Runtime/AutomationSliceEntryPoint.mjs +36 -22
  80. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +1 -0
  81. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +1 -1
  82. package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +39 -42
  83. package/src/adapter/Runtime/EventCollectorRuntime_Builder_PerEventCollector.res +7 -1
  84. package/src/adapter/Runtime/EventCollectorRuntime_Builder_PerEventCollector.res.mjs +4 -2
  85. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +19 -1
  86. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res.mjs +5 -2
  87. package/src/adapter/Runtime/EventMapperEntryPoint.mjs +25 -23
  88. package/src/adapter/Runtime/ExtensionPointEntryPoint.mjs +25 -41
  89. package/src/adapter/Runtime/ExtensionPointRuntime_Builder_PerExtensionPoint.res +1 -0
  90. package/src/adapter/Runtime/ExtensionPointRuntime_Builder_PerExtensionPoint.res.mjs +1 -1
  91. package/src/adapter/Runtime/HandlerFactoryHelpers.mjs +126 -0
  92. package/src/adapter/Runtime/PluginExtensionPointEntryPoint.mjs +24 -41
  93. package/src/adapter/Runtime/ReadModelEntryPoint.mjs +33 -24
  94. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +52 -2
  95. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +25 -7
  96. package/src/adapter/Runtime/SideEffectEntryPoint.mjs +25 -24
  97. package/src/adapter/Runtime/SideEffectHandlerRuntime_Builder_Single.res +9 -1
  98. package/src/adapter/Runtime/SideEffectHandlerRuntime_Builder_Single.res.mjs +4 -2
  99. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +1 -0
  100. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +1 -1
  101. package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +26 -21
  102. package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res +2 -1
  103. package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res.mjs +2 -2
  104. package/src/adapter/Runtime/TaskRuntime_Builder_PerBucket.res +1 -0
  105. package/src/adapter/Runtime/TaskRuntime_Builder_PerBucket.res.mjs +1 -1
  106. package/src/adapter/ScheduledPublisher/ScheduledPublisher_CloudWatchEvents.res +12 -0
  107. package/src/adapter/ScheduledPublisher/ScheduledPublisher_CloudWatchEvents.res.mjs +4 -2
  108. package/src/adapter/StateTopic/StateTopic_AppSync.res +7 -1
  109. package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +2 -2
  110. package/src/adapter/Task/TaskBucket_S3.res +1 -0
  111. package/src/adapter/Task/TaskBucket_S3.res.mjs +7 -1
  112. package/src/components/Aggregate_Builder_Micro.res.mjs +2 -0
  113. package/src/components/Aggregate_Builder_NoResolver.res.mjs +2 -0
  114. package/src/components/Aggregate_Builder_PerAggregate.res.mjs +2 -0
  115. package/src/components/Aggregate_Builder_Single.res.mjs +2 -0
  116. package/src/components/Aggregate_Builder_Single_Async.res.mjs +2 -0
  117. package/src/components/Api/AppSync_Adapter.res +12 -0
  118. package/src/components/Api/AppSync_Adapter.res.mjs +6 -2
  119. package/src/components/Api/AppSync_MergedApi.res +12 -0
  120. package/src/components/Api/AppSync_MergedApi.res.mjs +5 -2
  121. package/src/components/AutomationSlice_Builder.res.mjs +2 -0
  122. package/src/components/ExtensionPoint_Builder.res.mjs +2 -0
  123. package/src/components/ForeignReadModel_Builder.res.mjs +4 -0
  124. package/src/components/OutboundTranslationSlice_Builder.res.mjs +2 -0
  125. package/src/components/Plugin.res.mjs +2 -0
  126. package/src/components/ReadModel_Builder_NoResolver.res.mjs +2 -0
  127. package/src/components/ReadModel_Builder_NoResolver_Stream.res.mjs +2 -0
  128. package/src/components/ReadModel_Builder_PerReadModel.res.mjs +2 -0
  129. package/src/components/ReadModel_Builder_Single.res.mjs +2 -0
  130. package/src/components/ReadModel_Builder_Single_Stream.res.mjs +2 -0
  131. package/src/components/SideEffectHandler_PerSideEffectHandler.res.mjs +6 -0
  132. package/src/components/SideEffectHandler_Single.res.mjs +4 -0
  133. package/src/components/StateViewSlice_Builder.res.mjs +2 -0
  134. package/src/components/StateViewSlice_Builder_Stream.res.mjs +2 -0
  135. package/src/components/Task_Builder_PerBucket.res.mjs +4 -0
  136. package/src/components/Vpc.res +10 -1
  137. package/src/components/Vpc.res.mjs +3 -1
  138. package/src/plugin/cloner/ClonerRunner_Fargate.res +26 -1
  139. package/src/plugin/cloner/ClonerRunner_Fargate.res.mjs +9 -6
  140. package/src/plugin/heartbeat/HeartbeatRunner_CloudWatchEvents.res +6 -0
  141. package/src/plugin/heartbeat/HeartbeatRunner_CloudWatchEvents.res.mjs +4 -1
  142. package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res +1 -0
  143. package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res.mjs +1 -1
  144. package/src/plugin/runtime/PluginRuntime_Builder.res +2 -0
  145. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +2 -2
  146. package/src/plugin/stack/Plugin_ExtensionPoint_Builder.res.mjs +4 -0
  147. package/src/plugin/stack/Plugin_Stack.res +26 -1
  148. package/src/plugin/stack/Plugin_Stack.res.mjs +8 -3
  149. package/src/util/Util_CognitoGroupUser.res +6 -0
  150. package/src/util/Util_CognitoGroupUser.res.mjs +3 -1
  151. package/src/util/Util_DeadLetterQueue.res +8 -2
  152. package/src/util/Util_DeadLetterQueue.res.mjs +3 -4
  153. package/src/util/Util_LogAttribution.res +31 -0
  154. package/src/util/Util_LogAttribution.res.mjs +27 -0
  155. package/tests/AWS_TagsTest.res +236 -0
  156. package/tests/AWS_TagsTest.res.mjs +153 -0
  157. package/tests/HandlerFactoryDispatchTest.res +190 -0
  158. package/tests/HandlerFactoryDispatchTest.res.mjs +135 -0
  159. package/tests/integration/AggregateEntryPoint_IntegrationTest.res +5 -3
  160. package/tests/integration/AggregateEntryPoint_IntegrationTest.res.mjs +5 -3
@@ -0,0 +1,31 @@
1
+ // Deploy-time log attribution baked into HANDLER_CONFIG.
2
+ //
3
+ // The deployed entry points annotate each invocation with `comp` and `plugin`
4
+ // (HandlerFactoryHelpers.runEffect). `comp` a shell can usually derive itself
5
+ // from the spec name it loads; `plugin` it cannot — resolution goes through
6
+ // LogPrefix's component→plugin registry, which `Plugin_Builder.construct`
7
+ // populates at **deploy** time and which is empty inside a Lambda. And the
8
+ // Lambda-name fallback (`derivePluginFromLambdaName`) only works for Lambdas
9
+ // named `<Plugin>Plugin<Suffix>`: it yields nothing for a shared multi-plugin
10
+ // Lambda (`AllAggregatesCmdHandler`, `AllReadModels`) or a per-component one
11
+ // (`<Aggregate>Aggr`) — exactly the cases where the field matters most.
12
+ //
13
+ // So resolve it here, where the registry exists, and ship it in the config.
14
+ // See docs/plans/entrypoint-dispatch-parity-and-latency-fields.md.
15
+
16
+ /** `,"plugin":"<Name>"` for a HANDLER_CONFIG entry, or `""` when the comp
17
+ resolves to no plugin (the shell then falls back to the Lambda name). */
18
+ let pluginFragment = (~comp: string): string =>
19
+ switch ReventlessCore.Logger.resolvePlugin(~comp, ()) {
20
+ | Some(plugin) => `,"plugin":${plugin->JSON.Encode.string->JSON.stringify}`
21
+ | None => ""
22
+ }
23
+
24
+ /** `,"comp":"<Kind(Name)>"` for a HANDLER_CONFIG entry. For the shells that
25
+ cannot name the element themselves — a read model or side-effect handler is
26
+ known to its shell only by module path, while the aggregate/slice shells read
27
+ the name off the spec they load. */
28
+ let compFragment = (~comp: string): string => `,"comp":${comp->JSON.Encode.string->JSON.stringify}`
29
+
30
+ /** Both fragments, in the order the entry points expect. */
31
+ let fragments = (~comp: string): string => compFragment(~comp) ++ pluginFragment(~comp)
@@ -0,0 +1,27 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
4
+
5
+ function pluginFragment(comp) {
6
+ let plugin = Logger$ReventlessCore.resolvePlugin(comp, undefined);
7
+ if (plugin !== undefined) {
8
+ return `,"plugin":` + JSON.stringify(plugin);
9
+ } else {
10
+ return "";
11
+ }
12
+ }
13
+
14
+ function compFragment(comp) {
15
+ return `,"comp":` + JSON.stringify(comp);
16
+ }
17
+
18
+ function fragments(comp) {
19
+ return compFragment(comp) + pluginFragment(comp);
20
+ }
21
+
22
+ export {
23
+ pluginFragment,
24
+ compFragment,
25
+ fragments,
26
+ }
27
+ /* Logger-ReventlessCore Not a pure module */
@@ -0,0 +1,236 @@
1
+ open JestGlobals
2
+
3
+ // Guards the resource-attribution tag schema. A downstream inventory answers
4
+ // "which model component owns this resource, in what role, at what scope" purely
5
+ // from these keys, so the key set and its namespacing are load-bearing.
6
+ // See docs/plans/resource-attribution-tag-schema.md.
7
+
8
+ module Attribution = ReventlessCore.ResourceAttribution
9
+
10
+ // Pulumi.Input.make is %identity, so the tags value is the underlying dict.
11
+ external toDict: Pulumi.Input.t<dict<string>> => dict<string> = "%identity"
12
+
13
+ let tagsFor = (
14
+ ~name,
15
+ ~kind,
16
+ ~role,
17
+ ~scope: Attribution.Scope.t=Component,
18
+ ~component=?,
19
+ ~plugin=?,
20
+ ~platform=?,
21
+ ) =>
22
+ AWS_Tags.make(~name, ~kind, ~role, ~scope, ~component?, ~plugin?, ~platform?)->toDict
23
+
24
+ describe("AWS_Tags — key set", () => {
25
+ testSync("emits every reventless: fact exactly once, plus the bare Name", () => {
26
+ let keys =
27
+ tagsFor(
28
+ ~name="ProductsQueryDb",
29
+ ~kind=ReventlessCore.ComponentType.ReadModel,
30
+ ~role=QueryDb,
31
+ ~plugin="Catalog",
32
+ ~platform="online-shop",
33
+ )
34
+ ->Dict.keysToArray
35
+ ->Array.toSorted(String.compare)
36
+ expect(keys)->toEqual([
37
+ "Name",
38
+ "reventless:component",
39
+ "reventless:environment",
40
+ "reventless:kind",
41
+ "reventless:platform",
42
+ "reventless:plugin",
43
+ "reventless:role",
44
+ "reventless:scope",
45
+ ])
46
+ })
47
+
48
+ testSync("the dropped bare duplicates stay dropped", () => {
49
+ // `Type` and `Plugin` were exact duplicates of reventless:kind / :plugin, and
50
+ // `Environment` was a framework fact wearing an un-namespaced name. One
51
+ // authoritative name per fact — `Name` is the sole bare key, because the AWS
52
+ // console reads that literal spelling for its resource-name column.
53
+ let tags = tagsFor(~name="Products", ~kind=ReventlessCore.ComponentType.ReadModel, ~role=QueryDb)
54
+ expect(tags->Dict.get("Type"))->toEqual(None)
55
+ expect(tags->Dict.get("Plugin"))->toEqual(None)
56
+ expect(tags->Dict.get("Environment"))->toEqual(None)
57
+ expect(tags->Dict.get("Name"))->toEqual(Some("Products"))
58
+ })
59
+ })
60
+
61
+ describe("AWS_Tags — role and kind are independent facts", () => {
62
+ testSync("one component kind spans several piece roles", () => {
63
+ let kind = ReventlessCore.ComponentType.Aggregate
64
+ let eventLog = tagsFor(~name="OrderEventLog", ~kind, ~role=EventLog)
65
+ let commandTopic = tagsFor(~name="OrderCmdTopic", ~kind, ~role=CommandTopic)
66
+ expect(eventLog->Dict.get("reventless:kind"))->toEqual(Some("Aggregate"))
67
+ expect(commandTopic->Dict.get("reventless:kind"))->toEqual(Some("Aggregate"))
68
+ expect(eventLog->Dict.get("reventless:role"))->toEqual(Some("EventLog"))
69
+ expect(commandTopic->Dict.get("reventless:role"))->toEqual(Some("CommandTopic"))
70
+ })
71
+
72
+ testSync("one piece role spans several component kinds — the Lambda mis-tag", () => {
73
+ // Regression guard for the defect this schema exists to fix: every runtime
74
+ // Lambda used to report kind=role=CommandTopic regardless of what it ran.
75
+ let aggregate = tagsFor(
76
+ ~name="AllAggregatesCmdHandler",
77
+ ~kind=ReventlessCore.ComponentType.Aggregate,
78
+ ~role=Runtime,
79
+ )
80
+ let projection = tagsFor(
81
+ ~name="ProductsProjection",
82
+ ~kind=ReventlessCore.ComponentType.StateViewSlice,
83
+ ~role=Runtime,
84
+ )
85
+ expect(aggregate->Dict.get("reventless:role"))->toEqual(Some("Runtime"))
86
+ expect(projection->Dict.get("reventless:role"))->toEqual(Some("Runtime"))
87
+ expect(aggregate->Dict.get("reventless:kind"))->toEqual(Some("Aggregate"))
88
+ expect(projection->Dict.get("reventless:kind"))->toEqual(Some("StateViewSlice"))
89
+ // …and neither reports the old hardcoded lie.
90
+ expect(aggregate->Dict.get("reventless:kind") == Some("CommandTopic"))->toBe(false)
91
+ })
92
+ })
93
+
94
+ describe("AWS_Tags — owner overrides the piece adapter's own kind", () => {
95
+ // The piece adapters pass their OWN ComponentType as ~kind, which names the
96
+ // piece and so collapses onto ~role. A QueryDb is instantiated by six different
97
+ // component kinds, so without the owner a reader cannot tell a ReadModel's
98
+ // table from a StateViewSlice's. See docs/plans/resource-attribution-owner-kind.md.
99
+ let queryDbTagsFor = (~owner=?) =>
100
+ AWS_Tags.make(
101
+ ~name="ProductsQueryDb",
102
+ ~kind=ReventlessCore.ComponentType.QueryDb,
103
+ ~role=QueryDb,
104
+ ~owner?,
105
+ )->toDict
106
+
107
+ testSync("without an owner, kind still names the piece", () => {
108
+ let tags = queryDbTagsFor()
109
+ expect(tags->Dict.get("reventless:kind"))->toEqual(Some("QueryDB"))
110
+ expect(tags->Dict.get("reventless:role"))->toEqual(Some("QueryDb"))
111
+ })
112
+
113
+ testSync("two QueryDb tables under different owners differ in kind", () => {
114
+ let readModel = queryDbTagsFor(
115
+ ~owner={kind: ReventlessCore.ComponentType.ReadModel, name: "Products"},
116
+ )
117
+ let stateView = queryDbTagsFor(
118
+ ~owner={kind: ReventlessCore.ComponentType.StateViewSlice, name: "ProductAvailability"},
119
+ )
120
+ expect(readModel->Dict.get("reventless:kind"))->toEqual(Some("ReadModel"))
121
+ expect(stateView->Dict.get("reventless:kind"))->toEqual(Some("StateViewSlice"))
122
+ // …while both still report the same piece role.
123
+ expect(readModel->Dict.get("reventless:role"))->toEqual(Some("QueryDb"))
124
+ expect(stateView->Dict.get("reventless:role"))->toEqual(Some("QueryDb"))
125
+ })
126
+
127
+ testSync("the owner also supplies the component stem, not the resource name", () => {
128
+ let tags = queryDbTagsFor(
129
+ ~owner={kind: ReventlessCore.ComponentType.ReadModel, name: "Products"},
130
+ )
131
+ expect(tags->Dict.get("Name"))->toEqual(Some("ProductsQueryDb"))
132
+ expect(tags->Dict.get("reventless:component"))->toEqual(Some("Products"))
133
+ })
134
+
135
+ testSync("a Plugin-kinded owner attributes substrate to the plugin", () => {
136
+ // A plugin IS a model element, so shared substrate is owned by it rather than
137
+ // by nothing. The owner implies plugin scope even though the piece adapter
138
+ // passed no ~scope, and it names the plugin.
139
+ let tags =
140
+ AWS_Tags.make(
141
+ ~name="OrderingDcbEventTopic",
142
+ ~kind=ReventlessCore.ComponentType.EventTopic,
143
+ ~role=EventTopic,
144
+ ~owner={kind: ReventlessCore.ComponentType.Plugin, name: "Ordering"},
145
+ )->toDict
146
+ expect(tags->Dict.get("reventless:kind"))->toEqual(Some("Plugin"))
147
+ expect(tags->Dict.get("reventless:scope"))->toEqual(Some("plugin"))
148
+ expect(tags->Dict.get("reventless:plugin"))->toEqual(Some("Ordering"))
149
+ expect(tags->Dict.get("reventless:component"))->toEqual(Some(""))
150
+ // …and the piece it plays is still legible.
151
+ expect(tags->Dict.get("reventless:role"))->toEqual(Some("EventTopic"))
152
+ })
153
+
154
+ testSync("an owner does not leak a component onto plugin substrate", () => {
155
+ let tags =
156
+ AWS_Tags.make(
157
+ ~name="OrderingDcbEventLog",
158
+ ~kind=ReventlessCore.ComponentType.DcbEventLog,
159
+ ~role=DcbEventLog,
160
+ ~scope=Plugin,
161
+ ~owner={kind: ReventlessCore.ComponentType.Aggregate, name: "Order"},
162
+ )->toDict
163
+ expect(tags->Dict.get("reventless:component"))->toEqual(Some(""))
164
+ })
165
+ })
166
+
167
+ describe("AWS_Tags — scope", () => {
168
+ testSync("component scope names the component, defaulting to the resource name", () => {
169
+ let tags = tagsFor(~name="Products", ~kind=ReventlessCore.ComponentType.ReadModel, ~role=QueryDb)
170
+ expect(tags->Dict.get("reventless:scope"))->toEqual(Some("component"))
171
+ expect(tags->Dict.get("reventless:component"))->toEqual(Some("Products"))
172
+ })
173
+
174
+ testSync("an explicit component stem survives a suffixed resource name", () => {
175
+ let tags = tagsFor(
176
+ ~name="ProductsStateTopic",
177
+ ~kind=ReventlessCore.ComponentType.ReadModel,
178
+ ~role=StateTopic,
179
+ ~component="Products",
180
+ )
181
+ expect(tags->Dict.get("Name"))->toEqual(Some("ProductsStateTopic"))
182
+ expect(tags->Dict.get("reventless:component"))->toEqual(Some("Products"))
183
+ })
184
+
185
+ testSync("plugin substrate carries no component, but keeps its plugin", () => {
186
+ let tags = tagsFor(
187
+ ~name="OrderingDcbEventLog",
188
+ ~kind=ReventlessCore.ComponentType.DcbEventLog,
189
+ ~role=DcbEventLog,
190
+ ~scope=Plugin,
191
+ ~plugin="Ordering",
192
+ )
193
+ expect(tags->Dict.get("reventless:scope"))->toEqual(Some("plugin"))
194
+ expect(tags->Dict.get("reventless:component"))->toEqual(Some(""))
195
+ expect(tags->Dict.get("reventless:plugin"))->toEqual(Some("Ordering"))
196
+ })
197
+
198
+ testSync("platform substrate carries neither component nor plugin", () => {
199
+ let tags = tagsFor(
200
+ ~name="UIFragmentsLambda",
201
+ ~kind=ReventlessCore.ComponentType.ReadModel,
202
+ ~role=Runtime,
203
+ ~scope=Platform,
204
+ ~plugin="Ordering",
205
+ ~platform="online-shop",
206
+ )
207
+ expect(tags->Dict.get("reventless:scope"))->toEqual(Some("platform"))
208
+ expect(tags->Dict.get("reventless:component"))->toEqual(Some(""))
209
+ expect(tags->Dict.get("reventless:plugin"))->toEqual(Some(""))
210
+ expect(tags->Dict.get("reventless:platform"))->toEqual(Some("online-shop"))
211
+ })
212
+ })
213
+
214
+ describe("AWS_Tags — plugin attribution", () => {
215
+ testSync("falls back to the ambient plugin published by Plugin_Builder", () => {
216
+ // Adapters sit far below the builder that knows the plugin name, so the
217
+ // ambient context is how a DynamoDB table learns it belongs to Ordering.
218
+ let previous = Attribution.enter(~platform="online-shop", ~plugin="Ordering")
219
+ let tags = tagsFor(~name="OrderEventLog", ~kind=ReventlessCore.ComponentType.Aggregate, ~role=EventLog)
220
+ Attribution.restore(previous)
221
+ expect(tags->Dict.get("reventless:plugin"))->toEqual(Some("Ordering"))
222
+ expect(tags->Dict.get("reventless:platform"))->toEqual(Some("online-shop"))
223
+ })
224
+
225
+ testSync("an explicit plugin beats the ambient one", () => {
226
+ let previous = Attribution.enter(~platform="online-shop", ~plugin="Ordering")
227
+ let tags = tagsFor(
228
+ ~name="CatalogTable",
229
+ ~kind=ReventlessCore.ComponentType.ReadModel,
230
+ ~role=QueryDb,
231
+ ~plugin="Catalog",
232
+ )
233
+ Attribution.restore(previous)
234
+ expect(tags->Dict.get("reventless:plugin"))->toEqual(Some("Catalog"))
235
+ })
236
+ })
@@ -0,0 +1,153 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
4
+ import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
5
+ import * as AWS_Tags$ReventlessAws from "../src/adapter/AWS_Tags.res.mjs";
6
+ import * as ResourceAttribution$ReventlessCore from "@reventlessdev/reventless-core/src/ResourceAttribution.res.mjs";
7
+
8
+ function tagsFor(name, kind, role, scopeOpt, component, plugin, platform) {
9
+ let scope = scopeOpt !== undefined ? scopeOpt : "Component";
10
+ return AWS_Tags$ReventlessAws.make(name, kind, role, scope, component, undefined, plugin, platform);
11
+ }
12
+
13
+ globalThis.describe("AWS_Tags — key set", () => {
14
+ globalThis.test("emits every reventless: fact exactly once, plus the bare Name", () => {
15
+ let keys = Object.keys(tagsFor("ProductsQueryDb", "ReadModel", "QueryDb", undefined, undefined, "Catalog", "online-shop")).toSorted(Primitive_string.compare);
16
+ globalThis.expect(keys).toEqual([
17
+ "Name",
18
+ "reventless:component",
19
+ "reventless:environment",
20
+ "reventless:kind",
21
+ "reventless:platform",
22
+ "reventless:plugin",
23
+ "reventless:role",
24
+ "reventless:scope"
25
+ ]);
26
+ });
27
+ globalThis.test("the dropped bare duplicates stay dropped", () => {
28
+ let tags = tagsFor("Products", "ReadModel", "QueryDb", undefined, undefined, undefined, undefined);
29
+ globalThis.expect(tags["Type"]).toEqual(undefined);
30
+ globalThis.expect(tags["Plugin"]).toEqual(undefined);
31
+ globalThis.expect(tags["Environment"]).toEqual(undefined);
32
+ globalThis.expect(tags["Name"]).toEqual("Products");
33
+ });
34
+ });
35
+
36
+ globalThis.describe("AWS_Tags — role and kind are independent facts", () => {
37
+ globalThis.test("one component kind spans several piece roles", () => {
38
+ let eventLog = tagsFor("OrderEventLog", "Aggregate", "EventLog", undefined, undefined, undefined, undefined);
39
+ let commandTopic = tagsFor("OrderCmdTopic", "Aggregate", "CommandTopic", undefined, undefined, undefined, undefined);
40
+ globalThis.expect(eventLog["reventless:kind"]).toEqual("Aggregate");
41
+ globalThis.expect(commandTopic["reventless:kind"]).toEqual("Aggregate");
42
+ globalThis.expect(eventLog["reventless:role"]).toEqual("EventLog");
43
+ globalThis.expect(commandTopic["reventless:role"]).toEqual("CommandTopic");
44
+ });
45
+ globalThis.test("one piece role spans several component kinds — the Lambda mis-tag", () => {
46
+ let aggregate = tagsFor("AllAggregatesCmdHandler", "Aggregate", "Runtime", undefined, undefined, undefined, undefined);
47
+ let projection = tagsFor("ProductsProjection", "StateViewSlice", "Runtime", undefined, undefined, undefined, undefined);
48
+ globalThis.expect(aggregate["reventless:role"]).toEqual("Runtime");
49
+ globalThis.expect(projection["reventless:role"]).toEqual("Runtime");
50
+ globalThis.expect(aggregate["reventless:kind"]).toEqual("Aggregate");
51
+ globalThis.expect(projection["reventless:kind"]).toEqual("StateViewSlice");
52
+ globalThis.expect(Primitive_object.equal(aggregate["reventless:kind"], "CommandTopic")).toBe(false);
53
+ });
54
+ });
55
+
56
+ globalThis.describe("AWS_Tags — owner overrides the piece adapter's own kind", () => {
57
+ let queryDbTagsFor = owner => AWS_Tags$ReventlessAws.make("ProductsQueryDb", "QueryDb", "QueryDb", undefined, undefined, owner, undefined, undefined);
58
+ globalThis.test("without an owner, kind still names the piece", () => {
59
+ let tags = queryDbTagsFor(undefined);
60
+ globalThis.expect(tags["reventless:kind"]).toEqual("QueryDB");
61
+ globalThis.expect(tags["reventless:role"]).toEqual("QueryDb");
62
+ });
63
+ globalThis.test("two QueryDb tables under different owners differ in kind", () => {
64
+ let readModel = queryDbTagsFor({
65
+ kind: "ReadModel",
66
+ name: "Products"
67
+ });
68
+ let stateView = queryDbTagsFor({
69
+ kind: "StateViewSlice",
70
+ name: "ProductAvailability"
71
+ });
72
+ globalThis.expect(readModel["reventless:kind"]).toEqual("ReadModel");
73
+ globalThis.expect(stateView["reventless:kind"]).toEqual("StateViewSlice");
74
+ globalThis.expect(readModel["reventless:role"]).toEqual("QueryDb");
75
+ globalThis.expect(stateView["reventless:role"]).toEqual("QueryDb");
76
+ });
77
+ globalThis.test("the owner also supplies the component stem, not the resource name", () => {
78
+ let tags = queryDbTagsFor({
79
+ kind: "ReadModel",
80
+ name: "Products"
81
+ });
82
+ globalThis.expect(tags["Name"]).toEqual("ProductsQueryDb");
83
+ globalThis.expect(tags["reventless:component"]).toEqual("Products");
84
+ });
85
+ globalThis.test("a Plugin-kinded owner attributes substrate to the plugin", () => {
86
+ let tags = AWS_Tags$ReventlessAws.make("OrderingDcbEventTopic", "EventTopic", "EventTopic", undefined, undefined, {
87
+ kind: "Plugin",
88
+ name: "Ordering"
89
+ }, undefined, undefined);
90
+ globalThis.expect(tags["reventless:kind"]).toEqual("Plugin");
91
+ globalThis.expect(tags["reventless:scope"]).toEqual("plugin");
92
+ globalThis.expect(tags["reventless:plugin"]).toEqual("Ordering");
93
+ globalThis.expect(tags["reventless:component"]).toEqual("");
94
+ globalThis.expect(tags["reventless:role"]).toEqual("EventTopic");
95
+ });
96
+ globalThis.test("an owner does not leak a component onto plugin substrate", () => {
97
+ let tags = AWS_Tags$ReventlessAws.make("OrderingDcbEventLog", "DcbEventLog", "DcbEventLog", "Plugin", undefined, {
98
+ kind: "Aggregate",
99
+ name: "Order"
100
+ }, undefined, undefined);
101
+ globalThis.expect(tags["reventless:component"]).toEqual("");
102
+ });
103
+ });
104
+
105
+ globalThis.describe("AWS_Tags — scope", () => {
106
+ globalThis.test("component scope names the component, defaulting to the resource name", () => {
107
+ let tags = tagsFor("Products", "ReadModel", "QueryDb", undefined, undefined, undefined, undefined);
108
+ globalThis.expect(tags["reventless:scope"]).toEqual("component");
109
+ globalThis.expect(tags["reventless:component"]).toEqual("Products");
110
+ });
111
+ globalThis.test("an explicit component stem survives a suffixed resource name", () => {
112
+ let tags = tagsFor("ProductsStateTopic", "ReadModel", "StateTopic", undefined, "Products", undefined, undefined);
113
+ globalThis.expect(tags["Name"]).toEqual("ProductsStateTopic");
114
+ globalThis.expect(tags["reventless:component"]).toEqual("Products");
115
+ });
116
+ globalThis.test("plugin substrate carries no component, but keeps its plugin", () => {
117
+ let tags = tagsFor("OrderingDcbEventLog", "DcbEventLog", "DcbEventLog", "Plugin", undefined, "Ordering", undefined);
118
+ globalThis.expect(tags["reventless:scope"]).toEqual("plugin");
119
+ globalThis.expect(tags["reventless:component"]).toEqual("");
120
+ globalThis.expect(tags["reventless:plugin"]).toEqual("Ordering");
121
+ });
122
+ globalThis.test("platform substrate carries neither component nor plugin", () => {
123
+ let tags = tagsFor("UIFragmentsLambda", "ReadModel", "Runtime", "Platform", undefined, "Ordering", "online-shop");
124
+ globalThis.expect(tags["reventless:scope"]).toEqual("platform");
125
+ globalThis.expect(tags["reventless:component"]).toEqual("");
126
+ globalThis.expect(tags["reventless:plugin"]).toEqual("");
127
+ globalThis.expect(tags["reventless:platform"]).toEqual("online-shop");
128
+ });
129
+ });
130
+
131
+ globalThis.describe("AWS_Tags — plugin attribution", () => {
132
+ globalThis.test("falls back to the ambient plugin published by Plugin_Builder", () => {
133
+ let previous = ResourceAttribution$ReventlessCore.enter("online-shop", "Ordering");
134
+ let tags = tagsFor("OrderEventLog", "Aggregate", "EventLog", undefined, undefined, undefined, undefined);
135
+ ResourceAttribution$ReventlessCore.restore(previous);
136
+ globalThis.expect(tags["reventless:plugin"]).toEqual("Ordering");
137
+ globalThis.expect(tags["reventless:platform"]).toEqual("online-shop");
138
+ });
139
+ globalThis.test("an explicit plugin beats the ambient one", () => {
140
+ let previous = ResourceAttribution$ReventlessCore.enter("online-shop", "Ordering");
141
+ let tags = tagsFor("CatalogTable", "ReadModel", "QueryDb", undefined, undefined, "Catalog", undefined);
142
+ ResourceAttribution$ReventlessCore.restore(previous);
143
+ globalThis.expect(tags["reventless:plugin"]).toEqual("Catalog");
144
+ });
145
+ });
146
+
147
+ let Attribution;
148
+
149
+ export {
150
+ Attribution,
151
+ tagsFor,
152
+ }
153
+ /* Not a pure module */
@@ -0,0 +1,190 @@
1
+ // Regression guard for the deployed entry points' dispatch boundary.
2
+ //
3
+ // `HandlerFactoryHelpers.mjs` is the single place where every Lambda invocation
4
+ // gets its log annotations and its `RequestContext` — the AWS counterpart of
5
+ // ReventlessCore.Runtime.annotateInvocation, which does not run in a Lambda (the
6
+ // runtime builders take `~handler as _` and deploy the .mjs shell instead). Two
7
+ // things here are checkable only from JS and break silently otherwise:
8
+ //
9
+ // 1. `RequestContext.fromOptions` is called across the ReScript/JS seam. A
10
+ // labelled-argument constructor compiles to positional arguments, so an
11
+ // inserted field would silently shift every value; `fromOptions` takes one
12
+ // options object precisely to avoid that, and this test fails if the shell
13
+ // and the record ever disagree.
14
+ // 2. The transport extractors read two different record shapes — SQS (`body`
15
+ // envelope + `attributes`) and DynamoDB streams (flat top-level meta
16
+ // attributes on `NewImage` + `ApproximateCreationDateTime` in *seconds*).
17
+ //
18
+ // See docs/plans/entrypoint-dispatch-parity-and-latency-fields.md.
19
+
20
+ open JestGlobals
21
+
22
+ // ─── The shell under test ───────────────────────────────────────────────────
23
+
24
+ type dispatchOptions = {
25
+ correlationId?: string,
26
+ causationId?: string,
27
+ comp?: string,
28
+ plugin?: string,
29
+ timestamp?: float,
30
+ retryCount?: int,
31
+ }
32
+
33
+ @module("@reventlessdev/reventless-aws/src/adapter/Runtime/HandlerFactoryHelpers.mjs")
34
+ external runEffect: (Effect.t<'a, 'e, 'r>, dispatchOptions) => promise<'a> = "runEffect"
35
+
36
+ @module("@reventlessdev/reventless-aws/src/adapter/Runtime/HandlerFactoryHelpers.mjs")
37
+ external setRequestId: string => unit = "setRequestId"
38
+
39
+ @module("@reventlessdev/reventless-aws/src/adapter/Runtime/HandlerFactoryHelpers.mjs")
40
+ external extractMetaField: (array<'record>, string) => option<string> = "extractMetaField"
41
+
42
+ @module("@reventlessdev/reventless-aws/src/adapter/Runtime/HandlerFactoryHelpers.mjs")
43
+ external extractSentTimestamp: array<'record> => option<float> = "extractSentTimestamp"
44
+
45
+ @module("@reventlessdev/reventless-aws/src/adapter/Runtime/HandlerFactoryHelpers.mjs")
46
+ external extractRetryCount: array<'record> => int = "extractRetryCount"
47
+
48
+ // ─── Record fixtures, typed rather than cast ────────────────────────────────
49
+
50
+ type sqsAttributes = {
51
+ @as("SentTimestamp") sentTimestamp: string,
52
+ @as("ApproximateReceiveCount") approximateReceiveCount: string,
53
+ }
54
+ type sqsRecord = {body: string, attributes: sqsAttributes}
55
+ type bodyOnlyRecord = {body: string}
56
+
57
+ type streamAttribute = {@as("S") s: string}
58
+ type streamImage = {correlationId: streamAttribute, causationId: streamAttribute}
59
+ type streamPayload = {
60
+ @as("NewImage") newImage: streamImage,
61
+ @as("ApproximateCreationDateTime") approximateCreationDateTime: float,
62
+ }
63
+ type streamRecord = {dynamodb: streamPayload}
64
+
65
+ let sqsRecords = [
66
+ {
67
+ body: `{"meta":{"correlationId":"corr-1","causationId":"cause-1"}}`,
68
+ attributes: {sentTimestamp: "1750000000000", approximateReceiveCount: "3"},
69
+ },
70
+ ]
71
+
72
+ let streamRecords = [
73
+ {
74
+ dynamodb: {
75
+ newImage: {correlationId: {s: "corr-2"}, causationId: {s: "cause-2"}},
76
+ approximateCreationDateTime: 1750000001.0,
77
+ },
78
+ },
79
+ ]
80
+
81
+ // Read back what the dispatch boundary provided, so the assertions see exactly
82
+ // what an application handler would.
83
+ let capture = (): Effect.t<ReventlessCore.RequestContext.t, string, _> =>
84
+ Effect.serviceWith(ReventlessCore.RequestContext.tag, ctx => ctx)
85
+
86
+ describe("entry-point dispatch boundary", () => {
87
+ describe("SQS records", () => {
88
+ testSync("reads the envelope out of the body", () => {
89
+ expect(extractMetaField(sqsRecords, "correlationId"))->Expect.toEqual(Some("corr-1"))
90
+ expect(extractMetaField(sqsRecords, "causationId"))->Expect.toEqual(Some("cause-1"))
91
+ })
92
+
93
+ testSync("reads SentTimestamp as ms since epoch", () =>
94
+ expect(extractSentTimestamp(sqsRecords))->Expect.toEqual(Some(1750000000000.0))
95
+ )
96
+
97
+ testSync("reads ApproximateReceiveCount as the delivery attempt", () =>
98
+ expect(extractRetryCount(sqsRecords))->Expect.toBe(3)
99
+ )
100
+ })
101
+
102
+ describe("DynamoDB stream records", () => {
103
+ // EventLog rows store meta keys flat as top-level attributes
104
+ // (Message.composeMeta rebuilds `meta` from them) — not as a nested map.
105
+ testSync("reads the envelope off the flat NewImage attributes", () => {
106
+ expect(extractMetaField(streamRecords, "correlationId"))->Expect.toEqual(Some("corr-2"))
107
+ expect(extractMetaField(streamRecords, "causationId"))->Expect.toEqual(Some("cause-2"))
108
+ })
109
+
110
+ testSync("converts ApproximateCreationDateTime from seconds to ms", () =>
111
+ expect(extractSentTimestamp(streamRecords))->Expect.toEqual(Some(1750000001000.0))
112
+ )
113
+
114
+ testSync("reports a first delivery — streams have no receive count", () =>
115
+ expect(extractRetryCount(streamRecords))->Expect.toBe(1)
116
+ )
117
+ })
118
+
119
+ describe("absent or malformed input", () => {
120
+ testSync("an empty batch has no send time", () =>
121
+ expect(extractSentTimestamp([]))->Expect.toEqual(None)
122
+ )
123
+
124
+ testSync("an unparseable body yields no envelope field", () =>
125
+ expect(extractMetaField([{body: "not json"}], "correlationId"))->Expect.toEqual(None)
126
+ )
127
+ })
128
+
129
+ describe("RequestContext provided at dispatch", () => {
130
+ test("carries the full record, not a bare correlationId literal", async () => {
131
+ let ctx = await runEffect(
132
+ capture(),
133
+ {
134
+ correlationId: "corr-1",
135
+ causationId: "cause-1",
136
+ comp: "AggregateRuntime(Customer)",
137
+ plugin: "Catalog",
138
+ timestamp: 1750000000000.0,
139
+ retryCount: 3,
140
+ },
141
+ )
142
+ expect(ctx.correlationId)->Expect.toBe("corr-1")
143
+ expect(ctx.causationId)->Expect.toEqual(Some("cause-1"))
144
+ expect(ctx.component)->Expect.toEqual(Some("AggregateRuntime(Customer)"))
145
+ expect(ctx.pluginName)->Expect.toEqual(Some("Catalog"))
146
+ expect(ctx.timestamp)->Expect.toEqual(Some(1750000000000.0))
147
+ expect(ctx.retryCount)->Expect.toEqual(Some(3))
148
+ })
149
+
150
+ test("fills identity and claims, which the record declares non-optional", async () => {
151
+ let ctx = await runEffect(capture(), {correlationId: "corr-1"})
152
+ expect(ctx.identity.userId)->Expect.toBe(Reventless.Identity.anonymous.userId)
153
+ expect(ctx.claims->Dict.keysToArray)->Expect.toEqual([])
154
+ })
155
+
156
+ test("falls back to a placeholder when no correlationId is on the message", async () => {
157
+ let ctx = await runEffect(capture(), {comp: "X(Y)"})
158
+ expect(ctx.correlationId)->Expect.toBe("unknown")
159
+ })
160
+ })
161
+
162
+ describe("log annotations", () => {
163
+ // Effect keeps annotations in a HashMap; read it as a dict.
164
+ let annotations = (): Effect.t<Effect.logAnnotationMap, string, _> => Effect.logAnnotations
165
+
166
+ test("promotes comp, causationId and the request id onto every line", async () => {
167
+ setRequestId("req-abc")
168
+ let annotated = await runEffect(
169
+ annotations(),
170
+ {correlationId: "corr-1", causationId: "cause-1", comp: "EventCollector(Customers)"},
171
+ )
172
+ let annotated = annotated->Effect.annotationsToDict
173
+ expect(annotated->Dict.get("comp"))->Expect.toEqual(Some("EventCollector(Customers)"))
174
+ expect(annotated->Dict.get("causationId"))->Expect.toEqual(Some("cause-1"))
175
+ expect(annotated->Dict.get("correlationId"))->Expect.toEqual(Some("corr-1"))
176
+ expect(annotated->Dict.get("requestId"))->Expect.toEqual(Some("req-abc"))
177
+ })
178
+
179
+ // A constant "1" on every line is noise; its absence is what makes
180
+ // `filter retryCount > 1` a usable CloudWatch query.
181
+ test("annotates retryCount only on a redelivery", async () => {
182
+ let first = await runEffect(annotations(), {correlationId: "c", retryCount: 1})
183
+ expect(first->Effect.annotationsToDict->Dict.get("retryCount"))->Expect.toEqual(None)
184
+ let redelivered = await runEffect(annotations(), {correlationId: "c", retryCount: 2})
185
+ expect(redelivered->Effect.annotationsToDict->Dict.get("retryCount"))->Expect.toEqual(
186
+ Some("2"),
187
+ )
188
+ })
189
+ })
190
+ })