@opencontextprotocol/agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +198 -0
- package/dist/src/agent.d.ts +112 -0
- package/dist/src/agent.d.ts.map +1 -0
- package/dist/src/agent.js +358 -0
- package/dist/src/agent.js.map +1 -0
- package/dist/src/context.d.ts +108 -0
- package/dist/src/context.d.ts.map +1 -0
- package/dist/src/context.js +196 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/errors.d.ts +40 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +63 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/headers.d.ts +63 -0
- package/dist/src/headers.d.ts.map +1 -0
- package/dist/src/headers.js +238 -0
- package/dist/src/headers.js.map +1 -0
- package/dist/src/http_client.d.ts +82 -0
- package/dist/src/http_client.d.ts.map +1 -0
- package/dist/src/http_client.js +181 -0
- package/dist/src/http_client.js.map +1 -0
- package/dist/src/index.d.ts +25 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +35 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/registry.d.ts +52 -0
- package/dist/src/registry.d.ts.map +1 -0
- package/dist/src/registry.js +164 -0
- package/dist/src/registry.js.map +1 -0
- package/dist/src/schema_discovery.d.ts +149 -0
- package/dist/src/schema_discovery.d.ts.map +1 -0
- package/dist/src/schema_discovery.js +707 -0
- package/dist/src/schema_discovery.js.map +1 -0
- package/dist/src/schemas/ocp-context.json +138 -0
- package/dist/src/storage.d.ts +110 -0
- package/dist/src/storage.d.ts.map +1 -0
- package/dist/src/storage.js +399 -0
- package/dist/src/storage.js.map +1 -0
- package/dist/src/validation.d.ts +169 -0
- package/dist/src/validation.d.ts.map +1 -0
- package/dist/src/validation.js +92 -0
- package/dist/src/validation.js.map +1 -0
- package/dist/tests/agent.test.d.ts +5 -0
- package/dist/tests/agent.test.d.ts.map +1 -0
- package/dist/tests/agent.test.js +536 -0
- package/dist/tests/agent.test.js.map +1 -0
- package/dist/tests/context.test.d.ts +5 -0
- package/dist/tests/context.test.d.ts.map +1 -0
- package/dist/tests/context.test.js +285 -0
- package/dist/tests/context.test.js.map +1 -0
- package/dist/tests/headers.test.d.ts +5 -0
- package/dist/tests/headers.test.d.ts.map +1 -0
- package/dist/tests/headers.test.js +356 -0
- package/dist/tests/headers.test.js.map +1 -0
- package/dist/tests/http_client.test.d.ts +5 -0
- package/dist/tests/http_client.test.d.ts.map +1 -0
- package/dist/tests/http_client.test.js +373 -0
- package/dist/tests/http_client.test.js.map +1 -0
- package/dist/tests/registry.test.d.ts +5 -0
- package/dist/tests/registry.test.d.ts.map +1 -0
- package/dist/tests/registry.test.js +232 -0
- package/dist/tests/registry.test.js.map +1 -0
- package/dist/tests/schema_discovery.test.d.ts +5 -0
- package/dist/tests/schema_discovery.test.d.ts.map +1 -0
- package/dist/tests/schema_discovery.test.js +1074 -0
- package/dist/tests/schema_discovery.test.js.map +1 -0
- package/dist/tests/storage.test.d.ts +5 -0
- package/dist/tests/storage.test.d.ts.map +1 -0
- package/dist/tests/storage.test.js +414 -0
- package/dist/tests/storage.test.js.map +1 -0
- package/dist/tests/validation.test.d.ts +5 -0
- package/dist/tests/validation.test.d.ts.map +1 -0
- package/dist/tests/validation.test.js +254 -0
- package/dist/tests/validation.test.js.map +1 -0
- package/package.json +51 -0
|
@@ -0,0 +1,1074 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for OCP schema discovery functionality.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, test, expect, beforeEach, jest } from '@jest/globals';
|
|
5
|
+
import { OCPSchemaDiscovery } from '../src/schema_discovery.js';
|
|
6
|
+
import { dirname } from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = dirname(__filename);
|
|
10
|
+
// Mock fetch globally
|
|
11
|
+
global.fetch = jest.fn();
|
|
12
|
+
describe('OCP Schema Discovery', () => {
|
|
13
|
+
let discovery;
|
|
14
|
+
const sampleOpenApiSpec = {
|
|
15
|
+
openapi: '3.0.0',
|
|
16
|
+
info: {
|
|
17
|
+
title: 'Test API',
|
|
18
|
+
version: '1.0.0',
|
|
19
|
+
},
|
|
20
|
+
servers: [{ url: 'https://api.example.com' }],
|
|
21
|
+
paths: {
|
|
22
|
+
'/users': {
|
|
23
|
+
get: {
|
|
24
|
+
summary: 'List users',
|
|
25
|
+
description: 'Get a list of all users',
|
|
26
|
+
parameters: [
|
|
27
|
+
{
|
|
28
|
+
name: 'limit',
|
|
29
|
+
in: 'query',
|
|
30
|
+
schema: { type: 'integer' },
|
|
31
|
+
required: false,
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
responses: {
|
|
35
|
+
'200': {
|
|
36
|
+
description: 'List of users',
|
|
37
|
+
content: {
|
|
38
|
+
'application/json': {
|
|
39
|
+
schema: {
|
|
40
|
+
type: 'array',
|
|
41
|
+
items: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
properties: {
|
|
44
|
+
id: { type: 'integer' },
|
|
45
|
+
name: { type: 'string' },
|
|
46
|
+
email: { type: 'string' },
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
post: {
|
|
56
|
+
summary: 'Create user',
|
|
57
|
+
description: 'Create a new user',
|
|
58
|
+
requestBody: {
|
|
59
|
+
content: {
|
|
60
|
+
'application/json': {
|
|
61
|
+
schema: {
|
|
62
|
+
type: 'object',
|
|
63
|
+
properties: {
|
|
64
|
+
name: { type: 'string' },
|
|
65
|
+
email: { type: 'string' },
|
|
66
|
+
},
|
|
67
|
+
required: ['name', 'email'],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
responses: {
|
|
73
|
+
'201': {
|
|
74
|
+
description: 'User created',
|
|
75
|
+
content: {
|
|
76
|
+
'application/json': {
|
|
77
|
+
schema: {
|
|
78
|
+
type: 'object',
|
|
79
|
+
properties: {
|
|
80
|
+
id: { type: 'integer' },
|
|
81
|
+
name: { type: 'string' },
|
|
82
|
+
email: { type: 'string' },
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
'/users/{id}': {
|
|
92
|
+
get: {
|
|
93
|
+
summary: 'Get user',
|
|
94
|
+
description: 'Get a specific user by ID',
|
|
95
|
+
parameters: [
|
|
96
|
+
{
|
|
97
|
+
name: 'id',
|
|
98
|
+
in: 'path',
|
|
99
|
+
schema: { type: 'string' },
|
|
100
|
+
required: true,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
responses: {
|
|
104
|
+
'200': {
|
|
105
|
+
description: 'User details',
|
|
106
|
+
content: {
|
|
107
|
+
'application/json': {
|
|
108
|
+
schema: {
|
|
109
|
+
type: 'object',
|
|
110
|
+
properties: {
|
|
111
|
+
id: { type: 'integer' },
|
|
112
|
+
name: { type: 'string' },
|
|
113
|
+
email: { type: 'string' },
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
beforeEach(() => {
|
|
125
|
+
jest.clearAllMocks();
|
|
126
|
+
discovery = new OCPSchemaDiscovery();
|
|
127
|
+
});
|
|
128
|
+
describe('Parse OpenAPI Spec', () => {
|
|
129
|
+
test('parse openapi spec', () => {
|
|
130
|
+
const apiSpec = discovery._parseOpenApiSpec(sampleOpenApiSpec, 'https://api.example.com');
|
|
131
|
+
expect(apiSpec.title).toBe('Test API');
|
|
132
|
+
expect(apiSpec.version).toBe('1.0.0');
|
|
133
|
+
expect(apiSpec.base_url).toBe('https://api.example.com');
|
|
134
|
+
expect(apiSpec.tools.length).toBe(3); // GET /users, POST /users, GET /users/{id}
|
|
135
|
+
});
|
|
136
|
+
test('generate tools from spec', () => {
|
|
137
|
+
const apiSpec = discovery._parseOpenApiSpec(sampleOpenApiSpec, 'https://api.example.com');
|
|
138
|
+
const tools = apiSpec.tools;
|
|
139
|
+
expect(tools.length).toBe(3); // GET /users, POST /users, GET /users/{id}
|
|
140
|
+
// Check that we have the expected tools with deterministic names
|
|
141
|
+
const toolNames = tools.map((t) => t.name);
|
|
142
|
+
const expectedNames = ['getUsers', 'postUsers', 'getUsersId'];
|
|
143
|
+
for (const expectedName of expectedNames) {
|
|
144
|
+
expect(toolNames).toContain(expectedName);
|
|
145
|
+
}
|
|
146
|
+
// Check GET /users tool
|
|
147
|
+
const getUsers = tools.find((t) => t.name === 'getUsers');
|
|
148
|
+
expect(getUsers).toBeDefined();
|
|
149
|
+
expect(getUsers.method).toBe('GET');
|
|
150
|
+
expect(getUsers.path).toBe('/users');
|
|
151
|
+
expect(getUsers.description).toBe('List users');
|
|
152
|
+
expect(getUsers.parameters['limit']).toBeDefined();
|
|
153
|
+
expect(getUsers.parameters['limit'].type).toBe('integer');
|
|
154
|
+
expect(getUsers.parameters['limit'].location).toBe('query');
|
|
155
|
+
expect(getUsers.parameters['limit'].required).toBe(false);
|
|
156
|
+
expect(getUsers.response_schema).toBeDefined();
|
|
157
|
+
expect(getUsers.response_schema.type).toBe('array');
|
|
158
|
+
// Check POST /users tool
|
|
159
|
+
const postUsers = tools.find((t) => t.name === 'postUsers');
|
|
160
|
+
expect(postUsers).toBeDefined();
|
|
161
|
+
expect(postUsers.method).toBe('POST');
|
|
162
|
+
expect(postUsers.path).toBe('/users');
|
|
163
|
+
expect(postUsers.parameters['name']).toBeDefined();
|
|
164
|
+
expect(postUsers.parameters['email']).toBeDefined();
|
|
165
|
+
expect(postUsers.parameters['name'].required).toBe(true);
|
|
166
|
+
expect(postUsers.parameters['email'].required).toBe(true);
|
|
167
|
+
expect(postUsers.response_schema).toBeDefined();
|
|
168
|
+
expect(postUsers.response_schema.type).toBe('object');
|
|
169
|
+
// Check GET /users/{id} tool
|
|
170
|
+
const getUsersId = tools.find((t) => t.name === 'getUsersId');
|
|
171
|
+
expect(getUsersId).toBeDefined();
|
|
172
|
+
expect(getUsersId.method).toBe('GET');
|
|
173
|
+
expect(getUsersId.path).toBe('/users/{id}');
|
|
174
|
+
expect(getUsersId.parameters['id']).toBeDefined();
|
|
175
|
+
expect(getUsersId.parameters['id'].location).toBe('path');
|
|
176
|
+
expect(getUsersId.parameters['id'].required).toBe(true);
|
|
177
|
+
expect(getUsersId.response_schema).toBeDefined();
|
|
178
|
+
expect(getUsersId.response_schema.type).toBe('object');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
describe('Discover API', () => {
|
|
182
|
+
test('discover api success', async () => {
|
|
183
|
+
// Mock fetch response
|
|
184
|
+
global.fetch.mockResolvedValue({
|
|
185
|
+
ok: true,
|
|
186
|
+
json: async () => sampleOpenApiSpec,
|
|
187
|
+
});
|
|
188
|
+
const apiSpec = await discovery.discoverApi('https://api.example.com/openapi.json');
|
|
189
|
+
expect(apiSpec.title).toBe('Test API');
|
|
190
|
+
expect(apiSpec.tools.length).toBe(3);
|
|
191
|
+
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
192
|
+
});
|
|
193
|
+
test('discover api with base url override', async () => {
|
|
194
|
+
global.fetch.mockResolvedValue({
|
|
195
|
+
ok: true,
|
|
196
|
+
json: async () => sampleOpenApiSpec,
|
|
197
|
+
});
|
|
198
|
+
const apiSpec = await discovery.discoverApi('https://api.example.com/openapi.json', 'https://custom.example.com');
|
|
199
|
+
expect(apiSpec.base_url).toBe('https://custom.example.com');
|
|
200
|
+
});
|
|
201
|
+
test('discover api failure', async () => {
|
|
202
|
+
global.fetch.mockRejectedValue(new Error('Network error'));
|
|
203
|
+
await expect(discovery.discoverApi('https://api.example.com/openapi.json')).rejects.toThrow('Network error');
|
|
204
|
+
});
|
|
205
|
+
test('discover api with refs', async () => {
|
|
206
|
+
const openApiSpecWithRefs = {
|
|
207
|
+
openapi: '3.0.0',
|
|
208
|
+
info: { title: 'Test API', version: '1.0.0' },
|
|
209
|
+
servers: [{ url: 'https://api.example.com' }],
|
|
210
|
+
paths: {
|
|
211
|
+
'/queue': {
|
|
212
|
+
post: {
|
|
213
|
+
operationId: 'updateQueue',
|
|
214
|
+
summary: 'Update queue',
|
|
215
|
+
responses: {
|
|
216
|
+
'200': {
|
|
217
|
+
description: 'Queue updated',
|
|
218
|
+
content: {
|
|
219
|
+
'application/json': {
|
|
220
|
+
schema: {
|
|
221
|
+
$ref: '#/components/schemas/Queue',
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
components: {
|
|
231
|
+
schemas: {
|
|
232
|
+
Queue: {
|
|
233
|
+
type: 'object',
|
|
234
|
+
properties: {
|
|
235
|
+
sid: { type: 'string' },
|
|
236
|
+
friendly_name: { type: 'string' },
|
|
237
|
+
current_size: { type: 'integer' },
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
global.fetch.mockResolvedValueOnce({
|
|
244
|
+
ok: true,
|
|
245
|
+
json: async () => openApiSpecWithRefs,
|
|
246
|
+
});
|
|
247
|
+
const apiSpec = await discovery.discoverApi('https://api.example.com/openapi.json');
|
|
248
|
+
// Should have one tool
|
|
249
|
+
expect(apiSpec.tools.length).toBe(1);
|
|
250
|
+
const tool = apiSpec.tools[0];
|
|
251
|
+
// Verify the $ref was resolved
|
|
252
|
+
expect(tool.name).toBe('updateQueue');
|
|
253
|
+
expect(tool.response_schema).toBeDefined();
|
|
254
|
+
expect(tool.response_schema?.type).toBe('object');
|
|
255
|
+
expect(tool.response_schema?.properties).toBeDefined();
|
|
256
|
+
expect(tool.response_schema?.properties?.sid).toBeDefined();
|
|
257
|
+
expect(tool.response_schema?.properties?.friendly_name).toBeDefined();
|
|
258
|
+
expect(tool.response_schema?.properties?.current_size).toBeDefined();
|
|
259
|
+
// Should NOT contain $ref anymore
|
|
260
|
+
expect(JSON.stringify(tool.response_schema)).not.toContain('$ref');
|
|
261
|
+
});
|
|
262
|
+
test('discover api with circular refs', async () => {
|
|
263
|
+
const openApiSpecWithCircularRefs = {
|
|
264
|
+
openapi: '3.0.0',
|
|
265
|
+
info: { title: 'Test API', version: '1.0.0' },
|
|
266
|
+
servers: [{ url: 'https://api.example.com' }],
|
|
267
|
+
paths: {
|
|
268
|
+
'/node': {
|
|
269
|
+
get: {
|
|
270
|
+
operationId: 'getNode',
|
|
271
|
+
summary: 'Get node',
|
|
272
|
+
responses: {
|
|
273
|
+
'200': {
|
|
274
|
+
description: 'Node retrieved',
|
|
275
|
+
content: {
|
|
276
|
+
'application/json': {
|
|
277
|
+
schema: {
|
|
278
|
+
$ref: '#/components/schemas/Node',
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
components: {
|
|
288
|
+
schemas: {
|
|
289
|
+
Node: {
|
|
290
|
+
type: 'object',
|
|
291
|
+
properties: {
|
|
292
|
+
id: { type: 'string' },
|
|
293
|
+
children: {
|
|
294
|
+
type: 'array',
|
|
295
|
+
items: {
|
|
296
|
+
$ref: '#/components/schemas/Node',
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
global.fetch.mockResolvedValueOnce({
|
|
305
|
+
ok: true,
|
|
306
|
+
json: async () => openApiSpecWithCircularRefs,
|
|
307
|
+
});
|
|
308
|
+
// Should not raise an error
|
|
309
|
+
const apiSpec = await discovery.discoverApi('https://api.example.com/openapi.json');
|
|
310
|
+
// Should have one tool
|
|
311
|
+
expect(apiSpec.tools.length).toBe(1);
|
|
312
|
+
const tool = apiSpec.tools[0];
|
|
313
|
+
// Verify the response schema exists and has the expected structure
|
|
314
|
+
expect(tool.response_schema).toBeDefined();
|
|
315
|
+
expect(tool.response_schema?.type).toBe('object');
|
|
316
|
+
expect(tool.response_schema?.properties).toBeDefined();
|
|
317
|
+
expect(tool.response_schema?.properties?.id).toBeDefined();
|
|
318
|
+
expect(tool.response_schema?.properties?.children).toBeDefined();
|
|
319
|
+
// The circular ref in children.items should be replaced with a placeholder
|
|
320
|
+
const childrenSchema = tool.response_schema?.properties?.children;
|
|
321
|
+
expect(childrenSchema?.type).toBe('array');
|
|
322
|
+
expect(childrenSchema?.items).toBeDefined();
|
|
323
|
+
// The circular ref should be broken with a placeholder
|
|
324
|
+
expect(childrenSchema?.items?.description).toBe('Circular reference');
|
|
325
|
+
});
|
|
326
|
+
test('discover api with polymorphic keywords', async () => {
|
|
327
|
+
const openApiSpecWithPolymorphic = {
|
|
328
|
+
openapi: '3.0.0',
|
|
329
|
+
info: { title: 'Test API', version: '1.0.0' },
|
|
330
|
+
servers: [{ url: 'https://api.example.com' }],
|
|
331
|
+
paths: {
|
|
332
|
+
'/payment': {
|
|
333
|
+
get: {
|
|
334
|
+
operationId: 'getPayment',
|
|
335
|
+
summary: 'Get payment',
|
|
336
|
+
responses: {
|
|
337
|
+
'200': {
|
|
338
|
+
description: 'Payment retrieved',
|
|
339
|
+
content: {
|
|
340
|
+
'application/json': {
|
|
341
|
+
schema: {
|
|
342
|
+
$ref: '#/components/schemas/Payment',
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
components: {
|
|
352
|
+
schemas: {
|
|
353
|
+
Payment: {
|
|
354
|
+
type: 'object',
|
|
355
|
+
properties: {
|
|
356
|
+
id: { type: 'string' },
|
|
357
|
+
amount: { type: 'integer' },
|
|
358
|
+
status: {
|
|
359
|
+
anyOf: [
|
|
360
|
+
{ type: 'string' },
|
|
361
|
+
{ type: 'number' },
|
|
362
|
+
],
|
|
363
|
+
},
|
|
364
|
+
source: {
|
|
365
|
+
anyOf: [
|
|
366
|
+
{ $ref: '#/components/schemas/Card' },
|
|
367
|
+
{ $ref: '#/components/schemas/BankAccount' },
|
|
368
|
+
{ $ref: '#/components/schemas/Wallet' },
|
|
369
|
+
],
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
Card: {
|
|
374
|
+
type: 'object',
|
|
375
|
+
properties: {
|
|
376
|
+
brand: { type: 'string' },
|
|
377
|
+
last4: { type: 'string' },
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
BankAccount: {
|
|
381
|
+
type: 'object',
|
|
382
|
+
properties: {
|
|
383
|
+
routing_number: { type: 'string' },
|
|
384
|
+
account_number: { type: 'string' },
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
Wallet: {
|
|
388
|
+
type: 'object',
|
|
389
|
+
properties: {
|
|
390
|
+
provider: { type: 'string' },
|
|
391
|
+
wallet_id: { type: 'string' },
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
global.fetch.mockResolvedValueOnce({
|
|
398
|
+
ok: true,
|
|
399
|
+
json: async () => openApiSpecWithPolymorphic,
|
|
400
|
+
});
|
|
401
|
+
// Discover API
|
|
402
|
+
const apiSpec = await discovery.discoverApi('https://api.example.com/openapi.json');
|
|
403
|
+
// Should have one tool
|
|
404
|
+
expect(apiSpec.tools.length).toBe(1);
|
|
405
|
+
const tool = apiSpec.tools[0];
|
|
406
|
+
// Verify the response schema exists
|
|
407
|
+
expect(tool.response_schema).toBeDefined();
|
|
408
|
+
expect(tool.response_schema?.type).toBe('object');
|
|
409
|
+
expect(tool.response_schema?.properties).toBeDefined();
|
|
410
|
+
// Status field with anyOf should have string and number types
|
|
411
|
+
const statusSchema = tool?.response_schema?.properties?.status;
|
|
412
|
+
expect(statusSchema?.anyOf).toBeDefined();
|
|
413
|
+
expect(statusSchema?.anyOf[0]).toEqual({ type: 'string' });
|
|
414
|
+
expect(statusSchema?.anyOf[1]).toEqual({ type: 'number' });
|
|
415
|
+
// Should not contain any $refs
|
|
416
|
+
expect(JSON.stringify(statusSchema)).not.toContain('$ref');
|
|
417
|
+
// Source field with object $refs in anyOf should keep the refs unresolved
|
|
418
|
+
const sourceSchema = tool.response_schema?.properties?.source;
|
|
419
|
+
expect(sourceSchema?.anyOf).toBeDefined();
|
|
420
|
+
// The $refs to object schemas should be preserved
|
|
421
|
+
expect(sourceSchema?.anyOf[0]).toEqual({ $ref: '#/components/schemas/Card' });
|
|
422
|
+
expect(sourceSchema?.anyOf[1]).toEqual({ $ref: '#/components/schemas/BankAccount' });
|
|
423
|
+
expect(sourceSchema?.anyOf[2]).toEqual({ $ref: '#/components/schemas/Wallet' });
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
describe('Search Tools', () => {
|
|
427
|
+
test('search tools', () => {
|
|
428
|
+
// Create some sample tools
|
|
429
|
+
const tools = [
|
|
430
|
+
{
|
|
431
|
+
name: 'list_users',
|
|
432
|
+
description: 'Get all users from the system',
|
|
433
|
+
method: 'GET',
|
|
434
|
+
path: '/users',
|
|
435
|
+
parameters: {},
|
|
436
|
+
response_schema: undefined,
|
|
437
|
+
operation_id: undefined,
|
|
438
|
+
tags: [],
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
name: 'create_user',
|
|
442
|
+
description: 'Create a new user account',
|
|
443
|
+
method: 'POST',
|
|
444
|
+
path: '/users',
|
|
445
|
+
parameters: {},
|
|
446
|
+
response_schema: undefined,
|
|
447
|
+
operation_id: undefined,
|
|
448
|
+
tags: [],
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
name: 'list_orders',
|
|
452
|
+
description: 'Get customer orders',
|
|
453
|
+
method: 'GET',
|
|
454
|
+
path: '/orders',
|
|
455
|
+
parameters: {},
|
|
456
|
+
response_schema: undefined,
|
|
457
|
+
operation_id: undefined,
|
|
458
|
+
tags: [],
|
|
459
|
+
},
|
|
460
|
+
];
|
|
461
|
+
const apiSpec = {
|
|
462
|
+
title: 'Test API',
|
|
463
|
+
version: '1.0.0',
|
|
464
|
+
base_url: 'https://api.example.com',
|
|
465
|
+
description: 'A test API for testing purposes',
|
|
466
|
+
tools: tools,
|
|
467
|
+
raw_spec: {},
|
|
468
|
+
};
|
|
469
|
+
// Test search by name
|
|
470
|
+
const userTools = discovery.searchTools(apiSpec, 'user');
|
|
471
|
+
expect(userTools.length).toBe(2);
|
|
472
|
+
expect(userTools.every((tool) => tool.name.toLowerCase().includes('user') ||
|
|
473
|
+
tool.description.toLowerCase().includes('user'))).toBe(true);
|
|
474
|
+
// Test search by description
|
|
475
|
+
const createTools = discovery.searchTools(apiSpec, 'create');
|
|
476
|
+
expect(createTools.length).toBe(1);
|
|
477
|
+
expect(createTools[0].name).toBe('create_user');
|
|
478
|
+
// Test no matches
|
|
479
|
+
const noMatches = discovery.searchTools(apiSpec, 'nonexistent');
|
|
480
|
+
expect(noMatches.length).toBe(0);
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
describe('Generate Documentation', () => {
|
|
484
|
+
test('generate tool documentation', () => {
|
|
485
|
+
const tool = {
|
|
486
|
+
name: 'create_user',
|
|
487
|
+
description: 'Create a new user account',
|
|
488
|
+
method: 'POST',
|
|
489
|
+
path: '/users',
|
|
490
|
+
parameters: {
|
|
491
|
+
name: {
|
|
492
|
+
type: 'string',
|
|
493
|
+
description: "User's full name",
|
|
494
|
+
required: true,
|
|
495
|
+
location: 'body',
|
|
496
|
+
},
|
|
497
|
+
email: {
|
|
498
|
+
type: 'string',
|
|
499
|
+
description: "User's email address",
|
|
500
|
+
required: true,
|
|
501
|
+
location: 'body',
|
|
502
|
+
},
|
|
503
|
+
age: {
|
|
504
|
+
type: 'integer',
|
|
505
|
+
description: "User's age",
|
|
506
|
+
required: false,
|
|
507
|
+
location: 'body',
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
response_schema: undefined,
|
|
511
|
+
operation_id: undefined,
|
|
512
|
+
tags: [],
|
|
513
|
+
};
|
|
514
|
+
const doc = discovery.generateToolDocumentation(tool);
|
|
515
|
+
expect(doc).toContain('create_user');
|
|
516
|
+
expect(doc).toContain('Create a new user account');
|
|
517
|
+
expect(doc).toContain('POST');
|
|
518
|
+
expect(doc).toContain('/users');
|
|
519
|
+
expect(doc).toContain('name');
|
|
520
|
+
expect(doc).toContain('email');
|
|
521
|
+
expect(doc).toContain('age');
|
|
522
|
+
expect(doc.toLowerCase()).toContain('required');
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
describe('OCPTool', () => {
|
|
526
|
+
test('tool creation', () => {
|
|
527
|
+
const tool = {
|
|
528
|
+
name: 'test_tool',
|
|
529
|
+
description: 'A test tool',
|
|
530
|
+
method: 'GET',
|
|
531
|
+
path: '/test',
|
|
532
|
+
parameters: { param: { type: 'string' } },
|
|
533
|
+
response_schema: undefined,
|
|
534
|
+
operation_id: undefined,
|
|
535
|
+
tags: [],
|
|
536
|
+
};
|
|
537
|
+
expect(tool.name).toBe('test_tool');
|
|
538
|
+
expect(tool.description).toBe('A test tool');
|
|
539
|
+
expect(tool.method).toBe('GET');
|
|
540
|
+
expect(tool.path).toBe('/test');
|
|
541
|
+
expect(tool.parameters['param'].type).toBe('string');
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
describe('OCPAPISpec', () => {
|
|
545
|
+
test('api spec creation', () => {
|
|
546
|
+
const tools = [
|
|
547
|
+
{
|
|
548
|
+
name: 'tool1',
|
|
549
|
+
description: 'Description 1',
|
|
550
|
+
method: 'GET',
|
|
551
|
+
path: '/path1',
|
|
552
|
+
parameters: {},
|
|
553
|
+
response_schema: undefined,
|
|
554
|
+
operation_id: undefined,
|
|
555
|
+
tags: [],
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
name: 'tool2',
|
|
559
|
+
description: 'Description 2',
|
|
560
|
+
method: 'POST',
|
|
561
|
+
path: '/path2',
|
|
562
|
+
parameters: {},
|
|
563
|
+
response_schema: undefined,
|
|
564
|
+
operation_id: undefined,
|
|
565
|
+
tags: [],
|
|
566
|
+
},
|
|
567
|
+
];
|
|
568
|
+
const apiSpec = {
|
|
569
|
+
title: 'Test API',
|
|
570
|
+
version: '1.0.0',
|
|
571
|
+
base_url: 'https://api.example.com',
|
|
572
|
+
description: 'A test API for testing purposes',
|
|
573
|
+
tools: tools,
|
|
574
|
+
raw_spec: {},
|
|
575
|
+
};
|
|
576
|
+
expect(apiSpec.title).toBe('Test API');
|
|
577
|
+
expect(apiSpec.version).toBe('1.0.0');
|
|
578
|
+
expect(apiSpec.base_url).toBe('https://api.example.com');
|
|
579
|
+
expect(apiSpec.description).toBe('A test API for testing purposes');
|
|
580
|
+
expect(apiSpec.tools.length).toBe(2);
|
|
581
|
+
expect(apiSpec.tools[0].name).toBe('tool1');
|
|
582
|
+
expect(apiSpec.tools[1].name).toBe('tool2');
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
describe('Swagger 2.0 Support', () => {
|
|
586
|
+
const swagger2Spec = {
|
|
587
|
+
swagger: '2.0',
|
|
588
|
+
info: {
|
|
589
|
+
title: 'Swagger 2.0 API',
|
|
590
|
+
version: '1.0.0',
|
|
591
|
+
description: 'A test API using Swagger 2.0'
|
|
592
|
+
},
|
|
593
|
+
host: 'api.example.com',
|
|
594
|
+
basePath: '/v1',
|
|
595
|
+
schemes: ['https'],
|
|
596
|
+
paths: {
|
|
597
|
+
'/users': {
|
|
598
|
+
get: {
|
|
599
|
+
operationId: 'getUsers',
|
|
600
|
+
summary: 'List users',
|
|
601
|
+
description: 'Get a list of all users',
|
|
602
|
+
parameters: [
|
|
603
|
+
{
|
|
604
|
+
name: 'limit',
|
|
605
|
+
in: 'query',
|
|
606
|
+
type: 'integer',
|
|
607
|
+
required: false
|
|
608
|
+
}
|
|
609
|
+
],
|
|
610
|
+
responses: {
|
|
611
|
+
'200': {
|
|
612
|
+
description: 'List of users',
|
|
613
|
+
schema: {
|
|
614
|
+
type: 'array',
|
|
615
|
+
items: {
|
|
616
|
+
type: 'object',
|
|
617
|
+
properties: {
|
|
618
|
+
id: { type: 'integer' },
|
|
619
|
+
name: { type: 'string' },
|
|
620
|
+
email: { type: 'string' }
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
},
|
|
627
|
+
post: {
|
|
628
|
+
operationId: 'createUser',
|
|
629
|
+
summary: 'Create user',
|
|
630
|
+
description: 'Create a new user',
|
|
631
|
+
parameters: [
|
|
632
|
+
{
|
|
633
|
+
name: 'body',
|
|
634
|
+
in: 'body',
|
|
635
|
+
required: true,
|
|
636
|
+
schema: {
|
|
637
|
+
type: 'object',
|
|
638
|
+
properties: {
|
|
639
|
+
name: { type: 'string' },
|
|
640
|
+
email: { type: 'string' }
|
|
641
|
+
},
|
|
642
|
+
required: ['name', 'email']
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
],
|
|
646
|
+
responses: {
|
|
647
|
+
'201': {
|
|
648
|
+
description: 'User created',
|
|
649
|
+
schema: {
|
|
650
|
+
type: 'object',
|
|
651
|
+
properties: {
|
|
652
|
+
id: { type: 'integer' },
|
|
653
|
+
name: { type: 'string' },
|
|
654
|
+
email: { type: 'string' }
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
},
|
|
661
|
+
'/users/{id}': {
|
|
662
|
+
get: {
|
|
663
|
+
operationId: 'getUserById',
|
|
664
|
+
summary: 'Get user',
|
|
665
|
+
description: 'Get a specific user by ID',
|
|
666
|
+
parameters: [
|
|
667
|
+
{
|
|
668
|
+
name: 'id',
|
|
669
|
+
in: 'path',
|
|
670
|
+
type: 'string',
|
|
671
|
+
required: true
|
|
672
|
+
}
|
|
673
|
+
],
|
|
674
|
+
responses: {
|
|
675
|
+
'200': {
|
|
676
|
+
description: 'User details',
|
|
677
|
+
schema: {
|
|
678
|
+
type: 'object',
|
|
679
|
+
properties: {
|
|
680
|
+
id: { type: 'integer' },
|
|
681
|
+
name: { type: 'string' },
|
|
682
|
+
email: { type: 'string' }
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
it('should detect Swagger 2.0 version', () => {
|
|
692
|
+
const discovery = new OCPSchemaDiscovery();
|
|
693
|
+
const version = discovery._detectSpecVersion(swagger2Spec);
|
|
694
|
+
expect(version).toBe('swagger_2');
|
|
695
|
+
});
|
|
696
|
+
it('should extract base URL from Swagger 2.0 (host + basePath + schemes)', () => {
|
|
697
|
+
const discovery = new OCPSchemaDiscovery();
|
|
698
|
+
discovery._specVersion = 'swagger_2';
|
|
699
|
+
const baseUrl = discovery._extractBaseUrl(swagger2Spec);
|
|
700
|
+
expect(baseUrl).toBe('https://api.example.com/v1');
|
|
701
|
+
});
|
|
702
|
+
it('should extract base URL with multiple schemes (uses first one)', () => {
|
|
703
|
+
const discovery = new OCPSchemaDiscovery();
|
|
704
|
+
const spec = {
|
|
705
|
+
swagger: '2.0',
|
|
706
|
+
host: 'api.example.com',
|
|
707
|
+
basePath: '/api',
|
|
708
|
+
schemes: ['http', 'https']
|
|
709
|
+
};
|
|
710
|
+
discovery._specVersion = 'swagger_2';
|
|
711
|
+
const baseUrl = discovery._extractBaseUrl(spec);
|
|
712
|
+
expect(baseUrl).toBe('http://api.example.com/api');
|
|
713
|
+
});
|
|
714
|
+
it('should default to https when no schemes', () => {
|
|
715
|
+
const discovery = new OCPSchemaDiscovery();
|
|
716
|
+
const spec = {
|
|
717
|
+
swagger: '2.0',
|
|
718
|
+
host: 'api.example.com',
|
|
719
|
+
basePath: '/v2'
|
|
720
|
+
};
|
|
721
|
+
discovery._specVersion = 'swagger_2';
|
|
722
|
+
const baseUrl = discovery._extractBaseUrl(spec);
|
|
723
|
+
expect(baseUrl).toBe('https://api.example.com/v2');
|
|
724
|
+
});
|
|
725
|
+
it('should parse Swagger 2.0 response schemas', () => {
|
|
726
|
+
const discovery = new OCPSchemaDiscovery();
|
|
727
|
+
discovery._specVersion = 'swagger_2';
|
|
728
|
+
const responses = swagger2Spec.paths['/users'].get.responses;
|
|
729
|
+
const schema = discovery._parseResponses(responses, swagger2Spec, {});
|
|
730
|
+
expect(schema).not.toBeNull();
|
|
731
|
+
expect(schema.type).toBe('array');
|
|
732
|
+
expect(schema.items).toBeDefined();
|
|
733
|
+
expect(schema.items.type).toBe('object');
|
|
734
|
+
});
|
|
735
|
+
it('should parse Swagger 2.0 body parameters', () => {
|
|
736
|
+
const discovery = new OCPSchemaDiscovery();
|
|
737
|
+
discovery._specVersion = 'swagger_2';
|
|
738
|
+
const postOperation = swagger2Spec.paths['/users'].post;
|
|
739
|
+
const bodyParam = postOperation.parameters[0];
|
|
740
|
+
const params = discovery._parseSwagger2BodyParameter(bodyParam, swagger2Spec, {});
|
|
741
|
+
expect(params.name).toBeDefined();
|
|
742
|
+
expect(params.email).toBeDefined();
|
|
743
|
+
expect(params.name.type).toBe('string');
|
|
744
|
+
expect(params.name.required).toBe(true);
|
|
745
|
+
expect(params.name.location).toBe('body');
|
|
746
|
+
expect(params.email.required).toBe(true);
|
|
747
|
+
});
|
|
748
|
+
it('should discover full API with Swagger 2.0 spec', async () => {
|
|
749
|
+
global.fetch = jest.fn(() => Promise.resolve({
|
|
750
|
+
ok: true,
|
|
751
|
+
json: async () => swagger2Spec,
|
|
752
|
+
}));
|
|
753
|
+
const discovery = new OCPSchemaDiscovery();
|
|
754
|
+
const apiSpec = await discovery.discoverApi('https://api.example.com/swagger.json');
|
|
755
|
+
expect(apiSpec.title).toBe('Swagger 2.0 API');
|
|
756
|
+
expect(apiSpec.version).toBe('1.0.0');
|
|
757
|
+
expect(apiSpec.base_url).toBe('https://api.example.com/v1');
|
|
758
|
+
expect(apiSpec.tools.length).toBe(3);
|
|
759
|
+
// Check GET /users
|
|
760
|
+
const getUsers = apiSpec.tools.find(t => t.name === 'getUsers');
|
|
761
|
+
expect(getUsers).toBeDefined();
|
|
762
|
+
expect(getUsers.method).toBe('GET');
|
|
763
|
+
expect(getUsers.path).toBe('/users');
|
|
764
|
+
expect(getUsers.parameters.limit).toBeDefined();
|
|
765
|
+
expect(getUsers.response_schema).toBeDefined();
|
|
766
|
+
expect(getUsers.response_schema.type).toBe('array');
|
|
767
|
+
// Check POST /users
|
|
768
|
+
const postUsers = apiSpec.tools.find(t => t.name === 'createUser');
|
|
769
|
+
expect(postUsers).toBeDefined();
|
|
770
|
+
expect(postUsers.method).toBe('POST');
|
|
771
|
+
expect(postUsers.path).toBe('/users');
|
|
772
|
+
expect(postUsers.parameters.name).toBeDefined();
|
|
773
|
+
expect(postUsers.parameters.email).toBeDefined();
|
|
774
|
+
expect(postUsers.parameters.name.required).toBe(true);
|
|
775
|
+
expect(postUsers.response_schema).toBeDefined();
|
|
776
|
+
// Check GET /users/{id}
|
|
777
|
+
const getUser = apiSpec.tools.find(t => t.name === 'getUserById');
|
|
778
|
+
expect(getUser).toBeDefined();
|
|
779
|
+
expect(getUser.method).toBe('GET');
|
|
780
|
+
expect(getUser.path).toBe('/users/{id}');
|
|
781
|
+
expect(getUser.parameters.id).toBeDefined();
|
|
782
|
+
expect(getUser.parameters.id.location).toBe('path');
|
|
783
|
+
expect(getUser.response_schema).toBeDefined();
|
|
784
|
+
});
|
|
785
|
+
});
|
|
786
|
+
describe('Resource Filtering', () => {
|
|
787
|
+
const openApiSpecWithResources = {
|
|
788
|
+
openapi: '3.0.0',
|
|
789
|
+
info: { title: 'GitHub API', version: '3.0' },
|
|
790
|
+
servers: [{ url: 'https://api.github.com' }],
|
|
791
|
+
paths: {
|
|
792
|
+
'/repos/{owner}/{repo}': {
|
|
793
|
+
get: {
|
|
794
|
+
operationId: 'repos/get',
|
|
795
|
+
summary: 'Get a repository',
|
|
796
|
+
parameters: [
|
|
797
|
+
{ name: 'owner', in: 'path', required: true, schema: { type: 'string' } },
|
|
798
|
+
{ name: 'repo', in: 'path', required: true, schema: { type: 'string' } }
|
|
799
|
+
],
|
|
800
|
+
responses: { '200': { description: 'Repository details' } }
|
|
801
|
+
}
|
|
802
|
+
},
|
|
803
|
+
'/user/repos': {
|
|
804
|
+
get: {
|
|
805
|
+
operationId: 'repos/listForAuthenticatedUser',
|
|
806
|
+
summary: 'List user repositories',
|
|
807
|
+
responses: { '200': { description: 'List of repositories' } }
|
|
808
|
+
}
|
|
809
|
+
},
|
|
810
|
+
'/repos/{owner}/{repo}/issues': {
|
|
811
|
+
get: {
|
|
812
|
+
operationId: 'issues/listForRepo',
|
|
813
|
+
summary: 'List repository issues',
|
|
814
|
+
parameters: [
|
|
815
|
+
{ name: 'owner', in: 'path', required: true, schema: { type: 'string' } },
|
|
816
|
+
{ name: 'repo', in: 'path', required: true, schema: { type: 'string' } }
|
|
817
|
+
],
|
|
818
|
+
responses: { '200': { description: 'List of issues' } }
|
|
819
|
+
}
|
|
820
|
+
},
|
|
821
|
+
'/orgs/{org}/members': {
|
|
822
|
+
get: {
|
|
823
|
+
operationId: 'orgs/listMembers',
|
|
824
|
+
summary: 'List organization members',
|
|
825
|
+
parameters: [
|
|
826
|
+
{ name: 'org', in: 'path', required: true, schema: { type: 'string' } }
|
|
827
|
+
],
|
|
828
|
+
responses: { '200': { description: 'List of members' } }
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
const toolsWithResources = [
|
|
834
|
+
{
|
|
835
|
+
name: 'reposGet',
|
|
836
|
+
description: 'Get a repository',
|
|
837
|
+
method: 'GET',
|
|
838
|
+
path: '/repos/{owner}/{repo}',
|
|
839
|
+
parameters: {},
|
|
840
|
+
operation_id: 'repos/get',
|
|
841
|
+
tags: ['repos']
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
name: 'reposListForAuthenticatedUser',
|
|
845
|
+
description: 'List user repositories',
|
|
846
|
+
method: 'GET',
|
|
847
|
+
path: '/user/repos',
|
|
848
|
+
parameters: {},
|
|
849
|
+
operation_id: 'repos/listForAuthenticatedUser',
|
|
850
|
+
tags: ['repos']
|
|
851
|
+
},
|
|
852
|
+
{
|
|
853
|
+
name: 'issuesListForRepo',
|
|
854
|
+
description: 'List repository issues',
|
|
855
|
+
method: 'GET',
|
|
856
|
+
path: '/repos/{owner}/{repo}/issues',
|
|
857
|
+
parameters: {},
|
|
858
|
+
operation_id: 'issues/listForRepo',
|
|
859
|
+
tags: ['issues']
|
|
860
|
+
},
|
|
861
|
+
{
|
|
862
|
+
name: 'orgsListMembers',
|
|
863
|
+
description: 'List organization members',
|
|
864
|
+
method: 'GET',
|
|
865
|
+
path: '/orgs/{org}/members',
|
|
866
|
+
parameters: {},
|
|
867
|
+
operation_id: 'orgs/listMembers',
|
|
868
|
+
tags: ['orgs']
|
|
869
|
+
}
|
|
870
|
+
];
|
|
871
|
+
test('_filterToolsByResources with single resource', () => {
|
|
872
|
+
const filtered = discovery._filterToolsByResources(toolsWithResources, ['repos']);
|
|
873
|
+
expect(filtered.length).toBe(2); // /repos/{owner}/{repo}, /repos/{owner}/{repo}/issues (NOT /user/repos)
|
|
874
|
+
const paths = new Set(filtered.map((tool) => tool.path));
|
|
875
|
+
expect(paths.has('/repos/{owner}/{repo}')).toBe(true);
|
|
876
|
+
expect(paths.has('/repos/{owner}/{repo}/issues')).toBe(true);
|
|
877
|
+
});
|
|
878
|
+
test('_filterToolsByResources with multiple resources', () => {
|
|
879
|
+
const filtered = discovery._filterToolsByResources(toolsWithResources, ['repos', 'orgs']);
|
|
880
|
+
expect(filtered.length).toBe(3); // /repos/..., /repos/.../issues, /orgs/... (NOT /user/repos)
|
|
881
|
+
});
|
|
882
|
+
test('_filterToolsByResources case insensitive', () => {
|
|
883
|
+
const filtered = discovery._filterToolsByResources(toolsWithResources, ['REPOS', 'Orgs']);
|
|
884
|
+
expect(filtered.length).toBe(3);
|
|
885
|
+
});
|
|
886
|
+
test('_filterToolsByResources with no matches', () => {
|
|
887
|
+
const filtered = discovery._filterToolsByResources(toolsWithResources, ['payments', 'customers']);
|
|
888
|
+
expect(filtered.length).toBe(0);
|
|
889
|
+
});
|
|
890
|
+
test('_filterToolsByResources with empty includeResources', () => {
|
|
891
|
+
const filtered = discovery._filterToolsByResources(toolsWithResources, []);
|
|
892
|
+
expect(filtered.length).toBe(4);
|
|
893
|
+
expect(filtered).toEqual(toolsWithResources);
|
|
894
|
+
});
|
|
895
|
+
test('_filterToolsByResources with undefined includeResources', () => {
|
|
896
|
+
const filtered = discovery._filterToolsByResources(toolsWithResources, undefined);
|
|
897
|
+
expect(filtered.length).toBe(4);
|
|
898
|
+
expect(filtered).toEqual(toolsWithResources);
|
|
899
|
+
});
|
|
900
|
+
test('_filterToolsByResources exact match', () => {
|
|
901
|
+
const tools = [
|
|
902
|
+
{ name: 'listPaymentMethods', description: 'List payment methods', method: 'GET', path: '/payment_methods', parameters: {} },
|
|
903
|
+
{ name: 'createPaymentIntent', description: 'Create payment intent', method: 'POST', path: '/payment_intents', parameters: {} },
|
|
904
|
+
{ name: 'listPayments', description: 'List payments', method: 'GET', path: '/payments', parameters: {} }
|
|
905
|
+
];
|
|
906
|
+
// Filter for "payment" should not match any (no exact segment match)
|
|
907
|
+
const filtered1 = discovery._filterToolsByResources(tools, ['payment']);
|
|
908
|
+
expect(filtered1.length).toBe(0); // "payment" doesn't exactly match any first segment
|
|
909
|
+
// Filter for "payments" should match the exact first segment
|
|
910
|
+
const filtered2 = discovery._filterToolsByResources(tools, ['payments']);
|
|
911
|
+
expect(filtered2.length).toBe(1);
|
|
912
|
+
expect(filtered2[0].path).toBe('/payments');
|
|
913
|
+
// Filter for "payment_methods" should match
|
|
914
|
+
const filtered3 = discovery._filterToolsByResources(tools, ['payment_methods']);
|
|
915
|
+
expect(filtered3.length).toBe(1);
|
|
916
|
+
expect(filtered3[0].path).toBe('/payment_methods');
|
|
917
|
+
});
|
|
918
|
+
test('_filterToolsByResources with dots', () => {
|
|
919
|
+
const tools = [
|
|
920
|
+
{ name: 'conversationsReplies', description: 'Get conversation replies', method: 'GET', path: '/conversations.replies', parameters: {} },
|
|
921
|
+
{ name: 'conversationsHistory', description: 'Get conversation history', method: 'GET', path: '/conversations.history', parameters: {} },
|
|
922
|
+
{ name: 'chatPostMessage', description: 'Post a message', method: 'POST', path: '/chat.postMessage', parameters: {} }
|
|
923
|
+
];
|
|
924
|
+
// Filter for "conversations" should match both conversation endpoints
|
|
925
|
+
const filtered1 = discovery._filterToolsByResources(tools, ['conversations']);
|
|
926
|
+
expect(filtered1.length).toBe(2);
|
|
927
|
+
expect(filtered1.every((tool) => tool.path.includes('conversations'))).toBe(true);
|
|
928
|
+
// Filter for "chat" should match the chat endpoint
|
|
929
|
+
const filtered2 = discovery._filterToolsByResources(tools, ['chat']);
|
|
930
|
+
expect(filtered2.length).toBe(1);
|
|
931
|
+
expect(filtered2[0].path).toBe('/chat.postMessage');
|
|
932
|
+
});
|
|
933
|
+
test('_filterToolsByResources no substring match', () => {
|
|
934
|
+
const tools = [
|
|
935
|
+
{ name: 'listRepos', description: 'List repos', method: 'GET', path: '/repos/{owner}/{repo}', parameters: {} },
|
|
936
|
+
{ name: 'listRepositories', description: 'List enterprise repositories', method: 'GET',
|
|
937
|
+
path: '/enterprises/{enterprise}/code-security/configurations/{config_id}/repositories', parameters: {} }
|
|
938
|
+
];
|
|
939
|
+
// Filter for "repos" should match "/repos/{owner}/{repo}"
|
|
940
|
+
// Should NOT match "/enterprises/.../repositories" (repos != repositories)
|
|
941
|
+
const filtered1 = discovery._filterToolsByResources(tools, ['repos']);
|
|
942
|
+
expect(filtered1.length).toBe(1);
|
|
943
|
+
expect(filtered1[0].path).toBe('/repos/{owner}/{repo}');
|
|
944
|
+
// Filter for "repositories" should not match (first segment is "enterprises")
|
|
945
|
+
const filtered2 = discovery._filterToolsByResources(tools, ['repositories']);
|
|
946
|
+
expect(filtered2.length).toBe(0);
|
|
947
|
+
// Filter for "enterprises" should match the enterprise endpoint
|
|
948
|
+
const filtered3 = discovery._filterToolsByResources(tools, ['enterprises']);
|
|
949
|
+
expect(filtered3.length).toBe(1);
|
|
950
|
+
expect(filtered3[0].path.includes('/enterprises')).toBe(true);
|
|
951
|
+
});
|
|
952
|
+
test('_filterToolsByResources with path prefix', () => {
|
|
953
|
+
const tools = [
|
|
954
|
+
{ name: 'listPayments', description: 'List payments', method: 'GET', path: '/v1/payments', parameters: {} },
|
|
955
|
+
{ name: 'createCharge', description: 'Create charge', method: 'POST', path: '/v1/charges', parameters: {} },
|
|
956
|
+
{ name: 'legacyPayment', description: 'Legacy payment', method: 'GET', path: '/v2/payments', parameters: {} }
|
|
957
|
+
];
|
|
958
|
+
// Filter for "payments" with /v1 prefix
|
|
959
|
+
const filtered1 = discovery._filterToolsByResources(tools, ['payments'], '/v1');
|
|
960
|
+
expect(filtered1.length).toBe(1);
|
|
961
|
+
expect(filtered1[0].path).toBe('/v1/payments');
|
|
962
|
+
// Filter for "payments" with /v2 prefix
|
|
963
|
+
const filtered2 = discovery._filterToolsByResources(tools, ['payments'], '/v2');
|
|
964
|
+
expect(filtered2.length).toBe(1);
|
|
965
|
+
expect(filtered2[0].path).toBe('/v2/payments');
|
|
966
|
+
// Filter without prefix - no matches (first segment is "v1" or "v2")
|
|
967
|
+
const filtered3 = discovery._filterToolsByResources(tools, ['payments']);
|
|
968
|
+
expect(filtered3.length).toBe(0);
|
|
969
|
+
});
|
|
970
|
+
test('_filterToolsByResources first segment only', () => {
|
|
971
|
+
const tools = [
|
|
972
|
+
{ name: 'listRepoIssues', description: 'List repo issues', method: 'GET', path: '/repos/{owner}/{repo}/issues', parameters: {} },
|
|
973
|
+
{ name: 'listUserRepos', description: 'List user repos', method: 'GET', path: '/user/repos', parameters: {} }
|
|
974
|
+
];
|
|
975
|
+
// Filter for "repos" - should match /repos/... but NOT /user/repos (first segment is "user")
|
|
976
|
+
const filtered1 = discovery._filterToolsByResources(tools, ['repos']);
|
|
977
|
+
expect(filtered1.length).toBe(1);
|
|
978
|
+
expect(filtered1[0].path).toBe('/repos/{owner}/{repo}/issues');
|
|
979
|
+
// Filter for "user" - should match /user/repos
|
|
980
|
+
const filtered2 = discovery._filterToolsByResources(tools, ['user']);
|
|
981
|
+
expect(filtered2.length).toBe(1);
|
|
982
|
+
expect(filtered2[0].path).toBe('/user/repos');
|
|
983
|
+
// Filter for "issues" - should NOT match anything (issues is not first segment)
|
|
984
|
+
const filtered3 = discovery._filterToolsByResources(tools, ['issues']);
|
|
985
|
+
expect(filtered3.length).toBe(0);
|
|
986
|
+
});
|
|
987
|
+
test('discoverApi with includeResources parameter', async () => {
|
|
988
|
+
const mockFetch = global.fetch;
|
|
989
|
+
mockFetch.mockResolvedValueOnce({
|
|
990
|
+
ok: true,
|
|
991
|
+
json: async () => openApiSpecWithResources,
|
|
992
|
+
});
|
|
993
|
+
const apiSpec = await discovery.discoverApi('https://api.github.com/openapi.json', undefined, ['repos']);
|
|
994
|
+
expect(apiSpec.tools.length).toBe(2);
|
|
995
|
+
expect(apiSpec.tools.every(tool => tool.path.toLowerCase().startsWith('/repos'))).toBe(true);
|
|
996
|
+
});
|
|
997
|
+
test('discoverApi with multiple includeResources', async () => {
|
|
998
|
+
const mockFetch = global.fetch;
|
|
999
|
+
mockFetch.mockResolvedValueOnce({
|
|
1000
|
+
ok: true,
|
|
1001
|
+
json: async () => openApiSpecWithResources,
|
|
1002
|
+
});
|
|
1003
|
+
const apiSpec = await discovery.discoverApi('https://api.github.com/openapi.json', undefined, ['repos', 'orgs']);
|
|
1004
|
+
expect(apiSpec.tools.length).toBe(3);
|
|
1005
|
+
});
|
|
1006
|
+
test('discoverApi without includeResources returns all tools', async () => {
|
|
1007
|
+
const mockFetch = global.fetch;
|
|
1008
|
+
mockFetch.mockResolvedValueOnce({
|
|
1009
|
+
ok: true,
|
|
1010
|
+
json: async () => openApiSpecWithResources,
|
|
1011
|
+
});
|
|
1012
|
+
const apiSpec = await discovery.discoverApi('https://api.github.com/openapi.json');
|
|
1013
|
+
expect(apiSpec.tools.length).toBe(4);
|
|
1014
|
+
});
|
|
1015
|
+
});
|
|
1016
|
+
describe('Local File Loading', () => {
|
|
1017
|
+
test('load spec from absolute path (JSON)', async () => {
|
|
1018
|
+
const absolutePath = `${__dirname}/fixtures/test_spec.json`;
|
|
1019
|
+
const apiSpec = await discovery.discoverApi(absolutePath);
|
|
1020
|
+
expect(apiSpec.title).toBe('Test API from File');
|
|
1021
|
+
expect(apiSpec.version).toBe('1.0.0');
|
|
1022
|
+
expect(apiSpec.base_url).toBe('https://api.example.com');
|
|
1023
|
+
expect(apiSpec.tools.length).toBe(1);
|
|
1024
|
+
expect(apiSpec.tools[0].name).toBe('getTest');
|
|
1025
|
+
});
|
|
1026
|
+
test('load spec from relative path (JSON)', async () => {
|
|
1027
|
+
const relativePath = './tests/fixtures/test_spec.json';
|
|
1028
|
+
const apiSpec = await discovery.discoverApi(relativePath);
|
|
1029
|
+
expect(apiSpec.title).toBe('Test API from File');
|
|
1030
|
+
expect(apiSpec.tools.length).toBe(1);
|
|
1031
|
+
});
|
|
1032
|
+
test('load spec from absolute path (YAML)', async () => {
|
|
1033
|
+
const absolutePath = `${__dirname}/fixtures/test_spec.yaml`;
|
|
1034
|
+
const apiSpec = await discovery.discoverApi(absolutePath);
|
|
1035
|
+
expect(apiSpec.title).toBe('Test API from File');
|
|
1036
|
+
expect(apiSpec.version).toBe('1.0.0');
|
|
1037
|
+
expect(apiSpec.base_url).toBe('https://api.example.com');
|
|
1038
|
+
expect(apiSpec.tools.length).toBe(1);
|
|
1039
|
+
expect(apiSpec.tools[0].name).toBe('getTest');
|
|
1040
|
+
});
|
|
1041
|
+
test('load spec from relative path (YAML)', async () => {
|
|
1042
|
+
const relativePath = './tests/fixtures/test_spec.yaml';
|
|
1043
|
+
const apiSpec = await discovery.discoverApi(relativePath);
|
|
1044
|
+
expect(apiSpec.title).toBe('Test API from File');
|
|
1045
|
+
expect(apiSpec.tools.length).toBe(1);
|
|
1046
|
+
});
|
|
1047
|
+
test('error on file not found', async () => {
|
|
1048
|
+
await expect(discovery.discoverApi('./nonexistent.json')).rejects.toThrow('File not found');
|
|
1049
|
+
});
|
|
1050
|
+
test('error on unsupported file format', async () => {
|
|
1051
|
+
await expect(discovery.discoverApi('./some/file.txt')).rejects.toThrow('Unsupported file format');
|
|
1052
|
+
});
|
|
1053
|
+
test('error on invalid JSON', async () => {
|
|
1054
|
+
const invalidJsonPath = `${__dirname}/fixtures/invalid.json`;
|
|
1055
|
+
await expect(discovery.discoverApi(invalidJsonPath)).rejects.toThrow('Invalid JSON');
|
|
1056
|
+
});
|
|
1057
|
+
test('error on invalid YAML', async () => {
|
|
1058
|
+
const invalidYamlPath = `${__dirname}/fixtures/invalid.yaml`;
|
|
1059
|
+
await expect(discovery.discoverApi(invalidYamlPath)).rejects.toThrow('Invalid YAML');
|
|
1060
|
+
});
|
|
1061
|
+
test('cache normalization for file paths', async () => {
|
|
1062
|
+
// Load via absolute path
|
|
1063
|
+
const absolutePath = `${__dirname}/fixtures/test_spec.json`;
|
|
1064
|
+
const apiSpec1 = await discovery.discoverApi(absolutePath);
|
|
1065
|
+
// Load via relative path (should hit cache)
|
|
1066
|
+
const relativePath = './tests/fixtures/test_spec.json';
|
|
1067
|
+
const apiSpec2 = await discovery.discoverApi(relativePath);
|
|
1068
|
+
// Both should resolve to same spec (from cache)
|
|
1069
|
+
expect(apiSpec1.name).toBe(apiSpec2.name);
|
|
1070
|
+
expect(apiSpec1.tools.length).toBe(apiSpec2.tools.length);
|
|
1071
|
+
});
|
|
1072
|
+
});
|
|
1073
|
+
});
|
|
1074
|
+
//# sourceMappingURL=schema_discovery.test.js.map
|