@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.
Files changed (170) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/package.json +13 -13
  3. package/src/BakedManifest.res +5 -6
  4. package/src/EmitCapabilities.res +3 -1
  5. package/src/LocalPlatformRegistry.res +3 -1
  6. package/src/LocalPlatformStart.res +7 -6
  7. package/src/LocalSeedTarget.res +3 -1
  8. package/src/Platform.res +291 -215
  9. package/src/ShellConfig.res +10 -17
  10. package/src/UiHints.res +8 -14
  11. package/src/UiSlots.res +10 -17
  12. package/src/adapter/Api/LocalEvents_Server.res +5 -3
  13. package/src/adapter/Api/LocalGraphQL_Adapter.res +4 -4
  14. package/src/adapter/Auth/LocalAuth.res +11 -15
  15. package/src/adapter/Auth/UserStore.res +5 -7
  16. package/src/adapter/Backend.res +1 -2
  17. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +42 -26
  18. package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res +13 -3
  19. package/src/adapter/CommandTopic/LocalCommandTopicChannel.res +15 -17
  20. package/src/adapter/CommandTopic/LocalCommandTopicRemoteChannel.res +18 -22
  21. package/src/adapter/Counter/LocalCounterHandler.res +1 -1
  22. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +33 -22
  23. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +57 -57
  24. package/src/adapter/DomainGraphQL_Server.res +35 -34
  25. package/src/adapter/DomainMCP_Server.res +45 -41
  26. package/src/adapter/EventCollector/LocalEventCollectorChannel.res +47 -36
  27. package/src/adapter/EventHistory/EventHistoryResolvers_GraphQL.res +15 -12
  28. package/src/adapter/EventLog/EventLogStorage_InMemory.res +9 -4
  29. package/src/adapter/EventLog/EventLogStorage_Sqlite.res +36 -24
  30. package/src/adapter/EventTopic/LocalEventTopicPublisher.res +9 -3
  31. package/src/adapter/Heartbeat/LocalHeartbeatRunner.res +9 -14
  32. package/src/adapter/LocalBus.res +12 -12
  33. package/src/adapter/LocalCapabilities.res.mjs +1 -1
  34. package/src/adapter/LocalGeocodeResolvers.res +1 -4
  35. package/src/adapter/LocalSideEffectHandler.res +1 -1
  36. package/src/adapter/LocalStateChangeDescriptor.res +2 -1
  37. package/src/adapter/MCP_ServerInstance.res +51 -39
  38. package/src/adapter/ObjectStore/LocalObjectStore.res +5 -2
  39. package/src/adapter/ObjectStore/ObjectStoreStorage_FileSystem.res +1 -2
  40. package/src/adapter/PlatformGraphQL_Server.res +9 -5
  41. package/src/adapter/ProjectionCheckpoint.res +8 -1
  42. package/src/adapter/ProjectionPending.res +1 -3
  43. package/src/adapter/QueryDb/LocalQueryDbStorage.res +2 -1
  44. package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +367 -269
  45. package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +5 -3
  46. package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +63 -40
  47. package/src/adapter/QueryDb/SortKey_Filter.res +14 -6
  48. package/src/adapter/Runtime/LocalAggregateRuntime_Builder.res +12 -10
  49. package/src/adapter/Runtime/LocalEventCollectorRuntime_Builder.res +8 -2
  50. package/src/adapter/Runtime/LocalPluginRuntime_Builder.res +1 -4
  51. package/src/adapter/Scheduler/LocalScheduledPublisher.res +12 -12
  52. package/src/adapter/SqliteDriver.res +16 -4
  53. package/src/adapter/Task/TaskBucket_Sqlite.res +5 -6
  54. package/src/components/Aggregate_Builder.res +6 -1
  55. package/src/components/Counter_Builder.res +1 -5
  56. package/src/components/DcbEventLog_Builder.res +5 -3
  57. package/src/components/ExtensionPoint_Builder.res +13 -15
  58. package/src/components/InboundTranslationSlice_Builder.res +3 -2
  59. package/src/components/OutboundTranslationSlice_Builder.res +3 -2
  60. package/src/components/ReadModel_Builder.res +18 -20
  61. package/src/components/StateViewSlice_Builder.res +3 -2
  62. package/src/components/Task_Builder.res +12 -13
  63. package/src/reset/LocalSeedReset.res +2 -5
  64. package/src/test/Mocks/MockDcbEventLogStorage.res +5 -1
  65. package/src/test/Mocks/MockEventLogStorage.res +8 -3
  66. package/src/test/Mocks/MockQueryDbStorage.res +3 -2
  67. package/src/test/TestRunner.res +1 -1
  68. package/tests/BakedManifestFilesTest.res +7 -10
  69. package/tests/LocalEventTapTest.res +26 -21
  70. package/tests/LocalPlatformStartTest.res +33 -28
  71. package/tests/PluginEventDecodeTest.res +3 -3
  72. package/tests/ShellConfigTest.res +30 -21
  73. package/tests/SplitApiFixtures.res +34 -10
  74. package/tests/SplitApiTest.res +92 -62
  75. package/tests/adapter/BackendParityTest.res +180 -130
  76. package/tests/adapter/CollectNEventsTest.res +6 -4
  77. package/tests/adapter/CommandAuthorizationTest.res +47 -41
  78. package/tests/adapter/CommandTopicChannelTest.res +163 -139
  79. package/tests/adapter/CounterHandlerTest.res +95 -77
  80. package/tests/adapter/DcbEventLogStorageSqliteTest.res +171 -168
  81. package/tests/adapter/DcbEventLogStorageTest.res +264 -207
  82. package/tests/adapter/DomainGraphQL_ServerTest.res +53 -43
  83. package/tests/adapter/EventCollectorChannelTest.res +153 -124
  84. package/tests/adapter/EventHistoryResolverTest.res +46 -18
  85. package/tests/adapter/EventLogSnapshotParityTest.res +57 -45
  86. package/tests/adapter/EventLogStorageSqliteTest.res +9 -10
  87. package/tests/adapter/EventTapTest.res +37 -20
  88. package/tests/adapter/EventTopicPublisherTest.res +74 -50
  89. package/tests/adapter/GraphQLSubscriptionBridgeTest.res +4 -3
  90. package/tests/adapter/GraphQL_SchemaInspectorTest.res +666 -568
  91. package/tests/adapter/GraphQL_SubscriptionResolversTest.res +105 -96
  92. package/tests/adapter/HeartbeatRunnerTest.res +99 -81
  93. package/tests/adapter/InboundTranslationMutationTest.res +4 -1
  94. package/tests/adapter/LocalAuthHttpTest.res +79 -85
  95. package/tests/adapter/LocalAuthLoginTest.res +42 -50
  96. package/tests/adapter/LocalAuthTest.res +8 -16
  97. package/tests/adapter/LocalAuthUserStoreTest.res +2 -7
  98. package/tests/adapter/LocalBusBoundedTest.res +187 -117
  99. package/tests/adapter/LocalBusPubSubTest.res +118 -76
  100. package/tests/adapter/LocalDispatchContextTest.res +1 -2
  101. package/tests/adapter/LocalEvents_ServerTest.res +242 -187
  102. package/tests/adapter/PgProjectionCatchupTest.res +44 -10
  103. package/tests/adapter/ProjectionCheckpointTest.res +261 -206
  104. package/tests/adapter/QueryDbGsiTtlTest.res +123 -83
  105. package/tests/adapter/QueryDbListPushdownParityTest.res +107 -67
  106. package/tests/adapter/QueryDbListResolverTest.res +225 -208
  107. package/tests/adapter/QueryDbResolvedFieldsTest.res +17 -14
  108. package/tests/adapter/QueryDbStorageSqliteTest.res +33 -49
  109. package/tests/adapter/QueryDbStorageTest.res +163 -117
  110. package/tests/adapter/QueryDbUnionRoundTripTest.res +8 -10
  111. package/tests/adapter/QueryEngineTest.res +179 -126
  112. package/tests/adapter/ScheduledPublisherTest.res +171 -135
  113. package/tests/adapter/ServedBucketHttpTest.res +65 -59
  114. package/tests/adapter/SortKey_FilterTest.res +19 -3
  115. package/tests/adapter/SqliteDriverTest.res +13 -10
  116. package/tests/adapter/TaskBucketTest.res +94 -80
  117. package/tests/components/aggregate/AggregateTest.res +31 -18
  118. package/tests/components/automationslice/AutomationSliceCallbackTest.res +259 -222
  119. package/tests/components/automationslice/AutomationSliceFixtures.res +4 -2
  120. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +38 -38
  121. package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +12 -14
  122. package/tests/components/automationslice/MixedSourceAutomationSliceTest.res +8 -32
  123. package/tests/components/commandgenerator/CommandGeneratorFixtures.res +7 -5
  124. package/tests/components/commandgenerator/CommandGeneratorTest.res +40 -31
  125. package/tests/components/commandtopic/CommandTopicFixtures.res +3 -4
  126. package/tests/components/commandtopic/CommandTopicStreamFixtures.res +7 -5
  127. package/tests/components/commandtopic/CommandTopicTest.res +24 -15
  128. package/tests/components/counter/CounterFixtures.res +6 -8
  129. package/tests/components/dcb/DcbCrossPartitionFixtures.res +8 -2
  130. package/tests/components/dcb/DcbCrossPartitionReadTest.res +20 -13
  131. package/tests/components/dcb/DcbEventLogAppendStreamTest.res +12 -8
  132. package/tests/components/dcb/DcbEventLogStreamTest.res +88 -61
  133. package/tests/components/dcb/DcbFixtures.res +4 -1
  134. package/tests/components/dcb/DcbTest.res +9 -6
  135. package/tests/components/eventcollector/EventCollectorFixtures.res +0 -1
  136. package/tests/components/eventcollector/EventCollectorTest.res +6 -3
  137. package/tests/components/eventlog/EventLogAppendStreamTest.res +88 -68
  138. package/tests/components/eventlog/EventLogConflictTest.res +8 -4
  139. package/tests/components/eventlog/EventLogFixtures.res +3 -3
  140. package/tests/components/eventlog/EventLogProvisioningSeamTest.res +26 -13
  141. package/tests/components/eventlog/EventLogStreamTest.res +119 -56
  142. package/tests/components/eventlog/EventLogTest.res +20 -4
  143. package/tests/components/eventtopic/EventTopicFixtures.res +3 -3
  144. package/tests/components/eventtopic/EventTopicStreamFixtures.res +1 -2
  145. package/tests/components/eventtopic/EventTopicStreamTest.res +7 -1
  146. package/tests/components/extensionpoint/ExtensionPointFixtures.res +14 -9
  147. package/tests/components/extensionpoint/ExtensionPointTest.res +3 -2
  148. package/tests/components/heartbeat/HeartbeatFixtures.res +5 -6
  149. package/tests/components/heartbeat/HeartbeatTest.res +56 -47
  150. package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res +271 -249
  151. package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res +3 -2
  152. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +361 -276
  153. package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res +8 -10
  154. package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res +37 -43
  155. package/tests/components/querydb/QueryDbFixtures.res +1 -5
  156. package/tests/components/querydb/QueryDbTest.res +2 -4
  157. package/tests/components/readmodel/DcbReadModelE2EFixtures.res +15 -18
  158. package/tests/components/readmodel/MixedSourceReadModelFixtures.res +5 -7
  159. package/tests/components/readmodel/ReadModelFixtures.res +8 -8
  160. package/tests/components/scheduler/SchedulerTest.res +76 -58
  161. package/tests/components/sideeffecthandler/SideEffectHandlerFixtures.res +4 -4
  162. package/tests/components/stateviewslice/StateViewSliceDisplayNameFixtures.res +5 -7
  163. package/tests/components/stateviewslice/StateViewSliceFixtures.res +8 -6
  164. package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res +17 -11
  165. package/tests/components/stateviewslice/StateViewSliceSubIdTest.res +12 -2
  166. package/tests/components/task/TaskFixtures.res +5 -5
  167. package/tests/components/task/TaskTest.res +95 -80
  168. package/tests/plugin/PluginsProjection_GWT.res +3 -1
  169. package/tests/plugin/UiFragments_GWT.res +6 -1
  170. package/tests/reset/LocalSeedResetTest.res +1 -0
@@ -59,7 +59,7 @@ type unionCommand =
59
59
  // expose one `Plugin_Command` field per constructor).
60
60
  @schema
61
61
  type dcbSingleCommand =
62
- | AddCategory({categoryId: @s.matches(Reventless.DcbTag.string) string, name: string})
62
+ AddCategory({categoryId: @s.matches(Reventless.DcbTag.string) string, name: string})
63
63
 
64
64
  @schema
65
65
  type dcbMultiCommand =
@@ -184,410 +184,479 @@ let scanStateSchemaWithAnnotations = scanStateSchema->S.Metadata.set(
184
184
 
185
185
  describe("GraphQL_SchemaInspector", () => {
186
186
  describe("inspectScalar", () => {
187
- testPromise("plain string derives to String", async () => {
188
- expect(ReventlessCore.GraphQL_SchemaInspector.inspectScalar(stringSchema))->toBe("String")
189
- })
187
+ testPromise(
188
+ "plain string derives to String",
189
+ async () => {
190
+ expect(ReventlessCore.GraphQL_SchemaInspector.inspectScalar(stringSchema))->toBe("String")
191
+ },
192
+ )
190
193
 
191
- testPromise("tagged string derives to ID", async () => {
192
- expect(ReventlessCore.GraphQL_SchemaInspector.inspectScalar(taggedSchema))->toBe("ID")
193
- })
194
+ testPromise(
195
+ "tagged string derives to ID",
196
+ async () => {
197
+ expect(ReventlessCore.GraphQL_SchemaInspector.inspectScalar(taggedSchema))->toBe("ID")
198
+ },
199
+ )
194
200
 
195
- testPromise("float derives to Float", async () => {
196
- expect(ReventlessCore.GraphQL_SchemaInspector.inspectScalar(numberSchema))->toBe("Float")
197
- })
201
+ testPromise(
202
+ "float derives to Float",
203
+ async () => {
204
+ expect(ReventlessCore.GraphQL_SchemaInspector.inspectScalar(numberSchema))->toBe("Float")
205
+ },
206
+ )
198
207
 
199
- testPromise("bool derives to Boolean", async () => {
200
- expect(ReventlessCore.GraphQL_SchemaInspector.inspectScalar(boolSchema))->toBe("Boolean")
201
- })
208
+ testPromise(
209
+ "bool derives to Boolean",
210
+ async () => {
211
+ expect(ReventlessCore.GraphQL_SchemaInspector.inspectScalar(boolSchema))->toBe("Boolean")
212
+ },
213
+ )
202
214
  })
203
215
 
204
216
  describe("inspectObjectType", () => {
205
- testPromise("derives type definition from object schema", async () => {
206
- let result = ReventlessCore.GraphQL_SchemaInspector.inspectObjectType(
207
- ~typeName="TestState",
208
- testStateSchema,
209
- )
210
- expect(result->Option.isSome)->toBe(true)
211
- let sdl = result->Option.getOrThrow
212
- expect(sdl->String.includes("type TestState"))->toBe(true)
213
- expect(sdl->String.includes("id: ID!"))->toBe(true)
214
- expect(sdl->String.includes("name: String"))->toBe(true)
215
- expect(sdl->String.includes("price: Float"))->toBe(true)
216
- expect(sdl->String.includes("active: Boolean"))->toBe(true)
217
- })
217
+ testPromise(
218
+ "derives type definition from object schema",
219
+ async () => {
220
+ let result = ReventlessCore.GraphQL_SchemaInspector.inspectObjectType(
221
+ ~typeName="TestState",
222
+ testStateSchema,
223
+ )
224
+ expect(result->Option.isSome)->toBe(true)
225
+ let sdl = result->Option.getOrThrow
226
+ expect(sdl->String.includes("type TestState"))->toBe(true)
227
+ expect(sdl->String.includes("id: ID!"))->toBe(true)
228
+ expect(sdl->String.includes("name: String"))->toBe(true)
229
+ expect(sdl->String.includes("price: Float"))->toBe(true)
230
+ expect(sdl->String.includes("active: Boolean"))->toBe(true)
231
+ },
232
+ )
218
233
 
219
- testPromise("returns None for non-object schema", async () => {
220
- let result = ReventlessCore.GraphQL_SchemaInspector.inspectObjectType(
221
- ~typeName="Foo",
222
- stringSchema,
223
- )
224
- expect(result->Option.isNone)->toBe(true)
225
- })
234
+ testPromise(
235
+ "returns None for non-object schema",
236
+ async () => {
237
+ let result = ReventlessCore.GraphQL_SchemaInspector.inspectObjectType(
238
+ ~typeName="Foo",
239
+ stringSchema,
240
+ )
241
+ expect(result->Option.isNone)->toBe(true)
242
+ },
243
+ )
226
244
  })
227
245
 
228
246
  describe("inspectMutationFields", () => {
229
- testPromise("derives fields from single-variant (DCB slice) command", async () => {
230
- let fields = ReventlessCore.GraphQL_SchemaInspector.inspectMutationFields(
231
- ~fieldPrefix="Catalog_AddProduct",
232
- addCommandSchema,
233
- )
234
- expect(fields->Array.length)->toBe(1)
235
- let field = fields->Array.getUnsafe(0)
236
- expect(field->String.includes("Catalog_AddProduct"))->toBe(true)
237
- expect(field->String.includes("productId: ID!"))->toBe(true)
238
- expect(field->String.includes("name: String"))->toBe(true)
239
- })
247
+ testPromise(
248
+ "derives fields from single-variant (DCB slice) command",
249
+ async () => {
250
+ let fields = ReventlessCore.GraphQL_SchemaInspector.inspectMutationFields(
251
+ ~fieldPrefix="Catalog_AddProduct",
252
+ addCommandSchema,
253
+ )
254
+ expect(fields->Array.length)->toBe(1)
255
+ let field = fields->Array.getUnsafe(0)
256
+ expect(field->String.includes("Catalog_AddProduct"))->toBe(true)
257
+ expect(field->String.includes("productId: ID!"))->toBe(true)
258
+ expect(field->String.includes("name: String"))->toBe(true)
259
+ },
260
+ )
240
261
 
241
- testPromise("derives fields from union (aggregate) command", async () => {
242
- let fields = ReventlessCore.GraphQL_SchemaInspector.inspectMutationFields(
243
- ~fieldPrefix="App_Item",
244
- unionCommandSchema,
245
- )
246
- expect(fields->Array.length)->toBe(2)
247
- let createField = fields->Array.getUnsafe(0)
248
- expect(createField->String.includes("App_Item_Create"))->toBe(true)
249
- expect(createField->String.includes("itemId: ID!"))->toBe(true)
250
- let renameField = fields->Array.getUnsafe(1)
251
- expect(renameField->String.includes("App_Item_Rename"))->toBe(true)
252
- expect(renameField->String.includes("newName: String"))->toBe(true)
253
- })
262
+ testPromise(
263
+ "derives fields from union (aggregate) command",
264
+ async () => {
265
+ let fields = ReventlessCore.GraphQL_SchemaInspector.inspectMutationFields(
266
+ ~fieldPrefix="App_Item",
267
+ unionCommandSchema,
268
+ )
269
+ expect(fields->Array.length)->toBe(2)
270
+ let createField = fields->Array.getUnsafe(0)
271
+ expect(createField->String.includes("App_Item_Create"))->toBe(true)
272
+ expect(createField->String.includes("itemId: ID!"))->toBe(true)
273
+ let renameField = fields->Array.getUnsafe(1)
274
+ expect(renameField->String.includes("App_Item_Rename"))->toBe(true)
275
+ expect(renameField->String.includes("newName: String"))->toBe(true)
276
+ },
277
+ )
254
278
 
255
- testPromise("returns empty for non-object/non-union schema", async () => {
256
- let fields = ReventlessCore.GraphQL_SchemaInspector.inspectMutationFields(
257
- ~fieldPrefix="Foo",
258
- stringSchema,
259
- )
260
- expect(fields->Array.length)->toBe(0)
261
- })
279
+ testPromise(
280
+ "returns empty for non-object/non-union schema",
281
+ async () => {
282
+ let fields = ReventlessCore.GraphQL_SchemaInspector.inspectMutationFields(
283
+ ~fieldPrefix="Foo",
284
+ stringSchema,
285
+ )
286
+ expect(fields->Array.length)->toBe(0)
287
+ },
288
+ )
262
289
  })
263
290
 
264
291
  describe("inspectQueryFields", () => {
265
- testPromise("derives type def and query fields from state schema", async () => {
266
- let result = ReventlessCore.GraphQL_SchemaInspector.inspectQueryFields(
267
- ~name="Catalog_Product",
268
- ~typeName="CatalogProduct",
269
- testStateSchema,
270
- )
271
- expect(result.typeDef->Option.isSome)->toBe(true)
272
- let typeDef = result.typeDef->Option.getOrThrow
273
- expect(typeDef->String.includes("type CatalogProduct"))->toBe(true)
274
- expect(result.singleQuery->String.includes("Catalog_Product(id: ID!, includeRetired: Boolean)"))->toBe(true)
275
- expect(result.singleQuery->String.includes("CatalogProduct"))->toBe(true)
276
- expect(result.listQuery->Option.isSome)->toBe(true)
277
- let listQ = result.listQuery->Option.getOrThrow
278
- expect(listQ->String.includes("Catalog_Products"))->toBe(true)
279
- })
292
+ testPromise(
293
+ "derives type def and query fields from state schema",
294
+ async () => {
295
+ let result = ReventlessCore.GraphQL_SchemaInspector.inspectQueryFields(
296
+ ~name="Catalog_Product",
297
+ ~typeName="CatalogProduct",
298
+ testStateSchema,
299
+ )
300
+ expect(result.typeDef->Option.isSome)->toBe(true)
301
+ let typeDef = result.typeDef->Option.getOrThrow
302
+ expect(typeDef->String.includes("type CatalogProduct"))->toBe(true)
303
+ expect(
304
+ result.singleQuery->String.includes("Catalog_Product(id: ID!, includeRetired: Boolean)"),
305
+ )->toBe(true)
306
+ expect(result.singleQuery->String.includes("CatalogProduct"))->toBe(true)
307
+ expect(result.listQuery->Option.isSome)->toBe(true)
308
+ let listQ = result.listQuery->Option.getOrThrow
309
+ expect(listQ->String.includes("Catalog_Products"))->toBe(true)
310
+ },
311
+ )
280
312
  })
281
313
 
282
314
  // ── includeIdParam Tests ──────────────────────────────────────────────────
283
315
 
284
316
  describe("includeIdParam — ReadModel vs StateViewSlice", () => {
285
- testPromise("ReadModel fragment: query has (id: ID!) and type has injected id: ID!", async () => {
286
- let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
287
- ~mutationEntries=[],
288
- ~queryEntries=[
289
- {
290
- singleFieldName: "RM_Product",
291
- listFieldName: "RM_Products",
292
- returnTypeName: "RMProduct",
293
- stateSchema: testStateSchema->S.castToUnknown,
294
- authorization: None,
295
- includeIdParam: true,
296
- },
297
- ],
298
- )
299
- let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
300
- let sdl = inspection.sdlPreview
301
- expect(sdl->String.includes("RM_Product(id: ID!, includeRetired: Boolean): RMProduct"))->toBe(true)
302
- expect(sdl->String.includes("type RMProduct"))->toBe(true)
303
- // The type should have an injected id: ID! field (first field in the type)
304
- let typeLines = sdl->String.split("\n")
305
- let idFieldInType = typeLines->Array.some(line =>
306
- line->String.trim == "id: ID!" &&
307
- !(line->String.includes("("))
308
- )
309
- expect(idFieldInType)->toBe(true)
310
- })
317
+ testPromise(
318
+ "ReadModel fragment: query has (id: ID!) and type has injected id: ID!",
319
+ async () => {
320
+ let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
321
+ ~mutationEntries=[],
322
+ ~queryEntries=[
323
+ {
324
+ singleFieldName: "RM_Product",
325
+ listFieldName: "RM_Products",
326
+ returnTypeName: "RMProduct",
327
+ stateSchema: testStateSchema->S.castToUnknown,
328
+ authorization: None,
329
+ includeIdParam: true,
330
+ },
331
+ ],
332
+ )
333
+ let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
334
+ let sdl = inspection.sdlPreview
335
+ expect(
336
+ sdl->String.includes("RM_Product(id: ID!, includeRetired: Boolean): RMProduct"),
337
+ )->toBe(true)
338
+ expect(sdl->String.includes("type RMProduct"))->toBe(true)
339
+ // The type should have an injected id: ID! field (first field in the type)
340
+ let typeLines = sdl->String.split("\n")
341
+ let idFieldInType =
342
+ typeLines->Array.some(
343
+ line => line->String.trim == "id: ID!" && !(line->String.includes("(")),
344
+ )
345
+ expect(idFieldInType)->toBe(true)
346
+ },
347
+ )
311
348
 
312
- testPromise("StateViewSlice fragment: query has no (id: ID!) and type has no injected id", async () => {
313
- let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
314
- ~mutationEntries=[],
315
- ~queryEntries=[
316
- {
317
- singleFieldName: "SV_Item",
318
- listFieldName: "SV_Items",
319
- returnTypeName: "SVItem",
320
- stateSchema: svStateSchema->S.castToUnknown,
321
- authorization: None,
322
- includeIdParam: false,
323
- },
324
- ],
325
- )
326
- let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
327
- let sdl = inspection.sdlPreview
328
- // Single query should NOT have (id: ID!) parameter
329
- expect(sdl->String.includes("SV_Item: SVItem"))->toBe(true)
330
- expect(sdl->String.includes("SV_Item(id: ID!)"))->toBe(false)
331
- // The type should NOT have an injected id: ID! field
332
- expect(sdl->String.includes("type SVItem"))->toBe(true)
333
- expect(sdl->String.includes("productId: ID!"))->toBe(true)
334
- // Count id: ID! occurrences — should be zero (productId uses ID! but "id: ID!" standalone should not appear)
335
- let typeSection = sdl->String.split("type SVItem")->Array.get(1)->Option.getOr("")
336
- let typeEnd = typeSection->String.indexOf("}")
337
- let typeEnd = typeEnd >= 0 ? typeEnd : typeSection->String.length
338
- let typeBody = typeSection->String.slice(~start=0, ~end=typeEnd)
339
- let hasInjectedId = typeBody->String.split("\n")->Array.some(line =>
340
- line->String.trim == "id: ID!"
341
- )
342
- expect(hasInjectedId)->toBe(false)
343
- })
349
+ testPromise(
350
+ "StateViewSlice fragment: query has no (id: ID!) and type has no injected id",
351
+ async () => {
352
+ let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
353
+ ~mutationEntries=[],
354
+ ~queryEntries=[
355
+ {
356
+ singleFieldName: "SV_Item",
357
+ listFieldName: "SV_Items",
358
+ returnTypeName: "SVItem",
359
+ stateSchema: svStateSchema->S.castToUnknown,
360
+ authorization: None,
361
+ includeIdParam: false,
362
+ },
363
+ ],
364
+ )
365
+ let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
366
+ let sdl = inspection.sdlPreview
367
+ // Single query should NOT have (id: ID!) parameter
368
+ expect(sdl->String.includes("SV_Item: SVItem"))->toBe(true)
369
+ expect(sdl->String.includes("SV_Item(id: ID!)"))->toBe(false)
370
+ // The type should NOT have an injected id: ID! field
371
+ expect(sdl->String.includes("type SVItem"))->toBe(true)
372
+ expect(sdl->String.includes("productId: ID!"))->toBe(true)
373
+ // Count id: ID! occurrences — should be zero (productId uses ID! but "id: ID!" standalone should not appear)
374
+ let typeSection = sdl->String.split("type SVItem")->Array.get(1)->Option.getOr("")
375
+ let typeEnd = typeSection->String.indexOf("}")
376
+ let typeEnd = typeEnd >= 0 ? typeEnd : typeSection->String.length
377
+ let typeBody = typeSection->String.slice(~start=0, ~end=typeEnd)
378
+ let hasInjectedId =
379
+ typeBody->String.split("\n")->Array.some(line => line->String.trim == "id: ID!")
380
+ expect(hasInjectedId)->toBe(false)
381
+ },
382
+ )
344
383
 
345
- testPromise("default includeIdParam (omitted) behaves like ReadModel", async () => {
346
- let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
347
- ~mutationEntries=[],
348
- ~queryEntries=[
349
- {
350
- singleFieldName: "Default_Thing",
351
- listFieldName: "Default_Things",
352
- returnTypeName: "DefaultThing",
353
- stateSchema: testStateSchema->S.castToUnknown,
354
- authorization: None,
355
- },
356
- ],
357
- )
358
- let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
359
- let sdl = inspection.sdlPreview
360
- expect(sdl->String.includes("Default_Thing(id: ID!, includeRetired: Boolean): DefaultThing"))->toBe(true)
361
- })
384
+ testPromise(
385
+ "default includeIdParam (omitted) behaves like ReadModel",
386
+ async () => {
387
+ let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
388
+ ~mutationEntries=[],
389
+ ~queryEntries=[
390
+ {
391
+ singleFieldName: "Default_Thing",
392
+ listFieldName: "Default_Things",
393
+ returnTypeName: "DefaultThing",
394
+ stateSchema: testStateSchema->S.castToUnknown,
395
+ authorization: None,
396
+ },
397
+ ],
398
+ )
399
+ let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
400
+ let sdl = inspection.sdlPreview
401
+ expect(
402
+ sdl->String.includes("Default_Thing(id: ID!, includeRetired: Boolean): DefaultThing"),
403
+ )->toBe(true)
404
+ },
405
+ )
362
406
  })
363
407
 
364
408
  // ── Relay compliance — implements Node + Connection types ──────────────
365
409
 
366
410
  describe("Relay compliance", () => {
367
- testPromise("entity type includes 'implements Node' when includeIdParam is true", async () => {
368
- let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
369
- ~mutationEntries=[],
370
- ~queryEntries=[
371
- {
372
- singleFieldName: "Relay_Product",
373
- listFieldName: "Relay_Products",
374
- returnTypeName: "RelayProduct",
375
- stateSchema: testStateSchema->S.castToUnknown,
376
- authorization: None,
377
- includeIdParam: true,
378
- },
379
- ],
380
- )
381
- let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
382
- let sdl = inspection.sdlPreview
383
- expect(sdl->String.includes("type RelayProduct implements Node"))->toBe(true)
384
- expect(sdl->String.includes("id: ID!"))->toBe(true)
385
- })
411
+ testPromise(
412
+ "entity type includes 'implements Node' when includeIdParam is true",
413
+ async () => {
414
+ let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
415
+ ~mutationEntries=[],
416
+ ~queryEntries=[
417
+ {
418
+ singleFieldName: "Relay_Product",
419
+ listFieldName: "Relay_Products",
420
+ returnTypeName: "RelayProduct",
421
+ stateSchema: testStateSchema->S.castToUnknown,
422
+ authorization: None,
423
+ includeIdParam: true,
424
+ },
425
+ ],
426
+ )
427
+ let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
428
+ let sdl = inspection.sdlPreview
429
+ expect(sdl->String.includes("type RelayProduct implements Node"))->toBe(true)
430
+ expect(sdl->String.includes("id: ID!"))->toBe(true)
431
+ },
432
+ )
386
433
 
387
- testPromise("non-entity type does not include 'implements Node'", async () => {
388
- let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
389
- ~mutationEntries=[],
390
- ~queryEntries=[
391
- {
392
- singleFieldName: "Relay_View",
393
- listFieldName: "Relay_Views",
394
- returnTypeName: "RelayView",
395
- stateSchema: svStateSchema->S.castToUnknown,
396
- authorization: None,
397
- includeIdParam: false,
398
- },
399
- ],
400
- )
401
- let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
402
- let sdl = inspection.sdlPreview
403
- expect(sdl->String.includes("type RelayView implements Node"))->toBe(false)
404
- expect(sdl->String.includes("type RelayView {"))->toBe(true)
405
- })
434
+ testPromise(
435
+ "non-entity type does not include 'implements Node'",
436
+ async () => {
437
+ let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
438
+ ~mutationEntries=[],
439
+ ~queryEntries=[
440
+ {
441
+ singleFieldName: "Relay_View",
442
+ listFieldName: "Relay_Views",
443
+ returnTypeName: "RelayView",
444
+ stateSchema: svStateSchema->S.castToUnknown,
445
+ authorization: None,
446
+ includeIdParam: false,
447
+ },
448
+ ],
449
+ )
450
+ let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
451
+ let sdl = inspection.sdlPreview
452
+ expect(sdl->String.includes("type RelayView implements Node"))->toBe(false)
453
+ expect(sdl->String.includes("type RelayView {"))->toBe(true)
454
+ },
455
+ )
406
456
 
407
- testPromise("connectionSpec generates Edge and Connection types instead of plural wrapper", async () => {
408
- let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
409
- ~mutationEntries=[],
410
- ~queryEntries=[
411
- {
412
- singleFieldName: "Relay_Product",
413
- listFieldName: "Relay_Products",
414
- returnTypeName: "RelayProduct",
415
- stateSchema: testStateSchema->S.castToUnknown,
416
- authorization: None,
417
- includeIdParam: true,
418
- connectionSpec: true,
419
- },
420
- ],
421
- )
422
- let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
423
- let sdl = inspection.sdlPreview
424
- // Should have Edge and Connection types
425
- expect(sdl->String.includes("type RelayProductEdge"))->toBe(true)
426
- expect(sdl->String.includes("node: RelayProduct!"))->toBe(true)
427
- expect(sdl->String.includes("cursor: String!"))->toBe(true)
428
- expect(sdl->String.includes("type RelayProductConnection"))->toBe(true)
429
- expect(sdl->String.includes("edges: [RelayProductEdge!]!"))->toBe(true)
430
- expect(sdl->String.includes("pageInfo: PageInfo!"))->toBe(true)
431
- expect(sdl->String.includes("totalCount"))->toBe(false)
432
- // Query field should use filter + first/after/last/before args (Phase 4)
433
- expect(
434
- sdl->String.includes(
435
- "Relay_Products(filter: RelayProductFilter, first: Int, after: String, last: Int, before: String, includeRetired: Boolean): RelayProductConnection!",
436
- ),
437
- )->toBe(true)
438
- // Connection filter input with search/searchPrefix/ids
439
- expect(sdl->String.includes("input RelayProductFilter"))->toBe(true)
440
- expect(sdl->String.includes("search: String"))->toBe(true)
441
- expect(sdl->String.includes("searchPrefix: String"))->toBe(true)
442
- expect(sdl->String.includes("ids: [ID!]"))->toBe(true)
443
- // Should NOT have legacy plural wrapper type
444
- expect(sdl->String.includes("items: [RelayProduct!]!"))->toBe(false)
445
- expect(sdl->String.includes("nextToken:"))->toBe(false)
446
- })
457
+ testPromise(
458
+ "connectionSpec generates Edge and Connection types instead of plural wrapper",
459
+ async () => {
460
+ let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
461
+ ~mutationEntries=[],
462
+ ~queryEntries=[
463
+ {
464
+ singleFieldName: "Relay_Product",
465
+ listFieldName: "Relay_Products",
466
+ returnTypeName: "RelayProduct",
467
+ stateSchema: testStateSchema->S.castToUnknown,
468
+ authorization: None,
469
+ includeIdParam: true,
470
+ connectionSpec: true,
471
+ },
472
+ ],
473
+ )
474
+ let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
475
+ let sdl = inspection.sdlPreview
476
+ // Should have Edge and Connection types
477
+ expect(sdl->String.includes("type RelayProductEdge"))->toBe(true)
478
+ expect(sdl->String.includes("node: RelayProduct!"))->toBe(true)
479
+ expect(sdl->String.includes("cursor: String!"))->toBe(true)
480
+ expect(sdl->String.includes("type RelayProductConnection"))->toBe(true)
481
+ expect(sdl->String.includes("edges: [RelayProductEdge!]!"))->toBe(true)
482
+ expect(sdl->String.includes("pageInfo: PageInfo!"))->toBe(true)
483
+ expect(sdl->String.includes("totalCount"))->toBe(false)
484
+ // Query field should use filter + first/after/last/before args (Phase 4)
485
+ expect(
486
+ sdl->String.includes(
487
+ "Relay_Products(filter: RelayProductFilter, first: Int, after: String, last: Int, before: String, includeRetired: Boolean): RelayProductConnection!",
488
+ ),
489
+ )->toBe(true)
490
+ // Connection filter input with search/searchPrefix/ids
491
+ expect(sdl->String.includes("input RelayProductFilter"))->toBe(true)
492
+ expect(sdl->String.includes("search: String"))->toBe(true)
493
+ expect(sdl->String.includes("searchPrefix: String"))->toBe(true)
494
+ expect(sdl->String.includes("ids: [ID!]"))->toBe(true)
495
+ // Should NOT have legacy plural wrapper type
496
+ expect(sdl->String.includes("items: [RelayProduct!]!"))->toBe(false)
497
+ expect(sdl->String.includes("nextToken:"))->toBe(false)
498
+ },
499
+ )
447
500
 
448
- testPromise("subIdField generates ItemsFilter (distinct from connection Filter)", async () => {
449
- let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
450
- ~mutationEntries=[],
451
- ~queryEntries=[
452
- {
453
- singleFieldName: "Items_Product",
454
- listFieldName: "Items_Products",
455
- returnTypeName: "ItemsProduct",
456
- stateSchema: testStateSchema->S.castToUnknown,
457
- authorization: None,
458
- includeIdParam: true,
459
- connectionSpec: true,
460
- subIdField: "sku",
461
- },
462
- ],
463
- )
464
- let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
465
- let sdl = inspection.sdlPreview
466
- // Connection-level filter (search/searchPrefix/ids)
467
- expect(sdl->String.includes("input ItemsProductFilter"))->toBe(true)
468
- // Items query gets its own filter type (prefix/from/to/eq/order) — renamed
469
- // to ItemsFilter to disambiguate from the connection-level Filter.
470
- expect(sdl->String.includes("input ItemsProductItemsFilter"))->toBe(true)
471
- expect(
472
- sdl->String.includes(
473
- "Items_ProductItems(id: ID!, filter: ItemsProductItemsFilter,",
474
- ),
475
- )->toBe(true)
476
- })
501
+ testPromise(
502
+ "subIdField generates ItemsFilter (distinct from connection Filter)",
503
+ async () => {
504
+ let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
505
+ ~mutationEntries=[],
506
+ ~queryEntries=[
507
+ {
508
+ singleFieldName: "Items_Product",
509
+ listFieldName: "Items_Products",
510
+ returnTypeName: "ItemsProduct",
511
+ stateSchema: testStateSchema->S.castToUnknown,
512
+ authorization: None,
513
+ includeIdParam: true,
514
+ connectionSpec: true,
515
+ subIdField: "sku",
516
+ },
517
+ ],
518
+ )
519
+ let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
520
+ let sdl = inspection.sdlPreview
521
+ // Connection-level filter (search/searchPrefix/ids)
522
+ expect(sdl->String.includes("input ItemsProductFilter"))->toBe(true)
523
+ // Items query gets its own filter type (prefix/from/to/eq/order) — renamed
524
+ // to ItemsFilter to disambiguate from the connection-level Filter.
525
+ expect(sdl->String.includes("input ItemsProductItemsFilter"))->toBe(true)
526
+ expect(
527
+ sdl->String.includes("Items_ProductItems(id: ID!, filter: ItemsProductItemsFilter,"),
528
+ )->toBe(true)
529
+ },
530
+ )
477
531
 
478
- testPromise("explicit connectionSpec=false generates legacy plural wrapper (opt-out)", async () => {
479
- let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
480
- ~mutationEntries=[],
481
- ~queryEntries=[
482
- {
483
- singleFieldName: "Legacy_Product",
484
- listFieldName: "Legacy_Products",
485
- returnTypeName: "LegacyProduct",
486
- stateSchema: testStateSchema->S.castToUnknown,
487
- authorization: None,
488
- includeIdParam: true,
489
- connectionSpec: false,
490
- },
491
- ],
492
- )
493
- let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
494
- let sdl = inspection.sdlPreview
495
- expect(sdl->String.includes("items: [LegacyProduct!]!"))->toBe(true)
496
- expect(sdl->String.includes("nextToken: String"))->toBe(true)
497
- expect(sdl->String.includes("LegacyProductEdge"))->toBe(false)
498
- expect(sdl->String.includes("LegacyProductConnection"))->toBe(false)
499
- })
532
+ testPromise(
533
+ "explicit connectionSpec=false generates legacy plural wrapper (opt-out)",
534
+ async () => {
535
+ let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
536
+ ~mutationEntries=[],
537
+ ~queryEntries=[
538
+ {
539
+ singleFieldName: "Legacy_Product",
540
+ listFieldName: "Legacy_Products",
541
+ returnTypeName: "LegacyProduct",
542
+ stateSchema: testStateSchema->S.castToUnknown,
543
+ authorization: None,
544
+ includeIdParam: true,
545
+ connectionSpec: false,
546
+ },
547
+ ],
548
+ )
549
+ let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
550
+ let sdl = inspection.sdlPreview
551
+ expect(sdl->String.includes("items: [LegacyProduct!]!"))->toBe(true)
552
+ expect(sdl->String.includes("nextToken: String"))->toBe(true)
553
+ expect(sdl->String.includes("LegacyProductEdge"))->toBe(false)
554
+ expect(sdl->String.includes("LegacyProductConnection"))->toBe(false)
555
+ },
556
+ )
500
557
 
501
- testPromise("stitcher injects Node interface and node query", async () => {
502
- let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
503
- ~mutationEntries=[],
504
- ~queryEntries=[
505
- {
506
- singleFieldName: "Stitch_Product",
507
- listFieldName: "Stitch_Products",
508
- returnTypeName: "StitchProduct",
509
- stateSchema: testStateSchema->S.castToUnknown,
510
- authorization: None,
511
- includeIdParam: true,
512
- },
513
- ],
514
- )
515
- let sdl = ReventlessCore.GraphQL_Stitcher.stitch(~baseFragment=fragment, ~pluginFragments=[])
516
- expect(sdl->String.includes("interface Node"))->toBe(true)
517
- expect(sdl->String.includes("node(id: ID!): Node"))->toBe(true)
518
- expect(sdl->String.includes("type PageInfo"))->toBe(true)
519
- expect(sdl->String.includes("hasNextPage: Boolean!"))->toBe(true)
520
- })
558
+ testPromise(
559
+ "stitcher injects Node interface and node query",
560
+ async () => {
561
+ let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
562
+ ~mutationEntries=[],
563
+ ~queryEntries=[
564
+ {
565
+ singleFieldName: "Stitch_Product",
566
+ listFieldName: "Stitch_Products",
567
+ returnTypeName: "StitchProduct",
568
+ stateSchema: testStateSchema->S.castToUnknown,
569
+ authorization: None,
570
+ includeIdParam: true,
571
+ },
572
+ ],
573
+ )
574
+ let sdl = ReventlessCore.GraphQL_Stitcher.stitch(
575
+ ~baseFragment=fragment,
576
+ ~pluginFragments=[],
577
+ )
578
+ expect(sdl->String.includes("interface Node"))->toBe(true)
579
+ expect(sdl->String.includes("node(id: ID!): Node"))->toBe(true)
580
+ expect(sdl->String.includes("type PageInfo"))->toBe(true)
581
+ expect(sdl->String.includes("hasNextPage: Boolean!"))->toBe(true)
582
+ },
583
+ )
521
584
  })
522
585
 
523
586
  // ── Plugin Level — Fragment Inspector ───────────────────────────────────
524
587
 
525
588
  describe("inspectFragment", () => {
526
- testPromise("decodes and previews a fragment", async () => {
527
- let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
528
- ~mutationEntries=[
529
- {
530
- fieldNames: ["Test_Add"],
531
- commandSchema: addCommandSchema->S.castToUnknown,
532
- },
533
- ],
534
- ~queryEntries=[
535
- {
536
- singleFieldName: "Test_State",
537
- listFieldName: "Test_States",
538
- returnTypeName: "TestState",
539
- stateSchema: testStateSchema->S.castToUnknown,
540
- authorization: None,
541
- },
542
- ],
543
- )
544
- let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
545
- // TestState, TestStateEdge, TestStateConnection, TestStateFilter (connection filter)
546
- // + TestStateRef (the reference door's projection)
547
- // + CommandResult union + CommandAccepted + CommandRejected + CommandPending
548
- // (auto-injected whenever the fragment emits any mutation field).
549
- expect(inspection.types->Array.length)->toBe(9)
550
- expect(inspection.mutations->Array.length)->toBe(1)
551
- // single Test_State(id), list Test_States(...), Test_StatesByIds(ids: [String!]!)
552
- // and Test_StatesRefs(ids: [ID!]!) — the by-ids read projected to a reference.
553
- expect(inspection.queries->Array.length)->toBe(4)
554
- expect(inspection.sdlPreview->String.includes("type TestState"))->toBe(true)
555
- expect(inspection.sdlPreview->String.includes("type TestStateEdge"))->toBe(true)
556
- expect(inspection.sdlPreview->String.includes("type TestStateConnection"))->toBe(true)
557
- expect(inspection.sdlPreview->String.includes("input TestStateFilter"))->toBe(true)
558
- expect(inspection.sdlPreview->String.includes("edges: [TestStateEdge!]!"))->toBe(true)
559
- expect(inspection.sdlPreview->String.includes("Test_Add"))->toBe(true)
560
- expect(inspection.sdlPreview->String.includes("Test_State"))->toBe(true)
561
- expect(inspection.sdlPreview->String.includes("TestStateConnection!"))->toBe(true)
562
- expect(inspection.sdlPreview->String.includes("union CommandResult"))->toBe(true)
563
- })
589
+ testPromise(
590
+ "decodes and previews a fragment",
591
+ async () => {
592
+ let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
593
+ ~mutationEntries=[
594
+ {
595
+ fieldNames: ["Test_Add"],
596
+ commandSchema: addCommandSchema->S.castToUnknown,
597
+ },
598
+ ],
599
+ ~queryEntries=[
600
+ {
601
+ singleFieldName: "Test_State",
602
+ listFieldName: "Test_States",
603
+ returnTypeName: "TestState",
604
+ stateSchema: testStateSchema->S.castToUnknown,
605
+ authorization: None,
606
+ },
607
+ ],
608
+ )
609
+ let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
610
+ // TestState, TestStateEdge, TestStateConnection, TestStateFilter (connection filter)
611
+ // + TestStateRef (the reference door's projection)
612
+ // + CommandResult union + CommandAccepted + CommandRejected + CommandPending
613
+ // (auto-injected whenever the fragment emits any mutation field).
614
+ expect(inspection.types->Array.length)->toBe(9)
615
+ expect(inspection.mutations->Array.length)->toBe(1)
616
+ // single Test_State(id), list Test_States(...), Test_StatesByIds(ids: [String!]!)
617
+ // and Test_StatesRefs(ids: [ID!]!) — the by-ids read projected to a reference.
618
+ expect(inspection.queries->Array.length)->toBe(4)
619
+ expect(inspection.sdlPreview->String.includes("type TestState"))->toBe(true)
620
+ expect(inspection.sdlPreview->String.includes("type TestStateEdge"))->toBe(true)
621
+ expect(inspection.sdlPreview->String.includes("type TestStateConnection"))->toBe(true)
622
+ expect(inspection.sdlPreview->String.includes("input TestStateFilter"))->toBe(true)
623
+ expect(inspection.sdlPreview->String.includes("edges: [TestStateEdge!]!"))->toBe(true)
624
+ expect(inspection.sdlPreview->String.includes("Test_Add"))->toBe(true)
625
+ expect(inspection.sdlPreview->String.includes("Test_State"))->toBe(true)
626
+ expect(inspection.sdlPreview->String.includes("TestStateConnection!"))->toBe(true)
627
+ expect(inspection.sdlPreview->String.includes("union CommandResult"))->toBe(true)
628
+ },
629
+ )
564
630
  })
565
631
 
566
632
  describe("inspectPluginEntries", () => {
567
- testPromise("formats mutation and query entries", async () => {
568
- let summary = ReventlessCore.GraphQL_SchemaInspector.inspectPluginEntries(
569
- ~mutationEntries=[
570
- {
571
- fieldNames: ["Plugin_Add", "Plugin_Remove"],
572
- commandSchema: unionCommandSchema->S.castToUnknown,
573
- },
574
- ],
575
- ~queryEntries=[
576
- {
577
- singleFieldName: "Plugin_Item",
578
- listFieldName: "Plugin_Items",
579
- returnTypeName: "PluginItem",
580
- stateSchema: testStateSchema->S.castToUnknown,
581
- authorization: None,
582
- },
583
- ],
584
- )
585
- expect(summary->String.includes("Mutations (2)"))->toBe(true)
586
- expect(summary->String.includes("Plugin_Add"))->toBe(true)
587
- expect(summary->String.includes("Plugin_Remove"))->toBe(true)
588
- expect(summary->String.includes("Queries (1)"))->toBe(true)
589
- expect(summary->String.includes("Plugin_Item(id) -> PluginItem"))->toBe(true)
590
- })
633
+ testPromise(
634
+ "formats mutation and query entries",
635
+ async () => {
636
+ let summary = ReventlessCore.GraphQL_SchemaInspector.inspectPluginEntries(
637
+ ~mutationEntries=[
638
+ {
639
+ fieldNames: ["Plugin_Add", "Plugin_Remove"],
640
+ commandSchema: unionCommandSchema->S.castToUnknown,
641
+ },
642
+ ],
643
+ ~queryEntries=[
644
+ {
645
+ singleFieldName: "Plugin_Item",
646
+ listFieldName: "Plugin_Items",
647
+ returnTypeName: "PluginItem",
648
+ stateSchema: testStateSchema->S.castToUnknown,
649
+ authorization: None,
650
+ },
651
+ ],
652
+ )
653
+ expect(summary->String.includes("Mutations (2)"))->toBe(true)
654
+ expect(summary->String.includes("Plugin_Add"))->toBe(true)
655
+ expect(summary->String.includes("Plugin_Remove"))->toBe(true)
656
+ expect(summary->String.includes("Queries (1)"))->toBe(true)
657
+ expect(summary->String.includes("Plugin_Item(id) -> PluginItem"))->toBe(true)
658
+ },
659
+ )
591
660
  })
592
661
 
593
662
  // ── Platform Level — GraphQL_Server diagnostics ─────────────────────────
@@ -600,93 +669,102 @@ describe("GraphQL_SchemaInspector", () => {
600
669
  // one `*Id` field, so the key is knowable without `@id` and the queryable now
601
670
  // gets that key's eq filter and sort. The search-only case is the one below,
602
671
  // where nothing identifies the row.
603
- testPromise("read model with no annotations gets its inferred key's Eq + OrderBy", async () => {
604
- let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
605
- ~mutationEntries=[],
606
- ~queryEntries=[
607
- {
608
- singleFieldName: "Plain_View",
609
- listFieldName: "Plain_Views",
610
- returnTypeName: "PlainView",
611
- stateSchema: plainStateSchema->S.castToUnknown,
612
- authorization: None,
613
- includeIdParam: false,
614
- connectionSpec: true,
615
- },
616
- ],
617
- )
618
- let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
619
- let sdl = inspection.sdlPreview
620
- // The legacy block is untouched …
621
- expect(sdl->String.includes("input PlainViewFilter"))->toBe(true)
622
- expect(sdl->String.includes("search: String"))->toBe(true)
623
- expect(sdl->String.includes("searchPrefix: String"))->toBe(true)
624
- expect(sdl->String.includes("ids: [ID!]"))->toBe(true)
625
- // … and `productId`, the sole candidate, now narrows and sorts.
626
- expect(sdl->String.includes("productIdEq: ID"))->toBe(true)
627
- expect(sdl->String.includes("PlainViewOrderBy"))->toBe(true)
628
- expect(sdl->String.includes("PlainViewOrderField"))->toBe(true)
629
- expect(sdl->String.includes("orderBy:"))->toBe(true)
630
- // Only the key — an inferred key never drags the rest of the record in.
631
- expect(sdl->String.includes("nameEq:"))->toBe(false)
632
- })
672
+ testPromise(
673
+ "read model with no annotations gets its inferred key's Eq + OrderBy",
674
+ async () => {
675
+ let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
676
+ ~mutationEntries=[],
677
+ ~queryEntries=[
678
+ {
679
+ singleFieldName: "Plain_View",
680
+ listFieldName: "Plain_Views",
681
+ returnTypeName: "PlainView",
682
+ stateSchema: plainStateSchema->S.castToUnknown,
683
+ authorization: None,
684
+ includeIdParam: false,
685
+ connectionSpec: true,
686
+ },
687
+ ],
688
+ )
689
+ let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
690
+ let sdl = inspection.sdlPreview
691
+ // The legacy block is untouched …
692
+ expect(sdl->String.includes("input PlainViewFilter"))->toBe(true)
693
+ expect(sdl->String.includes("search: String"))->toBe(true)
694
+ expect(sdl->String.includes("searchPrefix: String"))->toBe(true)
695
+ expect(sdl->String.includes("ids: [ID!]"))->toBe(true)
696
+ // … and `productId`, the sole candidate, now narrows and sorts.
697
+ expect(sdl->String.includes("productIdEq: ID"))->toBe(true)
698
+ expect(sdl->String.includes("PlainViewOrderBy"))->toBe(true)
699
+ expect(sdl->String.includes("PlainViewOrderField"))->toBe(true)
700
+ expect(sdl->String.includes("orderBy:"))->toBe(true)
701
+ // Only the key — an inferred key never drags the rest of the record in.
702
+ expect(sdl->String.includes("nameEq:"))->toBe(false)
703
+ },
704
+ )
633
705
 
634
706
  // The remaining empty-capability case: no `*Id` field, so nothing says which
635
707
  // field identifies a row and the ladder declines rather than guessing.
636
- testPromise("read model whose key cannot be resolved stays search-only", async () => {
637
- let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
638
- ~mutationEntries=[],
639
- ~queryEntries=[
640
- {
641
- singleFieldName: "Keyless_View",
642
- listFieldName: "Keyless_Views",
643
- returnTypeName: "KeylessView",
644
- stateSchema: keylessStateSchema->S.castToUnknown,
645
- authorization: None,
646
- includeIdParam: false,
647
- connectionSpec: true,
648
- },
649
- ],
650
- )
651
- let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
652
- let sdl = inspection.sdlPreview
653
- expect(sdl->String.includes("input KeylessViewFilter"))->toBe(true)
654
- expect(sdl->String.includes("search: String"))->toBe(true)
655
- expect(sdl->String.includes("Eq:"))->toBe(false)
656
- expect(sdl->String.includes("KeylessViewOrderBy"))->toBe(false)
657
- expect(sdl->String.includes("KeylessViewOrderField"))->toBe(false)
658
- expect(sdl->String.includes("orderBy:"))->toBe(false)
659
- })
708
+ testPromise(
709
+ "read model whose key cannot be resolved stays search-only",
710
+ async () => {
711
+ let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
712
+ ~mutationEntries=[],
713
+ ~queryEntries=[
714
+ {
715
+ singleFieldName: "Keyless_View",
716
+ listFieldName: "Keyless_Views",
717
+ returnTypeName: "KeylessView",
718
+ stateSchema: keylessStateSchema->S.castToUnknown,
719
+ authorization: None,
720
+ includeIdParam: false,
721
+ connectionSpec: true,
722
+ },
723
+ ],
724
+ )
725
+ let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
726
+ let sdl = inspection.sdlPreview
727
+ expect(sdl->String.includes("input KeylessViewFilter"))->toBe(true)
728
+ expect(sdl->String.includes("search: String"))->toBe(true)
729
+ expect(sdl->String.includes("Eq:"))->toBe(false)
730
+ expect(sdl->String.includes("KeylessViewOrderBy"))->toBe(false)
731
+ expect(sdl->String.includes("KeylessViewOrderField"))->toBe(false)
732
+ expect(sdl->String.includes("orderBy:"))->toBe(false)
733
+ },
734
+ )
660
735
 
661
- testPromise("read model with @id + @index emits Eq filters + OrderBy + orderBy arg", async () => {
662
- let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
663
- ~mutationEntries=[],
664
- ~queryEntries=[
665
- {
666
- singleFieldName: "Indexed_Product",
667
- listFieldName: "Indexed_Products",
668
- returnTypeName: "IndexedProduct",
669
- stateSchema: indexedStateSchemaWithAnnotations->S.castToUnknown,
670
- authorization: None,
671
- includeIdParam: true,
672
- connectionSpec: true,
673
- },
674
- ],
675
- )
676
- let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
677
- let sdl = inspection.sdlPreview
678
- // Per-field eq inputs derived from @id and @index
679
- expect(sdl->String.includes("input IndexedProductFilter"))->toBe(true)
680
- expect(sdl->String.includes("productIdEq: ID"))->toBe(true)
681
- // ownerId is detected as an entity reference → ID (matches SchemaType.fromSury behaviour)
682
- expect(sdl->String.includes("ownerIdEq: ID"))->toBe(true)
683
- // OrderField enum lists the same fields
684
- expect(sdl->String.includes("enum IndexedProductOrderField"))->toBe(true)
685
- expect(sdl->String.includes("input IndexedProductOrderBy"))->toBe(true)
686
- expect(sdl->String.includes("direction: SortOrder!"))->toBe(true)
687
- // Connection field arg list includes orderBy
688
- expect(sdl->String.includes("orderBy: IndexedProductOrderBy"))->toBe(true)
689
- })
736
+ testPromise(
737
+ "read model with @id + @index emits Eq filters + OrderBy + orderBy arg",
738
+ async () => {
739
+ let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
740
+ ~mutationEntries=[],
741
+ ~queryEntries=[
742
+ {
743
+ singleFieldName: "Indexed_Product",
744
+ listFieldName: "Indexed_Products",
745
+ returnTypeName: "IndexedProduct",
746
+ stateSchema: indexedStateSchemaWithAnnotations->S.castToUnknown,
747
+ authorization: None,
748
+ includeIdParam: true,
749
+ connectionSpec: true,
750
+ },
751
+ ],
752
+ )
753
+ let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
754
+ let sdl = inspection.sdlPreview
755
+ // Per-field eq inputs derived from @id and @index
756
+ expect(sdl->String.includes("input IndexedProductFilter"))->toBe(true)
757
+ expect(sdl->String.includes("productIdEq: ID"))->toBe(true)
758
+ // ownerId is detected as an entity reference → ID (matches SchemaType.fromSury behaviour)
759
+ expect(sdl->String.includes("ownerIdEq: ID"))->toBe(true)
760
+ // OrderField enum lists the same fields
761
+ expect(sdl->String.includes("enum IndexedProductOrderField"))->toBe(true)
762
+ expect(sdl->String.includes("input IndexedProductOrderBy"))->toBe(true)
763
+ expect(sdl->String.includes("direction: SortOrder!"))->toBe(true)
764
+ // Connection field arg list includes orderBy
765
+ expect(sdl->String.includes("orderBy: IndexedProductOrderBy"))->toBe(true)
766
+ },
767
+ )
690
768
 
691
769
  testPromise(
692
770
  "read model with @scan / @scanSort folds opt-in fields into Filter / OrderBy",
@@ -763,94 +841,109 @@ describe("GraphQL_SchemaInspector", () => {
763
841
  expect(warnings)->toEqual([])
764
842
  },
765
843
  )
766
- testPromise("read model with @subId emits range filters + OrderBy on the sort key", async () => {
767
- let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
768
- ~mutationEntries=[],
769
- ~queryEntries=[
770
- {
771
- singleFieldName: "Ordered_Item",
772
- listFieldName: "Ordered_Items",
773
- returnTypeName: "OrderedItem",
774
- stateSchema: orderedStateSchemaWithAnnotations->S.castToUnknown,
775
- authorization: None,
776
- includeIdParam: true,
777
- connectionSpec: true,
778
- },
779
- ],
780
- )
781
- let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
782
- let sdl = inspection.sdlPreview
783
- expect(sdl->String.includes("createdAtEq: String"))->toBe(true)
784
- expect(sdl->String.includes("createdAtFrom: String"))->toBe(true)
785
- expect(sdl->String.includes("createdAtTo: String"))->toBe(true)
786
- expect(sdl->String.includes("enum OrderedItemOrderField"))->toBe(true)
787
- expect(sdl->String.includes("orderBy: OrderedItemOrderBy"))->toBe(true)
788
- })
844
+ testPromise(
845
+ "read model with @subId emits range filters + OrderBy on the sort key",
846
+ async () => {
847
+ let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
848
+ ~mutationEntries=[],
849
+ ~queryEntries=[
850
+ {
851
+ singleFieldName: "Ordered_Item",
852
+ listFieldName: "Ordered_Items",
853
+ returnTypeName: "OrderedItem",
854
+ stateSchema: orderedStateSchemaWithAnnotations->S.castToUnknown,
855
+ authorization: None,
856
+ includeIdParam: true,
857
+ connectionSpec: true,
858
+ },
859
+ ],
860
+ )
861
+ let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
862
+ let sdl = inspection.sdlPreview
863
+ expect(sdl->String.includes("createdAtEq: String"))->toBe(true)
864
+ expect(sdl->String.includes("createdAtFrom: String"))->toBe(true)
865
+ expect(sdl->String.includes("createdAtTo: String"))->toBe(true)
866
+ expect(sdl->String.includes("enum OrderedItemOrderField"))->toBe(true)
867
+ expect(sdl->String.includes("orderBy: OrderedItemOrderBy"))->toBe(true)
868
+ },
869
+ )
789
870
  })
790
871
 
791
872
  describe("GraphQL_Server diagnostics", () => {
792
- testPromise("diagnostics detects resolver without SDL field", async () => {
793
- GraphQL_Server.reset()
794
- let resolvers = Dict.make()
795
- resolvers->Dict.set("orphanResolver", async (_root, _args, _ctx) => JSON.Encode.string("ok"))
796
- GraphQL_Server.registerMutations(~sdlFields=[], ~resolvers)
797
- let d = GraphQL_Server.diagnostics()
798
- expect(d.resolverMutationCount)->toBe(1)
799
- expect(d.sdlMutationCount)->toBe(0)
800
- expect(d.mismatches->Array.length)->toBe(1)
801
- expect(
802
- (d.mismatches->Array.getUnsafe(0))->String.includes("resolver but no SDL field"),
803
- )->toBe(true)
804
- GraphQL_Server.reset()
805
- })
873
+ testPromise(
874
+ "diagnostics detects resolver without SDL field",
875
+ async () => {
876
+ GraphQL_Server.reset()
877
+ let resolvers = Dict.make()
878
+ resolvers->Dict.set(
879
+ "orphanResolver",
880
+ async (_root, _args, _ctx) => JSON.Encode.string("ok"),
881
+ )
882
+ GraphQL_Server.registerMutations(~sdlFields=[], ~resolvers)
883
+ let d = GraphQL_Server.diagnostics()
884
+ expect(d.resolverMutationCount)->toBe(1)
885
+ expect(d.sdlMutationCount)->toBe(0)
886
+ expect(d.mismatches->Array.length)->toBe(1)
887
+ expect(
888
+ d.mismatches->Array.getUnsafe(0)->String.includes("resolver but no SDL field"),
889
+ )->toBe(true)
890
+ GraphQL_Server.reset()
891
+ },
892
+ )
806
893
 
807
- testPromise("diagnostics detects SDL field without resolver", async () => {
808
- GraphQL_Server.reset()
809
- GraphQL_Server.registerQueries(
810
- ~sdlFields=[" orphanField(id: ID!): String"],
811
- ~resolvers=Dict.make(),
812
- )
813
- let d = GraphQL_Server.diagnostics()
814
- expect(d.sdlQueryCount)->toBe(1)
815
- expect(d.resolverQueryCount)->toBe(0)
816
- expect(d.mismatches->Array.length)->toBe(1)
817
- expect(
818
- (d.mismatches->Array.getUnsafe(0))->String.includes("SDL field but no resolver"),
819
- )->toBe(true)
820
- GraphQL_Server.reset()
821
- })
894
+ testPromise(
895
+ "diagnostics detects SDL field without resolver",
896
+ async () => {
897
+ GraphQL_Server.reset()
898
+ GraphQL_Server.registerQueries(
899
+ ~sdlFields=[" orphanField(id: ID!): String"],
900
+ ~resolvers=Dict.make(),
901
+ )
902
+ let d = GraphQL_Server.diagnostics()
903
+ expect(d.sdlQueryCount)->toBe(1)
904
+ expect(d.resolverQueryCount)->toBe(0)
905
+ expect(d.mismatches->Array.length)->toBe(1)
906
+ expect(
907
+ d.mismatches->Array.getUnsafe(0)->String.includes("SDL field but no resolver"),
908
+ )->toBe(true)
909
+ GraphQL_Server.reset()
910
+ },
911
+ )
822
912
 
823
- testPromise("diagnostics reports no mismatches when fields and resolvers match", async () => {
824
- GraphQL_Server.reset()
825
- let resolvers = Dict.make()
826
- resolvers->Dict.set("myQuery", async (_root, _args, _ctx) => JSON.Encode.string("ok"))
827
- GraphQL_Server.registerQueries(
828
- ~sdlFields=[" myQuery(id: ID!): String"],
829
- ~resolvers,
830
- )
831
- let d = GraphQL_Server.diagnostics()
832
- expect(d.mismatches->Array.length)->toBe(0)
833
- expect(d.sdlQueryCount)->toBe(1)
834
- expect(d.resolverQueryCount)->toBe(1)
835
- GraphQL_Server.reset()
836
- })
913
+ testPromise(
914
+ "diagnostics reports no mismatches when fields and resolvers match",
915
+ async () => {
916
+ GraphQL_Server.reset()
917
+ let resolvers = Dict.make()
918
+ resolvers->Dict.set("myQuery", async (_root, _args, _ctx) => JSON.Encode.string("ok"))
919
+ GraphQL_Server.registerQueries(~sdlFields=[" myQuery(id: ID!): String"], ~resolvers)
920
+ let d = GraphQL_Server.diagnostics()
921
+ expect(d.mismatches->Array.length)->toBe(0)
922
+ expect(d.sdlQueryCount)->toBe(1)
923
+ expect(d.resolverQueryCount)->toBe(1)
924
+ GraphQL_Server.reset()
925
+ },
926
+ )
837
927
 
838
- testPromise("getFullSdl returns None before start", async () => {
839
- GraphQL_Server.reset()
840
- expect(GraphQL_Server.getFullSdl()->Option.isNone)->toBe(true)
841
- })
928
+ testPromise(
929
+ "getFullSdl returns None before start",
930
+ async () => {
931
+ GraphQL_Server.reset()
932
+ expect(GraphQL_Server.getFullSdl()->Option.isNone)->toBe(true)
933
+ },
934
+ )
842
935
 
843
- testPromise("getRegisteredSdl returns Query/Mutation SDL", async () => {
844
- GraphQL_Server.reset()
845
- GraphQL_Server.registerQueries(
846
- ~sdlFields=[" hello: String"],
847
- ~resolvers=Dict.make(),
848
- )
849
- let sdl = GraphQL_Server.getRegisteredSdl()
850
- expect(sdl->String.includes("hello: String"))->toBe(true)
851
- expect(sdl->String.includes("type Query"))->toBe(true)
852
- GraphQL_Server.reset()
853
- })
936
+ testPromise(
937
+ "getRegisteredSdl returns Query/Mutation SDL",
938
+ async () => {
939
+ GraphQL_Server.reset()
940
+ GraphQL_Server.registerQueries(~sdlFields=[" hello: String"], ~resolvers=Dict.make())
941
+ let sdl = GraphQL_Server.getRegisteredSdl()
942
+ expect(sdl->String.includes("hello: String"))->toBe(true)
943
+ expect(sdl->String.includes("type Query"))->toBe(true)
944
+ GraphQL_Server.reset()
945
+ },
946
+ )
854
947
  })
855
948
  })
856
949
 
@@ -877,19 +970,22 @@ describe("DCB StateChangeSlice — one mutation per command constructor", () =>
877
970
  expect(fieldNames)->toEqual(["Catalog_AddCategory"])
878
971
  })
879
972
 
880
- testPromise("multi-command slice emits one `${plugin}_${command}` field per constructor", async () => {
881
- // The command constructor already names the operation; callers/plans expect
882
- // `${plugin}_${command}` (e.g. Platform_SyncComponent / Platform_RemoveComponent),
883
- // NOT the aggregate-style `${plugin}_${slice}_${command}` doubling which both
884
- // renamed the primary command and overflowed AppSync's subscription cap.
885
- let fieldNames =
886
- ReventlessCore.Api_Naming.sliceMutationFields(
887
- ~plugin="Ordering",
888
- ~slice="SyncCatalogProduct",
889
- ~commandSchema=dcbMultiCommandSchema->S.castToUnknown,
890
- )->Array.map(((f, _)) => f)
891
- expect(fieldNames)->toEqual(["Ordering_SyncNewProduct", "Ordering_ChangeSyncedPrice"])
892
- })
973
+ testPromise(
974
+ "multi-command slice emits one `${plugin}_${command}` field per constructor",
975
+ async () => {
976
+ // The command constructor already names the operation; callers/plans expect
977
+ // `${plugin}_${command}` (e.g. Platform_SyncComponent / Platform_RemoveComponent),
978
+ // NOT the aggregate-style `${plugin}_${slice}_${command}` doubling — which both
979
+ // renamed the primary command and overflowed AppSync's subscription cap.
980
+ let fieldNames =
981
+ ReventlessCore.Api_Naming.sliceMutationFields(
982
+ ~plugin="Ordering",
983
+ ~slice="SyncCatalogProduct",
984
+ ~commandSchema=dcbMultiCommandSchema->S.castToUnknown,
985
+ )->Array.map(((f, _)) => f)
986
+ expect(fieldNames)->toEqual(["Ordering_SyncNewProduct", "Ordering_ChangeSyncedPrice"])
987
+ },
988
+ )
893
989
 
894
990
  testPromise(
895
991
  "multi-command slice: both mutations carry their own args, plus both subscriptions (each ≤ 50 chars)",
@@ -914,9 +1010,7 @@ describe("DCB StateChangeSlice — one mutation per command constructor", () =>
914
1010
  ),
915
1011
  )->toBe(true)
916
1012
  expect(
917
- sdl->String.includes(
918
- "Ordering_ChangeSyncedPrice(id: ID!, productId: ID!, price: Float!)",
919
- ),
1013
+ sdl->String.includes("Ordering_ChangeSyncedPrice(id: ID!, productId: ID!, price: Float!)"),
920
1014
  )->toBe(true)
921
1015
 
922
1016
  // Subscriptions fan out one-per-field over the same entry.
@@ -929,13 +1023,14 @@ describe("DCB StateChangeSlice — one mutation per command constructor", () =>
929
1023
  expect(subSdl->String.includes("onOrdering_ChangeSyncedPrice"))->toBe(true)
930
1024
 
931
1025
  // Every emitted subscription field name stays within AppSync's 50-char cap.
932
- subs.subscriptionFields->Array.forEach(sub => {
933
- // `sub` is the full SDL line ` on<Field>(id: ID): CommandResult\n ...`;
934
- // extract the leading `on<Field>` token to length-check the field name itself.
935
- let name =
936
- sub->String.trim->String.split("(")->Array.getUnsafe(0)
937
- expect(name->String.length <= 50)->toBe(true)
938
- })
1026
+ subs.subscriptionFields->Array.forEach(
1027
+ sub => {
1028
+ // `sub` is the full SDL line ` on<Field>(id: ID): CommandResult\n ...`;
1029
+ // extract the leading `on<Field>` token to length-check the field name itself.
1030
+ let name = sub->String.trim->String.split("(")->Array.getUnsafe(0)
1031
+ expect(name->String.length <= 50)->toBe(true)
1032
+ },
1033
+ )
939
1034
  },
940
1035
  )
941
1036
 
@@ -962,22 +1057,25 @@ describe("DCB StateChangeSlice — one mutation per command constructor", () =>
962
1057
  })
963
1058
 
964
1059
  describe("Plugin admin fragment — kind enum + kindEq filter", () => {
965
- testPromise("Platform_Plugin exposes a PluginKind enum and Platform_Plugins gains kindEq", async () => {
966
- let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
967
- ~mutationEntries=[],
968
- ~queryEntries=ReventlessCore.PluginBaseFragment.queryEntries,
969
- )
970
- let sdl = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment).sdlPreview
971
- // kind is a real enum on the Plugin type (auto-derived from the payload-less variant)
972
- expect(sdl->String.includes("enum Platform_PluginKind"))->toBe(true)
973
- expect(sdl->String.includes("PlatformInfrastructure"))->toBe(true)
974
- // `kind` is `option<pluginKind>` — nullable on purpose so a kind-less legacy row
975
- // can't collapse the whole Platform_Plugins query. It must render without a `!`.
976
- expect(sdl->String.includes("kind: Platform_PluginKind"))->toBe(true)
977
- expect(sdl->String.includes("kind: Platform_PluginKind!"))->toBe(false)
978
- // @scan folds a server-side equality filter for the panel split
979
- expect(sdl->String.includes("kindEq: String"))->toBe(true)
980
- })
1060
+ testPromise(
1061
+ "Platform_Plugin exposes a PluginKind enum and Platform_Plugins gains kindEq",
1062
+ async () => {
1063
+ let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
1064
+ ~mutationEntries=[],
1065
+ ~queryEntries=ReventlessCore.PluginBaseFragment.queryEntries,
1066
+ )
1067
+ let sdl = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment).sdlPreview
1068
+ // kind is a real enum on the Plugin type (auto-derived from the payload-less variant)
1069
+ expect(sdl->String.includes("enum Platform_PluginKind"))->toBe(true)
1070
+ expect(sdl->String.includes("PlatformInfrastructure"))->toBe(true)
1071
+ // `kind` is `option<pluginKind>` — nullable on purpose so a kind-less legacy row
1072
+ // can't collapse the whole Platform_Plugins query. It must render without a `!`.
1073
+ expect(sdl->String.includes("kind: Platform_PluginKind"))->toBe(true)
1074
+ expect(sdl->String.includes("kind: Platform_PluginKind!"))->toBe(false)
1075
+ // @scan folds a server-side equality filter for the panel split
1076
+ expect(sdl->String.includes("kindEq: String"))->toBe(true)
1077
+ },
1078
+ )
981
1079
  })
982
1080
 
983
1081
  describe("optional enum fields preserve GraphQL nullability", () => {