@illuma-ai/agents 1.0.83 → 1.0.84

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,500 +1,500 @@
1
- // src/schemas/schema-preparation.test.ts
2
- import { prepareSchemaForProvider } from './validate';
3
- import { Providers } from '@/common';
4
-
5
- describe('prepareSchemaForProvider', () => {
6
- describe('basic passthrough', () => {
7
- it('returns schema unchanged when strict is false', () => {
8
- const schema = {
9
- type: 'object',
10
- properties: {
11
- name: { type: 'string' },
12
- },
13
- };
14
-
15
- const { schema: result, warnings } = prepareSchemaForProvider(
16
- schema,
17
- Providers.OPENAI,
18
- false,
19
- );
20
-
21
- expect(result).toEqual(schema);
22
- expect(warnings).toEqual([]);
23
- });
24
-
25
- it('returns schema with additionalProperties already set', () => {
26
- const schema = {
27
- type: 'object',
28
- properties: {
29
- name: { type: 'string' },
30
- },
31
- required: ['name'],
32
- additionalProperties: false,
33
- };
34
-
35
- const { schema: result } = prepareSchemaForProvider(
36
- schema,
37
- Providers.OPENAI,
38
- );
39
-
40
- expect(result.additionalProperties).toBe(false);
41
- });
42
- });
43
-
44
- describe('additionalProperties enforcement', () => {
45
- it('adds additionalProperties: false to root object', () => {
46
- const schema = {
47
- type: 'object',
48
- properties: {
49
- name: { type: 'string' },
50
- },
51
- };
52
-
53
- const { schema: result, warnings } = prepareSchemaForProvider(
54
- schema,
55
- Providers.ANTHROPIC,
56
- );
57
-
58
- expect(result.additionalProperties).toBe(false);
59
- // Root object doesn't produce a warning for additionalProperties
60
- });
61
-
62
- it('adds additionalProperties: false to nested objects', () => {
63
- const schema = {
64
- type: 'object',
65
- properties: {
66
- address: {
67
- type: 'object',
68
- properties: {
69
- street: { type: 'string' },
70
- city: { type: 'string' },
71
- },
72
- },
73
- },
74
- additionalProperties: false,
75
- };
76
-
77
- const { schema: result, warnings } = prepareSchemaForProvider(
78
- schema,
79
- Providers.OPENAI,
80
- );
81
-
82
- const addressProp = (result.properties as Record<string, Record<string, unknown>>).address;
83
- expect(addressProp.additionalProperties).toBe(false);
84
- expect(warnings.some(w => w.includes('additionalProperties'))).toBe(true);
85
- });
86
-
87
- it('adds additionalProperties: false to deeply nested objects', () => {
88
- const schema = {
89
- type: 'object',
90
- properties: {
91
- level1: {
92
- type: 'object',
93
- properties: {
94
- level2: {
95
- type: 'object',
96
- properties: {
97
- value: { type: 'string' },
98
- },
99
- },
100
- },
101
- },
102
- },
103
- additionalProperties: false,
104
- };
105
-
106
- const { schema: result } = prepareSchemaForProvider(
107
- schema,
108
- Providers.ANTHROPIC,
109
- );
110
-
111
- const level1 = (result.properties as Record<string, Record<string, unknown>>).level1;
112
- expect(level1.additionalProperties).toBe(false);
113
- const level2 = (level1.properties as Record<string, Record<string, unknown>>).level2;
114
- expect(level2.additionalProperties).toBe(false);
115
- });
116
- });
117
-
118
- describe('required properties enforcement', () => {
119
- it('adds all properties to required when missing', () => {
120
- const schema = {
121
- type: 'object',
122
- properties: {
123
- name: { type: 'string' },
124
- age: { type: 'number' },
125
- },
126
- };
127
-
128
- const { schema: result, warnings } = prepareSchemaForProvider(
129
- schema,
130
- Providers.OPENAI,
131
- );
132
-
133
- expect(result.required).toEqual(expect.arrayContaining(['name', 'age']));
134
- expect(warnings.some(w => w.includes('required'))).toBe(true);
135
- });
136
-
137
- it('adds missing properties to existing required array', () => {
138
- const schema = {
139
- type: 'object',
140
- properties: {
141
- name: { type: 'string' },
142
- age: { type: 'number' },
143
- email: { type: 'string' },
144
- },
145
- required: ['name'],
146
- };
147
-
148
- const { schema: result } = prepareSchemaForProvider(
149
- schema,
150
- Providers.ANTHROPIC,
151
- );
152
-
153
- expect(result.required).toEqual(expect.arrayContaining(['name', 'age', 'email']));
154
- });
155
-
156
- it('does not duplicate already-required properties', () => {
157
- const schema = {
158
- type: 'object',
159
- properties: {
160
- name: { type: 'string' },
161
- age: { type: 'number' },
162
- },
163
- required: ['name', 'age'],
164
- };
165
-
166
- const { schema: result } = prepareSchemaForProvider(
167
- schema,
168
- Providers.OPENAI,
169
- );
170
-
171
- expect(result.required).toEqual(['name', 'age']);
172
- });
173
-
174
- it('adds required to nested objects', () => {
175
- const schema = {
176
- type: 'object',
177
- properties: {
178
- person: {
179
- type: 'object',
180
- properties: {
181
- name: { type: 'string' },
182
- age: { type: 'number' },
183
- },
184
- },
185
- },
186
- additionalProperties: false,
187
- };
188
-
189
- const { schema: result } = prepareSchemaForProvider(
190
- schema,
191
- Providers.OPENAI,
192
- );
193
-
194
- const person = (result.properties as Record<string, Record<string, unknown>>).person;
195
- expect(person.required).toEqual(expect.arrayContaining(['name', 'age']));
196
- });
197
- });
198
-
199
- describe('numeric constraint stripping', () => {
200
- it('strips minimum/maximum from number properties', () => {
201
- const schema = {
202
- type: 'object',
203
- properties: {
204
- age: { type: 'number', minimum: 0, maximum: 150 },
205
- },
206
- required: ['age'],
207
- additionalProperties: false,
208
- };
209
-
210
- const { schema: result, warnings } = prepareSchemaForProvider(
211
- schema,
212
- Providers.ANTHROPIC,
213
- );
214
-
215
- const ageProp = (result.properties as Record<string, Record<string, unknown>>).age;
216
- expect(ageProp.minimum).toBeUndefined();
217
- expect(ageProp.maximum).toBeUndefined();
218
- expect(ageProp.description).toContain('minimum: 0');
219
- expect(ageProp.description).toContain('maximum: 150');
220
- expect(warnings.some(w => w.includes('minimum'))).toBe(true);
221
- expect(warnings.some(w => w.includes('maximum'))).toBe(true);
222
- });
223
-
224
- it('strips multipleOf from integer properties', () => {
225
- const schema = {
226
- type: 'object',
227
- properties: {
228
- count: { type: 'integer', multipleOf: 5 },
229
- },
230
- required: ['count'],
231
- additionalProperties: false,
232
- };
233
-
234
- const { schema: result, warnings } = prepareSchemaForProvider(
235
- schema,
236
- Providers.OPENAI,
237
- );
238
-
239
- const countProp = (result.properties as Record<string, Record<string, unknown>>).count;
240
- expect(countProp.multipleOf).toBeUndefined();
241
- expect(countProp.description).toContain('multipleOf: 5');
242
- expect(warnings.some(w => w.includes('multipleOf'))).toBe(true);
243
- });
244
-
245
- it('appends to existing description when stripping constraints', () => {
246
- const schema = {
247
- type: 'object',
248
- properties: {
249
- score: { type: 'number', description: 'User score', minimum: 0, maximum: 100 },
250
- },
251
- required: ['score'],
252
- additionalProperties: false,
253
- };
254
-
255
- const { schema: result } = prepareSchemaForProvider(
256
- schema,
257
- Providers.ANTHROPIC,
258
- );
259
-
260
- const scoreProp = (result.properties as Record<string, Record<string, unknown>>).score;
261
- expect(scoreProp.description).toContain('User score');
262
- expect(scoreProp.description).toContain('minimum: 0');
263
- });
264
- });
265
-
266
- describe('string constraint stripping', () => {
267
- it('strips minLength/maxLength from string properties', () => {
268
- const schema = {
269
- type: 'object',
270
- properties: {
271
- name: { type: 'string', minLength: 1, maxLength: 100 },
272
- },
273
- required: ['name'],
274
- additionalProperties: false,
275
- };
276
-
277
- const { schema: result, warnings } = prepareSchemaForProvider(
278
- schema,
279
- Providers.OPENAI,
280
- );
281
-
282
- const nameProp = (result.properties as Record<string, Record<string, unknown>>).name;
283
- expect(nameProp.minLength).toBeUndefined();
284
- expect(nameProp.maxLength).toBeUndefined();
285
- expect(nameProp.description).toContain('minLength: 1');
286
- expect(nameProp.description).toContain('maxLength: 100');
287
- expect(warnings.length).toBeGreaterThan(0);
288
- });
289
-
290
- it('strips pattern from string properties', () => {
291
- const schema = {
292
- type: 'object',
293
- properties: {
294
- email: { type: 'string', pattern: '^[a-z]+@[a-z]+\\.[a-z]+$' },
295
- },
296
- required: ['email'],
297
- additionalProperties: false,
298
- };
299
-
300
- const { schema: result } = prepareSchemaForProvider(
301
- schema,
302
- Providers.ANTHROPIC,
303
- );
304
-
305
- const emailProp = (result.properties as Record<string, Record<string, unknown>>).email;
306
- expect(emailProp.pattern).toBeUndefined();
307
- expect(emailProp.description).toContain('pattern:');
308
- });
309
- });
310
-
311
- describe('array constraint stripping', () => {
312
- it('strips minItems/maxItems from array properties', () => {
313
- const schema = {
314
- type: 'object',
315
- properties: {
316
- tags: { type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 10 },
317
- },
318
- required: ['tags'],
319
- additionalProperties: false,
320
- };
321
-
322
- const { schema: result, warnings } = prepareSchemaForProvider(
323
- schema,
324
- Providers.OPENAI,
325
- );
326
-
327
- const tagsProp = (result.properties as Record<string, Record<string, unknown>>).tags;
328
- expect(tagsProp.minItems).toBeUndefined();
329
- expect(tagsProp.maxItems).toBeUndefined();
330
- expect(tagsProp.description).toContain('minItems: 1');
331
- expect(warnings.some(w => w.includes('minItems'))).toBe(true);
332
- });
333
- });
334
-
335
- describe('array items with object schemas', () => {
336
- it('prepares object schemas inside array items', () => {
337
- const schema = {
338
- type: 'object',
339
- properties: {
340
- people: {
341
- type: 'array',
342
- items: {
343
- type: 'object',
344
- properties: {
345
- name: { type: 'string' },
346
- age: { type: 'number' },
347
- },
348
- },
349
- },
350
- },
351
- additionalProperties: false,
352
- };
353
-
354
- const { schema: result } = prepareSchemaForProvider(
355
- schema,
356
- Providers.OPENAI,
357
- );
358
-
359
- const peopleProp = (result.properties as Record<string, Record<string, unknown>>).people;
360
- const items = peopleProp.items as Record<string, unknown>;
361
- expect(items.additionalProperties).toBe(false);
362
- expect(items.required).toEqual(expect.arrayContaining(['name', 'age']));
363
- });
364
- });
365
-
366
- describe('anyOf handling', () => {
367
- it('prepares object schemas within anyOf', () => {
368
- const schema = {
369
- type: 'object',
370
- properties: {
371
- result: {
372
- anyOf: [
373
- { type: 'object', properties: { value: { type: 'string' } } },
374
- { type: 'null' },
375
- ],
376
- },
377
- },
378
- additionalProperties: false,
379
- };
380
-
381
- const { schema: result } = prepareSchemaForProvider(
382
- schema,
383
- Providers.ANTHROPIC,
384
- );
385
-
386
- const resultProp = (result.properties as Record<string, Record<string, unknown>>).result;
387
- const anyOf = resultProp.anyOf as Record<string, unknown>[];
388
- const objectVariant = anyOf.find(v => v.type === 'object');
389
- expect(objectVariant?.additionalProperties).toBe(false);
390
- });
391
- });
392
-
393
- describe('$defs handling', () => {
394
- it('prepares schemas in $defs', () => {
395
- const schema = {
396
- type: 'object',
397
- properties: {
398
- item: { $ref: '#/$defs/Item' },
399
- },
400
- $defs: {
401
- Item: {
402
- type: 'object',
403
- properties: {
404
- id: { type: 'string' },
405
- name: { type: 'string' },
406
- },
407
- },
408
- },
409
- additionalProperties: false,
410
- };
411
-
412
- const { schema: result } = prepareSchemaForProvider(
413
- schema,
414
- Providers.OPENAI,
415
- );
416
-
417
- const defs = result.$defs as Record<string, Record<string, unknown>>;
418
- expect(defs.Item.additionalProperties).toBe(false);
419
- expect(defs.Item.required).toEqual(expect.arrayContaining(['id', 'name']));
420
- });
421
- });
422
-
423
- describe('nesting depth warnings', () => {
424
- it('warns when nesting exceeds OpenAI limit of 5', () => {
425
- // Build a deeply nested schema
426
- let innermost: Record<string, unknown> = { type: 'string' };
427
- for (let i = 0; i < 7; i++) {
428
- innermost = {
429
- type: 'object',
430
- properties: { nested: innermost },
431
- additionalProperties: false,
432
- required: ['nested'],
433
- };
434
- }
435
-
436
- const { warnings } = prepareSchemaForProvider(
437
- innermost as Record<string, unknown>,
438
- Providers.OPENAI,
439
- );
440
-
441
- expect(warnings.some(w => w.includes('nesting depth') && w.includes('exceeds'))).toBe(true);
442
- });
443
-
444
- it('does not warn about nesting depth for Anthropic', () => {
445
- let innermost: Record<string, unknown> = { type: 'string' };
446
- for (let i = 0; i < 7; i++) {
447
- innermost = {
448
- type: 'object',
449
- properties: { nested: innermost },
450
- additionalProperties: false,
451
- required: ['nested'],
452
- };
453
- }
454
-
455
- const { warnings } = prepareSchemaForProvider(
456
- innermost as Record<string, unknown>,
457
- Providers.ANTHROPIC,
458
- );
459
-
460
- expect(warnings.some(w => w.includes('nesting depth'))).toBe(false);
461
- });
462
- });
463
-
464
- describe('provider-specific behavior', () => {
465
- it('works correctly for Bedrock', () => {
466
- const schema = {
467
- type: 'object',
468
- properties: {
469
- name: { type: 'string', maxLength: 50 },
470
- score: { type: 'number', minimum: 0 },
471
- },
472
- };
473
-
474
- const { schema: result } = prepareSchemaForProvider(
475
- schema,
476
- Providers.BEDROCK,
477
- );
478
-
479
- expect(result.additionalProperties).toBe(false);
480
- expect(result.required).toEqual(expect.arrayContaining(['name', 'score']));
481
-
482
- const nameProp = (result.properties as Record<string, Record<string, unknown>>).name;
483
- expect(nameProp.maxLength).toBeUndefined();
484
- });
485
-
486
- it('does not mutate the original schema', () => {
487
- const schema = {
488
- type: 'object',
489
- properties: {
490
- name: { type: 'string', minLength: 1 },
491
- },
492
- };
493
-
494
- const original = JSON.parse(JSON.stringify(schema));
495
- prepareSchemaForProvider(schema, Providers.OPENAI);
496
-
497
- expect(schema).toEqual(original);
498
- });
499
- });
500
- });
1
+ // src/schemas/schema-preparation.test.ts
2
+ import { prepareSchemaForProvider } from './validate';
3
+ import { Providers } from '@/common';
4
+
5
+ describe('prepareSchemaForProvider', () => {
6
+ describe('basic passthrough', () => {
7
+ it('returns schema unchanged when strict is false', () => {
8
+ const schema = {
9
+ type: 'object',
10
+ properties: {
11
+ name: { type: 'string' },
12
+ },
13
+ };
14
+
15
+ const { schema: result, warnings } = prepareSchemaForProvider(
16
+ schema,
17
+ Providers.OPENAI,
18
+ false,
19
+ );
20
+
21
+ expect(result).toEqual(schema);
22
+ expect(warnings).toEqual([]);
23
+ });
24
+
25
+ it('returns schema with additionalProperties already set', () => {
26
+ const schema = {
27
+ type: 'object',
28
+ properties: {
29
+ name: { type: 'string' },
30
+ },
31
+ required: ['name'],
32
+ additionalProperties: false,
33
+ };
34
+
35
+ const { schema: result } = prepareSchemaForProvider(
36
+ schema,
37
+ Providers.OPENAI,
38
+ );
39
+
40
+ expect(result.additionalProperties).toBe(false);
41
+ });
42
+ });
43
+
44
+ describe('additionalProperties enforcement', () => {
45
+ it('adds additionalProperties: false to root object', () => {
46
+ const schema = {
47
+ type: 'object',
48
+ properties: {
49
+ name: { type: 'string' },
50
+ },
51
+ };
52
+
53
+ const { schema: result, warnings } = prepareSchemaForProvider(
54
+ schema,
55
+ Providers.ANTHROPIC,
56
+ );
57
+
58
+ expect(result.additionalProperties).toBe(false);
59
+ // Root object doesn't produce a warning for additionalProperties
60
+ });
61
+
62
+ it('adds additionalProperties: false to nested objects', () => {
63
+ const schema = {
64
+ type: 'object',
65
+ properties: {
66
+ address: {
67
+ type: 'object',
68
+ properties: {
69
+ street: { type: 'string' },
70
+ city: { type: 'string' },
71
+ },
72
+ },
73
+ },
74
+ additionalProperties: false,
75
+ };
76
+
77
+ const { schema: result, warnings } = prepareSchemaForProvider(
78
+ schema,
79
+ Providers.OPENAI,
80
+ );
81
+
82
+ const addressProp = (result.properties as Record<string, Record<string, unknown>>).address;
83
+ expect(addressProp.additionalProperties).toBe(false);
84
+ expect(warnings.some(w => w.includes('additionalProperties'))).toBe(true);
85
+ });
86
+
87
+ it('adds additionalProperties: false to deeply nested objects', () => {
88
+ const schema = {
89
+ type: 'object',
90
+ properties: {
91
+ level1: {
92
+ type: 'object',
93
+ properties: {
94
+ level2: {
95
+ type: 'object',
96
+ properties: {
97
+ value: { type: 'string' },
98
+ },
99
+ },
100
+ },
101
+ },
102
+ },
103
+ additionalProperties: false,
104
+ };
105
+
106
+ const { schema: result } = prepareSchemaForProvider(
107
+ schema,
108
+ Providers.ANTHROPIC,
109
+ );
110
+
111
+ const level1 = (result.properties as Record<string, Record<string, unknown>>).level1;
112
+ expect(level1.additionalProperties).toBe(false);
113
+ const level2 = (level1.properties as Record<string, Record<string, unknown>>).level2;
114
+ expect(level2.additionalProperties).toBe(false);
115
+ });
116
+ });
117
+
118
+ describe('required properties enforcement', () => {
119
+ it('adds all properties to required when missing', () => {
120
+ const schema = {
121
+ type: 'object',
122
+ properties: {
123
+ name: { type: 'string' },
124
+ age: { type: 'number' },
125
+ },
126
+ };
127
+
128
+ const { schema: result, warnings } = prepareSchemaForProvider(
129
+ schema,
130
+ Providers.OPENAI,
131
+ );
132
+
133
+ expect(result.required).toEqual(expect.arrayContaining(['name', 'age']));
134
+ expect(warnings.some(w => w.includes('required'))).toBe(true);
135
+ });
136
+
137
+ it('adds missing properties to existing required array', () => {
138
+ const schema = {
139
+ type: 'object',
140
+ properties: {
141
+ name: { type: 'string' },
142
+ age: { type: 'number' },
143
+ email: { type: 'string' },
144
+ },
145
+ required: ['name'],
146
+ };
147
+
148
+ const { schema: result } = prepareSchemaForProvider(
149
+ schema,
150
+ Providers.ANTHROPIC,
151
+ );
152
+
153
+ expect(result.required).toEqual(expect.arrayContaining(['name', 'age', 'email']));
154
+ });
155
+
156
+ it('does not duplicate already-required properties', () => {
157
+ const schema = {
158
+ type: 'object',
159
+ properties: {
160
+ name: { type: 'string' },
161
+ age: { type: 'number' },
162
+ },
163
+ required: ['name', 'age'],
164
+ };
165
+
166
+ const { schema: result } = prepareSchemaForProvider(
167
+ schema,
168
+ Providers.OPENAI,
169
+ );
170
+
171
+ expect(result.required).toEqual(['name', 'age']);
172
+ });
173
+
174
+ it('adds required to nested objects', () => {
175
+ const schema = {
176
+ type: 'object',
177
+ properties: {
178
+ person: {
179
+ type: 'object',
180
+ properties: {
181
+ name: { type: 'string' },
182
+ age: { type: 'number' },
183
+ },
184
+ },
185
+ },
186
+ additionalProperties: false,
187
+ };
188
+
189
+ const { schema: result } = prepareSchemaForProvider(
190
+ schema,
191
+ Providers.OPENAI,
192
+ );
193
+
194
+ const person = (result.properties as Record<string, Record<string, unknown>>).person;
195
+ expect(person.required).toEqual(expect.arrayContaining(['name', 'age']));
196
+ });
197
+ });
198
+
199
+ describe('numeric constraint stripping', () => {
200
+ it('strips minimum/maximum from number properties', () => {
201
+ const schema = {
202
+ type: 'object',
203
+ properties: {
204
+ age: { type: 'number', minimum: 0, maximum: 150 },
205
+ },
206
+ required: ['age'],
207
+ additionalProperties: false,
208
+ };
209
+
210
+ const { schema: result, warnings } = prepareSchemaForProvider(
211
+ schema,
212
+ Providers.ANTHROPIC,
213
+ );
214
+
215
+ const ageProp = (result.properties as Record<string, Record<string, unknown>>).age;
216
+ expect(ageProp.minimum).toBeUndefined();
217
+ expect(ageProp.maximum).toBeUndefined();
218
+ expect(ageProp.description).toContain('minimum: 0');
219
+ expect(ageProp.description).toContain('maximum: 150');
220
+ expect(warnings.some(w => w.includes('minimum'))).toBe(true);
221
+ expect(warnings.some(w => w.includes('maximum'))).toBe(true);
222
+ });
223
+
224
+ it('strips multipleOf from integer properties', () => {
225
+ const schema = {
226
+ type: 'object',
227
+ properties: {
228
+ count: { type: 'integer', multipleOf: 5 },
229
+ },
230
+ required: ['count'],
231
+ additionalProperties: false,
232
+ };
233
+
234
+ const { schema: result, warnings } = prepareSchemaForProvider(
235
+ schema,
236
+ Providers.OPENAI,
237
+ );
238
+
239
+ const countProp = (result.properties as Record<string, Record<string, unknown>>).count;
240
+ expect(countProp.multipleOf).toBeUndefined();
241
+ expect(countProp.description).toContain('multipleOf: 5');
242
+ expect(warnings.some(w => w.includes('multipleOf'))).toBe(true);
243
+ });
244
+
245
+ it('appends to existing description when stripping constraints', () => {
246
+ const schema = {
247
+ type: 'object',
248
+ properties: {
249
+ score: { type: 'number', description: 'User score', minimum: 0, maximum: 100 },
250
+ },
251
+ required: ['score'],
252
+ additionalProperties: false,
253
+ };
254
+
255
+ const { schema: result } = prepareSchemaForProvider(
256
+ schema,
257
+ Providers.ANTHROPIC,
258
+ );
259
+
260
+ const scoreProp = (result.properties as Record<string, Record<string, unknown>>).score;
261
+ expect(scoreProp.description).toContain('User score');
262
+ expect(scoreProp.description).toContain('minimum: 0');
263
+ });
264
+ });
265
+
266
+ describe('string constraint stripping', () => {
267
+ it('strips minLength/maxLength from string properties', () => {
268
+ const schema = {
269
+ type: 'object',
270
+ properties: {
271
+ name: { type: 'string', minLength: 1, maxLength: 100 },
272
+ },
273
+ required: ['name'],
274
+ additionalProperties: false,
275
+ };
276
+
277
+ const { schema: result, warnings } = prepareSchemaForProvider(
278
+ schema,
279
+ Providers.OPENAI,
280
+ );
281
+
282
+ const nameProp = (result.properties as Record<string, Record<string, unknown>>).name;
283
+ expect(nameProp.minLength).toBeUndefined();
284
+ expect(nameProp.maxLength).toBeUndefined();
285
+ expect(nameProp.description).toContain('minLength: 1');
286
+ expect(nameProp.description).toContain('maxLength: 100');
287
+ expect(warnings.length).toBeGreaterThan(0);
288
+ });
289
+
290
+ it('strips pattern from string properties', () => {
291
+ const schema = {
292
+ type: 'object',
293
+ properties: {
294
+ email: { type: 'string', pattern: '^[a-z]+@[a-z]+\\.[a-z]+$' },
295
+ },
296
+ required: ['email'],
297
+ additionalProperties: false,
298
+ };
299
+
300
+ const { schema: result } = prepareSchemaForProvider(
301
+ schema,
302
+ Providers.ANTHROPIC,
303
+ );
304
+
305
+ const emailProp = (result.properties as Record<string, Record<string, unknown>>).email;
306
+ expect(emailProp.pattern).toBeUndefined();
307
+ expect(emailProp.description).toContain('pattern:');
308
+ });
309
+ });
310
+
311
+ describe('array constraint stripping', () => {
312
+ it('strips minItems/maxItems from array properties', () => {
313
+ const schema = {
314
+ type: 'object',
315
+ properties: {
316
+ tags: { type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 10 },
317
+ },
318
+ required: ['tags'],
319
+ additionalProperties: false,
320
+ };
321
+
322
+ const { schema: result, warnings } = prepareSchemaForProvider(
323
+ schema,
324
+ Providers.OPENAI,
325
+ );
326
+
327
+ const tagsProp = (result.properties as Record<string, Record<string, unknown>>).tags;
328
+ expect(tagsProp.minItems).toBeUndefined();
329
+ expect(tagsProp.maxItems).toBeUndefined();
330
+ expect(tagsProp.description).toContain('minItems: 1');
331
+ expect(warnings.some(w => w.includes('minItems'))).toBe(true);
332
+ });
333
+ });
334
+
335
+ describe('array items with object schemas', () => {
336
+ it('prepares object schemas inside array items', () => {
337
+ const schema = {
338
+ type: 'object',
339
+ properties: {
340
+ people: {
341
+ type: 'array',
342
+ items: {
343
+ type: 'object',
344
+ properties: {
345
+ name: { type: 'string' },
346
+ age: { type: 'number' },
347
+ },
348
+ },
349
+ },
350
+ },
351
+ additionalProperties: false,
352
+ };
353
+
354
+ const { schema: result } = prepareSchemaForProvider(
355
+ schema,
356
+ Providers.OPENAI,
357
+ );
358
+
359
+ const peopleProp = (result.properties as Record<string, Record<string, unknown>>).people;
360
+ const items = peopleProp.items as Record<string, unknown>;
361
+ expect(items.additionalProperties).toBe(false);
362
+ expect(items.required).toEqual(expect.arrayContaining(['name', 'age']));
363
+ });
364
+ });
365
+
366
+ describe('anyOf handling', () => {
367
+ it('prepares object schemas within anyOf', () => {
368
+ const schema = {
369
+ type: 'object',
370
+ properties: {
371
+ result: {
372
+ anyOf: [
373
+ { type: 'object', properties: { value: { type: 'string' } } },
374
+ { type: 'null' },
375
+ ],
376
+ },
377
+ },
378
+ additionalProperties: false,
379
+ };
380
+
381
+ const { schema: result } = prepareSchemaForProvider(
382
+ schema,
383
+ Providers.ANTHROPIC,
384
+ );
385
+
386
+ const resultProp = (result.properties as Record<string, Record<string, unknown>>).result;
387
+ const anyOf = resultProp.anyOf as Record<string, unknown>[];
388
+ const objectVariant = anyOf.find(v => v.type === 'object');
389
+ expect(objectVariant?.additionalProperties).toBe(false);
390
+ });
391
+ });
392
+
393
+ describe('$defs handling', () => {
394
+ it('prepares schemas in $defs', () => {
395
+ const schema = {
396
+ type: 'object',
397
+ properties: {
398
+ item: { $ref: '#/$defs/Item' },
399
+ },
400
+ $defs: {
401
+ Item: {
402
+ type: 'object',
403
+ properties: {
404
+ id: { type: 'string' },
405
+ name: { type: 'string' },
406
+ },
407
+ },
408
+ },
409
+ additionalProperties: false,
410
+ };
411
+
412
+ const { schema: result } = prepareSchemaForProvider(
413
+ schema,
414
+ Providers.OPENAI,
415
+ );
416
+
417
+ const defs = result.$defs as Record<string, Record<string, unknown>>;
418
+ expect(defs.Item.additionalProperties).toBe(false);
419
+ expect(defs.Item.required).toEqual(expect.arrayContaining(['id', 'name']));
420
+ });
421
+ });
422
+
423
+ describe('nesting depth warnings', () => {
424
+ it('warns when nesting exceeds OpenAI limit of 5', () => {
425
+ // Build a deeply nested schema
426
+ let innermost: Record<string, unknown> = { type: 'string' };
427
+ for (let i = 0; i < 7; i++) {
428
+ innermost = {
429
+ type: 'object',
430
+ properties: { nested: innermost },
431
+ additionalProperties: false,
432
+ required: ['nested'],
433
+ };
434
+ }
435
+
436
+ const { warnings } = prepareSchemaForProvider(
437
+ innermost as Record<string, unknown>,
438
+ Providers.OPENAI,
439
+ );
440
+
441
+ expect(warnings.some(w => w.includes('nesting depth') && w.includes('exceeds'))).toBe(true);
442
+ });
443
+
444
+ it('does not warn about nesting depth for Anthropic', () => {
445
+ let innermost: Record<string, unknown> = { type: 'string' };
446
+ for (let i = 0; i < 7; i++) {
447
+ innermost = {
448
+ type: 'object',
449
+ properties: { nested: innermost },
450
+ additionalProperties: false,
451
+ required: ['nested'],
452
+ };
453
+ }
454
+
455
+ const { warnings } = prepareSchemaForProvider(
456
+ innermost as Record<string, unknown>,
457
+ Providers.ANTHROPIC,
458
+ );
459
+
460
+ expect(warnings.some(w => w.includes('nesting depth'))).toBe(false);
461
+ });
462
+ });
463
+
464
+ describe('provider-specific behavior', () => {
465
+ it('works correctly for Bedrock', () => {
466
+ const schema = {
467
+ type: 'object',
468
+ properties: {
469
+ name: { type: 'string', maxLength: 50 },
470
+ score: { type: 'number', minimum: 0 },
471
+ },
472
+ };
473
+
474
+ const { schema: result } = prepareSchemaForProvider(
475
+ schema,
476
+ Providers.BEDROCK,
477
+ );
478
+
479
+ expect(result.additionalProperties).toBe(false);
480
+ expect(result.required).toEqual(expect.arrayContaining(['name', 'score']));
481
+
482
+ const nameProp = (result.properties as Record<string, Record<string, unknown>>).name;
483
+ expect(nameProp.maxLength).toBeUndefined();
484
+ });
485
+
486
+ it('does not mutate the original schema', () => {
487
+ const schema = {
488
+ type: 'object',
489
+ properties: {
490
+ name: { type: 'string', minLength: 1 },
491
+ },
492
+ };
493
+
494
+ const original = JSON.parse(JSON.stringify(schema));
495
+ prepareSchemaForProvider(schema, Providers.OPENAI);
496
+
497
+ expect(schema).toEqual(original);
498
+ });
499
+ });
500
+ });