@highstate/contract 0.18.0 → 0.20.0

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