@reventlessdev/reventless-core 3.0.0-alpha.250 → 3.0.0-alpha.252

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 (70) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/package.json +7 -7
  3. package/src/admin/Platform_Admin_Structure.res +4 -1
  4. package/src/admin/Platform_Admin_Structure.res.mjs +4 -2
  5. package/src/admin/Platform_BakedManifest.res.mjs +3 -1
  6. package/src/admin/Platform_ComponentDefinitionsApi.res +31 -1
  7. package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +29 -1
  8. package/src/admin/Platform_PluginStructuresApi.res +80 -8
  9. package/src/admin/Platform_PluginStructuresApi.res.mjs +76 -2
  10. package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res.mjs +8 -0
  11. package/src/components/Api/ApiNoApiHelpers.res +37 -3
  12. package/src/components/Api/ApiNoApiHelpers.res.mjs +29 -2
  13. package/src/components/AutomationSlice/AutomationSlice_Callback.res +4 -4
  14. package/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs +3 -3
  15. package/src/plugin/api/PluginBaseFragment.res +3 -3
  16. package/src/plugin/component/Plugin_Structure.res +787 -384
  17. package/src/plugin/component/Plugin_Structure.res.mjs +430 -20
  18. package/src/plugin/connect/PluginConnectExtension_Mapping.res +3 -6
  19. package/src/plugin/connect/PluginConnectExtension_Mapping.res.mjs +14 -2
  20. package/src/plugin/connect/PluginExtensionPoint_Plugin.res +0 -1
  21. package/src/plugin/connect/PluginExtensionPoint_Plugin.res.mjs +63 -3
  22. package/src/plugin/connect/PluginExtensionPoint_UiFragment.res.mjs +22 -3
  23. package/src/plugin/lifecycle/PluginBehavior.res +11 -11
  24. package/src/plugin/lifecycle/PluginBehavior.res.mjs +11 -11
  25. package/src/plugin/lifecycle/PluginSpec.res +11 -3
  26. package/src/plugin/lifecycle/PluginSpec.res.mjs +11 -3
  27. package/tests/admin/AdminApiSchemaDriftTest.res +486 -0
  28. package/tests/admin/AdminApiSchemaDriftTest.res.mjs +619 -0
  29. package/tests/admin/Platform_Admin_StructureTest.res +8 -3
  30. package/tests/admin/Platform_Admin_StructureTest.res.mjs +8 -2
  31. package/tests/admin/Platform_BakedManifestTest.res +2 -0
  32. package/tests/admin/Platform_BakedManifestTest.res.mjs +3 -1
  33. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +10 -0
  34. package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +30 -8
  35. package/tests/admin/Platform_PluginStructuresApiTest.res +28 -1
  36. package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +27 -5
  37. package/tests/aggregate/AggregateCacheTest.res.mjs +15 -3
  38. package/tests/aggregate/AggregateConflictTest.res.mjs +15 -3
  39. package/tests/aggregate/AggregateFixtures.res.mjs +15 -3
  40. package/tests/aggregate/AggregateSnapshotTest.res.mjs +21 -5
  41. package/tests/api/ApiNoApiTest.res +46 -0
  42. package/tests/api/ApiNoApiTest.res.mjs +49 -1
  43. package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +27 -3
  44. package/tests/commandgenerator/CommandGeneratorFixtures.res.mjs +12 -2
  45. package/tests/commandtopic/CommandOutcomeHookTest.res.mjs +2 -0
  46. package/tests/commandtopic/CommandTopicCallbackFixtures.res.mjs +12 -2
  47. package/tests/dcb/DcbFixtures.res.mjs +8 -0
  48. package/tests/dcb/DcbStateChangeSliceTest.res.mjs +18 -0
  49. package/tests/eventmapper/EventMapperFixtures.res.mjs +15 -3
  50. package/tests/extensionpoint/ExtensionPointFixtures.res.mjs +9 -1
  51. package/tests/extensionpoint/ExtensionPointOperationsTest.res.mjs +9 -1
  52. package/tests/message/MessageTest.res +2 -0
  53. package/tests/message/MessageTest.res.mjs +3 -1
  54. package/tests/plugin/PluginStructureAccessTest.res.mjs +2 -0
  55. package/tests/plugin/PluginStructureTest.res +294 -1
  56. package/tests/plugin/PluginStructureTest.res.mjs +373 -3
  57. package/tests/plugin/StateChangeSlice/PsAttachInvoice.res.mjs +8 -0
  58. package/tests/plugin/StateChangeSlice/PsChangePhoto.res.mjs +8 -0
  59. package/tests/plugin/StateChangeSlice/PsDispatchShipment.res.mjs +8 -0
  60. package/tests/plugin/StateChangeSlice/PsGatedCommands.res.mjs +8 -0
  61. package/tests/plugin/StateChangeSlice/PsPlaceOrder.res.mjs +8 -0
  62. package/tests/plugin/StateChangeSlice/PsReserveStock.res.mjs +8 -0
  63. package/tests/plugin/StateChangeSlice/PsShipOrder.res.mjs +8 -0
  64. package/tests/plugin/StateChangeSlice/PsTransitionSwitch.res +59 -0
  65. package/tests/plugin/StateChangeSlice/PsTransitionSwitch.res.mjs +115 -0
  66. package/tests/plugin/StateChangeSlice/PsTypoStore.res.mjs +8 -0
  67. package/tests/plugin/StateChangeSlice/PsUploadAvatar.res.mjs +8 -0
  68. package/tests/plugin/StateChangeSlice/PsUploadImages.res.mjs +8 -0
  69. package/tests/plugin/pluginDefinitionRequiredScalars.txt +26 -0
  70. package/tests/util/CommandPublisherTest.res.mjs +9 -1
@@ -0,0 +1,486 @@
1
+ // Structural guard for the three Platform_* admin queries. Every field of the
2
+ // record an entry is built from must reach both the SDL and the encoder, or be
3
+ // named below as a deliberate divergence. The tests beside this one pin values;
4
+ // this one pins the field SET — the thing that drifted silently when `chapter`
5
+ // and `externalSystem` were encoded for months with no SDL line to select them.
6
+
7
+ open JestGlobals
8
+ open Reventless.Plugin
9
+
10
+ // ── Field-name extraction ───────────────────────────────────────────────────
11
+
12
+ // The SDL blocks are one string per type, `type X {\n field: T\n}`. A line with
13
+ // no colon is the header or the brace.
14
+ let sdlFieldNames = (~typeName: string, sdlTypes: array<string>): array<string> =>
15
+ sdlTypes
16
+ ->Array.find(block => block->String.startsWith(`type ${typeName} {`))
17
+ ->Option.mapOr([], block =>
18
+ block
19
+ ->String.split("\n")
20
+ ->Array.filterMap(line => {
21
+ let trimmed = line->String.trim
22
+ switch trimmed->String.indexOf(":") {
23
+ | -1 => None
24
+ | i => Some(trimmed->String.slice(~start=0, ~end=i))
25
+ }
26
+ })
27
+ )
28
+
29
+ let jsonKeys = (j: JSON.t): array<string> =>
30
+ switch j {
31
+ | JSON.Object(d) => d->Dict.keysToArray
32
+ | _ => []
33
+ }
34
+
35
+ let schemaKeys = (schema: S.t<'a>): array<string> =>
36
+ SchemaType.fromSuryObject(~typeName="", schema->S.castToUnknown)->Option.mapOr(
37
+ [],
38
+ Dict.keysToArray,
39
+ )
40
+
41
+ // Nested types (`handledEvents`, `publishedEvents`) are encoded inline by their
42
+ // parent, so their wire keys are read off the first element rather than from an
43
+ // encoder of their own.
44
+ let firstOf = (j: JSON.t, key: string): JSON.t =>
45
+ switch j {
46
+ | JSON.Object(d) =>
47
+ switch d->Dict.get(key) {
48
+ | Some(JSON.Array(items)) => items->Array.get(0)->Option.getOr(JSON.Encode.null)
49
+ | _ => JSON.Encode.null
50
+ }
51
+ | _ => JSON.Encode.null
52
+ }
53
+
54
+ let missing = (names: array<string>, from: array<string>): array<string> =>
55
+ names->Array.filter(n => !(from->Array.includes(n)))
56
+
57
+ // ── Fixtures: every optional field populated, so no key can go missing ───────
58
+
59
+ let fieldRef: fieldReference = {
60
+ fieldName: "categoryId",
61
+ entity: "Category",
62
+ plugin: Some("Catalog"),
63
+ }
64
+
65
+ let cmd: commandDef = {
66
+ name: "Add",
67
+ schema: "{}",
68
+ level: Collection,
69
+ aggregateIdField: Some("productId"),
70
+ mutationField: "Catalog_Product_Add",
71
+ references: [fieldRef],
72
+ allowedStates: Some(["Draft"]),
73
+ targetState: Some("Active"),
74
+ apiExposed: Some(true),
75
+ requiredAccess: Some(["catalog.write"]),
76
+ ownerField: Some("ownerId"),
77
+ }
78
+
79
+ let evt: eventDef = {name: "ProductAdded", schema: "{}", references: [fieldRef]}
80
+ let err: errorDef = {name: "ProductAlreadyExists", schema: "{}", references: []}
81
+
82
+ let writable: writableDef = {
83
+ name: "Product",
84
+ commands: [cmd],
85
+ producedEventTypes: ["ProductAdded"],
86
+ consumedEventTypes: [],
87
+ linkedViews: ["Products"],
88
+ consistencyRead: Some("Products"),
89
+ events: [evt],
90
+ errors: [err],
91
+ chapter: Some("Catalogue"),
92
+ }
93
+
94
+ let queryable: queryableDef = {
95
+ name: "Products",
96
+ queryField: "Catalog_Products",
97
+ schema: "{}",
98
+ consumedEventTypes: ["ProductAdded"],
99
+ linkedWriteSide: ["Product"],
100
+ labelField: "displayName",
101
+ searchableFields: ["displayName"],
102
+ labelFieldSource: Some("annotation"),
103
+ lifecycleField: Some("lifecycle"),
104
+ ownerField: Some("ownerId"),
105
+ retiredField: Some("archived"),
106
+ retiredValues: Some(["Archived"]),
107
+ namedWhenRetired: Some(true),
108
+ visibility: None,
109
+ chapter: Some("Catalogue"),
110
+ singleQueryField: Some("Catalog_Product"),
111
+ idField: Some("productId"),
112
+ idFieldSource: Some("convention"),
113
+ requiredAccess: Some(["catalog.read"]),
114
+ }
115
+
116
+ let automation: automationSliceDef = {
117
+ name: "Restocker",
118
+ consumedEventTypes: ["StockLow"],
119
+ producedCommandTypes: ["Restock"],
120
+ targetName: "Product",
121
+ chapter: Some("Replenishment"),
122
+ }
123
+
124
+ let outbound: outboundTranslationSliceDef = {
125
+ name: "ToShipper",
126
+ consumedEventTypes: ["OrderPlaced"],
127
+ inboundCommandTypes: ["Ship"],
128
+ targetName: Some("Shipment"),
129
+ externalSystem: Some("Shipper"),
130
+ chapter: Some("Fulfilment"),
131
+ }
132
+
133
+ let inbound: inboundTranslationSliceDef = {
134
+ name: "FromPsp",
135
+ commandTypes: ["RecordPayment"],
136
+ targetName: "Order",
137
+ externalSystem: Some("Psp"),
138
+ chapter: Some("Payments"),
139
+ }
140
+
141
+ let extension: extensionDef = {
142
+ name: "Catalog.Products",
143
+ delegateNames: ["Product"],
144
+ eventTypes: ["ProductAdded"],
145
+ commandTypes: ["Add"],
146
+ handledEvents: Some([{name: "ProductAdded", toCommandTypes: ["Ordering.SyncNewProduct"]}]),
147
+ issuedCommands: Some([{name: "Catalog.Products.Ack", fromEventTypes: ["Ordering.Synced"]}]),
148
+ }
149
+
150
+ let extensionPoint: extensionPointDef = {
151
+ name: "Catalog.Products",
152
+ delegateNames: ["Product"],
153
+ sourceEventTypes: ["Catalog.ProductAdded"],
154
+ commandTypes: Some(["Add"]),
155
+ publishedEvents: Some([
156
+ {name: "Catalog.Products.ProductAdded", fromEventTypes: ["Catalog.ProductAdded"]},
157
+ ]),
158
+ acceptedCommands: Some([{name: "Catalog.Products.Add", toCommandTypes: ["Catalog.Add"]}]),
159
+ }
160
+
161
+ let storeDecl: requiredStoreDeclaration = {
162
+ store: "catalog.images",
163
+ component: "Product",
164
+ field: "image",
165
+ annotation: Some("images"),
166
+ }
167
+
168
+ let capabilityDecl: requiredCapabilityDeclaration = {
169
+ capability: "Geocoding",
170
+ component: "GeocodeAddress",
171
+ }
172
+
173
+ let structure: pluginStructure = {
174
+ readModels: [queryable],
175
+ stateViewSlices: [],
176
+ stateChangeSlices: [],
177
+ aggregates: [writable],
178
+ automationSlices: [automation],
179
+ outboundTranslationSlices: [outbound],
180
+ inboundTranslationSlices: [inbound],
181
+ extensions: [extension],
182
+ extensionPoints: Some([extensionPoint]),
183
+ requiredStores: Some(["catalog.images"]),
184
+ requiredStoreDeclarations: Some([storeDecl]),
185
+ requiredCapabilities: Some([capabilityDecl]),
186
+ traitDeclarations: None,
187
+ }
188
+
189
+ let panel: panelManifestEntry = {
190
+ fragmentId: "Catalog.Products.list",
191
+ title: "Products",
192
+ description: "",
193
+ positions: ["platform-summary"],
194
+ requiredAccess: Some("admin"),
195
+ }
196
+
197
+ let menu: menuEntry = {
198
+ label: "Products",
199
+ icon: Some("box"),
200
+ group: Some("Catalog"),
201
+ sortOrder: 0,
202
+ }
203
+
204
+ let page: pageManifestEntry = {
205
+ fragmentId: "Catalog.Products.list",
206
+ title: "Products",
207
+ menuEntry: menu,
208
+ requiredAccess: Some("admin"),
209
+ }
210
+
211
+ let uiState: UiFragments.state = {
212
+ pluginId: "Catalog",
213
+ remoteEntryUrl: "https://cdn.example.test/catalog@1.0/remoteEntry.js",
214
+ panels: [panel],
215
+ pages: [page],
216
+ registeredAt: "2026-01-01T00:00:00Z",
217
+ updatedAt: "2026-01-01T00:00:00Z",
218
+ }
219
+
220
+ // ── Guards ──────────────────────────────────────────────────────────────────
221
+
222
+ type guard = {
223
+ gqlType: string,
224
+ sdlTypes: array<string>,
225
+ recordFields: array<string>,
226
+ wireFields: array<string>,
227
+ /** Wire fields the encoder computes, with no counterpart on the record. */
228
+ wireOnly: array<string>,
229
+ /** Record fields deliberately kept off this query's wire. */
230
+ recordOnly: array<string>,
231
+ }
232
+
233
+ let componentSdl = Platform_ComponentDefinitionsApi.sdlTypes
234
+ let structuresSdl = Platform_PluginStructuresApi.sdlTypes
235
+ let fragmentsSdl = Platform_UIFragmentsApi.sdlTypes
236
+
237
+ let encodedExtension = Platform_ComponentDefinitionsApi.encodeExtensionDef(extension)
238
+ let encodedExtensionPoint = Platform_PluginStructuresApi.encodeExtensionPointDef(extensionPoint)
239
+
240
+ let guards: array<guard> = [
241
+ {
242
+ gqlType: "Platform_FieldReference",
243
+ sdlTypes: componentSdl,
244
+ recordFields: schemaKeys(fieldReferenceSchema),
245
+ wireFields: jsonKeys(Platform_ComponentDefinitionsApi.encodeFieldReference(fieldRef)),
246
+ wireOnly: [],
247
+ recordOnly: [],
248
+ },
249
+ {
250
+ gqlType: "Platform_CommandDef",
251
+ sdlTypes: componentSdl,
252
+ recordFields: schemaKeys(commandDefSchema),
253
+ wireFields: jsonKeys(Platform_ComponentDefinitionsApi.encodeCommandDef(cmd)),
254
+ wireOnly: [],
255
+ recordOnly: [],
256
+ },
257
+ {
258
+ gqlType: "Platform_EventDef",
259
+ sdlTypes: componentSdl,
260
+ recordFields: schemaKeys(eventDefSchema),
261
+ wireFields: jsonKeys(Platform_ComponentDefinitionsApi.encodeEventDef(evt)),
262
+ wireOnly: [],
263
+ recordOnly: [],
264
+ },
265
+ {
266
+ gqlType: "Platform_ErrorDef",
267
+ sdlTypes: componentSdl,
268
+ recordFields: schemaKeys(errorDefSchema),
269
+ wireFields: jsonKeys(Platform_ComponentDefinitionsApi.encodeErrorDef(err)),
270
+ wireOnly: [],
271
+ recordOnly: [],
272
+ },
273
+ {
274
+ gqlType: "Platform_WriteSideDef",
275
+ sdlTypes: componentSdl,
276
+ recordFields: schemaKeys(writableDefSchema),
277
+ wireFields: jsonKeys(Platform_ComponentDefinitionsApi.encodeWritableDef(writable)),
278
+ wireOnly: [],
279
+ recordOnly: [],
280
+ },
281
+ {
282
+ gqlType: "Platform_ReadSideDef",
283
+ sdlTypes: componentSdl,
284
+ recordFields: schemaKeys(queryableDefSchema),
285
+ wireFields: jsonKeys(Platform_ComponentDefinitionsApi.encodeQueryableDef(queryable)),
286
+ wireOnly: [],
287
+ recordOnly: [],
288
+ },
289
+ {
290
+ gqlType: "Platform_AutomationSliceDef",
291
+ sdlTypes: componentSdl,
292
+ recordFields: schemaKeys(automationSliceDefSchema),
293
+ wireFields: jsonKeys(Platform_ComponentDefinitionsApi.encodeAutomationSliceDef(automation)),
294
+ wireOnly: [],
295
+ recordOnly: [],
296
+ },
297
+ {
298
+ gqlType: "Platform_OutboundTranslationSliceDef",
299
+ sdlTypes: componentSdl,
300
+ recordFields: schemaKeys(outboundTranslationSliceDefSchema),
301
+ wireFields: jsonKeys(
302
+ Platform_ComponentDefinitionsApi.encodeOutboundTranslationSliceDef(outbound),
303
+ ),
304
+ wireOnly: [],
305
+ recordOnly: [],
306
+ },
307
+ {
308
+ gqlType: "Platform_InboundTranslationSliceDef",
309
+ sdlTypes: componentSdl,
310
+ recordFields: schemaKeys(inboundTranslationSliceDefSchema),
311
+ wireFields: jsonKeys(
312
+ Platform_ComponentDefinitionsApi.encodeInboundTranslationSliceDef(inbound),
313
+ ),
314
+ wireOnly: [],
315
+ recordOnly: [],
316
+ },
317
+ {
318
+ gqlType: "Platform_ExtensionDef",
319
+ sdlTypes: componentSdl,
320
+ recordFields: schemaKeys(extensionDefSchema),
321
+ wireFields: jsonKeys(encodedExtension),
322
+ wireOnly: [],
323
+ recordOnly: [],
324
+ },
325
+ {
326
+ gqlType: "Platform_HandledEventDef",
327
+ sdlTypes: componentSdl,
328
+ recordFields: schemaKeys(handledEventDefSchema),
329
+ wireFields: jsonKeys(encodedExtension->firstOf("handledEvents")),
330
+ wireOnly: [],
331
+ recordOnly: [],
332
+ },
333
+ {
334
+ gqlType: "Platform_ComponentDefinitionEntry",
335
+ sdlTypes: componentSdl,
336
+ recordFields: schemaKeys(pluginStructureSchema),
337
+ wireFields: jsonKeys(
338
+ structure->Platform_ComponentDefinitionsApi.encodePluginStructureEntry(
339
+ ~pluginId="Catalog",
340
+ _,
341
+ ),
342
+ ),
343
+ // `pluginId` names the entry; `internalQueryables` is the complement of the
344
+ // Internal filter, computed from the two lists beside it.
345
+ wireOnly: ["pluginId", "internalQueryables"],
346
+ // Producer-side and store fields belong to Platform_PluginStructures — the
347
+ // deployed AutoUI has no use for either.
348
+ recordOnly: [
349
+ "extensionPoints",
350
+ "requiredStores",
351
+ "requiredStoreDeclarations",
352
+ "requiredCapabilities",
353
+ // Structure-level provenance, like the three above: AutoUI renders
354
+ // components, and where one came from is the map's question rather than a
355
+ // generated page's. It reaches the wire on `Platform_PluginStructures`.
356
+ "traitDeclarations",
357
+ ],
358
+ },
359
+ {
360
+ gqlType: "Platform_PublishedEventDef",
361
+ sdlTypes: structuresSdl,
362
+ recordFields: schemaKeys(publishedEventDefSchema),
363
+ wireFields: jsonKeys(encodedExtensionPoint->firstOf("publishedEvents")),
364
+ wireOnly: [],
365
+ recordOnly: [],
366
+ },
367
+ {
368
+ gqlType: "Platform_ExtensionPointDef",
369
+ sdlTypes: structuresSdl,
370
+ recordFields: schemaKeys(extensionPointDefSchema),
371
+ wireFields: jsonKeys(encodedExtensionPoint),
372
+ wireOnly: [],
373
+ recordOnly: [],
374
+ },
375
+ {
376
+ gqlType: "Platform_RequiredStoreDeclaration",
377
+ sdlTypes: structuresSdl,
378
+ recordFields: schemaKeys(requiredStoreDeclarationSchema),
379
+ wireFields: jsonKeys(Platform_PluginStructuresApi.encodeRequiredStoreDeclaration(storeDecl)),
380
+ wireOnly: [],
381
+ recordOnly: [],
382
+ },
383
+ {
384
+ gqlType: "Platform_RequiredCapabilityDeclaration",
385
+ sdlTypes: structuresSdl,
386
+ recordFields: schemaKeys(requiredCapabilityDeclarationSchema),
387
+ wireFields: jsonKeys(
388
+ Platform_PluginStructuresApi.encodeRequiredCapabilityDeclaration(capabilityDecl),
389
+ ),
390
+ wireOnly: [],
391
+ recordOnly: [],
392
+ },
393
+ {
394
+ gqlType: "Platform_PluginStructureEntry",
395
+ sdlTypes: structuresSdl,
396
+ recordFields: schemaKeys(pluginStructureSchema),
397
+ wireFields: jsonKeys(
398
+ Platform_PluginStructuresApi.encodePluginStructureEntry(~pluginId="Catalog", structure),
399
+ ),
400
+ wireOnly: ["pluginId"],
401
+ // Nothing: this query is the unfiltered structure, by contract.
402
+ recordOnly: [],
403
+ },
404
+ {
405
+ gqlType: "Platform_UIPanel",
406
+ sdlTypes: fragmentsSdl,
407
+ recordFields: schemaKeys(panelManifestEntrySchema),
408
+ wireFields: jsonKeys(Platform_UIFragmentsApi.encodePanel(panel)),
409
+ wireOnly: [],
410
+ recordOnly: [],
411
+ },
412
+ {
413
+ gqlType: "Platform_UIMenuEntry",
414
+ sdlTypes: fragmentsSdl,
415
+ recordFields: schemaKeys(menuEntrySchema),
416
+ wireFields: jsonKeys(Platform_UIFragmentsApi.encodeMenuEntry(menu)),
417
+ wireOnly: [],
418
+ recordOnly: [],
419
+ },
420
+ {
421
+ gqlType: "Platform_UIPage",
422
+ sdlTypes: fragmentsSdl,
423
+ recordFields: schemaKeys(pageManifestEntrySchema),
424
+ wireFields: jsonKeys(Platform_UIFragmentsApi.encodePage(page)),
425
+ wireOnly: [],
426
+ recordOnly: [],
427
+ },
428
+ {
429
+ gqlType: "Platform_UIFragmentEntry",
430
+ sdlTypes: fragmentsSdl,
431
+ recordFields: schemaKeys(UiFragments.stateSchema),
432
+ wireFields: jsonKeys(Platform_UIFragmentsApi.encodeUIFragmentEntry(uiState)),
433
+ wireOnly: [],
434
+ recordOnly: [],
435
+ },
436
+ ]
437
+
438
+ guards->Array.forEach(g => {
439
+ let sdlFields = sdlFieldNames(~typeName=g.gqlType, g.sdlTypes)
440
+ describe(g.gqlType, () => {
441
+ // Without this, a renamed GraphQL type or an unreadable schema would make
442
+ // every assertion below pass over two empty lists.
443
+ testSync("the SDL block and the record schema are both readable", () => {
444
+ expect(sdlFields->Array.length > 0)->toEqual(true)
445
+ expect(g.recordFields->Array.length > 0)->toEqual(true)
446
+ })
447
+
448
+ testSync("every encoded field is selectable", () =>
449
+ expect(missing(g.wireFields, sdlFields))->toEqual([])
450
+ )
451
+
452
+ testSync("every declared field is encoded", () =>
453
+ expect(missing(sdlFields, g.wireFields))->toEqual([])
454
+ )
455
+
456
+ testSync("every record field reaches the wire", () =>
457
+ expect(missing(g.recordFields, Array.concat(sdlFields, g.recordOnly)))->toEqual([])
458
+ )
459
+
460
+ testSync("the wire carries nothing the record and the divergences do not name", () =>
461
+ expect(missing(sdlFields, Array.concat(g.recordFields, g.wireOnly)))->toEqual([])
462
+ )
463
+ })
464
+ })
465
+
466
+ // `encodePluginStructureEntry` serves two consumers: the query resolvers, which
467
+ // pass no `~derived`, and the baked manifest, which does. `derivedPages` is
468
+ // therefore absent from the SDL by design, and the guard above compares the
469
+ // resolver's call.
470
+ describe("derivedPages", () => {
471
+ testSync("is emitted only for the baked manifest, and is not selectable", () => {
472
+ let baked =
473
+ structure->Platform_ComponentDefinitionsApi.encodePluginStructureEntry(
474
+ ~pluginId="Catalog",
475
+ ~derived=["dashboard"],
476
+ _,
477
+ )
478
+ expect(jsonKeys(baked)->Array.includes("derivedPages"))->toEqual(true)
479
+ expect(
480
+ sdlFieldNames(
481
+ ~typeName="Platform_ComponentDefinitionEntry",
482
+ componentSdl,
483
+ )->Array.includes("derivedPages"),
484
+ )->toEqual(false)
485
+ })
486
+ })