@lemonmade/ucp-schema 0.1.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.
@@ -0,0 +1,885 @@
1
+ import {describe, it, expect} from 'vitest';
2
+
3
+ import {UcpSchemaComposer, type UcpProfileSchemaFetcher} from './compose.ts';
4
+
5
+ describe('UcpSchemaComposer', () => {
6
+ it('creates a schema from an empty profile', async () => {
7
+ const composer = await UcpSchemaComposer.fromProfile({
8
+ ucp: {
9
+ version: '2026-01-11',
10
+ capabilities: [],
11
+ },
12
+ });
13
+
14
+ expect(
15
+ composer.get('https://ucp.dev/schemas/shopping/checkout.json'),
16
+ ).toBeUndefined();
17
+ });
18
+
19
+ it('rejects schemas if a schema fails to fetch', async () => {
20
+ const composerPromise = UcpSchemaComposer.fromProfile(
21
+ {
22
+ ucp: {
23
+ version: '2026-01-11',
24
+ capabilities: [
25
+ {
26
+ name: 'dev.ucp.shopping.checkout',
27
+ version: '2026-01-11',
28
+ spec: 'https://ucp.dev/specification/checkout',
29
+ schema: 'https://ucp.dev/schemas/shopping/checkout.json',
30
+ },
31
+ ],
32
+ },
33
+ },
34
+ {
35
+ fetch() {
36
+ throw new Error();
37
+ },
38
+ },
39
+ );
40
+
41
+ await expect(composerPromise).rejects.toThrow(
42
+ 'Schema not found for URL: https://ucp.dev/schemas/shopping/checkout.json',
43
+ );
44
+ });
45
+
46
+ it('rejects schemas if a schema has a name that does not match its URL', async () => {
47
+ const composerPromise = UcpSchemaComposer.fromProfile(
48
+ {
49
+ ucp: {
50
+ version: '2026-01-11',
51
+ capabilities: [
52
+ {
53
+ name: 'dev.ucp.shopping.checkout',
54
+ version: '2026-01-11',
55
+ spec: 'https://ucp.dev/specification/checkout',
56
+ schema: 'https://mischief.dev/schemas/shopping/checkout.json',
57
+ },
58
+ ],
59
+ },
60
+ },
61
+ {
62
+ fetch: createMockSchemaFetcher({
63
+ 'https://mischief.dev/schemas/shopping/checkout.json': {
64
+ type: 'object',
65
+ },
66
+ }),
67
+ },
68
+ );
69
+
70
+ await expect(composerPromise).rejects.toThrow(
71
+ 'Invalid schema name: dev.ucp.shopping.checkout does not match URL https://mischief.dev/schemas/shopping/checkout.json',
72
+ );
73
+ });
74
+
75
+ it('creates an object that can look up schemas by URL', async () => {
76
+ const composer = await UcpSchemaComposer.fromProfile(
77
+ {
78
+ ucp: {
79
+ version: '2026-01-11',
80
+ capabilities: [
81
+ {
82
+ name: 'dev.ucp.shopping.checkout',
83
+ version: '2026-01-11',
84
+ spec: 'https://ucp.dev/specification/checkout',
85
+ schema: 'https://ucp.dev/schemas/shopping/checkout.json',
86
+ },
87
+ ],
88
+ },
89
+ },
90
+ {
91
+ fetch: createMockSchemaFetcher({
92
+ 'https://ucp.dev/schemas/shopping/checkout.json': {
93
+ type: 'object',
94
+ },
95
+ }),
96
+ },
97
+ );
98
+
99
+ expect(
100
+ composer
101
+ .get('https://ucp.dev/schemas/shopping/checkout.json')
102
+ ?.composedSchema(),
103
+ ).toEqual({
104
+ type: 'object',
105
+ });
106
+ });
107
+
108
+ it('creates an object that can look up schemas by name', async () => {
109
+ const composer = await UcpSchemaComposer.fromProfile(
110
+ {
111
+ ucp: {
112
+ version: '2026-01-11',
113
+ capabilities: [
114
+ {
115
+ name: 'dev.ucp.shopping.checkout',
116
+ version: '2026-01-11',
117
+ spec: 'https://ucp.dev/specification/checkout',
118
+ schema: 'https://ucp.dev/schemas/shopping/checkout.json',
119
+ },
120
+ ],
121
+ },
122
+ },
123
+ {
124
+ fetch: createMockSchemaFetcher({
125
+ 'https://ucp.dev/schemas/shopping/checkout.json': {
126
+ type: 'object',
127
+ },
128
+ }),
129
+ },
130
+ );
131
+
132
+ expect(composer.get('dev.ucp.shopping.checkout')?.composedSchema()).toEqual(
133
+ {
134
+ type: 'object',
135
+ },
136
+ );
137
+ });
138
+
139
+ it('merges schemas from extensions', async () => {
140
+ const composer = await UcpSchemaComposer.fromProfile(
141
+ {
142
+ ucp: {
143
+ version: '2026-01-11',
144
+ capabilities: [
145
+ {
146
+ name: 'dev.ucp.shopping.checkout',
147
+ version: '2026-01-11',
148
+ spec: 'https://ucp.dev/specification/checkout',
149
+ schema: 'https://ucp.dev/schemas/shopping/checkout.json',
150
+ },
151
+ {
152
+ name: 'dev.ucp.shopping.fulfillment',
153
+ version: '2026-01-11',
154
+ spec: 'https://ucp.dev/specification/fulfillment',
155
+ schema: 'https://ucp.dev/schemas/shopping/fulfillment.json',
156
+ extends: 'dev.ucp.shopping.checkout',
157
+ },
158
+ ],
159
+ },
160
+ },
161
+ {
162
+ fetch: createMockSchemaFetcher({
163
+ 'https://ucp.dev/schemas/shopping/checkout.json': {
164
+ type: 'object',
165
+ properties: {
166
+ id: {type: 'string'},
167
+ },
168
+ required: ['id'],
169
+ },
170
+ 'https://ucp.dev/schemas/shopping/fulfillment.json': {
171
+ $defs: {
172
+ 'dev.ucp.shopping.checkout': {
173
+ allOf: [
174
+ {$ref: 'checkout.json'},
175
+ {
176
+ properties: {
177
+ fulfillment: {
178
+ $ref: '#/$defs/fulfillment',
179
+ },
180
+ },
181
+ },
182
+ ],
183
+ },
184
+ fulfillment: {
185
+ type: 'object',
186
+ properties: {
187
+ methods: {
188
+ type: 'array',
189
+ items: {
190
+ $ref: '#/$defs/fulfillment_method',
191
+ },
192
+ },
193
+ },
194
+ required: ['methods'],
195
+ },
196
+ fulfillment_method: {
197
+ type: 'object',
198
+ properties: {
199
+ id: {type: 'string'},
200
+ },
201
+ required: ['id'],
202
+ },
203
+ },
204
+ },
205
+ }),
206
+ },
207
+ );
208
+
209
+ expect(
210
+ composer
211
+ .get('https://ucp.dev/schemas/shopping/checkout.json')
212
+ ?.composedSchema(),
213
+ ).toEqual({
214
+ type: 'object',
215
+ allOf: [
216
+ {type: 'object', $ref: '#/$defs/dev.ucp.shopping.checkout'},
217
+ {
218
+ type: 'object',
219
+ $ref: '#/$defs/dev.ucp.shopping.fulfillment~dev.ucp.shopping.checkout',
220
+ },
221
+ ],
222
+ $defs: {
223
+ 'dev.ucp.shopping.checkout': {
224
+ properties: {
225
+ id: {type: 'string'},
226
+ },
227
+ required: ['id'],
228
+ type: 'object',
229
+ title: 'dev.ucp.shopping.checkout (base)',
230
+ },
231
+ 'dev.ucp.shopping.fulfillment~dev.ucp.shopping.checkout': {
232
+ properties: {
233
+ fulfillment: {
234
+ $ref: '#/$defs/dev.ucp.shopping.fulfillment~fulfillment',
235
+ },
236
+ },
237
+ type: 'object',
238
+ },
239
+ 'dev.ucp.shopping.fulfillment~fulfillment': {
240
+ properties: {
241
+ methods: {
242
+ items: {
243
+ $ref: '#/$defs/dev.ucp.shopping.fulfillment~fulfillment_method',
244
+ },
245
+ type: 'array',
246
+ },
247
+ },
248
+ required: ['methods'],
249
+ type: 'object',
250
+ },
251
+ 'dev.ucp.shopping.fulfillment~fulfillment_method': {
252
+ properties: {
253
+ id: {type: 'string'},
254
+ },
255
+ required: ['id'],
256
+ type: 'object',
257
+ },
258
+ },
259
+ });
260
+ });
261
+
262
+ it('merges schemas from extensions that target multiple base capabilities', async () => {
263
+ const composer = await UcpSchemaComposer.fromProfile(
264
+ {
265
+ ucp: {
266
+ version: '2026-01-11',
267
+ capabilities: [
268
+ {
269
+ name: 'dev.ucp.shopping.checkout',
270
+ version: '2026-01-11',
271
+ spec: 'https://ucp.dev/specification/checkout',
272
+ schema: 'https://ucp.dev/schemas/shopping/checkout.json',
273
+ },
274
+ {
275
+ name: 'dev.ucp.shopping.cart',
276
+ version: '2026-01-11',
277
+ spec: 'https://ucp.dev/specification/cart',
278
+ schema: 'https://ucp.dev/schemas/shopping/cart.json',
279
+ },
280
+ {
281
+ name: 'dev.ucp.shopping.fulfillment',
282
+ version: '2026-01-11',
283
+ spec: 'https://ucp.dev/specification/fulfillment',
284
+ schema: 'https://ucp.dev/schemas/shopping/fulfillment.json',
285
+ extends: ['dev.ucp.shopping.checkout', 'dev.ucp.shopping.cart'],
286
+ },
287
+ ],
288
+ },
289
+ },
290
+ {
291
+ fetch: createMockSchemaFetcher({
292
+ 'https://ucp.dev/schemas/shopping/checkout.json': {
293
+ type: 'object',
294
+ properties: {
295
+ id: {type: 'string'},
296
+ },
297
+ required: ['id'],
298
+ },
299
+ 'https://ucp.dev/schemas/shopping/cart.json': {
300
+ type: 'object',
301
+ properties: {
302
+ id: {type: 'string'},
303
+ },
304
+ required: ['id'],
305
+ },
306
+ 'https://ucp.dev/schemas/shopping/fulfillment.json': {
307
+ $defs: {
308
+ 'dev.ucp.shopping.checkout': {
309
+ allOf: [
310
+ {$ref: 'checkout.json'},
311
+ {
312
+ properties: {
313
+ fulfillment: {
314
+ $ref: '#/$defs/fulfillment',
315
+ },
316
+ },
317
+ },
318
+ ],
319
+ },
320
+ 'dev.ucp.shopping.cart': {
321
+ allOf: [
322
+ {$ref: 'cart.json'},
323
+ {
324
+ properties: {
325
+ fulfillment: {
326
+ $ref: '#/$defs/fulfillment',
327
+ },
328
+ },
329
+ },
330
+ ],
331
+ },
332
+ fulfillment: {
333
+ type: 'object',
334
+ properties: {
335
+ methods: {
336
+ type: 'array',
337
+ items: {
338
+ $ref: '#/$defs/fulfillment_method',
339
+ },
340
+ },
341
+ },
342
+ required: ['methods'],
343
+ },
344
+ fulfillment_method: {
345
+ type: 'object',
346
+ properties: {
347
+ id: {type: 'string'},
348
+ },
349
+ required: ['id'],
350
+ },
351
+ },
352
+ },
353
+ }),
354
+ },
355
+ );
356
+
357
+ expect(
358
+ composer
359
+ .get('https://ucp.dev/schemas/shopping/cart.json')
360
+ ?.composedSchema(),
361
+ ).toEqual({
362
+ type: 'object',
363
+ allOf: [
364
+ {type: 'object', $ref: '#/$defs/dev.ucp.shopping.cart'},
365
+ {
366
+ type: 'object',
367
+ $ref: '#/$defs/dev.ucp.shopping.fulfillment~dev.ucp.shopping.cart',
368
+ },
369
+ ],
370
+ $defs: {
371
+ 'dev.ucp.shopping.cart': {
372
+ properties: {
373
+ id: {type: 'string'},
374
+ },
375
+ required: ['id'],
376
+ type: 'object',
377
+ title: 'dev.ucp.shopping.cart (base)',
378
+ },
379
+ 'dev.ucp.shopping.fulfillment~dev.ucp.shopping.cart': {
380
+ properties: {
381
+ fulfillment: {
382
+ $ref: '#/$defs/dev.ucp.shopping.fulfillment~fulfillment',
383
+ },
384
+ },
385
+ type: 'object',
386
+ },
387
+ 'dev.ucp.shopping.fulfillment~fulfillment': {
388
+ properties: {
389
+ methods: {
390
+ items: {
391
+ $ref: '#/$defs/dev.ucp.shopping.fulfillment~fulfillment_method',
392
+ },
393
+ type: 'array',
394
+ },
395
+ },
396
+ required: ['methods'],
397
+ type: 'object',
398
+ },
399
+ 'dev.ucp.shopping.fulfillment~fulfillment_method': {
400
+ properties: {
401
+ id: {type: 'string'},
402
+ },
403
+ required: ['id'],
404
+ type: 'object',
405
+ },
406
+ },
407
+ });
408
+ });
409
+
410
+ it('merges schemas from multiple extensions', async () => {
411
+ const composer = await UcpSchemaComposer.fromProfile(
412
+ {
413
+ ucp: {
414
+ version: '2026-01-11',
415
+ capabilities: [
416
+ {
417
+ name: 'dev.ucp.shopping.checkout',
418
+ version: '2026-01-11',
419
+ spec: 'https://ucp.dev/specification/checkout',
420
+ schema: 'https://ucp.dev/schemas/shopping/checkout.json',
421
+ },
422
+ {
423
+ name: 'dev.ucp.shopping.fulfillment',
424
+ version: '2026-01-11',
425
+ spec: 'https://ucp.dev/specification/fulfillment',
426
+ schema: 'https://ucp.dev/schemas/shopping/fulfillment.json',
427
+ extends: 'dev.ucp.shopping.checkout',
428
+ },
429
+ {
430
+ name: 'dev.ucp.shopping.discount',
431
+ version: '2026-01-11',
432
+ spec: 'https://ucp.dev/specification/discount',
433
+ schema: 'https://ucp.dev/schemas/shopping/discount.json',
434
+ extends: 'dev.ucp.shopping.checkout',
435
+ },
436
+ ],
437
+ },
438
+ },
439
+ {
440
+ fetch: createMockSchemaFetcher({
441
+ 'https://ucp.dev/schemas/shopping/checkout.json': {
442
+ type: 'object',
443
+ properties: {
444
+ id: {type: 'string'},
445
+ },
446
+ required: ['id'],
447
+ },
448
+ 'https://ucp.dev/schemas/shopping/discount.json': {
449
+ type: 'object',
450
+ $defs: {
451
+ 'dev.ucp.shopping.checkout': {
452
+ allOf: [
453
+ {$ref: 'checkout.json'},
454
+ {
455
+ properties: {
456
+ allocations: {items: {$ref: '#/$defs/allocation'}},
457
+ },
458
+ required: ['allocations'],
459
+ },
460
+ ],
461
+ },
462
+ allocation: {
463
+ type: 'object',
464
+ properties: {
465
+ path: {type: 'string'},
466
+ amount: {type: 'integer'},
467
+ },
468
+ required: ['path', 'amount'],
469
+ },
470
+ },
471
+ },
472
+ 'https://ucp.dev/schemas/shopping/fulfillment.json': {
473
+ $defs: {
474
+ 'dev.ucp.shopping.checkout': {
475
+ allOf: [
476
+ {$ref: 'checkout.json'},
477
+ {
478
+ properties: {
479
+ fulfillment: {
480
+ $ref: '#/$defs/fulfillment',
481
+ },
482
+ },
483
+ },
484
+ ],
485
+ },
486
+ fulfillment: {
487
+ type: 'object',
488
+ properties: {
489
+ methods: {
490
+ type: 'array',
491
+ items: {
492
+ $ref: '#/$defs/fulfillment_method',
493
+ },
494
+ },
495
+ },
496
+ required: ['methods'],
497
+ },
498
+ fulfillment_method: {
499
+ type: 'object',
500
+ properties: {
501
+ id: {type: 'string'},
502
+ },
503
+ required: ['id'],
504
+ },
505
+ },
506
+ },
507
+ }),
508
+ },
509
+ );
510
+
511
+ expect(
512
+ composer
513
+ .get('https://ucp.dev/schemas/shopping/checkout.json')
514
+ ?.composedSchema(),
515
+ ).toEqual({
516
+ type: 'object',
517
+ allOf: [
518
+ {type: 'object', $ref: '#/$defs/dev.ucp.shopping.checkout'},
519
+ {
520
+ type: 'object',
521
+ $ref: '#/$defs/dev.ucp.shopping.fulfillment~dev.ucp.shopping.checkout',
522
+ },
523
+ {
524
+ type: 'object',
525
+ $ref: '#/$defs/dev.ucp.shopping.discount~dev.ucp.shopping.checkout',
526
+ },
527
+ ],
528
+ $defs: {
529
+ 'dev.ucp.shopping.checkout': {
530
+ properties: {
531
+ id: {type: 'string'},
532
+ },
533
+ required: ['id'],
534
+ type: 'object',
535
+ title: 'dev.ucp.shopping.checkout (base)',
536
+ },
537
+ 'dev.ucp.shopping.discount~dev.ucp.shopping.checkout': {
538
+ properties: {
539
+ allocations: {
540
+ items: {$ref: '#/$defs/dev.ucp.shopping.discount~allocation'},
541
+ },
542
+ },
543
+ required: ['allocations'],
544
+ type: 'object',
545
+ },
546
+ 'dev.ucp.shopping.discount~allocation': {
547
+ properties: {
548
+ path: {type: 'string'},
549
+ amount: {type: 'integer'},
550
+ },
551
+ required: ['path', 'amount'],
552
+ type: 'object',
553
+ },
554
+ 'dev.ucp.shopping.fulfillment~dev.ucp.shopping.checkout': {
555
+ properties: {
556
+ fulfillment: {
557
+ $ref: '#/$defs/dev.ucp.shopping.fulfillment~fulfillment',
558
+ },
559
+ },
560
+ type: 'object',
561
+ },
562
+ 'dev.ucp.shopping.fulfillment~fulfillment': {
563
+ properties: {
564
+ methods: {
565
+ items: {
566
+ $ref: '#/$defs/dev.ucp.shopping.fulfillment~fulfillment_method',
567
+ },
568
+ type: 'array',
569
+ },
570
+ },
571
+ required: ['methods'],
572
+ type: 'object',
573
+ },
574
+ 'dev.ucp.shopping.fulfillment~fulfillment_method': {
575
+ properties: {
576
+ id: {type: 'string'},
577
+ },
578
+ required: ['id'],
579
+ type: 'object',
580
+ },
581
+ },
582
+ });
583
+ });
584
+
585
+ it('does not merge schemas from extensions that are not in the extends list', async () => {
586
+ const composer = await UcpSchemaComposer.fromProfile(
587
+ {
588
+ ucp: {
589
+ version: '2026-01-11',
590
+ capabilities: [
591
+ {
592
+ name: 'dev.ucp.shopping.checkout',
593
+ version: '2026-01-11',
594
+ spec: 'https://ucp.dev/specification/checkout',
595
+ schema: 'https://ucp.dev/schemas/shopping/checkout.json',
596
+ },
597
+ {
598
+ name: 'dev.ucp.shopping.fulfillment',
599
+ version: '2026-01-11',
600
+ spec: 'https://ucp.dev/specification/fulfillment',
601
+ schema: 'https://ucp.dev/schemas/shopping/fulfillment.json',
602
+ },
603
+ ],
604
+ },
605
+ },
606
+ {
607
+ fetch: createMockSchemaFetcher({
608
+ 'https://ucp.dev/schemas/shopping/checkout.json': {
609
+ type: 'object',
610
+ properties: {
611
+ id: {type: 'string'},
612
+ },
613
+ },
614
+ 'https://ucp.dev/schemas/shopping/fulfillment.json': {
615
+ $defs: {
616
+ 'dev.ucp.shopping.checkout': {
617
+ properties: {
618
+ fulfillment: {$ref: '#/$defs/fulfillment'},
619
+ },
620
+ },
621
+ },
622
+ },
623
+ }),
624
+ },
625
+ );
626
+
627
+ expect(
628
+ composer
629
+ .get('https://ucp.dev/schemas/shopping/checkout.json')
630
+ ?.composedSchema(),
631
+ ).toEqual({
632
+ type: 'object',
633
+ properties: {
634
+ id: {type: 'string'},
635
+ },
636
+ });
637
+ });
638
+
639
+ it('can restrict schemas based on operation name and the ucp_request annotations in the schema', async () => {
640
+ const composer = await UcpSchemaComposer.fromProfile(
641
+ {
642
+ ucp: {
643
+ version: '2026-01-11',
644
+ capabilities: [
645
+ {
646
+ name: 'dev.ucp.shopping.checkout',
647
+ version: '2026-01-11',
648
+ spec: 'https://ucp.dev/specification/checkout',
649
+ schema: 'https://ucp.dev/schemas/shopping/checkout.json',
650
+ },
651
+ {
652
+ name: 'dev.ucp.shopping.fulfillment',
653
+ version: '2026-01-11',
654
+ spec: 'https://ucp.dev/specification/fulfillment',
655
+ schema: 'https://ucp.dev/schemas/shopping/fulfillment.json',
656
+ extends: 'dev.ucp.shopping.checkout',
657
+ },
658
+ ],
659
+ },
660
+ },
661
+ {
662
+ fetch: createMockSchemaFetcher({
663
+ 'https://ucp.dev/schemas/shopping/checkout.json': {
664
+ type: 'object',
665
+ properties: {
666
+ id: {type: 'string'},
667
+ line_items: {
668
+ items: {
669
+ $ref: '#/$defs/line_item',
670
+ },
671
+ ucp_request: {
672
+ complete: 'omit',
673
+ },
674
+ },
675
+ },
676
+ required: ['id', 'line_items'],
677
+ $defs: {
678
+ line_item: {
679
+ type: 'object',
680
+ properties: {
681
+ id: {type: 'string'},
682
+ },
683
+ required: ['id'],
684
+ },
685
+ },
686
+ },
687
+ 'https://ucp.dev/schemas/shopping/fulfillment.json': {
688
+ $defs: {
689
+ 'dev.ucp.shopping.checkout': {
690
+ allOf: [
691
+ {$ref: 'checkout.json'},
692
+ {
693
+ properties: {
694
+ fulfillment: {
695
+ $ref: '#/$defs/fulfillment',
696
+ },
697
+ },
698
+ },
699
+ ],
700
+ },
701
+ fulfillment: {
702
+ type: 'object',
703
+ properties: {
704
+ methods: {
705
+ type: 'array',
706
+ items: {
707
+ $ref: '#/$defs/fulfillment_method',
708
+ },
709
+ ucp_request: 'omit',
710
+ },
711
+ },
712
+ required: ['methods'],
713
+ },
714
+ fulfillment_method: {
715
+ type: 'object',
716
+ properties: {
717
+ id: {
718
+ type: 'string',
719
+ ucp_request: {
720
+ create: 'omit',
721
+ update: 'required',
722
+ },
723
+ },
724
+ type: {
725
+ type: 'string',
726
+ ucp_request: {
727
+ create: 'required',
728
+ update: 'optional',
729
+ },
730
+ },
731
+ line_item_ids: {
732
+ items: {type: 'string'},
733
+ ucp_request: {
734
+ create: 'optional',
735
+ update: 'optional',
736
+ complete: 'omit',
737
+ },
738
+ },
739
+ },
740
+ required: ['id', 'type', 'line_item_ids'],
741
+ },
742
+ },
743
+ },
744
+ }),
745
+ },
746
+ );
747
+
748
+ const checkoutFile = composer.get(
749
+ 'https://ucp.dev/schemas/shopping/checkout.json',
750
+ )!;
751
+
752
+ expect(checkoutFile.composedSchema()).toEqual({
753
+ type: 'object',
754
+ allOf: [
755
+ {type: 'object', $ref: '#/$defs/dev.ucp.shopping.checkout'},
756
+ {
757
+ type: 'object',
758
+ $ref: '#/$defs/dev.ucp.shopping.fulfillment~dev.ucp.shopping.checkout',
759
+ },
760
+ ],
761
+ $defs: {
762
+ line_item: {
763
+ properties: {
764
+ id: {type: 'string'},
765
+ },
766
+ required: ['id'],
767
+ type: 'object',
768
+ },
769
+ 'dev.ucp.shopping.checkout': {
770
+ properties: {
771
+ id: {type: 'string'},
772
+ line_items: {
773
+ items: {
774
+ $ref: '#/$defs/line_item',
775
+ },
776
+ },
777
+ },
778
+ title: 'dev.ucp.shopping.checkout (base)',
779
+ required: ['id', 'line_items'],
780
+ type: 'object',
781
+ },
782
+ 'dev.ucp.shopping.fulfillment~dev.ucp.shopping.checkout': {
783
+ properties: {
784
+ fulfillment: {
785
+ $ref: '#/$defs/dev.ucp.shopping.fulfillment~fulfillment',
786
+ },
787
+ },
788
+ type: 'object',
789
+ },
790
+ 'dev.ucp.shopping.fulfillment~fulfillment': {
791
+ properties: {
792
+ methods: {
793
+ items: {
794
+ $ref: '#/$defs/dev.ucp.shopping.fulfillment~fulfillment_method',
795
+ },
796
+ type: 'array',
797
+ },
798
+ },
799
+ required: ['methods'],
800
+ type: 'object',
801
+ },
802
+ 'dev.ucp.shopping.fulfillment~fulfillment_method': {
803
+ properties: {
804
+ id: {type: 'string'},
805
+ type: {type: 'string'},
806
+ line_item_ids: {
807
+ items: {type: 'string'},
808
+ },
809
+ },
810
+ required: ['id', 'type', 'line_item_ids'],
811
+ type: 'object',
812
+ },
813
+ },
814
+ });
815
+
816
+ expect(checkoutFile.composedSchema({operation: 'create'})).toEqual({
817
+ type: 'object',
818
+ allOf: [
819
+ {type: 'object', $ref: '#/$defs/dev.ucp.shopping.checkout'},
820
+ {
821
+ type: 'object',
822
+ $ref: '#/$defs/dev.ucp.shopping.fulfillment~dev.ucp.shopping.checkout',
823
+ },
824
+ ],
825
+ $defs: {
826
+ line_item: {
827
+ properties: {
828
+ id: {type: 'string'},
829
+ },
830
+ required: ['id'],
831
+ type: 'object',
832
+ },
833
+ 'dev.ucp.shopping.checkout': {
834
+ properties: {
835
+ id: {type: 'string'},
836
+ line_items: {
837
+ items: {
838
+ $ref: '#/$defs/line_item',
839
+ },
840
+ },
841
+ },
842
+ title: 'dev.ucp.shopping.checkout (base)',
843
+ required: ['id', 'line_items'],
844
+ type: 'object',
845
+ },
846
+ 'dev.ucp.shopping.fulfillment~dev.ucp.shopping.checkout': {
847
+ properties: {
848
+ fulfillment: {
849
+ $ref: '#/$defs/dev.ucp.shopping.fulfillment~fulfillment',
850
+ },
851
+ },
852
+ type: 'object',
853
+ },
854
+ 'dev.ucp.shopping.fulfillment~fulfillment': {
855
+ properties: {},
856
+ required: [],
857
+ type: 'object',
858
+ },
859
+ 'dev.ucp.shopping.fulfillment~fulfillment_method': {
860
+ properties: {
861
+ type: {type: 'string'},
862
+ line_item_ids: {
863
+ items: {type: 'string'},
864
+ },
865
+ },
866
+ required: ['type'],
867
+ type: 'object',
868
+ },
869
+ },
870
+ });
871
+ });
872
+ });
873
+
874
+ function createMockSchemaFetcher(
875
+ mapping: Record<string, Awaited<ReturnType<UcpProfileSchemaFetcher>>> = {},
876
+ ) {
877
+ const urlMap = new Map(Object.entries(mapping));
878
+
879
+ return (async (url) => {
880
+ return (
881
+ urlMap.get(url) ??
882
+ Promise.reject(new Error(`Schema not found for URL: ${url}`))
883
+ );
884
+ }) satisfies UcpProfileSchemaFetcher;
885
+ }