librechat-data-provider 0.7.41 → 0.7.52

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,4 +1,5 @@
1
1
  import axios from 'axios';
2
+ import { z } from 'zod';
2
3
  import { OpenAPIV3 } from 'openapi-types';
3
4
  import {
4
5
  createURL,
@@ -8,7 +9,12 @@ import {
8
9
  FunctionSignature,
9
10
  validateAndParseOpenAPISpec,
10
11
  } from '../src/actions';
11
- import { getWeatherOpenapiSpec, whimsicalOpenapiSpec, scholarAIOpenapiSpec } from './openapiSpecs';
12
+ import {
13
+ getWeatherOpenapiSpec,
14
+ whimsicalOpenapiSpec,
15
+ scholarAIOpenapiSpec,
16
+ swapidev,
17
+ } from './openapiSpecs';
12
18
  import { AuthorizationTypeEnum, AuthTypeEnum } from '../src/types/assistants';
13
19
  import type { FlowchartSchema } from './openapiSpecs';
14
20
  import type { ParametersSchema } from '../src/actions';
@@ -59,7 +65,7 @@ describe('ActionRequest', () => {
59
65
  false,
60
66
  'application/json',
61
67
  );
62
- await actionRequest.setParams({ param1: 'value1' });
68
+ actionRequest.setParams({ param1: 'value1' });
63
69
  const response = await actionRequest.execute();
64
70
  expect(mockedAxios.get).toHaveBeenCalledWith('https://example.com/test', expect.anything());
65
71
  expect(response.data).toEqual({ success: true, method: 'GET' });
@@ -84,7 +90,7 @@ describe('ActionRequest', () => {
84
90
  false,
85
91
  'application/json',
86
92
  );
87
- await actionRequest.setParams({ param: 'test' });
93
+ actionRequest.setParams({ param: 'test' });
88
94
  const response = await actionRequest.execute();
89
95
  expect(mockedAxios.get).toHaveBeenCalled();
90
96
  expect(response.data.success).toBe(true);
@@ -100,7 +106,7 @@ describe('ActionRequest', () => {
100
106
  false,
101
107
  'application/json',
102
108
  );
103
- await actionRequest.setParams({ param: 'test' });
109
+ actionRequest.setParams({ param: 'test' });
104
110
  const response = await actionRequest.execute();
105
111
  expect(mockedAxios.post).toHaveBeenCalled();
106
112
  expect(response.data.success).toBe(true);
@@ -116,7 +122,7 @@ describe('ActionRequest', () => {
116
122
  false,
117
123
  'application/json',
118
124
  );
119
- await actionRequest.setParams({ param: 'test' });
125
+ actionRequest.setParams({ param: 'test' });
120
126
  const response = await actionRequest.execute();
121
127
  expect(mockedAxios.put).toHaveBeenCalled();
122
128
  expect(response.data.success).toBe(true);
@@ -132,7 +138,7 @@ describe('ActionRequest', () => {
132
138
  false,
133
139
  'application/json',
134
140
  );
135
- await actionRequest.setParams({ param: 'test' });
141
+ actionRequest.setParams({ param: 'test' });
136
142
  const response = await actionRequest.execute();
137
143
  expect(mockedAxios.delete).toHaveBeenCalled();
138
144
  expect(response.data.success).toBe(true);
@@ -148,7 +154,7 @@ describe('ActionRequest', () => {
148
154
  false,
149
155
  'application/json',
150
156
  );
151
- await actionRequest.setParams({ param: 'test' });
157
+ actionRequest.setParams({ param: 'test' });
152
158
  const response = await actionRequest.execute();
153
159
  expect(mockedAxios.patch).toHaveBeenCalled();
154
160
  expect(response.data.success).toBe(true);
@@ -163,7 +169,7 @@ describe('ActionRequest', () => {
163
169
  false,
164
170
  'application/json',
165
171
  );
166
- await expect(actionRequest.execute()).rejects.toThrow('Unsupported HTTP method: INVALID');
172
+ await expect(actionRequest.execute()).rejects.toThrow('Unsupported HTTP method: invalid');
167
173
  });
168
174
 
169
175
  it('replaces path parameters with values from toolInput', async () => {
@@ -176,20 +182,21 @@ describe('ActionRequest', () => {
176
182
  'application/json',
177
183
  );
178
184
 
179
- await actionRequest.setParams({
185
+ const executor = actionRequest.createExecutor();
186
+ executor.setParams({
180
187
  stocksTicker: 'AAPL',
181
188
  multiplier: 5,
182
189
  startDate: '2023-01-01',
183
190
  endDate: '2023-12-31',
184
191
  });
185
192
 
186
- expect(actionRequest.path).toBe('/stocks/AAPL/bars/5');
187
- expect(actionRequest.params).toEqual({
193
+ expect(executor.path).toBe('/stocks/AAPL/bars/5');
194
+ expect(executor.params).toEqual({
188
195
  startDate: '2023-01-01',
189
196
  endDate: '2023-12-31',
190
197
  });
191
198
 
192
- await actionRequest.execute();
199
+ await executor.execute();
193
200
  expect(mockedAxios.get).toHaveBeenCalledWith('https://example.com/stocks/AAPL/bars/5', {
194
201
  headers: expect.anything(),
195
202
  params: {
@@ -209,7 +216,271 @@ describe('ActionRequest', () => {
209
216
  false,
210
217
  'application/json',
211
218
  );
212
- await expect(actionRequest.execute()).rejects.toThrow('Unsupported HTTP method: INVALID');
219
+ await expect(actionRequest.execute()).rejects.toThrow('Unsupported HTTP method: invalid');
220
+ });
221
+
222
+ describe('ActionRequest Concurrent Execution', () => {
223
+ beforeEach(() => {
224
+ jest.clearAllMocks();
225
+ mockedAxios.get.mockImplementation(async (url, config) => ({
226
+ data: { url, params: config?.params, headers: config?.headers },
227
+ }));
228
+ });
229
+
230
+ it('maintains isolated state between concurrent executions with different parameters', async () => {
231
+ const actionRequest = new ActionRequest(
232
+ 'https://example.com',
233
+ '/math/sqrt/{number}',
234
+ 'GET',
235
+ 'getSqrt',
236
+ false,
237
+ 'application/json',
238
+ );
239
+
240
+ // Simulate concurrent requests with different numbers
241
+ const numbers = [20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30];
242
+ const requests = numbers.map((num) => ({
243
+ number: num.toString(),
244
+ precision: '2',
245
+ }));
246
+
247
+ const responses = await Promise.all(
248
+ requests.map((params) => {
249
+ const executor = actionRequest.createExecutor();
250
+ return executor.setParams(params).execute();
251
+ }),
252
+ );
253
+
254
+ // Verify each response used the correct path parameter
255
+ responses.forEach((response, index) => {
256
+ const expectedUrl = `https://example.com/math/sqrt/${numbers[index]}`;
257
+ expect(response.data.url).toBe(expectedUrl);
258
+ expect(response.data.params).toEqual({ precision: '2' });
259
+ });
260
+
261
+ // Verify the correct number of calls were made
262
+ expect(mockedAxios.get).toHaveBeenCalledTimes(numbers.length);
263
+ });
264
+
265
+ it('maintains isolated authentication state between concurrent executions', async () => {
266
+ const actionRequest = new ActionRequest(
267
+ 'https://example.com',
268
+ '/secure/resource/{id}',
269
+ 'GET',
270
+ 'getResource',
271
+ false,
272
+ 'application/json',
273
+ );
274
+
275
+ const requests = [
276
+ {
277
+ params: { id: '1' },
278
+ auth: {
279
+ auth: {
280
+ type: AuthTypeEnum.ServiceHttp,
281
+ authorization_type: AuthorizationTypeEnum.Bearer,
282
+ },
283
+ api_key: 'token1',
284
+ },
285
+ },
286
+ {
287
+ params: { id: '2' },
288
+ auth: {
289
+ auth: {
290
+ type: AuthTypeEnum.ServiceHttp,
291
+ authorization_type: AuthorizationTypeEnum.Bearer,
292
+ },
293
+ api_key: 'token2',
294
+ },
295
+ },
296
+ ];
297
+
298
+ const responses = await Promise.all(
299
+ requests.map(async ({ params, auth }) => {
300
+ const executor = actionRequest.createExecutor();
301
+ return (await executor.setParams(params).setAuth(auth)).execute();
302
+ }),
303
+ );
304
+
305
+ // Verify each response had its own auth token
306
+ responses.forEach((response, index) => {
307
+ const expectedUrl = `https://example.com/secure/resource/${index + 1}`;
308
+ expect(response.data.url).toBe(expectedUrl);
309
+ expect(response.data.headers).toMatchObject({
310
+ Authorization: `Bearer token${index + 1}`,
311
+ });
312
+ });
313
+ });
314
+
315
+ it('handles mixed authentication types concurrently', async () => {
316
+ const actionRequest = new ActionRequest(
317
+ 'https://example.com',
318
+ '/api/{version}/data',
319
+ 'GET',
320
+ 'getData',
321
+ false,
322
+ 'application/json',
323
+ );
324
+
325
+ const requests = [
326
+ {
327
+ params: { version: 'v1' },
328
+ auth: {
329
+ auth: {
330
+ type: AuthTypeEnum.ServiceHttp,
331
+ authorization_type: AuthorizationTypeEnum.Bearer,
332
+ },
333
+ api_key: 'bearer_token',
334
+ },
335
+ },
336
+ {
337
+ params: { version: 'v2' },
338
+ auth: {
339
+ auth: {
340
+ type: AuthTypeEnum.ServiceHttp,
341
+ authorization_type: AuthorizationTypeEnum.Basic,
342
+ },
343
+ api_key: 'basic:auth',
344
+ },
345
+ },
346
+ {
347
+ params: { version: 'v3' },
348
+ auth: {
349
+ auth: {
350
+ type: AuthTypeEnum.ServiceHttp,
351
+ authorization_type: AuthorizationTypeEnum.Custom,
352
+ custom_auth_header: 'X-API-Key',
353
+ },
354
+ api_key: 'custom_key',
355
+ },
356
+ },
357
+ ];
358
+
359
+ const responses = await Promise.all(
360
+ requests.map(async ({ params, auth }) => {
361
+ const executor = actionRequest.createExecutor();
362
+ return (await executor.setParams(params).setAuth(auth)).execute();
363
+ }),
364
+ );
365
+
366
+ // Verify each response had the correct auth type and headers
367
+ expect(responses[0].data.headers).toMatchObject({
368
+ Authorization: 'Bearer bearer_token',
369
+ });
370
+
371
+ expect(responses[1].data.headers).toMatchObject({
372
+ Authorization: `Basic ${Buffer.from('basic:auth').toString('base64')}`,
373
+ });
374
+
375
+ expect(responses[2].data.headers).toMatchObject({
376
+ 'X-API-Key': 'custom_key',
377
+ });
378
+ });
379
+
380
+ it('maintains parameter integrity during concurrent path parameter replacement', async () => {
381
+ const actionRequest = new ActionRequest(
382
+ 'https://example.com',
383
+ '/users/{userId}/posts/{postId}',
384
+ 'GET',
385
+ 'getUserPost',
386
+ false,
387
+ 'application/json',
388
+ );
389
+
390
+ const requests = [
391
+ { userId: '1', postId: 'a', filter: 'recent' },
392
+ { userId: '2', postId: 'b', filter: 'popular' },
393
+ { userId: '3', postId: 'c', filter: 'trending' },
394
+ ];
395
+
396
+ const responses = await Promise.all(
397
+ requests.map((params) => {
398
+ const executor = actionRequest.createExecutor();
399
+ return executor.setParams(params).execute();
400
+ }),
401
+ );
402
+
403
+ responses.forEach((response, index) => {
404
+ const expectedUrl = `https://example.com/users/${requests[index].userId}/posts/${requests[index].postId}`;
405
+ expect(response.data.url).toBe(expectedUrl);
406
+ expect(response.data.params).toEqual({ filter: requests[index].filter });
407
+ });
408
+ });
409
+
410
+ it('preserves original ActionRequest state after multiple executions', async () => {
411
+ const actionRequest = new ActionRequest(
412
+ 'https://example.com',
413
+ '/original/{param}',
414
+ 'GET',
415
+ 'testOp',
416
+ false,
417
+ 'application/json',
418
+ );
419
+
420
+ // Store original values
421
+ const originalPath = actionRequest.path;
422
+ const originalDomain = actionRequest.domain;
423
+ const originalMethod = actionRequest.method;
424
+
425
+ // Perform multiple concurrent executions
426
+ await Promise.all([
427
+ actionRequest.createExecutor().setParams({ param: '1' }).execute(),
428
+ actionRequest.createExecutor().setParams({ param: '2' }).execute(),
429
+ actionRequest.createExecutor().setParams({ param: '3' }).execute(),
430
+ ]);
431
+
432
+ // Verify original ActionRequest remains unchanged
433
+ expect(actionRequest.path).toBe(originalPath);
434
+ expect(actionRequest.domain).toBe(originalDomain);
435
+ expect(actionRequest.method).toBe(originalMethod);
436
+ });
437
+
438
+ it('shares immutable configuration between executors from the same ActionRequest', () => {
439
+ const actionRequest = new ActionRequest(
440
+ 'https://example.com',
441
+ '/api/{version}/data',
442
+ 'GET',
443
+ 'getData',
444
+ false,
445
+ 'application/json',
446
+ );
447
+
448
+ // Create multiple executors
449
+ const executor1 = actionRequest.createExecutor();
450
+ const executor2 = actionRequest.createExecutor();
451
+ const executor3 = actionRequest.createExecutor();
452
+
453
+ // Test that the configuration properties are shared
454
+ [executor1, executor2, executor3].forEach((executor) => {
455
+ expect(executor.getConfig()).toBeDefined();
456
+ expect(executor.getConfig()).toEqual({
457
+ domain: 'https://example.com',
458
+ basePath: '/api/{version}/data',
459
+ method: 'GET',
460
+ operation: 'getData',
461
+ isConsequential: false,
462
+ contentType: 'application/json',
463
+ });
464
+ });
465
+
466
+ // Verify that config objects are the exact same instance (shared reference)
467
+ expect(executor1.getConfig()).toBe(executor2.getConfig());
468
+ expect(executor2.getConfig()).toBe(executor3.getConfig());
469
+
470
+ // Verify that modifying mutable state doesn't affect other executors
471
+ executor1.setParams({ version: 'v1' });
472
+ executor2.setParams({ version: 'v2' });
473
+ executor3.setParams({ version: 'v3' });
474
+
475
+ expect(executor1.path).toBe('/api/v1/data');
476
+ expect(executor2.path).toBe('/api/v2/data');
477
+ expect(executor3.path).toBe('/api/v3/data');
478
+
479
+ // Verify that the original config remains unchanged
480
+ expect(executor1.getConfig().basePath).toBe('/api/{version}/data');
481
+ expect(executor2.getConfig().basePath).toBe('/api/{version}/data');
482
+ expect(executor3.getConfig().basePath).toBe('/api/{version}/data');
483
+ });
213
484
  });
214
485
  });
215
486
 
@@ -227,7 +498,8 @@ describe('Authentication Handling', () => {
227
498
  const api_key = 'user:pass';
228
499
  const encodedCredentials = Buffer.from('user:pass').toString('base64');
229
500
 
230
- actionRequest.setAuth({
501
+ const executor = actionRequest.createExecutor();
502
+ await executor.setParams({ param1: 'value1' }).setAuth({
231
503
  auth: {
232
504
  type: AuthTypeEnum.ServiceHttp,
233
505
  authorization_type: AuthorizationTypeEnum.Basic,
@@ -235,13 +507,13 @@ describe('Authentication Handling', () => {
235
507
  api_key,
236
508
  });
237
509
 
238
- await actionRequest.setParams({ param1: 'value1' });
239
- await actionRequest.execute();
510
+ await executor.execute();
240
511
  expect(mockedAxios.get).toHaveBeenCalledWith('https://example.com/test', {
241
512
  headers: expect.objectContaining({
242
513
  Authorization: `Basic ${encodedCredentials}`,
514
+ 'Content-Type': 'application/json',
243
515
  }),
244
- params: expect.anything(),
516
+ params: { param1: 'value1' },
245
517
  });
246
518
  });
247
519
 
@@ -254,20 +526,23 @@ describe('Authentication Handling', () => {
254
526
  false,
255
527
  'application/json',
256
528
  );
257
- actionRequest.setAuth({
529
+
530
+ const executor = actionRequest.createExecutor();
531
+ await executor.setParams({ param1: 'value1' }).setAuth({
258
532
  auth: {
259
533
  type: AuthTypeEnum.ServiceHttp,
260
534
  authorization_type: AuthorizationTypeEnum.Bearer,
261
535
  },
262
536
  api_key: 'token123',
263
537
  });
264
- await actionRequest.setParams({ param1: 'value1' });
265
- await actionRequest.execute();
538
+
539
+ await executor.execute();
266
540
  expect(mockedAxios.get).toHaveBeenCalledWith('https://example.com/test', {
267
541
  headers: expect.objectContaining({
268
542
  Authorization: 'Bearer token123',
543
+ 'Content-Type': 'application/json',
269
544
  }),
270
- params: expect.anything(),
545
+ params: { param1: 'value1' },
271
546
  });
272
547
  });
273
548
 
@@ -280,22 +555,24 @@ describe('Authentication Handling', () => {
280
555
  false,
281
556
  'application/json',
282
557
  );
283
- // Updated to match ActionMetadata structure
284
- actionRequest.setAuth({
558
+
559
+ const executor = actionRequest.createExecutor();
560
+ await executor.setParams({ param1: 'value1' }).setAuth({
285
561
  auth: {
286
- type: AuthTypeEnum.ServiceHttp, // Assuming this is a valid enum or value for your context
287
- authorization_type: AuthorizationTypeEnum.Custom, // Assuming Custom means using a custom header
562
+ type: AuthTypeEnum.ServiceHttp,
563
+ authorization_type: AuthorizationTypeEnum.Custom,
288
564
  custom_auth_header: 'X-API-KEY',
289
565
  },
290
566
  api_key: 'abc123',
291
567
  });
292
- await actionRequest.setParams({ param1: 'value1' });
293
- await actionRequest.execute();
568
+
569
+ await executor.execute();
294
570
  expect(mockedAxios.get).toHaveBeenCalledWith('https://example.com/test', {
295
571
  headers: expect.objectContaining({
296
572
  'X-API-KEY': 'abc123',
573
+ 'Content-Type': 'application/json',
297
574
  }),
298
- params: expect.anything(),
575
+ params: { param1: 'value1' },
299
576
  });
300
577
  });
301
578
  });
@@ -306,7 +583,7 @@ describe('resolveRef', () => {
306
583
  const flowchartRequestRef = (
307
584
  openapiSpec.paths['/ai.chatgpt.render-flowchart']?.post
308
585
  ?.requestBody as OpenAPIV3.RequestBodyObject
309
- )?.content['application/json'].schema;
586
+ ).content['application/json'].schema;
310
587
  expect(flowchartRequestRef).toBeDefined();
311
588
  const resolvedFlowchartRequest = resolveRef(
312
589
  flowchartRequestRef as OpenAPIV3.RequestBodyObject,
@@ -548,4 +825,273 @@ describe('createURL', () => {
548
825
  'https://example.com/subdirectory/api/v1/users',
549
826
  );
550
827
  });
828
+
829
+ describe('openapiToFunction zodSchemas', () => {
830
+ describe('getWeatherOpenapiSpec', () => {
831
+ const { zodSchemas } = openapiToFunction(getWeatherOpenapiSpec, true);
832
+
833
+ it('generates correct Zod schema for GetCurrentWeather', () => {
834
+ expect(zodSchemas).toBeDefined();
835
+ expect(zodSchemas?.GetCurrentWeather).toBeDefined();
836
+
837
+ const GetCurrentWeatherSchema = zodSchemas?.GetCurrentWeather;
838
+
839
+ expect(GetCurrentWeatherSchema instanceof z.ZodObject).toBe(true);
840
+
841
+ if (!(GetCurrentWeatherSchema instanceof z.ZodObject)) {
842
+ throw new Error('GetCurrentWeatherSchema is not a ZodObject');
843
+ }
844
+
845
+ const shape = GetCurrentWeatherSchema.shape;
846
+ expect(shape.location instanceof z.ZodString).toBe(true);
847
+
848
+ // Check locations property
849
+ expect(shape.locations).toBeDefined();
850
+ expect(shape.locations instanceof z.ZodOptional).toBe(true);
851
+
852
+ if (!(shape.locations instanceof z.ZodOptional)) {
853
+ throw new Error('locations is not a ZodOptional');
854
+ }
855
+
856
+ const locationsInnerType = shape.locations._def.innerType;
857
+ expect(locationsInnerType instanceof z.ZodArray).toBe(true);
858
+
859
+ if (!(locationsInnerType instanceof z.ZodArray)) {
860
+ throw new Error('locationsInnerType is not a ZodArray');
861
+ }
862
+
863
+ const locationsItemSchema = locationsInnerType.element;
864
+ expect(locationsItemSchema instanceof z.ZodObject).toBe(true);
865
+
866
+ if (!(locationsItemSchema instanceof z.ZodObject)) {
867
+ throw new Error('locationsItemSchema is not a ZodObject');
868
+ }
869
+
870
+ // Validate the structure of locationsItemSchema
871
+ expect(locationsItemSchema.shape.city instanceof z.ZodString).toBe(true);
872
+ expect(locationsItemSchema.shape.state instanceof z.ZodString).toBe(true);
873
+ expect(locationsItemSchema.shape.countryCode instanceof z.ZodString).toBe(true);
874
+
875
+ // Check if time is optional
876
+ const timeSchema = locationsItemSchema.shape.time;
877
+ expect(timeSchema instanceof z.ZodOptional).toBe(true);
878
+
879
+ if (!(timeSchema instanceof z.ZodOptional)) {
880
+ throw new Error('timeSchema is not a ZodOptional');
881
+ }
882
+
883
+ expect(timeSchema._def.innerType instanceof z.ZodString).toBe(true);
884
+
885
+ // Check the description
886
+ expect(shape.locations._def.description).toBe(
887
+ 'A list of locations to retrieve the weather for.',
888
+ );
889
+ });
890
+
891
+ it('validates correct data for GetCurrentWeather', () => {
892
+ const GetCurrentWeatherSchema = zodSchemas?.GetCurrentWeather as z.ZodTypeAny;
893
+ const validData = {
894
+ location: 'New York',
895
+ locations: [
896
+ { city: 'New York', state: 'NY', countryCode: 'US', time: '2023-12-04T14:00:00Z' },
897
+ ],
898
+ };
899
+ expect(() => GetCurrentWeatherSchema.parse(validData)).not.toThrow();
900
+ });
901
+
902
+ it('throws error for invalid data for GetCurrentWeather', () => {
903
+ const GetCurrentWeatherSchema = zodSchemas?.GetCurrentWeather as z.ZodTypeAny;
904
+ const invalidData = {
905
+ location: 123,
906
+ locations: [{ city: 'New York', state: 'NY', countryCode: 'US', time: 'invalid-time' }],
907
+ };
908
+ expect(() => GetCurrentWeatherSchema.parse(invalidData)).toThrow();
909
+ });
910
+ });
911
+
912
+ describe('whimsicalOpenapiSpec', () => {
913
+ const { zodSchemas } = openapiToFunction(whimsicalOpenapiSpec, true);
914
+
915
+ it('generates correct Zod schema for postRenderFlowchart', () => {
916
+ expect(zodSchemas).toBeDefined();
917
+ expect(zodSchemas?.postRenderFlowchart).toBeDefined();
918
+
919
+ const PostRenderFlowchartSchema = zodSchemas?.postRenderFlowchart;
920
+ expect(PostRenderFlowchartSchema).toBeInstanceOf(z.ZodObject);
921
+
922
+ if (!(PostRenderFlowchartSchema instanceof z.ZodObject)) {
923
+ return;
924
+ }
925
+
926
+ const shape = PostRenderFlowchartSchema.shape;
927
+ expect(shape.mermaid).toBeInstanceOf(z.ZodString);
928
+ expect(shape.title).toBeInstanceOf(z.ZodOptional);
929
+ expect((shape.title as z.ZodOptional<z.ZodString>)._def.innerType).toBeInstanceOf(
930
+ z.ZodString,
931
+ );
932
+ });
933
+
934
+ it('validates correct data for postRenderFlowchart', () => {
935
+ const PostRenderFlowchartSchema = zodSchemas?.postRenderFlowchart;
936
+ const validData = {
937
+ mermaid: 'graph TD; A-->B; B-->C; C-->D;',
938
+ title: 'Test Flowchart',
939
+ };
940
+ expect(() => PostRenderFlowchartSchema?.parse(validData)).not.toThrow();
941
+ });
942
+
943
+ it('throws error for invalid data for postRenderFlowchart', () => {
944
+ const PostRenderFlowchartSchema = zodSchemas?.postRenderFlowchart;
945
+ const invalidData = {
946
+ mermaid: 123,
947
+ title: 42,
948
+ };
949
+ expect(() => PostRenderFlowchartSchema?.parse(invalidData)).toThrow();
950
+ });
951
+ });
952
+
953
+ describe('scholarAIOpenapiSpec', () => {
954
+ const result = validateAndParseOpenAPISpec(scholarAIOpenapiSpec);
955
+ const spec = result.spec as OpenAPIV3.Document;
956
+ const { zodSchemas } = openapiToFunction(spec, true);
957
+
958
+ it('generates correct Zod schema for searchAbstracts', () => {
959
+ expect(zodSchemas).toBeDefined();
960
+ expect(zodSchemas?.searchAbstracts).toBeDefined();
961
+
962
+ const SearchAbstractsSchema = zodSchemas?.searchAbstracts;
963
+ expect(SearchAbstractsSchema).toBeInstanceOf(z.ZodObject);
964
+
965
+ if (!(SearchAbstractsSchema instanceof z.ZodObject)) {
966
+ return;
967
+ }
968
+
969
+ const shape = SearchAbstractsSchema.shape;
970
+ expect(shape.keywords).toBeInstanceOf(z.ZodString);
971
+ expect(shape.sort).toBeInstanceOf(z.ZodOptional);
972
+ expect(
973
+ (shape.sort as z.ZodOptional<z.ZodEnum<[string, ...string[]]>>)._def.innerType,
974
+ ).toBeInstanceOf(z.ZodEnum);
975
+ expect(shape.query).toBeInstanceOf(z.ZodString);
976
+ expect(shape.peer_reviewed_only).toBeInstanceOf(z.ZodOptional);
977
+ expect(shape.start_year).toBeInstanceOf(z.ZodOptional);
978
+ expect(shape.end_year).toBeInstanceOf(z.ZodOptional);
979
+ expect(shape.offset).toBeInstanceOf(z.ZodOptional);
980
+ });
981
+
982
+ it('validates correct data for searchAbstracts', () => {
983
+ const SearchAbstractsSchema = zodSchemas?.searchAbstracts;
984
+ const validData = {
985
+ keywords: 'machine learning',
986
+ sort: 'cited_by_count',
987
+ query: 'AI applications',
988
+ peer_reviewed_only: 'true',
989
+ start_year: '2020',
990
+ end_year: '2023',
991
+ offset: '0',
992
+ };
993
+ expect(() => SearchAbstractsSchema?.parse(validData)).not.toThrow();
994
+ });
995
+
996
+ it('throws error for invalid data for searchAbstracts', () => {
997
+ const SearchAbstractsSchema = zodSchemas?.searchAbstracts;
998
+ const invalidData = {
999
+ keywords: 123,
1000
+ sort: 'invalid_sort',
1001
+ query: 42,
1002
+ peer_reviewed_only: 'maybe',
1003
+ start_year: 2020,
1004
+ end_year: 2023,
1005
+ offset: 0,
1006
+ };
1007
+ expect(() => SearchAbstractsSchema?.parse(invalidData)).toThrow();
1008
+ });
1009
+
1010
+ it('generates correct Zod schema for getFullText', () => {
1011
+ expect(zodSchemas?.getFullText).toBeDefined();
1012
+
1013
+ const GetFullTextSchema = zodSchemas?.getFullText;
1014
+ expect(GetFullTextSchema).toBeInstanceOf(z.ZodObject);
1015
+
1016
+ if (!(GetFullTextSchema instanceof z.ZodObject)) {
1017
+ return;
1018
+ }
1019
+
1020
+ const shape = GetFullTextSchema.shape;
1021
+ expect(shape.pdf_url).toBeInstanceOf(z.ZodString);
1022
+ expect(shape.chunk).toBeInstanceOf(z.ZodOptional);
1023
+ expect((shape.chunk as z.ZodOptional<z.ZodNumber>)._def.innerType).toBeInstanceOf(
1024
+ z.ZodNumber,
1025
+ );
1026
+ });
1027
+
1028
+ it('generates correct Zod schema for saveCitation', () => {
1029
+ expect(zodSchemas?.saveCitation).toBeDefined();
1030
+
1031
+ const SaveCitationSchema = zodSchemas?.saveCitation;
1032
+ expect(SaveCitationSchema).toBeInstanceOf(z.ZodObject);
1033
+
1034
+ if (!(SaveCitationSchema instanceof z.ZodObject)) {
1035
+ return;
1036
+ }
1037
+
1038
+ const shape = SaveCitationSchema.shape;
1039
+ expect(shape.doi).toBeInstanceOf(z.ZodString);
1040
+ expect(shape.zotero_user_id).toBeInstanceOf(z.ZodString);
1041
+ expect(shape.zotero_api_key).toBeInstanceOf(z.ZodString);
1042
+ });
1043
+ });
1044
+ });
1045
+
1046
+ describe('openapiToFunction zodSchemas for SWAPI', () => {
1047
+ const result = validateAndParseOpenAPISpec(swapidev);
1048
+ const spec = result.spec as OpenAPIV3.Document;
1049
+ const { zodSchemas } = openapiToFunction(spec, true);
1050
+
1051
+ describe('getPeople schema', () => {
1052
+ it('does not generate Zod schema for getPeople (no parameters)', () => {
1053
+ expect(zodSchemas).toBeDefined();
1054
+ expect(zodSchemas?.getPeople).toBeUndefined();
1055
+ });
1056
+
1057
+ it('validates correct data for getPeople', () => {
1058
+ const GetPeopleSchema = zodSchemas?.getPeople;
1059
+ expect(GetPeopleSchema).toBeUndefined();
1060
+ });
1061
+
1062
+ it('does not throw for invalid data for getPeople', () => {
1063
+ const GetPeopleSchema = zodSchemas?.getPeople;
1064
+ expect(GetPeopleSchema).toBeUndefined();
1065
+ });
1066
+ });
1067
+
1068
+ describe('getPersonById schema', () => {
1069
+ it('generates correct Zod schema for getPersonById', () => {
1070
+ expect(zodSchemas).toBeDefined();
1071
+ expect(zodSchemas?.getPersonById).toBeDefined();
1072
+
1073
+ const GetPersonByIdSchema = zodSchemas?.getPersonById;
1074
+ expect(GetPersonByIdSchema).toBeInstanceOf(z.ZodObject);
1075
+
1076
+ if (!(GetPersonByIdSchema instanceof z.ZodObject)) {
1077
+ return;
1078
+ }
1079
+
1080
+ const shape = GetPersonByIdSchema.shape;
1081
+ expect(shape.id).toBeInstanceOf(z.ZodString);
1082
+ });
1083
+
1084
+ it('validates correct data for getPersonById', () => {
1085
+ const GetPersonByIdSchema = zodSchemas?.getPersonById;
1086
+ const validData = { id: '1' };
1087
+ expect(() => GetPersonByIdSchema?.parse(validData)).not.toThrow();
1088
+ });
1089
+
1090
+ it('throws error for invalid data for getPersonById', () => {
1091
+ const GetPersonByIdSchema = zodSchemas?.getPersonById;
1092
+ const invalidData = { id: 1 }; // should be string
1093
+ expect(() => GetPersonByIdSchema?.parse(invalidData)).toThrow();
1094
+ });
1095
+ });
1096
+ });
551
1097
  });