@squiz/dx-json-schema-lib 1.87.0 → 1.89.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.
Files changed (73) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/lib/browser.d.ts +21 -0
  3. package/lib/browser.js +61 -0
  4. package/lib/browser.js.map +1 -0
  5. package/lib/formatted-text/v1/resolveFormattedTextNodes.d.ts +5 -2
  6. package/lib/formatted-text/v1/resolveFormattedTextNodes.js +12 -6
  7. package/lib/formatted-text/v1/resolveFormattedTextNodes.js.map +1 -1
  8. package/lib/formatted-text/v1/resolveFormattedTextNodes.spec.js +58 -0
  9. package/lib/formatted-text/v1/resolveFormattedTextNodes.spec.js.map +1 -1
  10. package/lib/index.d.ts +1 -15
  11. package/lib/index.js +4 -38
  12. package/lib/index.js.map +1 -1
  13. package/lib/primitiveTypes/FormattedText.js +36 -3
  14. package/lib/primitiveTypes/FormattedText.js.map +1 -1
  15. package/lib/utils/jsonBreakDetector/checks/findArrayConstraintNarrowing.d.ts +6 -0
  16. package/lib/utils/jsonBreakDetector/checks/findArrayConstraintNarrowing.js +93 -0
  17. package/lib/utils/jsonBreakDetector/checks/findArrayConstraintNarrowing.js.map +1 -0
  18. package/lib/utils/jsonBreakDetector/checks/findConstChanges.d.ts +6 -0
  19. package/lib/utils/jsonBreakDetector/checks/findConstChanges.js +75 -0
  20. package/lib/utils/jsonBreakDetector/checks/findConstChanges.js.map +1 -0
  21. package/lib/utils/jsonBreakDetector/checks/findEnumNarrowing.d.ts +6 -0
  22. package/lib/utils/jsonBreakDetector/checks/findEnumNarrowing.js +90 -0
  23. package/lib/utils/jsonBreakDetector/checks/findEnumNarrowing.js.map +1 -0
  24. package/lib/utils/jsonBreakDetector/checks/findFieldTypeChanges.d.ts +8 -0
  25. package/lib/utils/jsonBreakDetector/checks/findFieldTypeChanges.js +86 -0
  26. package/lib/utils/jsonBreakDetector/checks/findFieldTypeChanges.js.map +1 -0
  27. package/lib/utils/jsonBreakDetector/checks/findNewRequiredFields.d.ts +6 -0
  28. package/lib/utils/jsonBreakDetector/checks/findNewRequiredFields.js +68 -0
  29. package/lib/utils/jsonBreakDetector/checks/findNewRequiredFields.js.map +1 -0
  30. package/lib/utils/jsonBreakDetector/checks/findNumericConstraintNarrowing.d.ts +6 -0
  31. package/lib/utils/jsonBreakDetector/checks/findNumericConstraintNarrowing.js +179 -0
  32. package/lib/utils/jsonBreakDetector/checks/findNumericConstraintNarrowing.js.map +1 -0
  33. package/lib/utils/jsonBreakDetector/checks/findStringConstraintNarrowing.d.ts +6 -0
  34. package/lib/utils/jsonBreakDetector/checks/findStringConstraintNarrowing.js +106 -0
  35. package/lib/utils/jsonBreakDetector/checks/findStringConstraintNarrowing.js.map +1 -0
  36. package/lib/utils/jsonBreakDetector/compareSubschemas.d.ts +9 -0
  37. package/lib/utils/jsonBreakDetector/compareSubschemas.js +49 -0
  38. package/lib/utils/jsonBreakDetector/compareSubschemas.js.map +1 -0
  39. package/lib/utils/jsonBreakDetector/dereference/dereference.d.ts +6 -0
  40. package/lib/utils/jsonBreakDetector/dereference/dereference.js +52 -0
  41. package/lib/utils/jsonBreakDetector/dereference/dereference.js.map +1 -0
  42. package/lib/utils/jsonBreakDetector/dereference/dereference.spec.d.ts +1 -0
  43. package/lib/utils/jsonBreakDetector/dereference/dereference.spec.js +225 -0
  44. package/lib/utils/jsonBreakDetector/dereference/dereference.spec.js.map +1 -0
  45. package/lib/utils/jsonBreakDetector/jsonBreakDetector.d.ts +8 -0
  46. package/lib/utils/jsonBreakDetector/jsonBreakDetector.js +35 -0
  47. package/lib/utils/jsonBreakDetector/jsonBreakDetector.js.map +1 -0
  48. package/lib/utils/jsonBreakDetector/jsonBreakDetector.spec.d.ts +1 -0
  49. package/lib/utils/jsonBreakDetector/jsonBreakDetector.spec.js +2761 -0
  50. package/lib/utils/jsonBreakDetector/jsonBreakDetector.spec.js.map +1 -0
  51. package/lib/utils/jsonBreakDetector/types.d.ts +25 -0
  52. package/lib/utils/jsonBreakDetector/types.js +28 -0
  53. package/lib/utils/jsonBreakDetector/types.js.map +1 -0
  54. package/package.json +10 -1
  55. package/src/browser.ts +27 -0
  56. package/src/formatted-text/v1/resolveFormattedTextNodes.spec.ts +70 -0
  57. package/src/formatted-text/v1/resolveFormattedTextNodes.ts +19 -4
  58. package/src/index.ts +4 -20
  59. package/src/primitiveTypes/FormattedText.ts +1 -1
  60. package/src/utils/jsonBreakDetector/checks/findArrayConstraintNarrowing.ts +121 -0
  61. package/src/utils/jsonBreakDetector/checks/findConstChanges.ts +97 -0
  62. package/src/utils/jsonBreakDetector/checks/findEnumNarrowing.ts +124 -0
  63. package/src/utils/jsonBreakDetector/checks/findFieldTypeChanges.ts +105 -0
  64. package/src/utils/jsonBreakDetector/checks/findNewRequiredFields.ts +97 -0
  65. package/src/utils/jsonBreakDetector/checks/findNumericConstraintNarrowing.ts +243 -0
  66. package/src/utils/jsonBreakDetector/checks/findStringConstraintNarrowing.ts +139 -0
  67. package/src/utils/jsonBreakDetector/compareSubschemas.ts +67 -0
  68. package/src/utils/jsonBreakDetector/dereference/dereference.spec.ts +245 -0
  69. package/src/utils/jsonBreakDetector/dereference/dereference.ts +65 -0
  70. package/src/utils/jsonBreakDetector/jsonBreakDetector.spec.ts +3041 -0
  71. package/src/utils/jsonBreakDetector/jsonBreakDetector.ts +36 -0
  72. package/src/utils/jsonBreakDetector/types.ts +29 -0
  73. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,3041 @@
1
+ jest.mock('./dereference/dereference', () => ({
2
+ dereferenceLocal: jest.fn(<T extends Record<string, unknown>>(schema: T): T => schema),
3
+ }));
4
+
5
+ import type { JSONSchema } from '@squiz/json-schema-library';
6
+ import { dereferenceLocal } from './dereference/dereference';
7
+
8
+ import jsonBreakDetector from './jsonBreakDetector';
9
+
10
+ const breakAt = (location: string, message: string) => ({ location, message });
11
+
12
+ describe('jsonBreakDetector', () => {
13
+ describe('required fields', () => {
14
+ it('returns no breaks when there are no required fields', async () => {
15
+ const beforeSchema: JSONSchema = {
16
+ type: 'object',
17
+ properties: {
18
+ title: { type: 'string' },
19
+ },
20
+ };
21
+ const afterSchema: JSONSchema = {
22
+ type: 'object',
23
+ properties: {
24
+ title: { type: 'string' },
25
+ },
26
+ };
27
+
28
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
29
+ });
30
+ it('returns no breaks when required fields are unchanged', async () => {
31
+ const beforeSchema: JSONSchema = {
32
+ type: 'object',
33
+ properties: {
34
+ title: { type: 'string' },
35
+ },
36
+ required: ['title'],
37
+ };
38
+ const afterSchema: JSONSchema = {
39
+ type: 'object',
40
+ properties: {
41
+ title: { type: 'string' },
42
+ subtitle: { type: 'string' },
43
+ },
44
+ required: ['title'],
45
+ };
46
+
47
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
48
+ });
49
+
50
+ it('detects a newly required field at the root', async () => {
51
+ const beforeSchema: JSONSchema = {
52
+ type: 'object',
53
+ properties: {
54
+ title: { type: 'string' },
55
+ subtitle: { type: 'string' },
56
+ },
57
+ required: ['title'],
58
+ };
59
+ const afterSchema: JSONSchema = {
60
+ type: 'object',
61
+ properties: {
62
+ title: { type: 'string' },
63
+ subtitle: { type: 'string' },
64
+ },
65
+ required: ['title', 'subtitle'],
66
+ };
67
+
68
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('subtitle', 'New required field')]);
69
+ });
70
+
71
+ it('detects required on a newly added property', async () => {
72
+ const beforeSchema: JSONSchema = {
73
+ type: 'object',
74
+ properties: {
75
+ title: { type: 'string' },
76
+ },
77
+ required: ['title'],
78
+ };
79
+ const afterSchema: JSONSchema = {
80
+ type: 'object',
81
+ properties: {
82
+ title: { type: 'string' },
83
+ subtitle: { type: 'string' },
84
+ },
85
+ required: ['title', 'subtitle'],
86
+ };
87
+
88
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('subtitle', 'New required field')]);
89
+ });
90
+
91
+ it('detects required fields on a newly added nested property', async () => {
92
+ const beforeSchema: JSONSchema = {
93
+ type: 'object',
94
+ properties: {
95
+ title: { type: 'string' },
96
+ },
97
+ required: ['title'],
98
+ };
99
+ const afterSchema: JSONSchema = {
100
+ type: 'object',
101
+ properties: {
102
+ title: { type: 'string' },
103
+ metadata: {
104
+ type: 'object',
105
+ properties: {
106
+ version: { type: 'string' },
107
+ },
108
+ required: ['version'],
109
+ },
110
+ },
111
+ required: ['title'],
112
+ };
113
+
114
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('metadata.version', 'New required field')]);
115
+ });
116
+
117
+ it('returns no breaks when a newly added property is optional', async () => {
118
+ const beforeSchema: JSONSchema = {
119
+ type: 'object',
120
+ properties: {
121
+ title: { type: 'string' },
122
+ },
123
+ required: ['title'],
124
+ };
125
+ const afterSchema: JSONSchema = {
126
+ type: 'object',
127
+ properties: {
128
+ title: { type: 'string' },
129
+ subtitle: { type: 'string' },
130
+ },
131
+ required: ['title'],
132
+ };
133
+
134
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
135
+ });
136
+
137
+ it('detects multiple newly required fields at the root', async () => {
138
+ const beforeSchema: JSONSchema = {
139
+ type: 'object',
140
+ properties: {
141
+ title: { type: 'string' },
142
+ subtitle: { type: 'string' },
143
+ author: { type: 'string' },
144
+ },
145
+ required: [],
146
+ };
147
+ const afterSchema: JSONSchema = {
148
+ type: 'object',
149
+ properties: {
150
+ title: { type: 'string' },
151
+ subtitle: { type: 'string' },
152
+ author: { type: 'string' },
153
+ },
154
+ required: ['title', 'author'],
155
+ };
156
+
157
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
158
+ breakAt('title', 'New required field'),
159
+ breakAt('author', 'New required field'),
160
+ ]);
161
+ });
162
+
163
+ it('detects a newly required field in a nested object property', async () => {
164
+ const beforeSchema: JSONSchema = {
165
+ type: 'object',
166
+ properties: {
167
+ image: {
168
+ type: 'object',
169
+ properties: {
170
+ url: { type: 'string' },
171
+ width: { type: 'number' },
172
+ },
173
+ required: ['url'],
174
+ },
175
+ },
176
+ };
177
+ const afterSchema: JSONSchema = {
178
+ type: 'object',
179
+ properties: {
180
+ image: {
181
+ type: 'object',
182
+ properties: {
183
+ url: { type: 'string' },
184
+ width: { type: 'number' },
185
+ },
186
+ required: ['url', 'width'],
187
+ },
188
+ },
189
+ };
190
+
191
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('image.width', 'New required field')]);
192
+ });
193
+
194
+ it('detects a newly required field in array item schemas', async () => {
195
+ const beforeSchema: JSONSchema = {
196
+ type: 'object',
197
+ properties: {
198
+ tags: {
199
+ type: 'array',
200
+ items: {
201
+ type: 'object',
202
+ properties: {
203
+ name: { type: 'string' },
204
+ value: { type: 'string' },
205
+ },
206
+ required: ['name'],
207
+ },
208
+ },
209
+ },
210
+ };
211
+ const afterSchema: JSONSchema = {
212
+ type: 'object',
213
+ properties: {
214
+ tags: {
215
+ type: 'array',
216
+ items: {
217
+ type: 'object',
218
+ properties: {
219
+ name: { type: 'string' },
220
+ value: { type: 'string' },
221
+ },
222
+ required: ['name', 'value'],
223
+ },
224
+ },
225
+ },
226
+ };
227
+
228
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('tags[].value', 'New required field')]);
229
+ });
230
+
231
+ it('treats a missing before property schema as having no required fields', async () => {
232
+ const beforeSchema: JSONSchema = {
233
+ type: 'object',
234
+ properties: {},
235
+ };
236
+ const afterSchema: JSONSchema = {
237
+ type: 'object',
238
+ properties: {
239
+ metadata: {
240
+ type: 'object',
241
+ properties: {
242
+ version: { type: 'string' },
243
+ },
244
+ required: ['version'],
245
+ },
246
+ },
247
+ };
248
+
249
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('metadata.version', 'New required field')]);
250
+ });
251
+
252
+ it('does not report a break when a field is removed from required', async () => {
253
+ const beforeSchema: JSONSchema = {
254
+ type: 'object',
255
+ properties: {
256
+ title: { type: 'string' },
257
+ subtitle: { type: 'string' },
258
+ },
259
+ required: ['title', 'subtitle'],
260
+ };
261
+ const afterSchema: JSONSchema = {
262
+ type: 'object',
263
+ properties: {
264
+ title: { type: 'string' },
265
+ subtitle: { type: 'string' },
266
+ },
267
+ required: ['title'],
268
+ };
269
+
270
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
271
+ });
272
+
273
+ it('does not report a break when required is removed entirely', async () => {
274
+ const beforeSchema: JSONSchema = {
275
+ type: 'object',
276
+ properties: {
277
+ title: { type: 'string' },
278
+ },
279
+ required: ['title'],
280
+ };
281
+ const afterSchema: JSONSchema = {
282
+ type: 'object',
283
+ properties: {
284
+ title: { type: 'string' },
285
+ },
286
+ };
287
+
288
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
289
+ });
290
+
291
+ it('does not report a break when required is an empty array', async () => {
292
+ const beforeSchema: JSONSchema = {
293
+ type: 'object',
294
+ properties: {
295
+ title: { type: 'string' },
296
+ },
297
+ required: [],
298
+ };
299
+ const afterSchema: JSONSchema = {
300
+ type: 'object',
301
+ properties: {
302
+ title: { type: 'string' },
303
+ },
304
+ required: [],
305
+ };
306
+
307
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
308
+ });
309
+
310
+ it('does not report a break when dependentRequired is unchanged and a new optional property is added', async () => {
311
+ const beforeSchema: JSONSchema = {
312
+ type: 'object',
313
+ properties: {
314
+ image: { type: 'object' },
315
+ width: { type: 'number' },
316
+ },
317
+ dependentRequired: {
318
+ image: ['width'],
319
+ },
320
+ };
321
+ const afterSchema: JSONSchema = {
322
+ type: 'object',
323
+ properties: {
324
+ image: { type: 'object' },
325
+ width: { type: 'number' },
326
+ height: { type: 'number' },
327
+ },
328
+ dependentRequired: {
329
+ image: ['width'],
330
+ },
331
+ };
332
+
333
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
334
+ });
335
+
336
+ it('detects a newly dependent required field at the root', async () => {
337
+ const beforeSchema: JSONSchema = {
338
+ type: 'object',
339
+ properties: {
340
+ image: { type: 'object' },
341
+ width: { type: 'number' },
342
+ height: { type: 'number' },
343
+ },
344
+ dependentRequired: {
345
+ image: ['width'],
346
+ },
347
+ };
348
+ const afterSchema: JSONSchema = {
349
+ type: 'object',
350
+ properties: {
351
+ image: { type: 'object' },
352
+ width: { type: 'number' },
353
+ height: { type: 'number' },
354
+ },
355
+ dependentRequired: {
356
+ image: ['width', 'height'],
357
+ },
358
+ };
359
+
360
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('height', 'New dependent required field')]);
361
+ });
362
+
363
+ it('detects a new dependentRequired rule at the root', async () => {
364
+ const beforeSchema: JSONSchema = {
365
+ type: 'object',
366
+ properties: {
367
+ image: { type: 'object' },
368
+ width: { type: 'number' },
369
+ },
370
+ };
371
+ const afterSchema: JSONSchema = {
372
+ type: 'object',
373
+ properties: {
374
+ image: { type: 'object' },
375
+ width: { type: 'number' },
376
+ },
377
+ dependentRequired: {
378
+ image: ['width'],
379
+ },
380
+ };
381
+
382
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('width', 'New dependent required field')]);
383
+ });
384
+
385
+ it('detects a newly dependent required field in a nested object property', async () => {
386
+ const beforeSchema: JSONSchema = {
387
+ type: 'object',
388
+ properties: {
389
+ metadata: {
390
+ type: 'object',
391
+ properties: {
392
+ thumbnail: { type: 'string' },
393
+ url: { type: 'string' },
394
+ width: { type: 'number' },
395
+ },
396
+ dependentRequired: {
397
+ thumbnail: ['url'],
398
+ },
399
+ },
400
+ },
401
+ };
402
+ const afterSchema: JSONSchema = {
403
+ type: 'object',
404
+ properties: {
405
+ metadata: {
406
+ type: 'object',
407
+ properties: {
408
+ thumbnail: { type: 'string' },
409
+ url: { type: 'string' },
410
+ width: { type: 'number' },
411
+ },
412
+ dependentRequired: {
413
+ thumbnail: ['url', 'width'],
414
+ },
415
+ },
416
+ },
417
+ };
418
+
419
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
420
+ breakAt('metadata.width', 'New dependent required field'),
421
+ ]);
422
+ });
423
+
424
+ it('detects a newly dependent required field in array item schemas', async () => {
425
+ const beforeSchema: JSONSchema = {
426
+ type: 'object',
427
+ properties: {
428
+ tags: {
429
+ type: 'array',
430
+ items: {
431
+ type: 'object',
432
+ properties: {
433
+ name: { type: 'string' },
434
+ value: { type: 'string' },
435
+ },
436
+ dependentRequired: {
437
+ name: ['value'],
438
+ },
439
+ },
440
+ },
441
+ },
442
+ };
443
+ const afterSchema: JSONSchema = {
444
+ type: 'object',
445
+ properties: {
446
+ tags: {
447
+ type: 'array',
448
+ items: {
449
+ type: 'object',
450
+ properties: {
451
+ name: { type: 'string' },
452
+ value: { type: 'string' },
453
+ category: { type: 'string' },
454
+ },
455
+ dependentRequired: {
456
+ name: ['value', 'category'],
457
+ },
458
+ },
459
+ },
460
+ },
461
+ };
462
+
463
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
464
+ breakAt('tags[].category', 'New dependent required field'),
465
+ ]);
466
+ });
467
+
468
+ it('does not report a break when a value is removed from dependentRequired', async () => {
469
+ const beforeSchema: JSONSchema = {
470
+ type: 'object',
471
+ properties: {
472
+ image: { type: 'object' },
473
+ width: { type: 'number' },
474
+ height: { type: 'number' },
475
+ },
476
+ dependentRequired: {
477
+ image: ['width', 'height'],
478
+ },
479
+ };
480
+ const afterSchema: JSONSchema = {
481
+ type: 'object',
482
+ properties: {
483
+ image: { type: 'object' },
484
+ width: { type: 'number' },
485
+ height: { type: 'number' },
486
+ },
487
+ dependentRequired: {
488
+ image: ['width'],
489
+ },
490
+ };
491
+
492
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
493
+ });
494
+
495
+ it('does not report a break when a dependentRequired trigger rule is removed entirely', async () => {
496
+ const beforeSchema: JSONSchema = {
497
+ type: 'object',
498
+ properties: {
499
+ image: { type: 'object' },
500
+ width: { type: 'number' },
501
+ },
502
+ dependentRequired: {
503
+ image: ['width'],
504
+ },
505
+ };
506
+ const afterSchema: JSONSchema = {
507
+ type: 'object',
508
+ properties: {
509
+ image: { type: 'object' },
510
+ width: { type: 'number' },
511
+ },
512
+ };
513
+
514
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
515
+ });
516
+
517
+ it('does not report a break when dependentRequired uses empty arrays', async () => {
518
+ const beforeSchema: JSONSchema = {
519
+ type: 'object',
520
+ properties: {
521
+ image: { type: 'object' },
522
+ width: { type: 'number' },
523
+ },
524
+ dependentRequired: {
525
+ image: ['width'],
526
+ },
527
+ };
528
+ const afterSchema: JSONSchema = {
529
+ type: 'object',
530
+ properties: {
531
+ image: { type: 'object' },
532
+ width: { type: 'number' },
533
+ },
534
+ dependentRequired: {
535
+ image: [],
536
+ },
537
+ };
538
+
539
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
540
+ });
541
+
542
+ it('does not report a break when dependentRequired is added with only empty arrays', async () => {
543
+ const beforeSchema: JSONSchema = {
544
+ type: 'object',
545
+ properties: {
546
+ image: { type: 'object' },
547
+ },
548
+ };
549
+ const afterSchema: JSONSchema = {
550
+ type: 'object',
551
+ properties: {
552
+ image: { type: 'object' },
553
+ },
554
+ dependentRequired: {
555
+ image: [],
556
+ },
557
+ };
558
+
559
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
560
+ });
561
+
562
+ it('does not report dependentRequired for a newly added property', async () => {
563
+ const beforeSchema: JSONSchema = {
564
+ type: 'object',
565
+ properties: {
566
+ title: { type: 'string' },
567
+ },
568
+ };
569
+ const afterSchema: JSONSchema = {
570
+ type: 'object',
571
+ properties: {
572
+ title: { type: 'string' },
573
+ metadata: {
574
+ type: 'object',
575
+ properties: {
576
+ thumbnail: { type: 'string' },
577
+ url: { type: 'string' },
578
+ },
579
+ dependentRequired: {
580
+ thumbnail: ['url'],
581
+ },
582
+ },
583
+ },
584
+ };
585
+
586
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
587
+ });
588
+
589
+ it('does not report dependentRequired when the trigger field is newly added', async () => {
590
+ const beforeSchema: JSONSchema = {
591
+ type: 'object',
592
+ properties: {
593
+ title: { type: 'string' },
594
+ },
595
+ };
596
+ const afterSchema: JSONSchema = {
597
+ type: 'object',
598
+ properties: {
599
+ title: { type: 'string' },
600
+ image: { type: 'object' },
601
+ width: { type: 'number' },
602
+ },
603
+ dependentRequired: {
604
+ image: ['width'],
605
+ },
606
+ };
607
+
608
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
609
+ });
610
+ });
611
+
612
+ describe('field types', () => {
613
+ it('returns no breaks when field types are unchanged', async () => {
614
+ const beforeSchema: JSONSchema = {
615
+ type: 'object',
616
+ properties: {
617
+ title: { type: 'string' },
618
+ count: { type: 'number' },
619
+ },
620
+ };
621
+ const afterSchema: JSONSchema = {
622
+ type: 'object',
623
+ properties: {
624
+ title: { type: 'string' },
625
+ count: { type: 'number' },
626
+ },
627
+ };
628
+
629
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
630
+ });
631
+
632
+ it('returns no breaks when a new field is added', async () => {
633
+ const beforeSchema: JSONSchema = {
634
+ type: 'object',
635
+ properties: {
636
+ title: { type: 'string' },
637
+ count: { type: 'number' },
638
+ },
639
+ };
640
+ const afterSchema: JSONSchema = {
641
+ type: 'object',
642
+ properties: {
643
+ title: { type: 'string' },
644
+ count: { type: 'number' },
645
+ subtitle: { type: 'string' },
646
+ },
647
+ };
648
+
649
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
650
+ });
651
+
652
+ it('returns no breaks when a "type" is added where it was undefined', async () => {
653
+ const beforeSchema: JSONSchema = {
654
+ properties: {
655
+ title: { type: 'string' },
656
+ count: { type: 'number' },
657
+ },
658
+ };
659
+ const afterSchema: JSONSchema = {
660
+ type: 'object',
661
+ properties: {
662
+ title: { type: 'string' },
663
+ count: { type: 'number' },
664
+ },
665
+ };
666
+
667
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
668
+ });
669
+
670
+ it('treats equivalent union types as unchanged', async () => {
671
+ const beforeSchema: JSONSchema = {
672
+ type: 'object',
673
+ properties: {
674
+ value: { type: ['string', 'null'] },
675
+ },
676
+ };
677
+ const afterSchema: JSONSchema = {
678
+ type: 'object',
679
+ properties: {
680
+ value: { type: ['null', 'string'] },
681
+ },
682
+ };
683
+
684
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
685
+ });
686
+
687
+ it('does not report a break when integer is relaxed to number', async () => {
688
+ const beforeSchema: JSONSchema = {
689
+ type: 'object',
690
+ properties: {
691
+ count: { type: 'integer' },
692
+ },
693
+ };
694
+ const afterSchema: JSONSchema = {
695
+ type: 'object',
696
+ properties: {
697
+ count: { type: 'number' },
698
+ },
699
+ };
700
+
701
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
702
+ });
703
+
704
+ it('does not report a break when integer is relaxed to number in a union type', async () => {
705
+ const beforeSchema: JSONSchema = {
706
+ type: 'object',
707
+ properties: {
708
+ count: { type: ['integer', 'null'] },
709
+ },
710
+ };
711
+ const afterSchema: JSONSchema = {
712
+ type: 'object',
713
+ properties: {
714
+ count: { type: ['number', 'null'] },
715
+ },
716
+ };
717
+
718
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
719
+ });
720
+
721
+ it('does not report a break when integer is relaxed to number in array item schemas', async () => {
722
+ const beforeSchema: JSONSchema = {
723
+ type: 'object',
724
+ properties: {
725
+ counts: {
726
+ type: 'array',
727
+ items: { type: 'integer' },
728
+ },
729
+ },
730
+ };
731
+ const afterSchema: JSONSchema = {
732
+ type: 'object',
733
+ properties: {
734
+ counts: {
735
+ type: 'array',
736
+ items: { type: 'number' },
737
+ },
738
+ },
739
+ };
740
+
741
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
742
+ });
743
+
744
+ it('detects a break when number is narrowed to integer', async () => {
745
+ const beforeSchema: JSONSchema = {
746
+ type: 'object',
747
+ properties: {
748
+ count: { type: 'number' },
749
+ },
750
+ };
751
+ const afterSchema: JSONSchema = {
752
+ type: 'object',
753
+ properties: {
754
+ count: { type: 'integer' },
755
+ },
756
+ };
757
+
758
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('count', 'Field type changed')]);
759
+ });
760
+
761
+ it('detects a break when number is narrowed to integer in a union type', async () => {
762
+ const beforeSchema: JSONSchema = {
763
+ type: 'object',
764
+ properties: {
765
+ count: { type: ['number', 'null'] },
766
+ },
767
+ };
768
+ const afterSchema: JSONSchema = {
769
+ type: 'object',
770
+ properties: {
771
+ count: { type: ['integer', 'null'] },
772
+ },
773
+ };
774
+
775
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('count', 'Field type changed')]);
776
+ });
777
+
778
+ it('detects a field type change at the root', async () => {
779
+ const beforeSchema: JSONSchema = {
780
+ type: 'object',
781
+ properties: {
782
+ sheep: { type: 'object', properties: { name: { type: 'string' } } },
783
+ },
784
+ };
785
+ const afterSchema: JSONSchema = {
786
+ type: 'object',
787
+ properties: {
788
+ sheep: { type: 'array', items: { type: 'object', properties: { name: { type: 'string' } } } },
789
+ },
790
+ };
791
+
792
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('sheep', 'Field type changed')]);
793
+ });
794
+
795
+ it('detects a field type change in a nested object property', async () => {
796
+ const beforeSchema: JSONSchema = {
797
+ type: 'object',
798
+ properties: {
799
+ image: {
800
+ type: 'object',
801
+ properties: {
802
+ width: { type: 'string' },
803
+ },
804
+ },
805
+ },
806
+ };
807
+ const afterSchema: JSONSchema = {
808
+ type: 'object',
809
+ properties: {
810
+ image: {
811
+ type: 'object',
812
+ properties: {
813
+ width: { type: 'number' },
814
+ },
815
+ },
816
+ },
817
+ };
818
+
819
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('image.width', 'Field type changed')]);
820
+ });
821
+
822
+ it('detects a field type change in array item schemas', async () => {
823
+ const beforeSchema: JSONSchema = {
824
+ type: 'object',
825
+ properties: {
826
+ tags: {
827
+ type: 'array',
828
+ items: { type: 'string' },
829
+ },
830
+ },
831
+ };
832
+ const afterSchema: JSONSchema = {
833
+ type: 'object',
834
+ properties: {
835
+ tags: {
836
+ type: 'array',
837
+ items: { type: 'number' },
838
+ },
839
+ },
840
+ };
841
+
842
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('tags[]', 'Field type changed')]);
843
+ });
844
+
845
+ it('detects when type is removed on an existing field', async () => {
846
+ const beforeSchema: JSONSchema = {
847
+ type: 'object',
848
+ properties: {
849
+ count: { type: 'number' },
850
+ },
851
+ };
852
+ const afterSchema: JSONSchema = {
853
+ type: 'object',
854
+ properties: {
855
+ count: {},
856
+ },
857
+ };
858
+
859
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('count', 'Field type changed')]);
860
+ });
861
+
862
+ it('detects a field type change in a deeply nested object property', async () => {
863
+ const beforeSchema: JSONSchema = {
864
+ type: 'object',
865
+ properties: {
866
+ content: {
867
+ type: 'object',
868
+ properties: {
869
+ sidebar: {
870
+ type: 'object',
871
+ properties: {
872
+ width: { type: 'string' },
873
+ },
874
+ },
875
+ },
876
+ },
877
+ },
878
+ };
879
+ const afterSchema: JSONSchema = {
880
+ type: 'object',
881
+ properties: {
882
+ content: {
883
+ type: 'object',
884
+ properties: {
885
+ sidebar: {
886
+ type: 'object',
887
+ properties: {
888
+ width: { type: 'number' },
889
+ },
890
+ },
891
+ },
892
+ },
893
+ },
894
+ };
895
+
896
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
897
+ breakAt('content.sidebar.width', 'Field type changed'),
898
+ ]);
899
+ });
900
+ });
901
+
902
+ describe('enum options', () => {
903
+ it('returns no breaks when enum options are unchanged', async () => {
904
+ const beforeSchema: JSONSchema = {
905
+ type: 'object',
906
+ properties: {
907
+ status: { type: 'string', enum: ['draft', 'published', 'archived'] },
908
+ },
909
+ };
910
+ const afterSchema: JSONSchema = {
911
+ type: 'object',
912
+ properties: {
913
+ status: { type: 'string', enum: ['draft', 'published', 'archived'] },
914
+ },
915
+ };
916
+
917
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
918
+ });
919
+
920
+ it('returns no breaks when enum options are added', async () => {
921
+ const beforeSchema: JSONSchema = {
922
+ type: 'object',
923
+ properties: {
924
+ status: { type: 'string', enum: ['draft', 'published'] },
925
+ },
926
+ };
927
+ const afterSchema: JSONSchema = {
928
+ type: 'object',
929
+ properties: {
930
+ status: { type: 'string', enum: ['draft', 'published', 'archived'] },
931
+ },
932
+ };
933
+
934
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
935
+ });
936
+
937
+ it('returns no breaks when a single-value enum becomes const with the same value', async () => {
938
+ const beforeSchema: JSONSchema = {
939
+ type: 'object',
940
+ properties: {
941
+ status: { type: 'string', enum: ['published'] },
942
+ },
943
+ };
944
+ const afterSchema: JSONSchema = {
945
+ type: 'object',
946
+ properties: {
947
+ status: { type: 'string', const: 'published' },
948
+ },
949
+ };
950
+
951
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
952
+ });
953
+
954
+ it('detects removed enum options when a multi-value enum is narrowed to const', async () => {
955
+ const beforeSchema: JSONSchema = {
956
+ type: 'object',
957
+ properties: {
958
+ status: { type: 'string', enum: ['draft', 'published', 'archived'] },
959
+ },
960
+ };
961
+ const afterSchema: JSONSchema = {
962
+ type: 'object',
963
+ properties: {
964
+ status: { type: 'string', const: 'published' },
965
+ },
966
+ };
967
+
968
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
969
+ breakAt('status', 'Enum options removed'),
970
+ breakAt('status', 'Const value changed'),
971
+ ]);
972
+ });
973
+
974
+ it('detects removed enum options at the root', async () => {
975
+ const beforeSchema: JSONSchema = {
976
+ type: 'object',
977
+ properties: {
978
+ status: { type: 'string', enum: ['draft', 'published', 'archived'] },
979
+ },
980
+ };
981
+ const afterSchema: JSONSchema = {
982
+ type: 'object',
983
+ properties: {
984
+ status: { type: 'string', enum: ['draft', 'published'] },
985
+ },
986
+ };
987
+
988
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('status', 'Enum options removed')]);
989
+ });
990
+
991
+ it('detects removed enum options in a nested object property', async () => {
992
+ const beforeSchema: JSONSchema = {
993
+ type: 'object',
994
+ properties: {
995
+ metadata: {
996
+ type: 'object',
997
+ properties: {
998
+ status: { type: 'string', enum: ['draft', 'published'] },
999
+ },
1000
+ },
1001
+ },
1002
+ };
1003
+ const afterSchema: JSONSchema = {
1004
+ type: 'object',
1005
+ properties: {
1006
+ metadata: {
1007
+ type: 'object',
1008
+ properties: {
1009
+ status: { type: 'string', enum: ['published'] },
1010
+ },
1011
+ },
1012
+ },
1013
+ };
1014
+
1015
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
1016
+ breakAt('metadata.status', 'Enum options removed'),
1017
+ breakAt('metadata.status', 'Const value changed'),
1018
+ ]);
1019
+ });
1020
+
1021
+ it('detects removed enum options in array item schemas', async () => {
1022
+ const beforeSchema: JSONSchema = {
1023
+ type: 'object',
1024
+ properties: {
1025
+ tags: {
1026
+ type: 'array',
1027
+ items: { type: 'string', enum: ['news', 'sport', 'weather'] },
1028
+ },
1029
+ },
1030
+ };
1031
+ const afterSchema: JSONSchema = {
1032
+ type: 'object',
1033
+ properties: {
1034
+ tags: {
1035
+ type: 'array',
1036
+ items: { type: 'string', enum: ['news', 'sport'] },
1037
+ },
1038
+ },
1039
+ };
1040
+
1041
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('tags[]', 'Enum options removed')]);
1042
+ });
1043
+
1044
+ it('returns no breaks when enum is removed entirely', async () => {
1045
+ const beforeSchema: JSONSchema = {
1046
+ type: 'object',
1047
+ properties: {
1048
+ status: { type: 'string', enum: ['draft', 'published', 'archived'] },
1049
+ },
1050
+ };
1051
+ const afterSchema: JSONSchema = {
1052
+ type: 'object',
1053
+ properties: {
1054
+ status: { type: 'string' },
1055
+ },
1056
+ };
1057
+
1058
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
1059
+ });
1060
+
1061
+ it('detects enum added to an existing field without enum', async () => {
1062
+ const beforeSchema: JSONSchema = {
1063
+ type: 'object',
1064
+ properties: {
1065
+ status: { type: 'string' },
1066
+ },
1067
+ };
1068
+ const afterSchema: JSONSchema = {
1069
+ type: 'object',
1070
+ properties: {
1071
+ status: { type: 'string', enum: ['draft', 'published'] },
1072
+ },
1073
+ };
1074
+
1075
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('status', 'Enum added')]);
1076
+ });
1077
+
1078
+ it('detects enum added in a nested object property', async () => {
1079
+ const beforeSchema: JSONSchema = {
1080
+ type: 'object',
1081
+ properties: {
1082
+ metadata: {
1083
+ type: 'object',
1084
+ properties: {
1085
+ status: { type: 'string' },
1086
+ },
1087
+ },
1088
+ },
1089
+ };
1090
+ const afterSchema: JSONSchema = {
1091
+ type: 'object',
1092
+ properties: {
1093
+ metadata: {
1094
+ type: 'object',
1095
+ properties: {
1096
+ status: { type: 'string', enum: ['draft', 'published'] },
1097
+ },
1098
+ },
1099
+ },
1100
+ };
1101
+
1102
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('metadata.status', 'Enum added')]);
1103
+ });
1104
+
1105
+ it('detects enum added in array item schemas', async () => {
1106
+ const beforeSchema: JSONSchema = {
1107
+ type: 'object',
1108
+ properties: {
1109
+ tags: {
1110
+ type: 'array',
1111
+ items: { type: 'string' },
1112
+ },
1113
+ },
1114
+ };
1115
+ const afterSchema: JSONSchema = {
1116
+ type: 'object',
1117
+ properties: {
1118
+ tags: {
1119
+ type: 'array',
1120
+ items: { type: 'string', enum: ['news', 'sport'] },
1121
+ },
1122
+ },
1123
+ };
1124
+
1125
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('tags[]', 'Enum added')]);
1126
+ });
1127
+
1128
+ it('does not report enum added for a newly added property', async () => {
1129
+ const beforeSchema: JSONSchema = {
1130
+ type: 'object',
1131
+ properties: {
1132
+ title: { type: 'string' },
1133
+ },
1134
+ };
1135
+ const afterSchema: JSONSchema = {
1136
+ type: 'object',
1137
+ properties: {
1138
+ title: { type: 'string' },
1139
+ status: { type: 'string', enum: ['draft', 'published'] },
1140
+ },
1141
+ };
1142
+
1143
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
1144
+ });
1145
+
1146
+ it('does not report enum added when a const is relaxed to an enum that includes the same value', async () => {
1147
+ const beforeSchema: JSONSchema = {
1148
+ type: 'object',
1149
+ properties: {
1150
+ status: { type: 'string', const: 'published' },
1151
+ },
1152
+ };
1153
+ const afterSchema: JSONSchema = {
1154
+ type: 'object',
1155
+ properties: {
1156
+ status: { type: 'string', enum: ['draft', 'published'] },
1157
+ },
1158
+ };
1159
+
1160
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
1161
+ });
1162
+
1163
+ it('does not report enum removal for a newly added property', async () => {
1164
+ const beforeSchema: JSONSchema = {
1165
+ type: 'object',
1166
+ properties: {
1167
+ title: { type: 'string' },
1168
+ },
1169
+ };
1170
+ const afterSchema: JSONSchema = {
1171
+ type: 'object',
1172
+ properties: {
1173
+ title: { type: 'string' },
1174
+ status: { type: 'string', enum: ['draft', 'published'] },
1175
+ },
1176
+ };
1177
+
1178
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
1179
+ });
1180
+ });
1181
+
1182
+ describe('const values', () => {
1183
+ it('returns no breaks when const values are unchanged', async () => {
1184
+ const beforeSchema: JSONSchema = {
1185
+ type: 'object',
1186
+ properties: {
1187
+ type: { type: 'string', const: 'article' },
1188
+ },
1189
+ };
1190
+ const afterSchema: JSONSchema = {
1191
+ type: 'object',
1192
+ properties: {
1193
+ type: { type: 'string', const: 'article' },
1194
+ },
1195
+ };
1196
+
1197
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
1198
+ });
1199
+
1200
+ it('returns no breaks when a single-value enum becomes const with the same value', async () => {
1201
+ const beforeSchema: JSONSchema = {
1202
+ type: 'object',
1203
+ properties: {
1204
+ type: { type: 'string', enum: ['article'] },
1205
+ },
1206
+ };
1207
+ const afterSchema: JSONSchema = {
1208
+ type: 'object',
1209
+ properties: {
1210
+ type: { type: 'string', const: 'article' },
1211
+ },
1212
+ };
1213
+
1214
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
1215
+ });
1216
+
1217
+ it('returns no breaks when const becomes a single-value enum with the same value', async () => {
1218
+ const beforeSchema: JSONSchema = {
1219
+ type: 'object',
1220
+ properties: {
1221
+ type: { type: 'string', const: 'article' },
1222
+ },
1223
+ };
1224
+ const afterSchema: JSONSchema = {
1225
+ type: 'object',
1226
+ properties: {
1227
+ type: { type: 'string', enum: ['article'] },
1228
+ },
1229
+ };
1230
+
1231
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
1232
+ });
1233
+
1234
+ it('returns no breaks when const is removed', async () => {
1235
+ const beforeSchema: JSONSchema = {
1236
+ type: 'object',
1237
+ properties: {
1238
+ type: { type: 'string', const: 'article' },
1239
+ },
1240
+ };
1241
+ const afterSchema: JSONSchema = {
1242
+ type: 'object',
1243
+ properties: {
1244
+ type: { type: 'string' },
1245
+ },
1246
+ };
1247
+
1248
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
1249
+ });
1250
+
1251
+ it('detects when const is added to an existing field', async () => {
1252
+ const beforeSchema: JSONSchema = {
1253
+ type: 'object',
1254
+ properties: {
1255
+ type: { type: 'string' },
1256
+ },
1257
+ };
1258
+ const afterSchema: JSONSchema = {
1259
+ type: 'object',
1260
+ properties: {
1261
+ type: { type: 'string', const: 'article' },
1262
+ },
1263
+ };
1264
+
1265
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('type', 'Const value changed')]);
1266
+ });
1267
+
1268
+ it('detects when const is changed on an existing field', async () => {
1269
+ const beforeSchema: JSONSchema = {
1270
+ type: 'object',
1271
+ properties: {
1272
+ type: { type: 'string', const: 'article' },
1273
+ },
1274
+ };
1275
+ const afterSchema: JSONSchema = {
1276
+ type: 'object',
1277
+ properties: {
1278
+ type: { type: 'string', const: 'page' },
1279
+ },
1280
+ };
1281
+
1282
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('type', 'Const value changed')]);
1283
+ });
1284
+
1285
+ it('detects const changes in a nested object property', async () => {
1286
+ const beforeSchema: JSONSchema = {
1287
+ type: 'object',
1288
+ properties: {
1289
+ metadata: {
1290
+ type: 'object',
1291
+ properties: {
1292
+ version: { type: 'number', const: 1 },
1293
+ },
1294
+ },
1295
+ },
1296
+ };
1297
+ const afterSchema: JSONSchema = {
1298
+ type: 'object',
1299
+ properties: {
1300
+ metadata: {
1301
+ type: 'object',
1302
+ properties: {
1303
+ version: { type: 'number', const: 2 },
1304
+ },
1305
+ },
1306
+ },
1307
+ };
1308
+
1309
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
1310
+ breakAt('metadata.version', 'Const value changed'),
1311
+ ]);
1312
+ });
1313
+
1314
+ it('detects const changes in array item schemas', async () => {
1315
+ const beforeSchema: JSONSchema = {
1316
+ type: 'object',
1317
+ properties: {
1318
+ tags: {
1319
+ type: 'array',
1320
+ items: { type: 'string', const: 'news' },
1321
+ },
1322
+ },
1323
+ };
1324
+ const afterSchema: JSONSchema = {
1325
+ type: 'object',
1326
+ properties: {
1327
+ tags: {
1328
+ type: 'array',
1329
+ items: { type: 'string', const: 'sport' },
1330
+ },
1331
+ },
1332
+ };
1333
+
1334
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('tags[]', 'Const value changed')]);
1335
+ });
1336
+
1337
+ it('does not report const changes for a newly added property', async () => {
1338
+ const beforeSchema: JSONSchema = {
1339
+ type: 'object',
1340
+ properties: {
1341
+ title: { type: 'string' },
1342
+ },
1343
+ };
1344
+ const afterSchema: JSONSchema = {
1345
+ type: 'object',
1346
+ properties: {
1347
+ title: { type: 'string' },
1348
+ type: { type: 'string', const: 'article' },
1349
+ },
1350
+ };
1351
+
1352
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
1353
+ });
1354
+ });
1355
+
1356
+ describe('array constraints', () => {
1357
+ it('returns no breaks when array constraints are unchanged', async () => {
1358
+ const beforeSchema: JSONSchema = {
1359
+ type: 'object',
1360
+ properties: {
1361
+ tags: {
1362
+ type: 'array',
1363
+ minItems: 1,
1364
+ maxItems: 10,
1365
+ uniqueItems: true,
1366
+ contains: { type: 'string' },
1367
+ items: { type: 'string' },
1368
+ },
1369
+ },
1370
+ };
1371
+ const afterSchema: JSONSchema = {
1372
+ type: 'object',
1373
+ properties: {
1374
+ tags: {
1375
+ type: 'array',
1376
+ minItems: 1,
1377
+ maxItems: 10,
1378
+ uniqueItems: true,
1379
+ contains: { type: 'string' },
1380
+ items: { type: 'string' },
1381
+ },
1382
+ },
1383
+ };
1384
+
1385
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
1386
+ });
1387
+
1388
+ it('returns no breaks when array constraints are relaxed', async () => {
1389
+ const beforeSchema: JSONSchema = {
1390
+ type: 'object',
1391
+ properties: {
1392
+ tags: {
1393
+ type: 'array',
1394
+ minItems: 2,
1395
+ maxItems: 5,
1396
+ uniqueItems: true,
1397
+ contains: { type: 'string' },
1398
+ items: { type: 'string' },
1399
+ },
1400
+ },
1401
+ };
1402
+ const afterSchema: JSONSchema = {
1403
+ type: 'object',
1404
+ properties: {
1405
+ tags: {
1406
+ type: 'array',
1407
+ minItems: 1,
1408
+ maxItems: 10,
1409
+ uniqueItems: false,
1410
+ items: { type: 'string' },
1411
+ },
1412
+ },
1413
+ };
1414
+
1415
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
1416
+ });
1417
+
1418
+ it('detects minItems being increased at the root', async () => {
1419
+ const beforeSchema: JSONSchema = {
1420
+ type: 'object',
1421
+ properties: {
1422
+ tags: { type: 'array', minItems: 1, items: { type: 'string' } },
1423
+ },
1424
+ };
1425
+ const afterSchema: JSONSchema = {
1426
+ type: 'object',
1427
+ properties: {
1428
+ tags: { type: 'array', minItems: 3, items: { type: 'string' } },
1429
+ },
1430
+ };
1431
+
1432
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('tags', 'Array minItems increased')]);
1433
+ });
1434
+
1435
+ it('detects minItems being added where it was previously unset', async () => {
1436
+ const beforeSchema: JSONSchema = {
1437
+ type: 'object',
1438
+ properties: {
1439
+ tags: { type: 'array', items: { type: 'string' } },
1440
+ },
1441
+ };
1442
+ const afterSchema: JSONSchema = {
1443
+ type: 'object',
1444
+ properties: {
1445
+ tags: { type: 'array', minItems: 1, items: { type: 'string' } },
1446
+ },
1447
+ };
1448
+
1449
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('tags', 'Array minItems increased')]);
1450
+ });
1451
+
1452
+ it('detects maxItems being lowered at the root', async () => {
1453
+ const beforeSchema: JSONSchema = {
1454
+ type: 'object',
1455
+ properties: {
1456
+ tags: { type: 'array', maxItems: 10, items: { type: 'string' } },
1457
+ },
1458
+ };
1459
+ const afterSchema: JSONSchema = {
1460
+ type: 'object',
1461
+ properties: {
1462
+ tags: { type: 'array', maxItems: 5, items: { type: 'string' } },
1463
+ },
1464
+ };
1465
+
1466
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('tags', 'Array maxItems lowered')]);
1467
+ });
1468
+
1469
+ it('detects maxItems being added where it was previously unset', async () => {
1470
+ const beforeSchema: JSONSchema = {
1471
+ type: 'object',
1472
+ properties: {
1473
+ tags: { type: 'array', items: { type: 'string' } },
1474
+ },
1475
+ };
1476
+ const afterSchema: JSONSchema = {
1477
+ type: 'object',
1478
+ properties: {
1479
+ tags: { type: 'array', maxItems: 5, items: { type: 'string' } },
1480
+ },
1481
+ };
1482
+
1483
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('tags', 'Array maxItems lowered')]);
1484
+ });
1485
+
1486
+ it('detects uniqueItems being added at the root', async () => {
1487
+ const beforeSchema: JSONSchema = {
1488
+ type: 'object',
1489
+ properties: {
1490
+ tags: { type: 'array', items: { type: 'string' } },
1491
+ },
1492
+ };
1493
+ const afterSchema: JSONSchema = {
1494
+ type: 'object',
1495
+ properties: {
1496
+ tags: { type: 'array', uniqueItems: true, items: { type: 'string' } },
1497
+ },
1498
+ };
1499
+
1500
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('tags', 'Array uniqueItems added')]);
1501
+ });
1502
+
1503
+ it('detects contains being added at the root', async () => {
1504
+ const beforeSchema: JSONSchema = {
1505
+ type: 'object',
1506
+ properties: {
1507
+ tags: { type: 'array', items: { type: 'string' } },
1508
+ },
1509
+ };
1510
+ const afterSchema: JSONSchema = {
1511
+ type: 'object',
1512
+ properties: {
1513
+ tags: { type: 'array', contains: { type: 'string' }, items: { type: 'string' } },
1514
+ },
1515
+ };
1516
+
1517
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('tags.{contains}', 'Field type changed')]);
1518
+ });
1519
+
1520
+ it('detects contains type changes at the root', async () => {
1521
+ const beforeSchema: JSONSchema = {
1522
+ type: 'object',
1523
+ properties: {
1524
+ tags: {
1525
+ type: 'array',
1526
+ contains: { type: 'string' },
1527
+ items: { type: 'string' },
1528
+ },
1529
+ },
1530
+ };
1531
+ const afterSchema: JSONSchema = {
1532
+ type: 'object',
1533
+ properties: {
1534
+ tags: {
1535
+ type: 'array',
1536
+ contains: { type: 'number' },
1537
+ items: { type: 'string' },
1538
+ },
1539
+ },
1540
+ };
1541
+
1542
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('tags.{contains}', 'Field type changed')]);
1543
+ });
1544
+
1545
+ it('detects string constraint narrowing inside contains subschemas', async () => {
1546
+ const beforeSchema: JSONSchema = {
1547
+ type: 'object',
1548
+ properties: {
1549
+ tags: {
1550
+ type: 'array',
1551
+ contains: { type: 'string', minLength: 1 },
1552
+ items: { type: 'string' },
1553
+ },
1554
+ },
1555
+ };
1556
+ const afterSchema: JSONSchema = {
1557
+ type: 'object',
1558
+ properties: {
1559
+ tags: {
1560
+ type: 'array',
1561
+ contains: { type: 'string', minLength: 5 },
1562
+ items: { type: 'string' },
1563
+ },
1564
+ },
1565
+ };
1566
+
1567
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
1568
+ breakAt('tags.{contains}', 'String minLength increased'),
1569
+ ]);
1570
+ });
1571
+
1572
+ it('detects array constraint narrowing in nested object properties', async () => {
1573
+ const beforeSchema: JSONSchema = {
1574
+ type: 'object',
1575
+ properties: {
1576
+ metadata: {
1577
+ type: 'object',
1578
+ properties: {
1579
+ tags: { type: 'array', minItems: 1, items: { type: 'string' } },
1580
+ },
1581
+ },
1582
+ },
1583
+ };
1584
+ const afterSchema: JSONSchema = {
1585
+ type: 'object',
1586
+ properties: {
1587
+ metadata: {
1588
+ type: 'object',
1589
+ properties: {
1590
+ tags: { type: 'array', minItems: 2, items: { type: 'string' } },
1591
+ },
1592
+ },
1593
+ },
1594
+ };
1595
+
1596
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
1597
+ breakAt('metadata.tags', 'Array minItems increased'),
1598
+ ]);
1599
+ });
1600
+
1601
+ it('detects array constraint narrowing in deeply nested object properties', async () => {
1602
+ const beforeSchema: JSONSchema = {
1603
+ type: 'object',
1604
+ properties: {
1605
+ content: {
1606
+ type: 'object',
1607
+ properties: {
1608
+ sidebar: {
1609
+ type: 'object',
1610
+ properties: {
1611
+ links: { type: 'array', maxItems: 10, items: { type: 'string' } },
1612
+ },
1613
+ },
1614
+ },
1615
+ },
1616
+ },
1617
+ };
1618
+ const afterSchema: JSONSchema = {
1619
+ type: 'object',
1620
+ properties: {
1621
+ content: {
1622
+ type: 'object',
1623
+ properties: {
1624
+ sidebar: {
1625
+ type: 'object',
1626
+ properties: {
1627
+ links: { type: 'array', maxItems: 3, items: { type: 'string' } },
1628
+ },
1629
+ },
1630
+ },
1631
+ },
1632
+ },
1633
+ };
1634
+
1635
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
1636
+ breakAt('content.sidebar.links', 'Array maxItems lowered'),
1637
+ ]);
1638
+ });
1639
+
1640
+ it('detects uniqueItems being added in nested object properties', async () => {
1641
+ const beforeSchema: JSONSchema = {
1642
+ type: 'object',
1643
+ properties: {
1644
+ metadata: {
1645
+ type: 'object',
1646
+ properties: {
1647
+ roles: { type: 'array', items: { type: 'string' } },
1648
+ },
1649
+ },
1650
+ },
1651
+ };
1652
+ const afterSchema: JSONSchema = {
1653
+ type: 'object',
1654
+ properties: {
1655
+ metadata: {
1656
+ type: 'object',
1657
+ properties: {
1658
+ roles: { type: 'array', uniqueItems: true, items: { type: 'string' } },
1659
+ },
1660
+ },
1661
+ },
1662
+ };
1663
+
1664
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
1665
+ breakAt('metadata.roles', 'Array uniqueItems added'),
1666
+ ]);
1667
+ });
1668
+
1669
+ it('detects contains constraint changes in nested object properties', async () => {
1670
+ const beforeSchema: JSONSchema = {
1671
+ type: 'object',
1672
+ properties: {
1673
+ metadata: {
1674
+ type: 'object',
1675
+ properties: {
1676
+ tags: {
1677
+ type: 'array',
1678
+ contains: { type: 'string' },
1679
+ items: { type: 'string' },
1680
+ },
1681
+ },
1682
+ },
1683
+ },
1684
+ };
1685
+ const afterSchema: JSONSchema = {
1686
+ type: 'object',
1687
+ properties: {
1688
+ metadata: {
1689
+ type: 'object',
1690
+ properties: {
1691
+ tags: {
1692
+ type: 'array',
1693
+ contains: { type: 'number' },
1694
+ items: { type: 'string' },
1695
+ },
1696
+ },
1697
+ },
1698
+ },
1699
+ };
1700
+
1701
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
1702
+ breakAt('metadata.tags.{contains}', 'Field type changed'),
1703
+ ]);
1704
+ });
1705
+
1706
+ it('detects array constraint narrowing in array item schemas', async () => {
1707
+ const beforeSchema: JSONSchema = {
1708
+ type: 'object',
1709
+ properties: {
1710
+ groups: {
1711
+ type: 'array',
1712
+ items: {
1713
+ type: 'array',
1714
+ maxItems: 10,
1715
+ items: { type: 'string' },
1716
+ },
1717
+ },
1718
+ },
1719
+ };
1720
+ const afterSchema: JSONSchema = {
1721
+ type: 'object',
1722
+ properties: {
1723
+ groups: {
1724
+ type: 'array',
1725
+ items: {
1726
+ type: 'array',
1727
+ maxItems: 3,
1728
+ items: { type: 'string' },
1729
+ },
1730
+ },
1731
+ },
1732
+ };
1733
+
1734
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('groups[]', 'Array maxItems lowered')]);
1735
+ });
1736
+
1737
+ it('detects minItems being increased on arrays inside array item objects', async () => {
1738
+ const beforeSchema: JSONSchema = {
1739
+ type: 'object',
1740
+ properties: {
1741
+ groups: {
1742
+ type: 'array',
1743
+ items: {
1744
+ type: 'object',
1745
+ properties: {
1746
+ tags: { type: 'array', minItems: 1, items: { type: 'string' } },
1747
+ },
1748
+ },
1749
+ },
1750
+ },
1751
+ };
1752
+ const afterSchema: JSONSchema = {
1753
+ type: 'object',
1754
+ properties: {
1755
+ groups: {
1756
+ type: 'array',
1757
+ items: {
1758
+ type: 'object',
1759
+ properties: {
1760
+ tags: { type: 'array', minItems: 3, items: { type: 'string' } },
1761
+ },
1762
+ },
1763
+ },
1764
+ },
1765
+ };
1766
+
1767
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
1768
+ breakAt('groups[].tags', 'Array minItems increased'),
1769
+ ]);
1770
+ });
1771
+
1772
+ it('detects maxItems being lowered on arrays inside array item objects', async () => {
1773
+ const beforeSchema: JSONSchema = {
1774
+ type: 'object',
1775
+ properties: {
1776
+ groups: {
1777
+ type: 'array',
1778
+ items: {
1779
+ type: 'object',
1780
+ properties: {
1781
+ tags: { type: 'array', maxItems: 10, items: { type: 'string' } },
1782
+ },
1783
+ },
1784
+ },
1785
+ },
1786
+ };
1787
+ const afterSchema: JSONSchema = {
1788
+ type: 'object',
1789
+ properties: {
1790
+ groups: {
1791
+ type: 'array',
1792
+ items: {
1793
+ type: 'object',
1794
+ properties: {
1795
+ tags: { type: 'array', maxItems: 3, items: { type: 'string' } },
1796
+ },
1797
+ },
1798
+ },
1799
+ },
1800
+ };
1801
+
1802
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
1803
+ breakAt('groups[].tags', 'Array maxItems lowered'),
1804
+ ]);
1805
+ });
1806
+
1807
+ it('detects uniqueItems being added on arrays inside array item objects', async () => {
1808
+ const beforeSchema: JSONSchema = {
1809
+ type: 'object',
1810
+ properties: {
1811
+ groups: {
1812
+ type: 'array',
1813
+ items: {
1814
+ type: 'object',
1815
+ properties: {
1816
+ members: { type: 'array', items: { type: 'string' } },
1817
+ },
1818
+ },
1819
+ },
1820
+ },
1821
+ };
1822
+ const afterSchema: JSONSchema = {
1823
+ type: 'object',
1824
+ properties: {
1825
+ groups: {
1826
+ type: 'array',
1827
+ items: {
1828
+ type: 'object',
1829
+ properties: {
1830
+ members: { type: 'array', uniqueItems: true, items: { type: 'string' } },
1831
+ },
1832
+ },
1833
+ },
1834
+ },
1835
+ };
1836
+
1837
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
1838
+ breakAt('groups[].members', 'Array uniqueItems added'),
1839
+ ]);
1840
+ });
1841
+
1842
+ it('detects contains constraint changes on arrays inside array item objects', async () => {
1843
+ const beforeSchema: JSONSchema = {
1844
+ type: 'object',
1845
+ properties: {
1846
+ groups: {
1847
+ type: 'array',
1848
+ items: {
1849
+ type: 'object',
1850
+ properties: {
1851
+ labels: {
1852
+ type: 'array',
1853
+ contains: { type: 'string' },
1854
+ items: { type: 'string' },
1855
+ },
1856
+ },
1857
+ },
1858
+ },
1859
+ },
1860
+ };
1861
+ const afterSchema: JSONSchema = {
1862
+ type: 'object',
1863
+ properties: {
1864
+ groups: {
1865
+ type: 'array',
1866
+ items: {
1867
+ type: 'object',
1868
+ properties: {
1869
+ labels: {
1870
+ type: 'array',
1871
+ contains: { type: 'number' },
1872
+ items: { type: 'string' },
1873
+ },
1874
+ },
1875
+ },
1876
+ },
1877
+ },
1878
+ };
1879
+
1880
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
1881
+ breakAt('groups[].labels.{contains}', 'Field type changed'),
1882
+ ]);
1883
+ });
1884
+
1885
+ it('detects array constraint narrowing in deeply nested array item schemas', async () => {
1886
+ const beforeSchema: JSONSchema = {
1887
+ type: 'object',
1888
+ properties: {
1889
+ sections: {
1890
+ type: 'array',
1891
+ items: {
1892
+ type: 'object',
1893
+ properties: {
1894
+ blocks: {
1895
+ type: 'array',
1896
+ items: {
1897
+ type: 'object',
1898
+ properties: {
1899
+ items: { type: 'array', minItems: 1, items: { type: 'string' } },
1900
+ },
1901
+ },
1902
+ },
1903
+ },
1904
+ },
1905
+ },
1906
+ },
1907
+ };
1908
+ const afterSchema: JSONSchema = {
1909
+ type: 'object',
1910
+ properties: {
1911
+ sections: {
1912
+ type: 'array',
1913
+ items: {
1914
+ type: 'object',
1915
+ properties: {
1916
+ blocks: {
1917
+ type: 'array',
1918
+ items: {
1919
+ type: 'object',
1920
+ properties: {
1921
+ items: { type: 'array', minItems: 2, items: { type: 'string' } },
1922
+ },
1923
+ },
1924
+ },
1925
+ },
1926
+ },
1927
+ },
1928
+ },
1929
+ };
1930
+
1931
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
1932
+ breakAt('sections[].blocks[].items', 'Array minItems increased'),
1933
+ ]);
1934
+ });
1935
+
1936
+ it('detects multiple array constraint breaks on arrays inside nested objects within array items', async () => {
1937
+ const beforeSchema: JSONSchema = {
1938
+ type: 'object',
1939
+ properties: {
1940
+ sections: {
1941
+ type: 'array',
1942
+ items: {
1943
+ type: 'object',
1944
+ properties: {
1945
+ content: {
1946
+ type: 'object',
1947
+ properties: {
1948
+ tags: { type: 'array', minItems: 1, maxItems: 10, items: { type: 'string' } },
1949
+ },
1950
+ },
1951
+ },
1952
+ },
1953
+ },
1954
+ },
1955
+ };
1956
+ const afterSchema: JSONSchema = {
1957
+ type: 'object',
1958
+ properties: {
1959
+ sections: {
1960
+ type: 'array',
1961
+ items: {
1962
+ type: 'object',
1963
+ properties: {
1964
+ content: {
1965
+ type: 'object',
1966
+ properties: {
1967
+ tags: {
1968
+ type: 'array',
1969
+ minItems: 3,
1970
+ maxItems: 5,
1971
+ uniqueItems: true,
1972
+ items: { type: 'string' },
1973
+ },
1974
+ },
1975
+ },
1976
+ },
1977
+ },
1978
+ },
1979
+ },
1980
+ };
1981
+
1982
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
1983
+ breakAt('sections[].content.tags', 'Array minItems increased'),
1984
+ breakAt('sections[].content.tags', 'Array maxItems lowered'),
1985
+ breakAt('sections[].content.tags', 'Array uniqueItems added'),
1986
+ ]);
1987
+ });
1988
+
1989
+ it('detects multiple array constraint breaks on the same field', async () => {
1990
+ const beforeSchema: JSONSchema = {
1991
+ type: 'object',
1992
+ properties: {
1993
+ tags: { type: 'array', minItems: 1, maxItems: 10, items: { type: 'string' } },
1994
+ },
1995
+ };
1996
+ const afterSchema: JSONSchema = {
1997
+ type: 'object',
1998
+ properties: {
1999
+ tags: {
2000
+ type: 'array',
2001
+ minItems: 3,
2002
+ maxItems: 5,
2003
+ uniqueItems: true,
2004
+ items: { type: 'string' },
2005
+ },
2006
+ },
2007
+ };
2008
+
2009
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
2010
+ breakAt('tags', 'Array minItems increased'),
2011
+ breakAt('tags', 'Array maxItems lowered'),
2012
+ breakAt('tags', 'Array uniqueItems added'),
2013
+ ]);
2014
+ });
2015
+
2016
+ it('does not report array constraint changes for a newly added property', async () => {
2017
+ const beforeSchema: JSONSchema = {
2018
+ type: 'object',
2019
+ properties: {
2020
+ title: { type: 'string' },
2021
+ },
2022
+ };
2023
+ const afterSchema: JSONSchema = {
2024
+ type: 'object',
2025
+ properties: {
2026
+ title: { type: 'string' },
2027
+ tags: { type: 'array', minItems: 1, uniqueItems: true, items: { type: 'string' } },
2028
+ },
2029
+ };
2030
+
2031
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
2032
+ });
2033
+ });
2034
+
2035
+ describe('numeric constraints', () => {
2036
+ it('returns no breaks when numeric constraints are unchanged', async () => {
2037
+ const beforeSchema: JSONSchema = {
2038
+ type: 'object',
2039
+ properties: {
2040
+ count: { type: 'number', minimum: 0, maximum: 100, exclusiveMinimum: 0, exclusiveMaximum: 100 },
2041
+ },
2042
+ };
2043
+ const afterSchema: JSONSchema = {
2044
+ type: 'object',
2045
+ properties: {
2046
+ count: { type: 'number', minimum: 0, maximum: 100, exclusiveMinimum: 0, exclusiveMaximum: 100 },
2047
+ },
2048
+ };
2049
+
2050
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
2051
+ });
2052
+
2053
+ it('returns no breaks when numeric constraints are relaxed', async () => {
2054
+ const beforeSchema: JSONSchema = {
2055
+ type: 'object',
2056
+ properties: {
2057
+ count: { type: 'number', minimum: 5, maximum: 10, exclusiveMinimum: 5, exclusiveMaximum: 10, multipleOf: 4 },
2058
+ },
2059
+ };
2060
+ const afterSchema: JSONSchema = {
2061
+ type: 'object',
2062
+ properties: {
2063
+ count: { type: 'number', minimum: 0, maximum: 100, multipleOf: 2 },
2064
+ },
2065
+ };
2066
+
2067
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
2068
+ });
2069
+
2070
+ it('returns no breaks when both bounds are set and minimum remains the effective lower bound', async () => {
2071
+ const beforeSchema: JSONSchema = {
2072
+ type: 'object',
2073
+ properties: {
2074
+ count: { type: 'number', minimum: 5, exclusiveMinimum: 3 },
2075
+ },
2076
+ };
2077
+ const afterSchema: JSONSchema = {
2078
+ type: 'object',
2079
+ properties: {
2080
+ count: { type: 'number', minimum: 5, exclusiveMinimum: 4 },
2081
+ },
2082
+ };
2083
+
2084
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
2085
+ });
2086
+
2087
+ it('returns no breaks when both bounds are set and maximum remains the effective upper bound', async () => {
2088
+ const beforeSchema: JSONSchema = {
2089
+ type: 'object',
2090
+ properties: {
2091
+ count: { type: 'number', maximum: 10, exclusiveMaximum: 20 },
2092
+ },
2093
+ };
2094
+ const afterSchema: JSONSchema = {
2095
+ type: 'object',
2096
+ properties: {
2097
+ count: { type: 'number', maximum: 10, exclusiveMaximum: 15 },
2098
+ },
2099
+ };
2100
+
2101
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
2102
+ });
2103
+
2104
+ it('detects minimum being increased at the root', async () => {
2105
+ const beforeSchema: JSONSchema = {
2106
+ type: 'object',
2107
+ properties: {
2108
+ count: { type: 'number', minimum: 0 },
2109
+ },
2110
+ };
2111
+ const afterSchema: JSONSchema = {
2112
+ type: 'object',
2113
+ properties: {
2114
+ count: { type: 'number', minimum: 5 },
2115
+ },
2116
+ };
2117
+
2118
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('count', 'Numeric minimum increased')]);
2119
+ });
2120
+
2121
+ it('detects minimum being added where it was previously unset', async () => {
2122
+ const beforeSchema: JSONSchema = {
2123
+ type: 'object',
2124
+ properties: {
2125
+ count: { type: 'number' },
2126
+ },
2127
+ };
2128
+ const afterSchema: JSONSchema = {
2129
+ type: 'object',
2130
+ properties: {
2131
+ count: { type: 'number', minimum: 1 },
2132
+ },
2133
+ };
2134
+
2135
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('count', 'Numeric minimum increased')]);
2136
+ });
2137
+
2138
+ it('detects exclusiveMinimum being increased at the root', async () => {
2139
+ const beforeSchema: JSONSchema = {
2140
+ type: 'object',
2141
+ properties: {
2142
+ count: { type: 'number', exclusiveMinimum: 0 },
2143
+ },
2144
+ };
2145
+ const afterSchema: JSONSchema = {
2146
+ type: 'object',
2147
+ properties: {
2148
+ count: { type: 'number', exclusiveMinimum: 5 },
2149
+ },
2150
+ };
2151
+
2152
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
2153
+ breakAt('count', 'Numeric exclusiveMinimum increased'),
2154
+ ]);
2155
+ });
2156
+
2157
+ it('detects exclusiveMinimum being added where it was previously unset', async () => {
2158
+ const beforeSchema: JSONSchema = {
2159
+ type: 'object',
2160
+ properties: {
2161
+ count: { type: 'number' },
2162
+ },
2163
+ };
2164
+ const afterSchema: JSONSchema = {
2165
+ type: 'object',
2166
+ properties: {
2167
+ count: { type: 'number', exclusiveMinimum: 0 },
2168
+ },
2169
+ };
2170
+
2171
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
2172
+ breakAt('count', 'Numeric exclusiveMinimum increased'),
2173
+ ]);
2174
+ });
2175
+
2176
+ it('detects maximum being lowered at the root', async () => {
2177
+ const beforeSchema: JSONSchema = {
2178
+ type: 'object',
2179
+ properties: {
2180
+ count: { type: 'number', maximum: 100 },
2181
+ },
2182
+ };
2183
+ const afterSchema: JSONSchema = {
2184
+ type: 'object',
2185
+ properties: {
2186
+ count: { type: 'number', maximum: 50 },
2187
+ },
2188
+ };
2189
+
2190
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('count', 'Numeric maximum lowered')]);
2191
+ });
2192
+
2193
+ it('detects maximum being added where it was previously unset', async () => {
2194
+ const beforeSchema: JSONSchema = {
2195
+ type: 'object',
2196
+ properties: {
2197
+ count: { type: 'number' },
2198
+ },
2199
+ };
2200
+ const afterSchema: JSONSchema = {
2201
+ type: 'object',
2202
+ properties: {
2203
+ count: { type: 'number', maximum: 100 },
2204
+ },
2205
+ };
2206
+
2207
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('count', 'Numeric maximum lowered')]);
2208
+ });
2209
+
2210
+ it('detects exclusiveMaximum being lowered at the root', async () => {
2211
+ const beforeSchema: JSONSchema = {
2212
+ type: 'object',
2213
+ properties: {
2214
+ count: { type: 'number', exclusiveMaximum: 100 },
2215
+ },
2216
+ };
2217
+ const afterSchema: JSONSchema = {
2218
+ type: 'object',
2219
+ properties: {
2220
+ count: { type: 'number', exclusiveMaximum: 50 },
2221
+ },
2222
+ };
2223
+
2224
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
2225
+ breakAt('count', 'Numeric exclusiveMaximum lowered'),
2226
+ ]);
2227
+ });
2228
+
2229
+ it('detects exclusiveMaximum being added where it was previously unset', async () => {
2230
+ const beforeSchema: JSONSchema = {
2231
+ type: 'object',
2232
+ properties: {
2233
+ count: { type: 'number' },
2234
+ },
2235
+ };
2236
+ const afterSchema: JSONSchema = {
2237
+ type: 'object',
2238
+ properties: {
2239
+ count: { type: 'number', exclusiveMaximum: 100 },
2240
+ },
2241
+ };
2242
+
2243
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
2244
+ breakAt('count', 'Numeric exclusiveMaximum lowered'),
2245
+ ]);
2246
+ });
2247
+
2248
+ it('detects numeric constraint narrowing in nested object properties', async () => {
2249
+ const beforeSchema: JSONSchema = {
2250
+ type: 'object',
2251
+ properties: {
2252
+ metadata: {
2253
+ type: 'object',
2254
+ properties: {
2255
+ priority: { type: 'integer', minimum: 1 },
2256
+ },
2257
+ },
2258
+ },
2259
+ };
2260
+ const afterSchema: JSONSchema = {
2261
+ type: 'object',
2262
+ properties: {
2263
+ metadata: {
2264
+ type: 'object',
2265
+ properties: {
2266
+ priority: { type: 'integer', minimum: 3 },
2267
+ },
2268
+ },
2269
+ },
2270
+ };
2271
+
2272
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
2273
+ breakAt('metadata.priority', 'Numeric minimum increased'),
2274
+ ]);
2275
+ });
2276
+
2277
+ it('detects numeric constraint narrowing in array item schemas', async () => {
2278
+ const beforeSchema: JSONSchema = {
2279
+ type: 'object',
2280
+ properties: {
2281
+ scores: {
2282
+ type: 'array',
2283
+ items: { type: 'number', maximum: 100 },
2284
+ },
2285
+ },
2286
+ };
2287
+ const afterSchema: JSONSchema = {
2288
+ type: 'object',
2289
+ properties: {
2290
+ scores: {
2291
+ type: 'array',
2292
+ items: { type: 'number', maximum: 50 },
2293
+ },
2294
+ },
2295
+ };
2296
+
2297
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('scores[]', 'Numeric maximum lowered')]);
2298
+ });
2299
+
2300
+ it('detects multiple numeric constraint breaks on the same field', async () => {
2301
+ const beforeSchema: JSONSchema = {
2302
+ type: 'object',
2303
+ properties: {
2304
+ count: { type: 'number', minimum: 0, maximum: 100 },
2305
+ },
2306
+ };
2307
+ const afterSchema: JSONSchema = {
2308
+ type: 'object',
2309
+ properties: {
2310
+ count: { type: 'number', minimum: 5, maximum: 50, exclusiveMinimum: 5, exclusiveMaximum: 50 },
2311
+ },
2312
+ };
2313
+
2314
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
2315
+ breakAt('count', 'Numeric exclusiveMinimum increased'),
2316
+ breakAt('count', 'Numeric exclusiveMaximum lowered'),
2317
+ ]);
2318
+ });
2319
+
2320
+ it('detects minimum being changed to exclusiveMinimum at the same value', async () => {
2321
+ const beforeSchema: JSONSchema = {
2322
+ type: 'object',
2323
+ properties: {
2324
+ count: { type: 'number', minimum: 5 },
2325
+ },
2326
+ };
2327
+ const afterSchema: JSONSchema = {
2328
+ type: 'object',
2329
+ properties: {
2330
+ count: { type: 'number', exclusiveMinimum: 5 },
2331
+ },
2332
+ };
2333
+
2334
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
2335
+ breakAt('count', 'Numeric exclusiveMinimum increased'),
2336
+ ]);
2337
+ });
2338
+
2339
+ it('does not report a break when minimum is replaced by exclusiveMinimum at an equivalent value', async () => {
2340
+ const beforeSchema: JSONSchema = {
2341
+ type: 'object',
2342
+ properties: {
2343
+ count: { type: 'number', minimum: 5 },
2344
+ },
2345
+ };
2346
+ const afterSchema: JSONSchema = {
2347
+ type: 'object',
2348
+ properties: {
2349
+ count: { type: 'number', exclusiveMinimum: 4 },
2350
+ },
2351
+ };
2352
+
2353
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
2354
+ });
2355
+
2356
+ it('does not report a break when exclusiveMinimum is relaxed to minimum at the same value', async () => {
2357
+ const beforeSchema: JSONSchema = {
2358
+ type: 'object',
2359
+ properties: {
2360
+ count: { type: 'number', exclusiveMinimum: 5 },
2361
+ },
2362
+ };
2363
+ const afterSchema: JSONSchema = {
2364
+ type: 'object',
2365
+ properties: {
2366
+ count: { type: 'number', minimum: 5 },
2367
+ },
2368
+ };
2369
+
2370
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
2371
+ });
2372
+
2373
+ it('detects maximum being changed to exclusiveMaximum at the same value', async () => {
2374
+ const beforeSchema: JSONSchema = {
2375
+ type: 'object',
2376
+ properties: {
2377
+ count: { type: 'number', maximum: 10 },
2378
+ },
2379
+ };
2380
+ const afterSchema: JSONSchema = {
2381
+ type: 'object',
2382
+ properties: {
2383
+ count: { type: 'number', exclusiveMaximum: 10 },
2384
+ },
2385
+ };
2386
+
2387
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
2388
+ breakAt('count', 'Numeric exclusiveMaximum lowered'),
2389
+ ]);
2390
+ });
2391
+
2392
+ it('does not report a break when maximum is replaced by exclusiveMaximum at an equivalent value', async () => {
2393
+ const beforeSchema: JSONSchema = {
2394
+ type: 'object',
2395
+ properties: {
2396
+ count: { type: 'number', maximum: 10 },
2397
+ },
2398
+ };
2399
+ const afterSchema: JSONSchema = {
2400
+ type: 'object',
2401
+ properties: {
2402
+ count: { type: 'number', exclusiveMaximum: 11 },
2403
+ },
2404
+ };
2405
+
2406
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
2407
+ });
2408
+
2409
+ it('does not report a break when exclusiveMaximum is relaxed to maximum at the same value', async () => {
2410
+ const beforeSchema: JSONSchema = {
2411
+ type: 'object',
2412
+ properties: {
2413
+ count: { type: 'number', exclusiveMaximum: 10 },
2414
+ },
2415
+ };
2416
+ const afterSchema: JSONSchema = {
2417
+ type: 'object',
2418
+ properties: {
2419
+ count: { type: 'number', maximum: 10 },
2420
+ },
2421
+ };
2422
+
2423
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
2424
+ });
2425
+
2426
+ it('does not report a break when exclusiveMinimum is redundant alongside a stricter minimum', async () => {
2427
+ const beforeSchema: JSONSchema = {
2428
+ type: 'object',
2429
+ properties: {
2430
+ count: { type: 'number', minimum: 5 },
2431
+ },
2432
+ };
2433
+ const afterSchema: JSONSchema = {
2434
+ type: 'object',
2435
+ properties: {
2436
+ count: { type: 'number', minimum: 5, exclusiveMinimum: 3 },
2437
+ },
2438
+ };
2439
+
2440
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
2441
+ });
2442
+
2443
+ it('detects when exclusiveMinimum is added at the same value as an existing minimum', async () => {
2444
+ const beforeSchema: JSONSchema = {
2445
+ type: 'object',
2446
+ properties: {
2447
+ count: { type: 'number', minimum: 5 },
2448
+ },
2449
+ };
2450
+ const afterSchema: JSONSchema = {
2451
+ type: 'object',
2452
+ properties: {
2453
+ count: { type: 'number', minimum: 5, exclusiveMinimum: 5 },
2454
+ },
2455
+ };
2456
+
2457
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
2458
+ breakAt('count', 'Numeric exclusiveMinimum increased'),
2459
+ ]);
2460
+ });
2461
+
2462
+ it('detects when exclusiveMaximum is added at the same value as an existing maximum', async () => {
2463
+ const beforeSchema: JSONSchema = {
2464
+ type: 'object',
2465
+ properties: {
2466
+ count: { type: 'number', maximum: 10 },
2467
+ },
2468
+ };
2469
+ const afterSchema: JSONSchema = {
2470
+ type: 'object',
2471
+ properties: {
2472
+ count: { type: 'number', maximum: 10, exclusiveMaximum: 10 },
2473
+ },
2474
+ };
2475
+
2476
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
2477
+ breakAt('count', 'Numeric exclusiveMaximum lowered'),
2478
+ ]);
2479
+ });
2480
+
2481
+ it('detects when exclusiveMinimum tightens beyond an existing minimum', async () => {
2482
+ const beforeSchema: JSONSchema = {
2483
+ type: 'object',
2484
+ properties: {
2485
+ count: { type: 'number', minimum: 3, exclusiveMinimum: 1 },
2486
+ },
2487
+ };
2488
+ const afterSchema: JSONSchema = {
2489
+ type: 'object',
2490
+ properties: {
2491
+ count: { type: 'number', minimum: 3, exclusiveMinimum: 5 },
2492
+ },
2493
+ };
2494
+
2495
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
2496
+ breakAt('count', 'Numeric exclusiveMinimum increased'),
2497
+ ]);
2498
+ });
2499
+
2500
+ it('detects minimum being increased when both lower bound keywords are set', async () => {
2501
+ const beforeSchema: JSONSchema = {
2502
+ type: 'object',
2503
+ properties: {
2504
+ count: { type: 'number', minimum: 3, exclusiveMinimum: 1 },
2505
+ },
2506
+ };
2507
+ const afterSchema: JSONSchema = {
2508
+ type: 'object',
2509
+ properties: {
2510
+ count: { type: 'number', minimum: 5, exclusiveMinimum: 1 },
2511
+ },
2512
+ };
2513
+
2514
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('count', 'Numeric minimum increased')]);
2515
+ });
2516
+
2517
+ it('does not report a break when exclusiveMaximum is redundant alongside a stricter maximum', async () => {
2518
+ const beforeSchema: JSONSchema = {
2519
+ type: 'object',
2520
+ properties: {
2521
+ count: { type: 'number', maximum: 10 },
2522
+ },
2523
+ };
2524
+ const afterSchema: JSONSchema = {
2525
+ type: 'object',
2526
+ properties: {
2527
+ count: { type: 'number', maximum: 10, exclusiveMaximum: 20 },
2528
+ },
2529
+ };
2530
+
2531
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
2532
+ });
2533
+
2534
+ it('detects when exclusiveMaximum tightens beyond an existing maximum', async () => {
2535
+ const beforeSchema: JSONSchema = {
2536
+ type: 'object',
2537
+ properties: {
2538
+ count: { type: 'number', maximum: 20, exclusiveMaximum: 30 },
2539
+ },
2540
+ };
2541
+ const afterSchema: JSONSchema = {
2542
+ type: 'object',
2543
+ properties: {
2544
+ count: { type: 'number', maximum: 20, exclusiveMaximum: 10 },
2545
+ },
2546
+ };
2547
+
2548
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
2549
+ breakAt('count', 'Numeric exclusiveMaximum lowered'),
2550
+ ]);
2551
+ });
2552
+
2553
+ it('detects maximum being lowered when both upper bound keywords are set', async () => {
2554
+ const beforeSchema: JSONSchema = {
2555
+ type: 'object',
2556
+ properties: {
2557
+ count: { type: 'number', maximum: 20, exclusiveMaximum: 30 },
2558
+ },
2559
+ };
2560
+ const afterSchema: JSONSchema = {
2561
+ type: 'object',
2562
+ properties: {
2563
+ count: { type: 'number', maximum: 10, exclusiveMaximum: 30 },
2564
+ },
2565
+ };
2566
+
2567
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('count', 'Numeric maximum lowered')]);
2568
+ });
2569
+
2570
+ it('detects multipleOf being added where it was previously unset', async () => {
2571
+ const beforeSchema: JSONSchema = {
2572
+ type: 'object',
2573
+ properties: {
2574
+ count: { type: 'number' },
2575
+ },
2576
+ };
2577
+ const afterSchema: JSONSchema = {
2578
+ type: 'object',
2579
+ properties: {
2580
+ count: { type: 'number', multipleOf: 2 },
2581
+ },
2582
+ };
2583
+
2584
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('count', 'Numeric multipleOf narrowed')]);
2585
+ });
2586
+
2587
+ it('detects multipleOf being narrowed to a coarser divisor', async () => {
2588
+ const beforeSchema: JSONSchema = {
2589
+ type: 'object',
2590
+ properties: {
2591
+ count: { type: 'number', multipleOf: 2 },
2592
+ },
2593
+ };
2594
+ const afterSchema: JSONSchema = {
2595
+ type: 'object',
2596
+ properties: {
2597
+ count: { type: 'number', multipleOf: 4 },
2598
+ },
2599
+ };
2600
+
2601
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('count', 'Numeric multipleOf narrowed')]);
2602
+ });
2603
+
2604
+ it('detects multipleOf being changed to an incompatible divisor', async () => {
2605
+ const beforeSchema: JSONSchema = {
2606
+ type: 'object',
2607
+ properties: {
2608
+ count: { type: 'number', multipleOf: 2 },
2609
+ },
2610
+ };
2611
+ const afterSchema: JSONSchema = {
2612
+ type: 'object',
2613
+ properties: {
2614
+ count: { type: 'number', multipleOf: 3 },
2615
+ },
2616
+ };
2617
+
2618
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('count', 'Numeric multipleOf narrowed')]);
2619
+ });
2620
+
2621
+ it('does not report a break when multipleOf is relaxed to a finer divisor', async () => {
2622
+ const beforeSchema: JSONSchema = {
2623
+ type: 'object',
2624
+ properties: {
2625
+ count: { type: 'number', multipleOf: 4 },
2626
+ },
2627
+ };
2628
+ const afterSchema: JSONSchema = {
2629
+ type: 'object',
2630
+ properties: {
2631
+ count: { type: 'number', multipleOf: 2 },
2632
+ },
2633
+ };
2634
+
2635
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
2636
+ });
2637
+
2638
+ it('does not report numeric constraint changes for a newly added property', async () => {
2639
+ const beforeSchema: JSONSchema = {
2640
+ type: 'object',
2641
+ properties: {
2642
+ title: { type: 'string' },
2643
+ },
2644
+ };
2645
+ const afterSchema: JSONSchema = {
2646
+ type: 'object',
2647
+ properties: {
2648
+ title: { type: 'string' },
2649
+ count: { type: 'number', minimum: 1, maximum: 10 },
2650
+ },
2651
+ };
2652
+
2653
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
2654
+ });
2655
+ });
2656
+
2657
+ describe('string constraints', () => {
2658
+ it('returns no breaks when string constraints are unchanged', async () => {
2659
+ const beforeSchema: JSONSchema = {
2660
+ type: 'object',
2661
+ properties: {
2662
+ title: { type: 'string', minLength: 1, maxLength: 100, format: 'email', pattern: '^[a-z]+$' },
2663
+ },
2664
+ };
2665
+ const afterSchema: JSONSchema = {
2666
+ type: 'object',
2667
+ properties: {
2668
+ title: { type: 'string', minLength: 1, maxLength: 100, format: 'email', pattern: '^[a-z]+$' },
2669
+ },
2670
+ };
2671
+
2672
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
2673
+ });
2674
+
2675
+ it('returns no breaks when string constraints are relaxed', async () => {
2676
+ const beforeSchema: JSONSchema = {
2677
+ type: 'object',
2678
+ properties: {
2679
+ title: { type: 'string', minLength: 5, maxLength: 10, format: 'email', pattern: '^[a-z]+$' },
2680
+ },
2681
+ };
2682
+ const afterSchema: JSONSchema = {
2683
+ type: 'object',
2684
+ properties: {
2685
+ title: { type: 'string', minLength: 1, maxLength: 100 },
2686
+ },
2687
+ };
2688
+
2689
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
2690
+ });
2691
+
2692
+ it('detects minLength being increased at the root', async () => {
2693
+ const beforeSchema: JSONSchema = {
2694
+ type: 'object',
2695
+ properties: {
2696
+ title: { type: 'string', minLength: 1 },
2697
+ },
2698
+ };
2699
+ const afterSchema: JSONSchema = {
2700
+ type: 'object',
2701
+ properties: {
2702
+ title: { type: 'string', minLength: 5 },
2703
+ },
2704
+ };
2705
+
2706
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('title', 'String minLength increased')]);
2707
+ });
2708
+
2709
+ it('detects minLength being added where it was previously unset', async () => {
2710
+ const beforeSchema: JSONSchema = {
2711
+ type: 'object',
2712
+ properties: {
2713
+ title: { type: 'string' },
2714
+ },
2715
+ };
2716
+ const afterSchema: JSONSchema = {
2717
+ type: 'object',
2718
+ properties: {
2719
+ title: { type: 'string', minLength: 1 },
2720
+ },
2721
+ };
2722
+
2723
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('title', 'String minLength increased')]);
2724
+ });
2725
+
2726
+ it('detects maxLength being lowered at the root', async () => {
2727
+ const beforeSchema: JSONSchema = {
2728
+ type: 'object',
2729
+ properties: {
2730
+ title: { type: 'string', maxLength: 100 },
2731
+ },
2732
+ };
2733
+ const afterSchema: JSONSchema = {
2734
+ type: 'object',
2735
+ properties: {
2736
+ title: { type: 'string', maxLength: 50 },
2737
+ },
2738
+ };
2739
+
2740
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('title', 'String maxLength lowered')]);
2741
+ });
2742
+
2743
+ it('detects maxLength being added where it was previously unset', async () => {
2744
+ const beforeSchema: JSONSchema = {
2745
+ type: 'object',
2746
+ properties: {
2747
+ title: { type: 'string' },
2748
+ },
2749
+ };
2750
+ const afterSchema: JSONSchema = {
2751
+ type: 'object',
2752
+ properties: {
2753
+ title: { type: 'string', maxLength: 100 },
2754
+ },
2755
+ };
2756
+
2757
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('title', 'String maxLength lowered')]);
2758
+ });
2759
+
2760
+ it('detects format being added where it was previously unset', async () => {
2761
+ const beforeSchema: JSONSchema = {
2762
+ type: 'object',
2763
+ properties: {
2764
+ email: { type: 'string' },
2765
+ },
2766
+ };
2767
+ const afterSchema: JSONSchema = {
2768
+ type: 'object',
2769
+ properties: {
2770
+ email: { type: 'string', format: 'email' },
2771
+ },
2772
+ };
2773
+
2774
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('email', 'String format changed')]);
2775
+ });
2776
+
2777
+ it('detects format being changed on an existing field', async () => {
2778
+ const beforeSchema: JSONSchema = {
2779
+ type: 'object',
2780
+ properties: {
2781
+ value: { type: 'string', format: 'email' },
2782
+ },
2783
+ };
2784
+ const afterSchema: JSONSchema = {
2785
+ type: 'object',
2786
+ properties: {
2787
+ value: { type: 'string', format: 'uri' },
2788
+ },
2789
+ };
2790
+
2791
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('value', 'String format changed')]);
2792
+ });
2793
+
2794
+ it('detects pattern being added where it was previously unset', async () => {
2795
+ const beforeSchema: JSONSchema = {
2796
+ type: 'object',
2797
+ properties: {
2798
+ slug: { type: 'string' },
2799
+ },
2800
+ };
2801
+ const afterSchema: JSONSchema = {
2802
+ type: 'object',
2803
+ properties: {
2804
+ slug: { type: 'string', pattern: '^[a-z]+$' },
2805
+ },
2806
+ };
2807
+
2808
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('slug', 'String pattern changed')]);
2809
+ });
2810
+
2811
+ it('detects pattern being changed on an existing field', async () => {
2812
+ const beforeSchema: JSONSchema = {
2813
+ type: 'object',
2814
+ properties: {
2815
+ slug: { type: 'string', pattern: '^[a-z]+$' },
2816
+ },
2817
+ };
2818
+ const afterSchema: JSONSchema = {
2819
+ type: 'object',
2820
+ properties: {
2821
+ slug: { type: 'string', pattern: '^[a-z0-9-]+$' },
2822
+ },
2823
+ };
2824
+
2825
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('slug', 'String pattern changed')]);
2826
+ });
2827
+
2828
+ it('does not report a break when format is removed', async () => {
2829
+ const beforeSchema: JSONSchema = {
2830
+ type: 'object',
2831
+ properties: {
2832
+ email: { type: 'string', format: 'email' },
2833
+ },
2834
+ };
2835
+ const afterSchema: JSONSchema = {
2836
+ type: 'object',
2837
+ properties: {
2838
+ email: { type: 'string' },
2839
+ },
2840
+ };
2841
+
2842
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
2843
+ });
2844
+
2845
+ it('does not report a break when pattern is removed', async () => {
2846
+ const beforeSchema: JSONSchema = {
2847
+ type: 'object',
2848
+ properties: {
2849
+ slug: { type: 'string', pattern: '^[a-z]+$' },
2850
+ },
2851
+ };
2852
+ const afterSchema: JSONSchema = {
2853
+ type: 'object',
2854
+ properties: {
2855
+ slug: { type: 'string' },
2856
+ },
2857
+ };
2858
+
2859
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
2860
+ });
2861
+
2862
+ it('detects string constraint narrowing in nested object properties', async () => {
2863
+ const beforeSchema: JSONSchema = {
2864
+ type: 'object',
2865
+ properties: {
2866
+ metadata: {
2867
+ type: 'object',
2868
+ properties: {
2869
+ slug: { type: 'string', minLength: 1 },
2870
+ },
2871
+ },
2872
+ },
2873
+ };
2874
+ const afterSchema: JSONSchema = {
2875
+ type: 'object',
2876
+ properties: {
2877
+ metadata: {
2878
+ type: 'object',
2879
+ properties: {
2880
+ slug: { type: 'string', minLength: 3 },
2881
+ },
2882
+ },
2883
+ },
2884
+ };
2885
+
2886
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
2887
+ breakAt('metadata.slug', 'String minLength increased'),
2888
+ ]);
2889
+ });
2890
+
2891
+ it('detects string constraint narrowing in array item schemas', async () => {
2892
+ const beforeSchema: JSONSchema = {
2893
+ type: 'object',
2894
+ properties: {
2895
+ tags: {
2896
+ type: 'array',
2897
+ items: { type: 'string', maxLength: 100 },
2898
+ },
2899
+ },
2900
+ };
2901
+ const afterSchema: JSONSchema = {
2902
+ type: 'object',
2903
+ properties: {
2904
+ tags: {
2905
+ type: 'array',
2906
+ items: { type: 'string', maxLength: 50 },
2907
+ },
2908
+ },
2909
+ };
2910
+
2911
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('tags[]', 'String maxLength lowered')]);
2912
+ });
2913
+
2914
+ it('detects multiple string constraint breaks on the same field', async () => {
2915
+ const beforeSchema: JSONSchema = {
2916
+ type: 'object',
2917
+ properties: {
2918
+ slug: { type: 'string', minLength: 1, maxLength: 100 },
2919
+ },
2920
+ };
2921
+ const afterSchema: JSONSchema = {
2922
+ type: 'object',
2923
+ properties: {
2924
+ slug: {
2925
+ type: 'string',
2926
+ minLength: 3,
2927
+ maxLength: 50,
2928
+ format: 'hostname',
2929
+ pattern: '^[a-z0-9-]+$',
2930
+ },
2931
+ },
2932
+ };
2933
+
2934
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
2935
+ breakAt('slug', 'String minLength increased'),
2936
+ breakAt('slug', 'String maxLength lowered'),
2937
+ breakAt('slug', 'String format changed'),
2938
+ breakAt('slug', 'String pattern changed'),
2939
+ ]);
2940
+ });
2941
+
2942
+ it('does not report string constraint changes for a newly added property', async () => {
2943
+ const beforeSchema: JSONSchema = {
2944
+ type: 'object',
2945
+ properties: {
2946
+ title: { type: 'string' },
2947
+ },
2948
+ };
2949
+ const afterSchema: JSONSchema = {
2950
+ type: 'object',
2951
+ properties: {
2952
+ title: { type: 'string' },
2953
+ slug: { type: 'string', minLength: 1, maxLength: 10, format: 'hostname', pattern: '^[a-z]+$' },
2954
+ },
2955
+ };
2956
+
2957
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([]);
2958
+ });
2959
+ });
2960
+
2961
+ describe('schema dereferencing', () => {
2962
+ beforeEach(() => {
2963
+ jest.mocked(dereferenceLocal).mockClear();
2964
+ });
2965
+
2966
+ it('calls dereference for the before and after schemas', () => {
2967
+ const beforeSchema: JSONSchema = {
2968
+ type: 'object',
2969
+ properties: {
2970
+ title: { type: 'string' },
2971
+ },
2972
+ };
2973
+ const afterSchema: JSONSchema = {
2974
+ type: 'object',
2975
+ properties: {
2976
+ title: { type: 'string' },
2977
+ subtitle: { type: 'string' },
2978
+ },
2979
+ };
2980
+
2981
+ jsonBreakDetector(beforeSchema, afterSchema);
2982
+
2983
+ expect(dereferenceLocal).toHaveBeenCalledTimes(2);
2984
+ expect(dereferenceLocal).toHaveBeenNthCalledWith(1, beforeSchema);
2985
+ expect(dereferenceLocal).toHaveBeenNthCalledWith(2, afterSchema);
2986
+ });
2987
+
2988
+ it('reports a break when before schema dereferencing fails', () => {
2989
+ const beforeSchema: JSONSchema = { $ref: '#/definitions/Missing' };
2990
+ const afterSchema: JSONSchema = { type: 'string' };
2991
+ const errorMessage = 'Could not resolve JSON pointer reference: "#/definitions/Missing" at segment "definitions"';
2992
+
2993
+ jest.mocked(dereferenceLocal).mockImplementationOnce(() => {
2994
+ throw new Error(errorMessage);
2995
+ });
2996
+
2997
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('', errorMessage)]);
2998
+ expect(dereferenceLocal).toHaveBeenCalledTimes(2);
2999
+ });
3000
+
3001
+ it('reports a break when after schema dereferencing fails', () => {
3002
+ const beforeSchema: JSONSchema = { type: 'string' };
3003
+ const afterSchema: JSONSchema = { $ref: '#/definitions/Missing' };
3004
+ const errorMessage = 'Could not resolve JSON pointer reference: "#/definitions/Missing" at segment "definitions"';
3005
+
3006
+ jest
3007
+ .mocked(dereferenceLocal)
3008
+ .mockImplementationOnce((schema) => schema)
3009
+ .mockImplementationOnce(() => {
3010
+ throw new Error(errorMessage);
3011
+ });
3012
+
3013
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([breakAt('', errorMessage)]);
3014
+ expect(dereferenceLocal).toHaveBeenCalledTimes(2);
3015
+ });
3016
+
3017
+ it('reports breaks for both schemas when dereferencing fails for each', () => {
3018
+ const beforeSchema: JSONSchema = { $ref: '#/definitions/MissingBefore' };
3019
+ const afterSchema: JSONSchema = { $ref: '#/definitions/MissingAfter' };
3020
+ const beforeErrorMessage =
3021
+ 'Could not resolve JSON pointer reference: "#/definitions/MissingBefore" at segment "definitions"';
3022
+ const afterErrorMessage =
3023
+ 'Could not resolve JSON pointer reference: "#/definitions/MissingAfter" at segment "definitions"';
3024
+
3025
+ jest
3026
+ .mocked(dereferenceLocal)
3027
+ .mockImplementationOnce(() => {
3028
+ throw new Error(beforeErrorMessage);
3029
+ })
3030
+ .mockImplementationOnce(() => {
3031
+ throw new Error(afterErrorMessage);
3032
+ });
3033
+
3034
+ expect(jsonBreakDetector(beforeSchema, afterSchema)).toEqual([
3035
+ breakAt('', beforeErrorMessage),
3036
+ breakAt('', afterErrorMessage),
3037
+ ]);
3038
+ expect(dereferenceLocal).toHaveBeenCalledTimes(2);
3039
+ });
3040
+ });
3041
+ });