librechat-data-provider 0.3.9 → 0.4.1
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.
- package/dist/index.es.js +1 -1
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/react-query/index.es.js +1 -1
- package/dist/react-query/index.es.js.map +1 -1
- package/dist/react-query/package.json +2 -1
- package/package.json +4 -1
- package/specs/actions.spec.ts +475 -0
- package/specs/filetypes.spec.ts +181 -0
- package/specs/openapiSpecs.ts +350 -0
- package/src/actions.ts +347 -0
- package/src/config.ts +130 -13
- package/src/createPayload.ts +1 -5
- package/src/data-service.ts +39 -2
- package/src/file-config.ts +264 -0
- package/src/index.ts +2 -0
- package/src/keys.ts +8 -1
- package/src/parsers.ts +10 -10
- package/src/react-query/index.ts +0 -1
- package/src/react-query/react-query-service.ts +16 -1
- package/src/schemas.ts +31 -14
- package/src/types/assistants.ts +255 -3
- package/src/types/files.ts +46 -13
- package/src/types/mutations.ts +90 -1
- package/src/types.ts +7 -0
- package/tsconfig.spec.json +10 -0
- package/src/react-query/assistants.ts +0 -138
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { OpenAPIV3 } from 'openapi-types';
|
|
3
|
+
import {
|
|
4
|
+
resolveRef,
|
|
5
|
+
ActionRequest,
|
|
6
|
+
openapiToFunction,
|
|
7
|
+
FunctionSignature,
|
|
8
|
+
validateAndParseOpenAPISpec,
|
|
9
|
+
} from '../src/actions';
|
|
10
|
+
import { getWeatherOpenapiSpec, whimsicalOpenapiSpec, scholarAIOpenapiSpec } from './openapiSpecs';
|
|
11
|
+
import { AuthorizationTypeEnum, AuthTypeEnum } from '../src/types/assistants';
|
|
12
|
+
import type { FlowchartSchema } from './openapiSpecs';
|
|
13
|
+
import type { ParametersSchema } from '../src/actions';
|
|
14
|
+
|
|
15
|
+
jest.mock('axios');
|
|
16
|
+
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
|
17
|
+
|
|
18
|
+
describe('FunctionSignature', () => {
|
|
19
|
+
it('creates a function signature and converts to JSON tool', () => {
|
|
20
|
+
const signature = new FunctionSignature('testFunction', 'A test function', {
|
|
21
|
+
param1: { type: 'string' },
|
|
22
|
+
} as unknown as ParametersSchema);
|
|
23
|
+
expect(signature.name).toBe('testFunction');
|
|
24
|
+
expect(signature.description).toBe('A test function');
|
|
25
|
+
expect(signature.toObjectTool()).toEqual({
|
|
26
|
+
type: 'function',
|
|
27
|
+
function: {
|
|
28
|
+
name: 'testFunction',
|
|
29
|
+
description: 'A test function',
|
|
30
|
+
parameters: {
|
|
31
|
+
param1: { type: 'string' },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('ActionRequest', () => {
|
|
39
|
+
// Mocking responses for each method
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
mockedAxios.get.mockResolvedValue({ data: { success: true, method: 'GET' } });
|
|
42
|
+
mockedAxios.post.mockResolvedValue({ data: { success: true, method: 'POST' } });
|
|
43
|
+
mockedAxios.put.mockResolvedValue({ data: { success: true, method: 'PUT' } });
|
|
44
|
+
mockedAxios.delete.mockResolvedValue({ data: { success: true, method: 'DELETE' } });
|
|
45
|
+
mockedAxios.patch.mockResolvedValue({ data: { success: true, method: 'PATCH' } });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
jest.clearAllMocks();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should make a GET request', async () => {
|
|
53
|
+
const actionRequest = new ActionRequest(
|
|
54
|
+
'https://example.com',
|
|
55
|
+
'/test',
|
|
56
|
+
'GET',
|
|
57
|
+
'testOp',
|
|
58
|
+
false,
|
|
59
|
+
'application/json',
|
|
60
|
+
);
|
|
61
|
+
await actionRequest.setParams({ param1: 'value1' });
|
|
62
|
+
const response = await actionRequest.execute();
|
|
63
|
+
expect(mockedAxios.get).toHaveBeenCalledWith('https://example.com/test', expect.anything());
|
|
64
|
+
expect(response.data).toEqual({ success: true, method: 'GET' });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('ActionRequest', () => {
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
mockedAxios.get.mockClear();
|
|
70
|
+
mockedAxios.post.mockClear();
|
|
71
|
+
mockedAxios.put.mockClear();
|
|
72
|
+
mockedAxios.delete.mockClear();
|
|
73
|
+
mockedAxios.patch.mockClear();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('handles GET requests', async () => {
|
|
77
|
+
mockedAxios.get.mockResolvedValue({ data: { success: true } });
|
|
78
|
+
const actionRequest = new ActionRequest(
|
|
79
|
+
'https://example.com',
|
|
80
|
+
'/get',
|
|
81
|
+
'GET',
|
|
82
|
+
'testGet',
|
|
83
|
+
false,
|
|
84
|
+
'application/json',
|
|
85
|
+
);
|
|
86
|
+
await actionRequest.setParams({ param: 'test' });
|
|
87
|
+
const response = await actionRequest.execute();
|
|
88
|
+
expect(mockedAxios.get).toHaveBeenCalled();
|
|
89
|
+
expect(response.data.success).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('handles POST requests', async () => {
|
|
93
|
+
mockedAxios.post.mockResolvedValue({ data: { success: true } });
|
|
94
|
+
const actionRequest = new ActionRequest(
|
|
95
|
+
'https://example.com',
|
|
96
|
+
'/post',
|
|
97
|
+
'POST',
|
|
98
|
+
'testPost',
|
|
99
|
+
false,
|
|
100
|
+
'application/json',
|
|
101
|
+
);
|
|
102
|
+
await actionRequest.setParams({ param: 'test' });
|
|
103
|
+
const response = await actionRequest.execute();
|
|
104
|
+
expect(mockedAxios.post).toHaveBeenCalled();
|
|
105
|
+
expect(response.data.success).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('handles PUT requests', async () => {
|
|
109
|
+
mockedAxios.put.mockResolvedValue({ data: { success: true } });
|
|
110
|
+
const actionRequest = new ActionRequest(
|
|
111
|
+
'https://example.com',
|
|
112
|
+
'/put',
|
|
113
|
+
'PUT',
|
|
114
|
+
'testPut',
|
|
115
|
+
false,
|
|
116
|
+
'application/json',
|
|
117
|
+
);
|
|
118
|
+
await actionRequest.setParams({ param: 'test' });
|
|
119
|
+
const response = await actionRequest.execute();
|
|
120
|
+
expect(mockedAxios.put).toHaveBeenCalled();
|
|
121
|
+
expect(response.data.success).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('handles DELETE requests', async () => {
|
|
125
|
+
mockedAxios.delete.mockResolvedValue({ data: { success: true } });
|
|
126
|
+
const actionRequest = new ActionRequest(
|
|
127
|
+
'https://example.com',
|
|
128
|
+
'/delete',
|
|
129
|
+
'DELETE',
|
|
130
|
+
'testDelete',
|
|
131
|
+
false,
|
|
132
|
+
'application/json',
|
|
133
|
+
);
|
|
134
|
+
await actionRequest.setParams({ param: 'test' });
|
|
135
|
+
const response = await actionRequest.execute();
|
|
136
|
+
expect(mockedAxios.delete).toHaveBeenCalled();
|
|
137
|
+
expect(response.data.success).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('handles PATCH requests', async () => {
|
|
141
|
+
mockedAxios.patch.mockResolvedValue({ data: { success: true } });
|
|
142
|
+
const actionRequest = new ActionRequest(
|
|
143
|
+
'https://example.com',
|
|
144
|
+
'/patch',
|
|
145
|
+
'PATCH',
|
|
146
|
+
'testPatch',
|
|
147
|
+
false,
|
|
148
|
+
'application/json',
|
|
149
|
+
);
|
|
150
|
+
await actionRequest.setParams({ param: 'test' });
|
|
151
|
+
const response = await actionRequest.execute();
|
|
152
|
+
expect(mockedAxios.patch).toHaveBeenCalled();
|
|
153
|
+
expect(response.data.success).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('throws an error for unsupported HTTP methods', async () => {
|
|
157
|
+
const actionRequest = new ActionRequest(
|
|
158
|
+
'https://example.com',
|
|
159
|
+
'/invalid',
|
|
160
|
+
'INVALID',
|
|
161
|
+
'testInvalid',
|
|
162
|
+
false,
|
|
163
|
+
'application/json',
|
|
164
|
+
);
|
|
165
|
+
await expect(actionRequest.execute()).rejects.toThrow('Unsupported HTTP method: INVALID');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('throws an error for unsupported HTTP method', async () => {
|
|
170
|
+
const actionRequest = new ActionRequest(
|
|
171
|
+
'https://example.com',
|
|
172
|
+
'/test',
|
|
173
|
+
'INVALID',
|
|
174
|
+
'testOp',
|
|
175
|
+
false,
|
|
176
|
+
'application/json',
|
|
177
|
+
);
|
|
178
|
+
await expect(actionRequest.execute()).rejects.toThrow('Unsupported HTTP method: INVALID');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe('Authentication Handling', () => {
|
|
183
|
+
it('correctly sets Basic Auth header', async () => {
|
|
184
|
+
const actionRequest = new ActionRequest(
|
|
185
|
+
'https://example.com',
|
|
186
|
+
'/test',
|
|
187
|
+
'GET',
|
|
188
|
+
'testOp',
|
|
189
|
+
false,
|
|
190
|
+
'application/json',
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const api_key = 'user:pass';
|
|
194
|
+
const encodedCredentials = Buffer.from('user:pass').toString('base64');
|
|
195
|
+
|
|
196
|
+
actionRequest.setAuth({
|
|
197
|
+
auth: {
|
|
198
|
+
type: AuthTypeEnum.ServiceHttp,
|
|
199
|
+
authorization_type: AuthorizationTypeEnum.Basic,
|
|
200
|
+
},
|
|
201
|
+
api_key,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
await actionRequest.setParams({ param1: 'value1' });
|
|
205
|
+
await actionRequest.execute();
|
|
206
|
+
expect(mockedAxios.get).toHaveBeenCalledWith('https://example.com/test', {
|
|
207
|
+
headers: expect.objectContaining({
|
|
208
|
+
Authorization: `Basic ${encodedCredentials}`,
|
|
209
|
+
}),
|
|
210
|
+
params: expect.anything(),
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('correctly sets Bearer token', async () => {
|
|
215
|
+
const actionRequest = new ActionRequest(
|
|
216
|
+
'https://example.com',
|
|
217
|
+
'/test',
|
|
218
|
+
'GET',
|
|
219
|
+
'testOp',
|
|
220
|
+
false,
|
|
221
|
+
'application/json',
|
|
222
|
+
);
|
|
223
|
+
actionRequest.setAuth({
|
|
224
|
+
auth: {
|
|
225
|
+
type: AuthTypeEnum.ServiceHttp,
|
|
226
|
+
authorization_type: AuthorizationTypeEnum.Bearer,
|
|
227
|
+
},
|
|
228
|
+
api_key: 'token123',
|
|
229
|
+
});
|
|
230
|
+
await actionRequest.setParams({ param1: 'value1' });
|
|
231
|
+
await actionRequest.execute();
|
|
232
|
+
expect(mockedAxios.get).toHaveBeenCalledWith('https://example.com/test', {
|
|
233
|
+
headers: expect.objectContaining({
|
|
234
|
+
Authorization: 'Bearer token123',
|
|
235
|
+
}),
|
|
236
|
+
params: expect.anything(),
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('correctly sets API Key', async () => {
|
|
241
|
+
const actionRequest = new ActionRequest(
|
|
242
|
+
'https://example.com',
|
|
243
|
+
'/test',
|
|
244
|
+
'GET',
|
|
245
|
+
'testOp',
|
|
246
|
+
false,
|
|
247
|
+
'application/json',
|
|
248
|
+
);
|
|
249
|
+
// Updated to match ActionMetadata structure
|
|
250
|
+
actionRequest.setAuth({
|
|
251
|
+
auth: {
|
|
252
|
+
type: AuthTypeEnum.ServiceHttp, // Assuming this is a valid enum or value for your context
|
|
253
|
+
authorization_type: AuthorizationTypeEnum.Custom, // Assuming Custom means using a custom header
|
|
254
|
+
custom_auth_header: 'X-API-KEY',
|
|
255
|
+
},
|
|
256
|
+
api_key: 'abc123',
|
|
257
|
+
});
|
|
258
|
+
await actionRequest.setParams({ param1: 'value1' });
|
|
259
|
+
await actionRequest.execute();
|
|
260
|
+
expect(mockedAxios.get).toHaveBeenCalledWith('https://example.com/test', {
|
|
261
|
+
headers: expect.objectContaining({
|
|
262
|
+
'X-API-KEY': 'abc123',
|
|
263
|
+
}),
|
|
264
|
+
params: expect.anything(),
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe('resolveRef', () => {
|
|
270
|
+
it('correctly resolves $ref references in the OpenAPI spec', () => {
|
|
271
|
+
const openapiSpec = whimsicalOpenapiSpec;
|
|
272
|
+
const flowchartRequestRef = (
|
|
273
|
+
openapiSpec.paths['/ai.chatgpt.render-flowchart']?.post
|
|
274
|
+
?.requestBody as OpenAPIV3.RequestBodyObject
|
|
275
|
+
)?.content['application/json'].schema;
|
|
276
|
+
expect(flowchartRequestRef).toBeDefined();
|
|
277
|
+
const resolvedFlowchartRequest = resolveRef(
|
|
278
|
+
flowchartRequestRef as OpenAPIV3.RequestBodyObject,
|
|
279
|
+
openapiSpec.components,
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
expect(resolvedFlowchartRequest).toBeDefined();
|
|
283
|
+
expect(resolvedFlowchartRequest.type).toBe('object');
|
|
284
|
+
const properties = resolvedFlowchartRequest.properties as FlowchartSchema;
|
|
285
|
+
expect(properties).toBeDefined();
|
|
286
|
+
expect(properties.mermaid).toBeDefined();
|
|
287
|
+
expect(properties.mermaid.type).toBe('string');
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe('openapiToFunction', () => {
|
|
292
|
+
it('converts OpenAPI spec to function signatures and request builders', () => {
|
|
293
|
+
const { functionSignatures, requestBuilders } = openapiToFunction(getWeatherOpenapiSpec);
|
|
294
|
+
expect(functionSignatures.length).toBe(1);
|
|
295
|
+
expect(functionSignatures[0].name).toBe('GetCurrentWeather');
|
|
296
|
+
|
|
297
|
+
const parameters = functionSignatures[0].parameters as ParametersSchema & {
|
|
298
|
+
properties: {
|
|
299
|
+
location: {
|
|
300
|
+
type: 'string';
|
|
301
|
+
};
|
|
302
|
+
locations: {
|
|
303
|
+
type: 'array';
|
|
304
|
+
items: {
|
|
305
|
+
type: 'object';
|
|
306
|
+
properties: {
|
|
307
|
+
city: {
|
|
308
|
+
type: 'string';
|
|
309
|
+
};
|
|
310
|
+
state: {
|
|
311
|
+
type: 'string';
|
|
312
|
+
};
|
|
313
|
+
countryCode: {
|
|
314
|
+
type: 'string';
|
|
315
|
+
};
|
|
316
|
+
time: {
|
|
317
|
+
type: 'string';
|
|
318
|
+
};
|
|
319
|
+
};
|
|
320
|
+
};
|
|
321
|
+
};
|
|
322
|
+
};
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
expect(parameters).toBeDefined();
|
|
326
|
+
expect(parameters.properties.locations).toBeDefined();
|
|
327
|
+
expect(parameters.properties.locations.type).toBe('array');
|
|
328
|
+
expect(parameters.properties.locations.items.type).toBe('object');
|
|
329
|
+
|
|
330
|
+
expect(parameters.properties.locations.items.properties.city.type).toBe('string');
|
|
331
|
+
expect(parameters.properties.locations.items.properties.state.type).toBe('string');
|
|
332
|
+
expect(parameters.properties.locations.items.properties.countryCode.type).toBe('string');
|
|
333
|
+
expect(parameters.properties.locations.items.properties.time.type).toBe('string');
|
|
334
|
+
|
|
335
|
+
expect(requestBuilders).toHaveProperty('GetCurrentWeather');
|
|
336
|
+
expect(requestBuilders.GetCurrentWeather).toBeInstanceOf(ActionRequest);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe('openapiToFunction with $ref resolution', () => {
|
|
340
|
+
it('correctly converts OpenAPI spec to function signatures and request builders, resolving $ref references', () => {
|
|
341
|
+
const { functionSignatures, requestBuilders } = openapiToFunction(whimsicalOpenapiSpec);
|
|
342
|
+
|
|
343
|
+
expect(functionSignatures.length).toBeGreaterThan(0);
|
|
344
|
+
|
|
345
|
+
const postRenderFlowchartSignature = functionSignatures.find(
|
|
346
|
+
(sig) => sig.name === 'postRenderFlowchart',
|
|
347
|
+
);
|
|
348
|
+
expect(postRenderFlowchartSignature).toBeDefined();
|
|
349
|
+
expect(postRenderFlowchartSignature?.name).toBe('postRenderFlowchart');
|
|
350
|
+
expect(postRenderFlowchartSignature?.parameters).toBeDefined();
|
|
351
|
+
|
|
352
|
+
expect(requestBuilders).toHaveProperty('postRenderFlowchart');
|
|
353
|
+
const postRenderFlowchartRequestBuilder = requestBuilders['postRenderFlowchart'];
|
|
354
|
+
expect(postRenderFlowchartRequestBuilder).toBeDefined();
|
|
355
|
+
expect(postRenderFlowchartRequestBuilder.method).toBe('post');
|
|
356
|
+
expect(postRenderFlowchartRequestBuilder.path).toBe('/ai.chatgpt.render-flowchart');
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const invalidServerURL = 'Could not find a valid URL in `servers`';
|
|
362
|
+
|
|
363
|
+
describe('validateAndParseOpenAPISpec', () => {
|
|
364
|
+
it('validates a correct OpenAPI spec successfully', () => {
|
|
365
|
+
const validSpec = JSON.stringify({
|
|
366
|
+
openapi: '3.0.0',
|
|
367
|
+
info: { title: 'Test API', version: '1.0.0' },
|
|
368
|
+
servers: [{ url: 'https://test.api' }],
|
|
369
|
+
paths: { '/test': {} },
|
|
370
|
+
components: { schemas: {} },
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
const result = validateAndParseOpenAPISpec(validSpec);
|
|
374
|
+
expect(result.status).toBe(true);
|
|
375
|
+
expect(result.message).toBe('OpenAPI spec is valid.');
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('returns an error for spec with no servers', () => {
|
|
379
|
+
const noServerSpec = JSON.stringify({
|
|
380
|
+
openapi: '3.0.0',
|
|
381
|
+
info: { title: 'Test API', version: '1.0.0' },
|
|
382
|
+
paths: { '/test': {} },
|
|
383
|
+
components: { schemas: {} },
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
const result = validateAndParseOpenAPISpec(noServerSpec);
|
|
387
|
+
expect(result.status).toBe(false);
|
|
388
|
+
expect(result.message).toBe(invalidServerURL);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('returns an error for spec with empty server URL', () => {
|
|
392
|
+
const emptyURLSpec = `{
|
|
393
|
+
"openapi": "3.1.0",
|
|
394
|
+
"info": {
|
|
395
|
+
"title": "Untitled",
|
|
396
|
+
"description": "Your OpenAPI specification",
|
|
397
|
+
"version": "v1.0.0"
|
|
398
|
+
},
|
|
399
|
+
"servers": [
|
|
400
|
+
{
|
|
401
|
+
"url": ""
|
|
402
|
+
}
|
|
403
|
+
],
|
|
404
|
+
"paths": {},
|
|
405
|
+
"components": {
|
|
406
|
+
"schemas": {}
|
|
407
|
+
}
|
|
408
|
+
}`;
|
|
409
|
+
|
|
410
|
+
const result = validateAndParseOpenAPISpec(emptyURLSpec);
|
|
411
|
+
expect(result.status).toBe(false);
|
|
412
|
+
expect(result.message).toBe(invalidServerURL);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('returns an error for spec with no paths', () => {
|
|
416
|
+
const noPathsSpec = JSON.stringify({
|
|
417
|
+
openapi: '3.0.0',
|
|
418
|
+
info: { title: 'Test API', version: '1.0.0' },
|
|
419
|
+
servers: [{ url: 'https://test.api' }],
|
|
420
|
+
components: { schemas: {} },
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
const result = validateAndParseOpenAPISpec(noPathsSpec);
|
|
424
|
+
expect(result.status).toBe(false);
|
|
425
|
+
expect(result.message).toBe('No paths found in the OpenAPI spec.');
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('detects missing components in spec', () => {
|
|
429
|
+
const missingComponentSpec = JSON.stringify({
|
|
430
|
+
openapi: '3.0.0',
|
|
431
|
+
info: { title: 'Test API', version: '1.0.0' },
|
|
432
|
+
servers: [{ url: 'https://test.api' }],
|
|
433
|
+
paths: {
|
|
434
|
+
'/test': {
|
|
435
|
+
get: {
|
|
436
|
+
responses: {
|
|
437
|
+
'200': {
|
|
438
|
+
content: {
|
|
439
|
+
'application/json': { schema: { $ref: '#/components/schemas/Missing' } },
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
const result = validateAndParseOpenAPISpec(missingComponentSpec);
|
|
449
|
+
expect(result.status).toBe(true);
|
|
450
|
+
expect(result.message).toContain('reference to unknown component Missing');
|
|
451
|
+
expect(result.spec).toBeDefined();
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it('handles invalid spec formats', () => {
|
|
455
|
+
const invalidSpec = 'not a valid spec';
|
|
456
|
+
|
|
457
|
+
const result = validateAndParseOpenAPISpec(invalidSpec);
|
|
458
|
+
expect(result.status).toBe(false);
|
|
459
|
+
expect(result.message).toBe(invalidServerURL);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('handles YAML spec and correctly converts to Function Signatures', () => {
|
|
463
|
+
const result = validateAndParseOpenAPISpec(scholarAIOpenapiSpec);
|
|
464
|
+
expect(result.status).toBe(true);
|
|
465
|
+
|
|
466
|
+
const spec = result.spec;
|
|
467
|
+
expect(spec).toBeDefined();
|
|
468
|
+
|
|
469
|
+
const { functionSignatures, requestBuilders } = openapiToFunction(spec as OpenAPIV3.Document);
|
|
470
|
+
expect(functionSignatures.length).toBe(3);
|
|
471
|
+
expect(requestBuilders).toHaveProperty('searchAbstracts');
|
|
472
|
+
expect(requestBuilders).toHaveProperty('getFullText');
|
|
473
|
+
expect(requestBuilders).toHaveProperty('saveCitation');
|
|
474
|
+
});
|
|
475
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fileConfig,
|
|
3
|
+
fullMimeTypesList,
|
|
4
|
+
codeInterpreterMimeTypesList,
|
|
5
|
+
retrievalMimeTypesList,
|
|
6
|
+
supportedMimeTypes,
|
|
7
|
+
codeInterpreterMimeTypes,
|
|
8
|
+
retrievalMimeTypes,
|
|
9
|
+
excelFileTypes,
|
|
10
|
+
excelMimeTypes,
|
|
11
|
+
fileConfigSchema,
|
|
12
|
+
mergeFileConfig,
|
|
13
|
+
mbToBytes,
|
|
14
|
+
} from '../src/file-config';
|
|
15
|
+
|
|
16
|
+
describe('MIME Type Regex Patterns', () => {
|
|
17
|
+
const unsupportedMimeTypes = [
|
|
18
|
+
'text/x-unknown',
|
|
19
|
+
'application/unknown',
|
|
20
|
+
'image/bmp',
|
|
21
|
+
'image/svg',
|
|
22
|
+
'audio/mp3',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
// Testing general supported MIME types
|
|
26
|
+
fullMimeTypesList.forEach((mimeType) => {
|
|
27
|
+
test(`"${mimeType}" should match one of the supported regex patterns in supportedMimeTypes`, () => {
|
|
28
|
+
const matches = supportedMimeTypes.some((regex) => regex.test(mimeType));
|
|
29
|
+
expect(matches).toBeTruthy();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Testing unsupported MIME types
|
|
34
|
+
unsupportedMimeTypes.forEach((mimeType) => {
|
|
35
|
+
test(`"${mimeType}" should not match any of the supported regex patterns in supportedMimeTypes`, () => {
|
|
36
|
+
const matches = supportedMimeTypes.some((regex) => regex.test(mimeType));
|
|
37
|
+
expect(matches).toBeFalsy();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Testing MIME types for Code Interpreter support
|
|
42
|
+
codeInterpreterMimeTypesList.forEach((mimeType) => {
|
|
43
|
+
test(`"${mimeType}" should be supported by codeInterpreterMimeTypes`, () => {
|
|
44
|
+
const matches = codeInterpreterMimeTypes.some((regex) => regex.test(mimeType));
|
|
45
|
+
expect(matches).toBeTruthy();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Testing MIME types for Retrieval support
|
|
50
|
+
retrievalMimeTypesList.forEach((mimeType) => {
|
|
51
|
+
test(`"${mimeType}" should be supported by retrievalMimeTypes`, () => {
|
|
52
|
+
const matches = retrievalMimeTypes.some((regex) => regex.test(mimeType));
|
|
53
|
+
expect(matches).toBeTruthy();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('MIME Types Exclusive to Code Interpreter', () => {
|
|
59
|
+
const exclusiveCodeInterpreterMimeTypes = codeInterpreterMimeTypesList.filter(
|
|
60
|
+
(mimeType) => !retrievalMimeTypesList.includes(mimeType),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
exclusiveCodeInterpreterMimeTypes.forEach((mimeType) => {
|
|
64
|
+
test(`"${mimeType}" should not be supported by retrievalMimeTypes`, () => {
|
|
65
|
+
const isSupportedByRetrieval = retrievalMimeTypes.some((regex) => regex.test(mimeType));
|
|
66
|
+
expect(isSupportedByRetrieval).toBeFalsy();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('Testing Excel MIME types', () => {
|
|
72
|
+
excelFileTypes.forEach((mimeType) => {
|
|
73
|
+
test(`"${mimeType}" should match one of the supported regex patterns in supportedMimeTypes`, () => {
|
|
74
|
+
const matches = supportedMimeTypes.some((regex) => regex.test(mimeType));
|
|
75
|
+
expect(matches).toBeTruthy();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('Excel MIME types should match the regex pattern in excelMimeTypes', () => {
|
|
80
|
+
const matches = excelFileTypes.every((mimeType) => excelMimeTypes.test(mimeType));
|
|
81
|
+
expect(matches).toBeTruthy();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('Testing `fileConfig`', () => {
|
|
86
|
+
describe('checkType function', () => {
|
|
87
|
+
test('should return true for supported MIME types', () => {
|
|
88
|
+
const fileTypes = ['text/csv', 'application/json', 'application/pdf', 'image/jpeg'];
|
|
89
|
+
fileTypes.forEach((fileType) => {
|
|
90
|
+
const isSupported = fileConfig.checkType(fileType);
|
|
91
|
+
expect(isSupported).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('should return false for unsupported MIME types', () => {
|
|
96
|
+
const fileTypes = ['text/mamba', 'application/exe', 'no-image', ''];
|
|
97
|
+
fileTypes.forEach((fileType) => {
|
|
98
|
+
const isSupported = fileConfig.checkType(fileType);
|
|
99
|
+
expect(isSupported).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const dynamicConfigs = {
|
|
106
|
+
minimalUpdate: {
|
|
107
|
+
serverFileSizeLimit: 1024, // Increasing server file size limit
|
|
108
|
+
},
|
|
109
|
+
fullOverrideDefaultEndpoint: {
|
|
110
|
+
endpoints: {
|
|
111
|
+
default: {
|
|
112
|
+
fileLimit: 15,
|
|
113
|
+
fileSizeLimit: 30,
|
|
114
|
+
totalSizeLimit: 60,
|
|
115
|
+
supportedMimeTypes: ['^video/.*$'], // Changing to support video files
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
newEndpointAddition: {
|
|
120
|
+
endpoints: {
|
|
121
|
+
newEndpoint: {
|
|
122
|
+
fileLimit: 5,
|
|
123
|
+
fileSizeLimit: 10,
|
|
124
|
+
totalSizeLimit: 20,
|
|
125
|
+
supportedMimeTypes: ['^application/json$', '^application/xml$'],
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
describe('mergeFileConfig', () => {
|
|
132
|
+
test('merges minimal update correctly', () => {
|
|
133
|
+
const result = mergeFileConfig(dynamicConfigs.minimalUpdate);
|
|
134
|
+
expect(result.serverFileSizeLimit).toEqual(mbToBytes(1024));
|
|
135
|
+
const parsedResult = fileConfigSchema.safeParse(result);
|
|
136
|
+
expect(parsedResult.success).toBeTruthy();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('overrides default endpoint with full new configuration', () => {
|
|
140
|
+
const result = mergeFileConfig(dynamicConfigs.fullOverrideDefaultEndpoint);
|
|
141
|
+
expect(result.endpoints.default.fileLimit).toEqual(15);
|
|
142
|
+
expect(result.endpoints.default.supportedMimeTypes).toEqual(
|
|
143
|
+
expect.arrayContaining([new RegExp('^video/.*$')]),
|
|
144
|
+
);
|
|
145
|
+
const parsedResult = fileConfigSchema.safeParse(result);
|
|
146
|
+
expect(parsedResult.success).toBeTruthy();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('adds new endpoint configuration correctly', () => {
|
|
150
|
+
const result = mergeFileConfig(dynamicConfigs.newEndpointAddition);
|
|
151
|
+
expect(result.endpoints.newEndpoint).toBeDefined();
|
|
152
|
+
expect(result.endpoints.newEndpoint.fileLimit).toEqual(5);
|
|
153
|
+
expect(result.endpoints.newEndpoint.supportedMimeTypes).toEqual(
|
|
154
|
+
expect.arrayContaining([new RegExp('^application/json$')]),
|
|
155
|
+
);
|
|
156
|
+
const parsedResult = fileConfigSchema.safeParse(result);
|
|
157
|
+
expect(parsedResult.success).toBeTruthy();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('disables an endpoint and sets numeric fields to 0 and empties supportedMimeTypes', () => {
|
|
161
|
+
const configWithDisabledEndpoint = {
|
|
162
|
+
endpoints: {
|
|
163
|
+
disabledEndpoint: {
|
|
164
|
+
disabled: true,
|
|
165
|
+
fileLimit: 15,
|
|
166
|
+
fileSizeLimit: 30,
|
|
167
|
+
totalSizeLimit: 60,
|
|
168
|
+
supportedMimeTypes: ['^video/.*$'],
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const result = mergeFileConfig(configWithDisabledEndpoint);
|
|
174
|
+
expect(result.endpoints.disabledEndpoint).toBeDefined();
|
|
175
|
+
expect(result.endpoints.disabledEndpoint.disabled).toEqual(true);
|
|
176
|
+
expect(result.endpoints.disabledEndpoint.fileLimit).toEqual(0);
|
|
177
|
+
expect(result.endpoints.disabledEndpoint.fileSizeLimit).toEqual(0);
|
|
178
|
+
expect(result.endpoints.disabledEndpoint.totalSizeLimit).toEqual(0);
|
|
179
|
+
expect(result.endpoints.disabledEndpoint.supportedMimeTypes).toEqual([]);
|
|
180
|
+
});
|
|
181
|
+
});
|