@reventlessdev/reventless-core 3.0.0-alpha.190 → 3.0.0-alpha.192
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/package.json +5 -5
- package/src/admin/Platform_Admin.res +2 -2
- package/src/admin/Platform_Admin.res.mjs +6 -2
- package/src/admin/Platform_Admin_Structure.res +4 -0
- package/src/admin/Platform_Admin_Structure.res.mjs +5 -1
- package/src/admin/Platform_ComponentDefinitionsApi.res +5 -1
- package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +5 -1
- package/src/components/Aggregate/Aggregate.res +7 -7
- package/src/components/Aggregate/Aggregate.res.mjs +7 -2
- package/src/components/Aggregate/Aggregate_Builder.res +4 -1
- package/src/components/Aggregate/Aggregate_Builder.res.mjs +11 -3
- package/src/components/Builder_Helpers.res +37 -4
- package/src/components/Builder_Helpers.res.mjs +24 -2
- package/src/components/Dcb/Dcb_Builder.res +2 -2
- package/src/components/Dcb/Dcb_Builder.res.mjs +4 -4
- package/src/components/SideEffectHandler/SideEffectHandler.res +11 -0
- package/src/components/SideEffectHandler/SideEffectHandler_Builder.res +2 -0
- package/src/components/SideEffectHandler/SideEffectHandler_Builder.res.mjs +3 -1
- package/src/components/Task/Task_Builder.res +11 -0
- package/src/components/Task/Task_Builder.res.mjs +2 -0
- package/src/plugin/component/Capability_Inference.res +68 -0
- package/src/plugin/component/Capability_Inference.res.mjs +79 -0
- package/src/plugin/component/Plugin_Builder.res +2 -2
- package/src/plugin/component/Plugin_Builder.res.mjs +4 -4
- package/src/plugin/component/Plugin_Helpers.res +18 -7
- package/src/plugin/component/Plugin_Helpers.res.mjs +20 -1
- package/src/plugin/component/Plugin_Structure.res +215 -79
- package/src/plugin/component/Plugin_Structure.res.mjs +195 -50
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +5 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +15 -4
- package/tests/message/MessageTest.res +1 -0
- package/tests/message/MessageTest.res.mjs +2 -1
- package/tests/plugin/PluginStructureTest.res +391 -0
- package/tests/plugin/PluginStructureTest.res.mjs +355 -0
- package/tests/plugin/StateChangeSlice/PsChangePhoto.res +33 -0
- package/tests/plugin/StateChangeSlice/PsChangePhoto.res.mjs +64 -0
|
@@ -93,6 +93,20 @@ module PsAttachInvoiceSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
|
93
93
|
let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
module PsChangePhotoSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
97
|
+
module Spec = PsChangePhoto
|
|
98
|
+
module Behavior = {
|
|
99
|
+
type state = PsChangePhoto.state
|
|
100
|
+
let initialState = PsChangePhoto.initialState
|
|
101
|
+
let evolve = PsChangePhoto.evolve
|
|
102
|
+
let decide = PsChangePhoto.decide
|
|
103
|
+
let moduleUrl = PsChangePhoto.moduleUrl
|
|
104
|
+
}
|
|
105
|
+
let isAsync = false
|
|
106
|
+
type component = scsComponent
|
|
107
|
+
let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
|
|
108
|
+
}
|
|
109
|
+
|
|
96
110
|
let structure = Plugin_Structure.make(
|
|
97
111
|
~name="TestPlugin",
|
|
98
112
|
~stateChangeSlices=[module(PsPlaceOrderSlice), module(PsShipOrderSlice)],
|
|
@@ -275,6 +289,252 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
275
289
|
})
|
|
276
290
|
})
|
|
277
291
|
|
|
292
|
+
// What a field *is* comes from `SchemaType`, the IR every other schema
|
|
293
|
+
// consumer reads, rather than from a `String(_)` match here. These pin the
|
|
294
|
+
// shapes that separates the two: a date, a reference, a semantic-carrying
|
|
295
|
+
// string and an optional one all match `String(_)` loosely or not at all, and
|
|
296
|
+
// each was answered wrong before.
|
|
297
|
+
describe("labelField — the shape rule", () => {
|
|
298
|
+
let labelOf = (~entityName="Test", schema) => {
|
|
299
|
+
let r = Plugin_Structure.labelFieldsFromStateSchema(~entityName, schema->S.castToUnknown)
|
|
300
|
+
(r.field, r.searchableFields)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
testSync("a DateTime is not a name — a state whose only string is one falls to id", () => {
|
|
304
|
+
// The shipped `Orders` case: `placedAt` was added so date views have a
|
|
305
|
+
// field to key off, and a positional rule let it rename the entity.
|
|
306
|
+
let schema = S.schema(s =>
|
|
307
|
+
{
|
|
308
|
+
"orderId": s.matches(Reventless.Reference.to_("Order")),
|
|
309
|
+
"placedAt": s.matches(Reventless.DateTime.string),
|
|
310
|
+
"shippedAt": s.matches(Reventless.DateTime.string),
|
|
311
|
+
}
|
|
312
|
+
)
|
|
313
|
+
expect(labelOf(schema))->toEqual(("id", []))
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
testSync("a DateTime is skipped, not fatal — a later plain string still wins", () => {
|
|
317
|
+
let schema = S.schema(s =>
|
|
318
|
+
{
|
|
319
|
+
"placedAt": s.matches(Reventless.DateTime.string),
|
|
320
|
+
"reference": s.matches(S.string),
|
|
321
|
+
}
|
|
322
|
+
)
|
|
323
|
+
expect(labelOf(schema))->toEqual(("reference", ["reference"]))
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
testSync("an optional string is still the entity's name", () => {
|
|
327
|
+
let schema = S.schema(s =>
|
|
328
|
+
{
|
|
329
|
+
"nickname": s.matches(S.option(S.string)),
|
|
330
|
+
}
|
|
331
|
+
)
|
|
332
|
+
expect(labelOf(schema))->toEqual(("nickname", ["nickname"]))
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
testSync("a reference is not a name even when its field is not called `*Id`", () => {
|
|
336
|
+
let schema = S.schema(s =>
|
|
337
|
+
{
|
|
338
|
+
"customer": s.matches(Reventless.Reference.to_("Customer")),
|
|
339
|
+
"note": s.matches(S.string),
|
|
340
|
+
}
|
|
341
|
+
)
|
|
342
|
+
expect(labelOf(schema))->toEqual(("note", ["note"]))
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
testSync("a DCB-tagged string is not a name", () => {
|
|
346
|
+
let schema = S.schema(s =>
|
|
347
|
+
{
|
|
348
|
+
"partition": s.matches(Reventless.DcbTag.string),
|
|
349
|
+
"note": s.matches(S.string),
|
|
350
|
+
}
|
|
351
|
+
)
|
|
352
|
+
expect(labelOf(schema))->toEqual(("note", ["note"]))
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
testSync("the `*Id` test is case-insensitive — `productID` is a reference", () => {
|
|
356
|
+
let schema = S.schema(s =>
|
|
357
|
+
{
|
|
358
|
+
"productID": s.matches(S.string),
|
|
359
|
+
"note": s.matches(S.string),
|
|
360
|
+
}
|
|
361
|
+
)
|
|
362
|
+
expect(labelOf(schema))->toEqual(("note", ["note"]))
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
testSync("a semantic-carrying string is a value, not a name", () => {
|
|
366
|
+
// `imageUrl` names a stored object. A bucket key read as a product's name
|
|
367
|
+
// is what the previous rule did whenever no plain string preceded it.
|
|
368
|
+
let schema = S.schema(s =>
|
|
369
|
+
{
|
|
370
|
+
"imageUrl": s.matches(Reventless.StorageRef.forStore(~store="productImages")),
|
|
371
|
+
"note": s.matches(S.string),
|
|
372
|
+
}
|
|
373
|
+
)
|
|
374
|
+
expect(labelOf(schema))->toEqual(("note", ["note"]))
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
testSync("a state whose only string is a semantic falls to id", () => {
|
|
378
|
+
let schema = S.schema(s =>
|
|
379
|
+
{
|
|
380
|
+
"imageUrl": s.matches(Reventless.StorageRef.forStore(~store="productImages")),
|
|
381
|
+
}
|
|
382
|
+
)
|
|
383
|
+
expect(labelOf(schema))->toEqual(("id", []))
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
testSync("`id` itself is still excluded by name", () => {
|
|
387
|
+
// `SchemaType` reads a name that short as an ordinary string, so the
|
|
388
|
+
// exclusion cannot come from the IR.
|
|
389
|
+
let schema = S.schema(s =>
|
|
390
|
+
{
|
|
391
|
+
"id": s.matches(S.string),
|
|
392
|
+
"note": s.matches(S.string),
|
|
393
|
+
}
|
|
394
|
+
)
|
|
395
|
+
expect(labelOf(schema))->toEqual(("note", ["note"]))
|
|
396
|
+
})
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
describe("labelField — the conventional-name rung", () => {
|
|
400
|
+
let labelOf = schema => {
|
|
401
|
+
let r = Plugin_Structure.labelFieldsFromStateSchema(~entityName="Test", schema->S.castToUnknown)
|
|
402
|
+
(r.field, r.searchableFields)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
testSync("a field named `name` beats an earlier string", () => {
|
|
406
|
+
let schema = S.schema(s =>
|
|
407
|
+
{
|
|
408
|
+
"sku": s.matches(S.string),
|
|
409
|
+
"name": s.matches(S.string),
|
|
410
|
+
}
|
|
411
|
+
)
|
|
412
|
+
expect(labelOf(schema))->toEqual(("name", ["name"]))
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
testSync("`title` counts too, case-insensitively", () => {
|
|
416
|
+
let schema = S.schema(s =>
|
|
417
|
+
{
|
|
418
|
+
"body": s.matches(S.string),
|
|
419
|
+
"Title": s.matches(S.string),
|
|
420
|
+
}
|
|
421
|
+
)
|
|
422
|
+
expect(labelOf(schema))->toEqual(("Title", ["Title"]))
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
testSync("the match is exact — `customerName` is a customer's name, not this record's", () => {
|
|
426
|
+
// The shipped `OrdersView` fixture depends on this: it has exactly one
|
|
427
|
+
// eligible field and must be picked by position, not by convention.
|
|
428
|
+
let schema = S.schema(s =>
|
|
429
|
+
{
|
|
430
|
+
"customerName": s.matches(S.string),
|
|
431
|
+
"note": s.matches(S.string),
|
|
432
|
+
}
|
|
433
|
+
)
|
|
434
|
+
expect(labelOf(schema))->toEqual(("customerName", ["customerName"]))
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
testSync("an optional `name` still outranks an earlier string", () => {
|
|
438
|
+
let schema = S.schema(s =>
|
|
439
|
+
{
|
|
440
|
+
"sku": s.matches(S.string),
|
|
441
|
+
"name": s.matches(S.option(S.string)),
|
|
442
|
+
}
|
|
443
|
+
)
|
|
444
|
+
expect(labelOf(schema))->toEqual(("name", ["name"]))
|
|
445
|
+
})
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
// The rung is published because the four are not equally believable: a
|
|
449
|
+
// consumer with a name rule of its own has to know whether it is ranking
|
|
450
|
+
// against a declaration or against a guess.
|
|
451
|
+
describe("labelFieldSource — which rung answered", () => {
|
|
452
|
+
let sourceOf = schema =>
|
|
453
|
+
Plugin_Structure.labelFieldsFromStateSchema(
|
|
454
|
+
~entityName="Test",
|
|
455
|
+
schema->S.castToUnknown,
|
|
456
|
+
).source->Plugin_Structure.labelFieldSourceToString
|
|
457
|
+
|
|
458
|
+
testSync("a @displayName spec is a declaration", () => {
|
|
459
|
+
let customers = structure.stateViewSlices->Array.getUnsafe(2)
|
|
460
|
+
expect((customers.labelField, customers.labelFieldSource))->toEqual((
|
|
461
|
+
"displayName",
|
|
462
|
+
Some("annotation"),
|
|
463
|
+
))
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
testSync("a field named `name` is a guess the consumer can also make", () => {
|
|
467
|
+
let schema = S.schema(s =>
|
|
468
|
+
{
|
|
469
|
+
"sku": s.matches(S.string),
|
|
470
|
+
"name": s.matches(S.string),
|
|
471
|
+
}
|
|
472
|
+
)
|
|
473
|
+
expect(sourceOf(schema))->toEqual("convention")
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
testSync("declaration order is a guess only this side can make", () => {
|
|
477
|
+
// `customerName` is eligible and unconventional, so it wins by position —
|
|
478
|
+
// and a consumer told only "customerName" could not tell that from a
|
|
479
|
+
// declaration.
|
|
480
|
+
let schema = S.schema(s =>
|
|
481
|
+
{
|
|
482
|
+
"customerName": s.matches(S.string),
|
|
483
|
+
"note": s.matches(S.string),
|
|
484
|
+
}
|
|
485
|
+
)
|
|
486
|
+
expect(sourceOf(schema))->toEqual("position")
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
testSync("no candidate at all is the state saying it has no human field", () => {
|
|
490
|
+
let schema = S.schema(s =>
|
|
491
|
+
{
|
|
492
|
+
"orderId": s.matches(Reventless.Reference.to_("Order")),
|
|
493
|
+
"placedAt": s.matches(Reventless.DateTime.string),
|
|
494
|
+
}
|
|
495
|
+
)
|
|
496
|
+
expect(sourceOf(schema))->toEqual("fallback")
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
testSync("every built def states one", () => {
|
|
500
|
+
let stated =
|
|
501
|
+
Array.concat(structure.readModels, structure.stateViewSlices)->Array.every(q =>
|
|
502
|
+
q.labelFieldSource->Option.isSome
|
|
503
|
+
)
|
|
504
|
+
expect(stated)->toEqual(true)
|
|
505
|
+
})
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
describe("statusField — the shape rule", () => {
|
|
509
|
+
let statusOf = schema =>
|
|
510
|
+
Plugin_Structure.statusFieldFromStateSchema(~entityName="Test", schema->S.castToUnknown)
|
|
511
|
+
|
|
512
|
+
testSync("a `status` field holding a closed set of values is the lifecycle field", () => {
|
|
513
|
+
let schema = S.schema(s =>
|
|
514
|
+
{
|
|
515
|
+
"status": s.matches(S.union([S.literal("Placed"), S.literal("Shipped")])),
|
|
516
|
+
}
|
|
517
|
+
)
|
|
518
|
+
expect(statusOf(schema))->toEqual(Some("status"))
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
testSync("an optional one counts too", () => {
|
|
522
|
+
let schema = S.schema(s =>
|
|
523
|
+
{
|
|
524
|
+
"status": s.matches(S.option(S.union([S.literal("Placed"), S.literal("Shipped")]))),
|
|
525
|
+
}
|
|
526
|
+
)
|
|
527
|
+
expect(statusOf(schema))->toEqual(Some("status"))
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
testSync("free text named `status` is not a lifecycle", () => {
|
|
531
|
+
// `allowedStates` filtering needs states to compare against; a string
|
|
532
|
+
// field named `status` gives a command menu nothing to match.
|
|
533
|
+
let schema = S.schema(s => {"status": s.matches(S.string)})
|
|
534
|
+
expect(statusOf(schema))->toEqual(None)
|
|
535
|
+
})
|
|
536
|
+
})
|
|
537
|
+
|
|
278
538
|
// Phase 4: queryableDef.schema must carry x-reventless-* extension keys for
|
|
279
539
|
// annotated state types. Plugin_Structure now uses SuryToJsonSchema.deriveObjectSchema
|
|
280
540
|
// (annotation-aware) instead of S.toJSONSchema (metadata-blind).
|
|
@@ -429,5 +689,136 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
429
689
|
testSync("a plugin declaring no stores collects none — not None", () =>
|
|
430
690
|
expect(structure.requiredStores)->toEqual(Some([]))
|
|
431
691
|
)
|
|
692
|
+
|
|
693
|
+
testSync("each requirement keeps its declaring (component, field) sites", () => {
|
|
694
|
+
// `documents` is declared on a command field AND an event field of the
|
|
695
|
+
// same component under the same field name — one declaration site, so
|
|
696
|
+
// the identical triples collapse to one entry.
|
|
697
|
+
let expected: array<Reventless.Plugin.requiredStoreDeclaration> = [
|
|
698
|
+
{store: "TestPlugin.documents", component: "AttachInvoice", field: "documentUrl"},
|
|
699
|
+
{store: "branding.logos", component: "AttachInvoice", field: "logoUrl"},
|
|
700
|
+
]
|
|
701
|
+
expect(withStores.requiredStoreDeclarations)->toEqual(Some(expected))
|
|
702
|
+
})
|
|
703
|
+
|
|
704
|
+
// The manifest is a rendering of the structure. Its key set must be
|
|
705
|
+
// byte-identical to what the deployed plugin reports as `requiredStores`,
|
|
706
|
+
// because the deploy-time coverage assertion compares against exactly that
|
|
707
|
+
// — this equality is the whole contract of `capabilities.json`.
|
|
708
|
+
describe("capability manifest", () => {
|
|
709
|
+
let manifest = Reventless.CapabilityManifest.fromStructure(withStores)
|
|
710
|
+
|
|
711
|
+
testSync("keys are byte-identical to requiredStores", () =>
|
|
712
|
+
expect(manifest.capabilities->Array.map(c => c.key))->toEqual(
|
|
713
|
+
withStores.requiredStores->Option.getOr([]),
|
|
714
|
+
)
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
testSync("each entry carries its provenance", () => {
|
|
718
|
+
let entry =
|
|
719
|
+
manifest.capabilities->Array.find(c => c.key == "TestPlugin.documents")->Option.getOrThrow
|
|
720
|
+
let expected: array<Reventless.CapabilityManifest.provenance> = [
|
|
721
|
+
{component: "AttachInvoice", field: "documentUrl"},
|
|
722
|
+
]
|
|
723
|
+
expect(entry.declaredBy)->toEqual(expected)
|
|
724
|
+
})
|
|
725
|
+
|
|
726
|
+
testSync("a structure declaring nothing yields an empty list, not a missing manifest", () =>
|
|
727
|
+
expect(Reventless.CapabilityManifest.fromStructure(structure).capabilities)->toEqual([])
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
testSync("rendering is deterministic and newline-terminated", () => {
|
|
731
|
+
let rendered = Reventless.CapabilityManifest.renderForStructure(withStores)
|
|
732
|
+
expect(rendered)->toBe(
|
|
733
|
+
`{
|
|
734
|
+
"capabilities": [
|
|
735
|
+
{
|
|
736
|
+
"kind": "ObjectStore",
|
|
737
|
+
"key": "TestPlugin.documents",
|
|
738
|
+
"declaredBy": [
|
|
739
|
+
{
|
|
740
|
+
"component": "AttachInvoice",
|
|
741
|
+
"field": "documentUrl"
|
|
742
|
+
}
|
|
743
|
+
]
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
"kind": "ObjectStore",
|
|
747
|
+
"key": "branding.logos",
|
|
748
|
+
"declaredBy": [
|
|
749
|
+
{
|
|
750
|
+
"component": "AttachInvoice",
|
|
751
|
+
"field": "logoUrl"
|
|
752
|
+
}
|
|
753
|
+
]
|
|
754
|
+
}
|
|
755
|
+
]
|
|
756
|
+
}
|
|
757
|
+
`,
|
|
758
|
+
)
|
|
759
|
+
})
|
|
760
|
+
})
|
|
761
|
+
})
|
|
762
|
+
|
|
763
|
+
// Declarations are authoritative; the name heuristics are a lint. A
|
|
764
|
+
// heuristic-only match warns and never reaches the manifest, so nothing is
|
|
765
|
+
// provisioned from a guess — `imageUrl` is genuinely ambiguous between an
|
|
766
|
+
// uploaded object and an external URL, and only the author can settle it.
|
|
767
|
+
describe("capability inference", () => {
|
|
768
|
+
let commandWarnings = Capability_Inference.scanSchema(
|
|
769
|
+
~component="ChangePhoto",
|
|
770
|
+
PsChangePhoto.commandSchema->S.castToUnknown,
|
|
771
|
+
)
|
|
772
|
+
let eventWarnings = Capability_Inference.scanSchema(
|
|
773
|
+
~component="ChangePhoto",
|
|
774
|
+
PsChangePhoto.eventSchema->S.castToUnknown,
|
|
775
|
+
)
|
|
776
|
+
|
|
777
|
+
testSync("a heuristic-only match produces a warning", () => {
|
|
778
|
+
let expected: array<Capability_Inference.warning> = [
|
|
779
|
+
{component: "ChangePhoto", field: "photoUrl"},
|
|
780
|
+
]
|
|
781
|
+
expect(commandWarnings)->toEqual(expected)
|
|
782
|
+
})
|
|
783
|
+
|
|
784
|
+
testSync("a declared field with a heuristic name does not warn", () => {
|
|
785
|
+
// The event's `thumbnail` matches the name heuristic but carries
|
|
786
|
+
// @storageRef — only the undeclared `photoUrl` warns.
|
|
787
|
+
let expected: array<Capability_Inference.warning> = [
|
|
788
|
+
{component: "ChangePhoto", field: "photoUrl"},
|
|
789
|
+
]
|
|
790
|
+
expect(eventWarnings)->toEqual(expected)
|
|
791
|
+
})
|
|
792
|
+
|
|
793
|
+
testSync("a heuristic-only match reaches no manifest entry", () => {
|
|
794
|
+
let withHeuristicField = Plugin_Structure.make(
|
|
795
|
+
~name="TestPlugin",
|
|
796
|
+
~stateChangeSlices=[module(PsChangePhotoSlice)],
|
|
797
|
+
)
|
|
798
|
+
// Only the *declared* `thumbnail` store is collected; `photoUrl` — the
|
|
799
|
+
// heuristic-only match — provisions nothing.
|
|
800
|
+
expect(withHeuristicField.requiredStores)->toEqual(Some(["TestPlugin.productPhotos"]))
|
|
801
|
+
let manifest = Reventless.CapabilityManifest.fromStructure(withHeuristicField)
|
|
802
|
+
expect(
|
|
803
|
+
manifest.capabilities->Array.every(entry =>
|
|
804
|
+
entry.declaredBy->Array.every(site => site.field != "photoUrl")
|
|
805
|
+
),
|
|
806
|
+
)->toBe(true)
|
|
807
|
+
})
|
|
808
|
+
|
|
809
|
+
testSync("the warning names the field and the settling annotation", () => {
|
|
810
|
+
let text =
|
|
811
|
+
commandWarnings->Array.getUnsafe(0)->Capability_Inference.message
|
|
812
|
+
expect(text->String.includes("ChangePhoto.photoUrl"))->toBe(true)
|
|
813
|
+
expect(text->String.includes("@storageRef"))->toBe(true)
|
|
814
|
+
})
|
|
815
|
+
|
|
816
|
+
testSync("suffix and exact name rules match; ordinary names do not", () => {
|
|
817
|
+
expect(
|
|
818
|
+
["invoiceFileRef", "upload", "thumbnail", "customerName", "documentUrl"]->Array.map(
|
|
819
|
+
Capability_Inference.nameMatches,
|
|
820
|
+
),
|
|
821
|
+
)->toEqual([true, true, true, false, false])
|
|
822
|
+
})
|
|
432
823
|
})
|
|
433
824
|
})
|