@highstate/contract 0.19.1 → 0.21.1

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.
@@ -2,7 +2,9 @@ import { beforeEach, describe, expect, it } from "vitest"
2
2
  import { z } from "zod"
3
3
  import { defineComponent } from "./component"
4
4
  import { defineEntity } from "./entity"
5
- import { resetEvaluation } from "./evaluation"
5
+ import { getRuntimeInstances, resetEvaluation } from "./evaluation"
6
+ import { type EntityInput, type InstanceInput, selectInput } from "./instance"
7
+ import { boundaryInput, kind } from "./shared"
6
8
 
7
9
  describe("defineComponent", () => {
8
10
  beforeEach(resetEvaluation)
@@ -77,8 +79,16 @@ describe("defineComponent", () => {
77
79
  ])
78
80
 
79
81
  const embeddedResult = server.schema.safeParse({
82
+ $meta: {
83
+ type: "common.server.v1",
84
+ identity: "server-1",
85
+ },
80
86
  endpoint: "127.0.0.1",
81
87
  resource: {
88
+ $meta: {
89
+ type: "common.resource.v1",
90
+ identity: "resource-1",
91
+ },
82
92
  id: "test",
83
93
  },
84
94
  })
@@ -182,9 +192,21 @@ describe("defineComponent", () => {
182
192
 
183
193
  expect(
184
194
  server.schema.safeParse({
195
+ $meta: {
196
+ type: "common.server.v1",
197
+ identity: "server-1",
198
+ },
185
199
  tag: "prod",
186
200
  endpoint: "127.0.0.1",
187
- "common.resource.v1": [{ id: "test" }],
201
+ "common.resource.v1": [
202
+ {
203
+ $meta: {
204
+ type: "common.resource.v1",
205
+ identity: "resource-1",
206
+ },
207
+ id: "test",
208
+ },
209
+ ],
188
210
  }).success,
189
211
  ).toBe(true)
190
212
  })
@@ -242,4 +264,892 @@ describe("defineComponent", () => {
242
264
 
243
265
  void servers
244
266
  })
267
+
268
+ it("should create deep output accessors with dynamic paths", () => {
269
+ const subnet = defineEntity({
270
+ type: "network.subnet.v1",
271
+ schema: z.object({ cidr: z.string() }),
272
+ })
273
+
274
+ const endpoint = defineEntity({
275
+ type: "network.l4-endpoint.v1",
276
+ includes: {
277
+ subnet,
278
+ },
279
+ schema: z.object({ host: z.string() }),
280
+ })
281
+
282
+ const peer = defineEntity({
283
+ type: "wireguard.peer.v1",
284
+ includes: {
285
+ endpoints: {
286
+ entity: endpoint,
287
+ multiple: true,
288
+ required: false,
289
+ },
290
+ },
291
+ schema: z.object({ name: z.string() }),
292
+ })
293
+
294
+ const producer = defineComponent({
295
+ type: "test.producer.v1",
296
+ outputs: {
297
+ peer,
298
+ },
299
+ create: ({ id }) => ({
300
+ peer: {
301
+ instanceId: id,
302
+ output: "peer",
303
+ } as never,
304
+ }),
305
+ })
306
+
307
+ const { peer: peerInput } = producer({ name: "test" })
308
+
309
+ expect(Array.isArray(peerInput.endpoints)).toBe(false)
310
+ expect(peerInput.endpoints.path).toBe("endpoints")
311
+ expect(peerInput.endpoints.subnet.path).toBe("endpoints.subnet")
312
+ })
313
+
314
+ it("should preserve parent boundary for selected provided input", () => {
315
+ const cluster = defineEntity({
316
+ type: "example.cluster.v1",
317
+ schema: z.object({ id: z.string() }),
318
+ })
319
+
320
+ const source = defineComponent({
321
+ type: "example.source.v1",
322
+ outputs: { k8sCluster: cluster },
323
+ create: ({ id }) => ({
324
+ k8sCluster: { instanceId: id, output: "k8sCluster" },
325
+ }),
326
+ })
327
+
328
+ const child = defineComponent({
329
+ type: "example.child.v1",
330
+ inputs: { k8sCluster: cluster },
331
+ create: () => ({}),
332
+ })
333
+
334
+ const parent = defineComponent({
335
+ type: "example.parent.v1",
336
+ inputs: {
337
+ k8sClusters: {
338
+ entity: cluster,
339
+ multiple: true,
340
+ required: false,
341
+ },
342
+ },
343
+ create: ({ name, inputs }) => {
344
+ child({
345
+ name: `${name}.selected-child`,
346
+ inputs: {
347
+ k8sCluster: selectInput(inputs.k8sClusters, "cluster-1"),
348
+ },
349
+ })
350
+
351
+ return {}
352
+ },
353
+ })
354
+
355
+ const { k8sCluster } = source({ name: "cluster-1" })
356
+
357
+ parent({
358
+ name: "parent-1",
359
+ inputs: { k8sClusters: [k8sCluster] },
360
+ })
361
+
362
+ const childInstance = getRuntimeInstances().find(
363
+ runtime => runtime.instance.id === "example.child.v1:parent-1.selected-child",
364
+ )?.instance
365
+
366
+ expect(childInstance).toBeDefined()
367
+ expect(childInstance?.inputs?.k8sCluster).toEqual([
368
+ {
369
+ instanceId: "example.parent.v1:parent-1",
370
+ output: "k8sClusters",
371
+ },
372
+ ])
373
+ })
374
+
375
+ it("should preserve nested dynamic accessors for selected provided input", () => {
376
+ const subnet = defineEntity({
377
+ type: "network.subnet.v1",
378
+ schema: z.object({ cidr: z.string() }),
379
+ })
380
+
381
+ const endpoint = defineEntity({
382
+ type: "network.l4-endpoint.v1",
383
+ includes: {
384
+ subnet,
385
+ },
386
+ schema: z.object({ host: z.string() }),
387
+ })
388
+
389
+ const peer = defineEntity({
390
+ type: "wireguard.peer.v1",
391
+ includes: {
392
+ endpoints: {
393
+ entity: endpoint,
394
+ multiple: true,
395
+ required: false,
396
+ },
397
+ },
398
+ schema: z.object({ name: z.string() }),
399
+ })
400
+
401
+ let selectedSubnetPath: string | undefined
402
+
403
+ const producer = defineComponent({
404
+ type: "example.peer-source.v1",
405
+ outputs: {
406
+ peer,
407
+ },
408
+ create: ({ id }) => ({
409
+ peer: {
410
+ instanceId: id,
411
+ output: "peer",
412
+ } as never,
413
+ }),
414
+ })
415
+
416
+ const selector = defineComponent({
417
+ type: "example.peer-selector.v1",
418
+ inputs: {
419
+ peers: {
420
+ entity: peer,
421
+ multiple: true,
422
+ },
423
+ },
424
+ create: ({ inputs }) => {
425
+ const selectedPeer = selectInput(inputs.peers, "peer-1")
426
+ selectedSubnetPath = selectedPeer.endpoints.subnet.path
427
+
428
+ return {}
429
+ },
430
+ })
431
+
432
+ const { peer: peerInput } = producer({ name: "peer-1" })
433
+
434
+ selector({
435
+ name: "selector-1",
436
+ inputs: {
437
+ peers: [peerInput],
438
+ },
439
+ })
440
+
441
+ expect(selectedSubnetPath).toBe("endpoints.subnet")
442
+ })
443
+
444
+ it("should not mutate original boundary when selecting provided input", () => {
445
+ const peer = defineEntity({
446
+ type: "wireguard.peer.v1",
447
+ schema: z.object({ name: z.string() }),
448
+ })
449
+
450
+ const source = defineComponent({
451
+ type: "example.peer-source.v1",
452
+ outputs: { peer },
453
+ create: ({ id }) => ({
454
+ peer: { instanceId: id, output: "peer" },
455
+ }),
456
+ })
457
+
458
+ const { peer: peerInput } = source({ name: "peer-1" })
459
+ const originalBoundary = peerInput[boundaryInput]
460
+
461
+ const selectedGroup = [peerInput] as (typeof peerInput)[] & { [boundaryInput]: InstanceInput }
462
+ selectedGroup[boundaryInput] = {
463
+ instanceId: "example.parent.v1:parent-1",
464
+ output: "peers",
465
+ }
466
+
467
+ const selected = selectInput(selectedGroup, "peer-1")
468
+
469
+ expect(peerInput[boundaryInput]).toEqual(originalBoundary)
470
+ expect(selected[boundaryInput]).toEqual({
471
+ instanceId: "example.parent.v1:parent-1",
472
+ output: "peers",
473
+ })
474
+ })
475
+
476
+ it("should return chained missing proxy when selected input is absent", () => {
477
+ const peer = defineEntity({
478
+ type: "wireguard.peer.v1",
479
+ schema: z.object({ name: z.string() }),
480
+ })
481
+
482
+ const source = defineComponent({
483
+ type: "example.peer-source.v1",
484
+ outputs: { peer },
485
+ create: ({ id }) => ({
486
+ peer: { instanceId: id, output: "peer" },
487
+ }),
488
+ })
489
+
490
+ const { peer: peerInput } = source({ name: "peer-1" })
491
+
492
+ const selectedGroup = [peerInput] as (typeof peerInput)[] & { [boundaryInput]: InstanceInput }
493
+ selectedGroup[boundaryInput] = {
494
+ instanceId: "example.parent.v1:parent-1",
495
+ output: "peers",
496
+ }
497
+
498
+ const missingPeer = selectInput(selectedGroup, "missing-peer")
499
+
500
+ expect(missingPeer.provided).toBe(false)
501
+ expect(missingPeer[boundaryInput]).toEqual({
502
+ instanceId: "example.parent.v1:parent-1",
503
+ output: "peers",
504
+ })
505
+
506
+ const nestedMissing = (missingPeer as Record<string, unknown>).network as Record<
507
+ string,
508
+ unknown
509
+ >
510
+ expect((nestedMissing.provided as boolean) ?? false).toBe(false)
511
+ })
512
+
513
+ it("should preserve parent boundary for indexed multiple input item", () => {
514
+ const cluster = defineEntity({
515
+ type: "example.cluster.v1",
516
+ schema: z.object({ id: z.string() }),
517
+ })
518
+
519
+ const source = defineComponent({
520
+ type: "example.source.v1",
521
+ outputs: { k8sCluster: cluster },
522
+ create: ({ id }) => ({
523
+ k8sCluster: { instanceId: id, output: "k8sCluster" },
524
+ }),
525
+ })
526
+
527
+ const child = defineComponent({
528
+ type: "example.child.v1",
529
+ inputs: { k8sCluster: cluster },
530
+ create: () => ({}),
531
+ })
532
+
533
+ const parent = defineComponent({
534
+ type: "example.parent.v1",
535
+ inputs: {
536
+ k8sClusters: {
537
+ entity: cluster,
538
+ multiple: true,
539
+ required: false,
540
+ },
541
+ },
542
+ create: ({ name, inputs }) => {
543
+ child({
544
+ name: `${name}.indexed-child`,
545
+ inputs: {
546
+ k8sCluster: inputs.k8sClusters[0],
547
+ },
548
+ })
549
+
550
+ return {}
551
+ },
552
+ })
553
+
554
+ const { k8sCluster } = source({ name: "cluster-1" })
555
+
556
+ parent({
557
+ name: "parent-2",
558
+ inputs: { k8sClusters: [k8sCluster] },
559
+ })
560
+
561
+ const childInstance = getRuntimeInstances().find(
562
+ runtime => runtime.instance.id === "example.child.v1:parent-2.indexed-child",
563
+ )?.instance
564
+
565
+ expect(childInstance).toBeDefined()
566
+ expect(childInstance?.inputs?.k8sCluster).toEqual([
567
+ {
568
+ instanceId: "example.parent.v1:parent-2",
569
+ output: "k8sClusters",
570
+ },
571
+ ])
572
+ })
573
+
574
+ it("should preserve nested composite output boundary when passing to another component", () => {
575
+ const peer = defineEntity({
576
+ type: "example.peer.v1",
577
+ schema: z.object({ name: z.string() }),
578
+ })
579
+
580
+ const producer = defineComponent({
581
+ type: "example.peer-source.v1",
582
+ outputs: { peer },
583
+ create: ({ id }) => ({
584
+ peer: {
585
+ instanceId: id,
586
+ output: "peer",
587
+ },
588
+ }),
589
+ })
590
+
591
+ const location = defineComponent({
592
+ type: "example.location.v1",
593
+ outputs: { peer },
594
+ create: ({ name }) => {
595
+ const { peer: producedPeer } = producer({ name })
596
+
597
+ return {
598
+ peer: producedPeer,
599
+ }
600
+ },
601
+ })
602
+
603
+ const consumer = defineComponent({
604
+ type: "example.peer-consumer.v1",
605
+ inputs: {
606
+ peers: {
607
+ entity: peer,
608
+ multiple: true,
609
+ },
610
+ },
611
+ create: () => ({}),
612
+ })
613
+
614
+ const locationSet = defineComponent({
615
+ type: "example.location-set.v1",
616
+ create: () => {
617
+ const { peer: locationPeer } = location({ name: "amsterdam" })
618
+
619
+ consumer({
620
+ name: "identity",
621
+ inputs: {
622
+ peers: [locationPeer],
623
+ },
624
+ })
625
+
626
+ return {}
627
+ },
628
+ })
629
+
630
+ locationSet({ name: "test" })
631
+
632
+ const consumerInstance = getRuntimeInstances().find(
633
+ runtime => runtime.instance.id === "example.peer-consumer.v1:identity",
634
+ )?.instance
635
+
636
+ expect(consumerInstance?.inputs?.peers).toEqual([
637
+ {
638
+ instanceId: "example.location.v1:amsterdam",
639
+ output: "peer",
640
+ },
641
+ ])
642
+ })
643
+
644
+ it("should preserve boundaries from empty nested input groups", () => {
645
+ const cluster = defineEntity({
646
+ type: "example.cluster.v1",
647
+ schema: z.object({ id: z.string() }),
648
+ })
649
+
650
+ const component = defineComponent({
651
+ type: "example.boundary-preserving.v1",
652
+ inputs: {
653
+ k8sClusters: {
654
+ entity: cluster,
655
+ multiple: true,
656
+ required: false,
657
+ },
658
+ },
659
+ create: () => ({}),
660
+ })
661
+
662
+ const emptyGroup = [] as unknown as InstanceInput[] & { [boundaryInput]: InstanceInput }
663
+
664
+ emptyGroup[boundaryInput] = {
665
+ instanceId: "example.source.v1:source",
666
+ output: "k8sClusters",
667
+ }
668
+
669
+ component({
670
+ name: "test",
671
+ inputs: {
672
+ k8sClusters: [emptyGroup],
673
+ },
674
+ })
675
+
676
+ const instance = getRuntimeInstances().find(
677
+ runtime => runtime.instance.id === "example.boundary-preserving.v1:test",
678
+ )?.instance
679
+
680
+ expect(instance?.inputs?.k8sClusters).toEqual([
681
+ {
682
+ instanceId: "example.source.v1:source",
683
+ output: "k8sClusters",
684
+ },
685
+ ])
686
+ })
687
+
688
+ it("should throw when selecting from empty input array without boundary", () => {
689
+ expect(() => selectInput([], "missing")).toThrow(
690
+ 'Cannot select input "missing": empty input group has no boundary metadata to build a missing input reference.',
691
+ )
692
+ })
693
+
694
+ it("should not leak optional metadata into resolved outputs", () => {
695
+ const cluster = defineEntity({
696
+ type: "example.cluster.v1",
697
+ schema: z.object({ id: z.string() }),
698
+ })
699
+
700
+ const passthrough = defineComponent({
701
+ type: "example.passthrough.v1",
702
+ inputs: {
703
+ k8sCluster: {
704
+ entity: cluster,
705
+ required: false,
706
+ },
707
+ },
708
+ outputs: {
709
+ k8sCluster: {
710
+ entity: cluster,
711
+ required: false,
712
+ },
713
+ },
714
+ create: ({ inputs }) => ({
715
+ k8sCluster: inputs.k8sCluster,
716
+ }),
717
+ })
718
+
719
+ passthrough({ name: "test" })
720
+
721
+ const instance = getRuntimeInstances().find(
722
+ runtime => runtime.instance.id === "example.passthrough.v1:test",
723
+ )?.instance
724
+
725
+ expect(instance).toBeDefined()
726
+ expect(instance?.resolvedOutputs?.k8sCluster).toEqual([])
727
+ expect(instance?.outputs?.k8sCluster).toEqual([
728
+ {
729
+ instanceId: "example.passthrough.v1:test",
730
+ output: "k8sCluster",
731
+ },
732
+ ])
733
+ })
734
+
735
+ it("should keep output path as plain string metadata", () => {
736
+ const peer = defineEntity({
737
+ type: "example.peer.v1",
738
+ schema: z.object({ id: z.string() }),
739
+ })
740
+
741
+ const source = defineComponent({
742
+ type: "example.path-source.v1",
743
+ outputs: { peer },
744
+ create: ({ id }) => ({
745
+ peer: {
746
+ instanceId: id,
747
+ output: "peer",
748
+ },
749
+ }),
750
+ })
751
+
752
+ const passthrough = defineComponent({
753
+ type: "example.path-passthrough.v1",
754
+ inputs: {
755
+ peer: {
756
+ entity: peer,
757
+ required: false,
758
+ },
759
+ },
760
+ outputs: {
761
+ peer: {
762
+ entity: peer,
763
+ required: false,
764
+ },
765
+ },
766
+ create: ({ inputs }) => ({
767
+ peer: inputs.peer.provided
768
+ ? {
769
+ instanceId: inputs.peer.instanceId,
770
+ output: inputs.peer.output,
771
+ path: "id",
772
+ }
773
+ : undefined,
774
+ }),
775
+ })
776
+
777
+ const { peer: sourcePeer } = source({ name: "source" })
778
+
779
+ passthrough({
780
+ name: "test",
781
+ inputs: {
782
+ peer: sourcePeer,
783
+ },
784
+ })
785
+
786
+ const instance = getRuntimeInstances().find(
787
+ runtime => runtime.instance.id === "example.path-passthrough.v1:test",
788
+ )?.instance
789
+
790
+ expect(instance?.resolvedOutputs?.peer).toEqual([
791
+ {
792
+ instanceId: "example.path-source.v1:source",
793
+ output: "peer",
794
+ path: "id",
795
+ },
796
+ ])
797
+ })
798
+
799
+ it("should track missing input in model and pass provided-false placeholder with parent boundary", () => {
800
+ const cluster = defineEntity({
801
+ type: "example.cluster.v1",
802
+ schema: z.object({ id: z.string() }),
803
+ })
804
+
805
+ let createInputs: Record<string, unknown> | undefined
806
+
807
+ const component = defineComponent({
808
+ type: "example.missing-input-tracking.v1",
809
+ inputs: {
810
+ k8sCluster: {
811
+ entity: cluster,
812
+ required: false,
813
+ },
814
+ },
815
+ create: ({ inputs }) => {
816
+ createInputs = inputs as Record<string, unknown>
817
+ return {}
818
+ },
819
+ })
820
+
821
+ component({
822
+ name: "test",
823
+ inputs: {
824
+ k8sCluster: {
825
+ provided: false,
826
+ [boundaryInput]: {
827
+ instanceId: "example.source.v1:source",
828
+ output: "k8sCluster",
829
+ },
830
+ } as never,
831
+ },
832
+ })
833
+
834
+ const instance = getRuntimeInstances().find(
835
+ runtime => runtime.instance.id === "example.missing-input-tracking.v1:test",
836
+ )?.instance
837
+
838
+ expect(instance?.inputs?.k8sCluster).toEqual([
839
+ {
840
+ instanceId: "example.source.v1:source",
841
+ output: "k8sCluster",
842
+ },
843
+ ])
844
+ expect(createInputs).toEqual({
845
+ k8sCluster: {
846
+ provided: false,
847
+ [boundaryInput]: {
848
+ instanceId: "example.missing-input-tracking.v1:test",
849
+ output: "k8sCluster",
850
+ },
851
+ },
852
+ })
853
+ })
854
+
855
+ it("should pass missing multiple input with parent boundary", () => {
856
+ const endpoint = defineEntity({
857
+ type: "example.endpoint.v1",
858
+ schema: z.object({ host: z.string() }),
859
+ })
860
+
861
+ let receivedBoundary: InstanceInput | undefined
862
+
863
+ const component = defineComponent({
864
+ type: "example.missing-multiple-boundary.v1",
865
+ inputs: {
866
+ endpoints: {
867
+ entity: endpoint,
868
+ multiple: true,
869
+ required: false,
870
+ },
871
+ },
872
+ create: ({ inputs }) => {
873
+ receivedBoundary = (inputs.endpoints as unknown as { [boundaryInput]: InstanceInput })[
874
+ boundaryInput
875
+ ]
876
+ return {}
877
+ },
878
+ })
879
+
880
+ const upstreamGroup = [] as unknown as EntityInput<typeof endpoint>[] & {
881
+ [boundaryInput]: InstanceInput
882
+ }
883
+ upstreamGroup[boundaryInput] = {
884
+ instanceId: "example.upstream.v1:source",
885
+ output: "endpoints",
886
+ }
887
+
888
+ component({
889
+ name: "test",
890
+ inputs: {
891
+ endpoints: upstreamGroup,
892
+ },
893
+ })
894
+
895
+ const instance = getRuntimeInstances().find(
896
+ runtime => runtime.instance.id === "example.missing-multiple-boundary.v1:test",
897
+ )?.instance
898
+
899
+ expect(instance?.inputs?.endpoints).toEqual([
900
+ {
901
+ instanceId: "example.upstream.v1:source",
902
+ output: "endpoints",
903
+ },
904
+ ])
905
+
906
+ expect(receivedBoundary).toEqual({
907
+ instanceId: "example.missing-multiple-boundary.v1:test",
908
+ output: "endpoints",
909
+ })
910
+ })
911
+
912
+ it("should pass only provided items for multiple input", () => {
913
+ const cluster = defineEntity({
914
+ type: "example.cluster.v1",
915
+ schema: z.object({ id: z.string() }),
916
+ })
917
+
918
+ const source = defineComponent({
919
+ type: "example.source.v1",
920
+ outputs: { k8sCluster: cluster },
921
+ create: ({ id }) => ({
922
+ k8sCluster: { instanceId: id, output: "k8sCluster" },
923
+ }),
924
+ })
925
+
926
+ let receivedCount = -1
927
+
928
+ const component = defineComponent({
929
+ type: "example.multiple-filtering.v1",
930
+ inputs: {
931
+ k8sClusters: {
932
+ entity: cluster,
933
+ multiple: true,
934
+ required: false,
935
+ },
936
+ },
937
+ create: ({ inputs }) => {
938
+ receivedCount = inputs.k8sClusters?.length ?? 0
939
+ return {}
940
+ },
941
+ })
942
+
943
+ const { k8sCluster } = source({ name: "cluster-1" })
944
+
945
+ component({
946
+ name: "test",
947
+ inputs: {
948
+ k8sClusters: [
949
+ k8sCluster,
950
+ {
951
+ provided: false,
952
+ [boundaryInput]: {
953
+ instanceId: "example.source.v1:missing",
954
+ output: "k8sCluster",
955
+ },
956
+ } as never,
957
+ ],
958
+ },
959
+ })
960
+
961
+ expect(receivedCount).toBe(1)
962
+ })
963
+
964
+ it("should keep empty multiple input iterable", () => {
965
+ const cluster = defineEntity({
966
+ type: "example.cluster.v1",
967
+ schema: z.object({ id: z.string() }),
968
+ })
969
+
970
+ let receivedCount = -1
971
+
972
+ const component = defineComponent({
973
+ type: "example.empty-multiple.v1",
974
+ inputs: {
975
+ k8sClusters: {
976
+ entity: cluster,
977
+ multiple: true,
978
+ required: false,
979
+ },
980
+ },
981
+ create: ({ inputs }) => {
982
+ receivedCount = [...(inputs.k8sClusters ?? [])].length
983
+ return {}
984
+ },
985
+ })
986
+
987
+ component({
988
+ name: "test",
989
+ inputs: {
990
+ k8sClusters: [
991
+ {
992
+ provided: false,
993
+ [boundaryInput]: {
994
+ instanceId: "example.source.v1:missing",
995
+ output: "k8sCluster",
996
+ },
997
+ } as never,
998
+ ],
999
+ },
1000
+ })
1001
+
1002
+ expect(receivedCount).toBe(0)
1003
+ })
1004
+
1005
+ it("should keep declared multiple output defined when omitted", () => {
1006
+ const endpoint = defineEntity({
1007
+ type: "example.endpoint.v1",
1008
+ schema: z.object({ id: z.string() }),
1009
+ })
1010
+
1011
+ const component = defineComponent({
1012
+ type: "example.missing-multiple-output.v1",
1013
+ outputs: {
1014
+ endpoints: {
1015
+ entity: endpoint,
1016
+ multiple: true,
1017
+ },
1018
+ },
1019
+ create: () => ({}),
1020
+ })
1021
+
1022
+ const outputs = component({ name: "test" }) as Record<string, unknown>
1023
+ const endpoints = outputs.endpoints as Array<Record<string, unknown>> & {
1024
+ [boundaryInput]?: InstanceInput
1025
+ address: {
1026
+ asSubnet: Array<Record<string, unknown>>
1027
+ }
1028
+ }
1029
+
1030
+ expect(Array.isArray(endpoints)).toBe(true)
1031
+ expect(endpoints.length).toBe(0)
1032
+ expect(endpoints.address.asSubnet.length).toBe(0)
1033
+ expect(endpoints[boundaryInput]).toEqual({
1034
+ instanceId: "example.missing-multiple-output.v1:test",
1035
+ output: "endpoints",
1036
+ })
1037
+ })
1038
+
1039
+ it("should keep declared single output defined when omitted", () => {
1040
+ const endpoint = defineEntity({
1041
+ type: "example.endpoint.v1",
1042
+ schema: z.object({ id: z.string() }),
1043
+ })
1044
+
1045
+ const component = defineComponent({
1046
+ type: "example.missing-single-output.v1",
1047
+ outputs: {
1048
+ endpoint,
1049
+ },
1050
+ create: () => ({}),
1051
+ })
1052
+
1053
+ const outputs = component({ name: "test" }) as Record<string, unknown>
1054
+ const output = outputs.endpoint as Record<string | symbol, unknown>
1055
+
1056
+ expect(output).toBeDefined()
1057
+ expect(output.provided).toBe(false)
1058
+ expect(output[boundaryInput]).toEqual({
1059
+ instanceId: "example.missing-single-output.v1:test",
1060
+ output: "endpoint",
1061
+ })
1062
+ })
1063
+
1064
+ it("should allow non-provided single output in composite component", () => {
1065
+ const endpoint = defineEntity({
1066
+ type: "example.endpoint.v1",
1067
+ schema: z.object({ id: z.string() }),
1068
+ })
1069
+
1070
+ const component = defineComponent({
1071
+ type: "example.non-provided-single-output.v1",
1072
+ outputs: {
1073
+ endpoint,
1074
+ },
1075
+ create: ({ id }) => ({
1076
+ endpoint: {
1077
+ provided: false as never,
1078
+ [boundaryInput]: {
1079
+ instanceId: id,
1080
+ output: "endpoint",
1081
+ },
1082
+ },
1083
+ }),
1084
+ })
1085
+
1086
+ const outputs = component({ name: "test" }) as Record<string, unknown>
1087
+ const output = outputs.endpoint as Record<string | symbol, unknown>
1088
+
1089
+ expect(output.provided).toBe(false)
1090
+ expect(output[boundaryInput]).toEqual({
1091
+ instanceId: "example.non-provided-single-output.v1:test",
1092
+ output: "endpoint",
1093
+ })
1094
+ })
1095
+
1096
+ it("should reject non-provided items in multiple outputs", () => {
1097
+ const endpoint = defineEntity({
1098
+ type: "example.endpoint.v1",
1099
+ schema: z.object({ id: z.string() }),
1100
+ })
1101
+
1102
+ const component = defineComponent({
1103
+ type: "example.invalid-multiple-output.v1",
1104
+ outputs: {
1105
+ endpoints: {
1106
+ entity: endpoint,
1107
+ multiple: true,
1108
+ },
1109
+ },
1110
+ create: ({ id }) => ({
1111
+ endpoints: [
1112
+ {
1113
+ provided: false,
1114
+ [boundaryInput]: {
1115
+ instanceId: id,
1116
+ output: "endpoints",
1117
+ },
1118
+ } as never,
1119
+ ],
1120
+ }),
1121
+ })
1122
+
1123
+ expect(() => component({ name: "test" })).toThrow(
1124
+ 'Multiple output "endpoints" in instance "example.invalid-multiple-output.v1:test" cannot contain non-provided items',
1125
+ )
1126
+ })
1127
+
1128
+ it("should require provided outputs for unit components", () => {
1129
+ const endpoint = defineEntity({
1130
+ type: "example.endpoint.v1",
1131
+ schema: z.object({ id: z.string() }),
1132
+ })
1133
+
1134
+ const unitLike = defineComponent({
1135
+ type: "example.invalid-unit-output.v1",
1136
+ [kind]: "unit",
1137
+ outputs: {
1138
+ endpoint,
1139
+ },
1140
+ create: ({ id }) => ({
1141
+ endpoint: {
1142
+ provided: false,
1143
+ [boundaryInput]: {
1144
+ instanceId: id,
1145
+ output: "endpoint",
1146
+ },
1147
+ },
1148
+ }),
1149
+ })
1150
+
1151
+ expect(() => unitLike({ name: "test" })).toThrow(
1152
+ 'Unit output "endpoint" in instance "example.invalid-unit-output.v1:test" must be provided',
1153
+ )
1154
+ })
245
1155
  })