@optimizely-opal/opal-tool-ocp-sdk 1.0.0-OCP-1442.6 → 1.0.0-OCP-1449.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/README.md +114 -72
- package/dist/auth/AuthUtils.d.ts +12 -5
- package/dist/auth/AuthUtils.d.ts.map +1 -1
- package/dist/auth/AuthUtils.js +80 -25
- package/dist/auth/AuthUtils.js.map +1 -1
- package/dist/auth/AuthUtils.test.js +161 -117
- package/dist/auth/AuthUtils.test.js.map +1 -1
- package/dist/function/GlobalToolFunction.d.ts +1 -1
- package/dist/function/GlobalToolFunction.d.ts.map +1 -1
- package/dist/function/GlobalToolFunction.js +17 -4
- package/dist/function/GlobalToolFunction.js.map +1 -1
- package/dist/function/GlobalToolFunction.test.js +54 -8
- package/dist/function/GlobalToolFunction.test.js.map +1 -1
- package/dist/function/ToolFunction.d.ts +1 -1
- package/dist/function/ToolFunction.d.ts.map +1 -1
- package/dist/function/ToolFunction.js +24 -4
- package/dist/function/ToolFunction.js.map +1 -1
- package/dist/function/ToolFunction.test.js +260 -8
- package/dist/function/ToolFunction.test.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/logging/ToolLogger.d.ts.map +1 -1
- package/dist/logging/ToolLogger.js +2 -1
- package/dist/logging/ToolLogger.js.map +1 -1
- package/dist/logging/ToolLogger.test.js +114 -2
- package/dist/logging/ToolLogger.test.js.map +1 -1
- package/dist/service/Service.d.ts +88 -2
- package/dist/service/Service.d.ts.map +1 -1
- package/dist/service/Service.js +227 -55
- package/dist/service/Service.js.map +1 -1
- package/dist/service/Service.test.js +464 -36
- package/dist/service/Service.test.js.map +1 -1
- package/dist/types/ToolError.d.ts +59 -0
- package/dist/types/ToolError.d.ts.map +1 -0
- package/dist/types/ToolError.js +79 -0
- package/dist/types/ToolError.js.map +1 -0
- package/dist/types/ToolError.test.d.ts +2 -0
- package/dist/types/ToolError.test.d.ts.map +1 -0
- package/dist/types/ToolError.test.js +161 -0
- package/dist/types/ToolError.test.js.map +1 -0
- package/dist/validation/ParameterValidator.d.ts +5 -16
- package/dist/validation/ParameterValidator.d.ts.map +1 -1
- package/dist/validation/ParameterValidator.js +10 -3
- package/dist/validation/ParameterValidator.js.map +1 -1
- package/dist/validation/ParameterValidator.test.js +186 -146
- package/dist/validation/ParameterValidator.test.js.map +1 -1
- package/package.json +1 -1
- package/src/auth/AuthUtils.test.ts +176 -157
- package/src/auth/AuthUtils.ts +96 -33
- package/src/function/GlobalToolFunction.test.ts +54 -8
- package/src/function/GlobalToolFunction.ts +26 -6
- package/src/function/ToolFunction.test.ts +274 -8
- package/src/function/ToolFunction.ts +33 -7
- package/src/index.ts +1 -0
- package/src/logging/ToolLogger.test.ts +118 -2
- package/src/logging/ToolLogger.ts +2 -1
- package/src/service/Service.test.ts +577 -34
- package/src/service/Service.ts +286 -54
- package/src/types/ToolError.test.ts +192 -0
- package/src/types/ToolError.ts +95 -0
- package/src/validation/ParameterValidator.test.ts +185 -158
- package/src/validation/ParameterValidator.ts +17 -20
|
@@ -1,46 +1,80 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
|
3
4
|
const Service_1 = require("./Service");
|
|
4
5
|
const Models_1 = require("../types/Models");
|
|
6
|
+
const ToolError_1 = require("../types/ToolError");
|
|
5
7
|
const ToolFunction_1 = require("../function/ToolFunction");
|
|
6
8
|
const app_sdk_1 = require("@zaiusinc/app-sdk");
|
|
7
9
|
// Mock the logger and other app-sdk exports
|
|
8
|
-
jest.mock('@zaiusinc/app-sdk', () =>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
jest.mock('@zaiusinc/app-sdk', () => {
|
|
11
|
+
const mockKvStoreInstance = {
|
|
12
|
+
get: jest.fn(),
|
|
13
|
+
put: jest.fn(),
|
|
14
|
+
patch: jest.fn(),
|
|
15
|
+
delete: jest.fn()
|
|
16
|
+
};
|
|
17
|
+
const mockApp = {
|
|
18
|
+
storage: {
|
|
19
|
+
kvStore: mockKvStoreInstance
|
|
20
|
+
},
|
|
21
|
+
Response: jest.fn().mockImplementation((status, data, headers) => ({
|
|
22
|
+
status,
|
|
23
|
+
data,
|
|
24
|
+
bodyJSON: data,
|
|
25
|
+
bodyAsU8Array: new Uint8Array(),
|
|
26
|
+
headers: headers || { get: jest.fn(), has: jest.fn(), set: jest.fn() }
|
|
27
|
+
})),
|
|
28
|
+
Function: class {
|
|
29
|
+
request;
|
|
30
|
+
constructor(request) {
|
|
31
|
+
this.request = request;
|
|
32
|
+
}
|
|
16
33
|
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
...mockApp,
|
|
37
|
+
default: mockApp,
|
|
38
|
+
App: mockApp,
|
|
39
|
+
getAppContext: jest.fn().mockReturnValue({
|
|
40
|
+
manifest: { meta: { version: 'app-v1' } }
|
|
41
|
+
}),
|
|
42
|
+
logger: {
|
|
43
|
+
error: jest.fn(),
|
|
44
|
+
info: jest.fn(),
|
|
45
|
+
warn: jest.fn(),
|
|
46
|
+
debug: jest.fn()
|
|
47
|
+
},
|
|
48
|
+
Headers: class {
|
|
49
|
+
constructor(headers = []) {
|
|
50
|
+
this.headers = {};
|
|
51
|
+
headers.forEach(([name, value]) => {
|
|
52
|
+
this.headers[name.toLowerCase()] = value;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
headers;
|
|
56
|
+
set(name, value) {
|
|
22
57
|
this.headers[name.toLowerCase()] = value;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}));
|
|
58
|
+
}
|
|
59
|
+
get(name) {
|
|
60
|
+
return this.headers[name.toLowerCase()];
|
|
61
|
+
}
|
|
62
|
+
has(name) {
|
|
63
|
+
return name.toLowerCase() in this.headers;
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
Response: jest.fn().mockImplementation((status, data, headers) => ({
|
|
67
|
+
status,
|
|
68
|
+
data,
|
|
69
|
+
bodyJSON: data,
|
|
70
|
+
bodyAsU8Array: new Uint8Array(),
|
|
71
|
+
headers: headers || { get: jest.fn(), has: jest.fn(), set: jest.fn() }
|
|
72
|
+
}))
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
// Get the mocked kvStore for use in tests
|
|
76
|
+
const { storage } = jest.requireMock('@zaiusinc/app-sdk');
|
|
77
|
+
const mockKvStore = storage.kvStore;
|
|
44
78
|
describe('ToolsService', () => {
|
|
45
79
|
let mockTool;
|
|
46
80
|
let mockInteraction;
|
|
@@ -262,12 +296,19 @@ describe('ToolsService', () => {
|
|
|
262
296
|
expect(mockTool.handler).toHaveBeenCalledWith(mockToolFunction, // functionContext
|
|
263
297
|
{ param1: 'test-value' }, undefined);
|
|
264
298
|
});
|
|
265
|
-
it('should return 500 error when tool handler throws
|
|
299
|
+
it('should return 500 error in RFC 9457 format when tool handler throws a regular error', async () => {
|
|
266
300
|
const errorMessage = 'Tool execution failed';
|
|
267
301
|
jest.mocked(mockTool.handler).mockRejectedValueOnce(new Error(errorMessage));
|
|
268
302
|
const mockRequest = createMockRequest();
|
|
269
303
|
const response = await Service_1.toolsService.processRequest(mockRequest, mockToolFunction);
|
|
270
304
|
expect(response.status).toBe(500);
|
|
305
|
+
expect(response.bodyJSON).toEqual({
|
|
306
|
+
title: 'Internal Server Error',
|
|
307
|
+
status: 500,
|
|
308
|
+
detail: errorMessage,
|
|
309
|
+
instance: mockTool.endpoint
|
|
310
|
+
});
|
|
311
|
+
expect(response.headers.get('content-type')).toBe('application/problem+json');
|
|
271
312
|
expect(app_sdk_1.logger.error).toHaveBeenCalledWith(`Error in function ${mockTool.name}:`, expect.any(Error));
|
|
272
313
|
});
|
|
273
314
|
it('should return 500 error with generic message when error has no message', async () => {
|
|
@@ -275,6 +316,55 @@ describe('ToolsService', () => {
|
|
|
275
316
|
const mockRequest = createMockRequest();
|
|
276
317
|
const response = await Service_1.toolsService.processRequest(mockRequest, mockToolFunction);
|
|
277
318
|
expect(response.status).toBe(500);
|
|
319
|
+
expect(response.bodyJSON).toEqual({
|
|
320
|
+
title: 'Internal Server Error',
|
|
321
|
+
status: 500,
|
|
322
|
+
detail: 'An unexpected error occurred',
|
|
323
|
+
instance: mockTool.endpoint
|
|
324
|
+
});
|
|
325
|
+
expect(response.headers.get('content-type')).toBe('application/problem+json');
|
|
326
|
+
});
|
|
327
|
+
it('should return custom status code when tool handler throws ToolError', async () => {
|
|
328
|
+
const toolError = new ToolError_1.ToolError('Resource not found', 404, 'The requested task does not exist');
|
|
329
|
+
jest.mocked(mockTool.handler).mockRejectedValueOnce(toolError);
|
|
330
|
+
const mockRequest = createMockRequest();
|
|
331
|
+
const response = await Service_1.toolsService.processRequest(mockRequest, mockToolFunction);
|
|
332
|
+
expect(response.status).toBe(404);
|
|
333
|
+
expect(response.bodyJSON).toEqual({
|
|
334
|
+
title: 'Resource not found',
|
|
335
|
+
status: 404,
|
|
336
|
+
detail: 'The requested task does not exist',
|
|
337
|
+
instance: mockTool.endpoint
|
|
338
|
+
});
|
|
339
|
+
expect(response.headers.get('content-type')).toBe('application/problem+json');
|
|
340
|
+
expect(app_sdk_1.logger.error).toHaveBeenCalledWith(`Error in function ${mockTool.name}:`, expect.any(ToolError_1.ToolError));
|
|
341
|
+
});
|
|
342
|
+
it('should return ToolError without detail field when detail is not provided', async () => {
|
|
343
|
+
const toolError = new ToolError_1.ToolError('Bad request', 400);
|
|
344
|
+
jest.mocked(mockTool.handler).mockRejectedValueOnce(toolError);
|
|
345
|
+
const mockRequest = createMockRequest();
|
|
346
|
+
const response = await Service_1.toolsService.processRequest(mockRequest, mockToolFunction);
|
|
347
|
+
expect(response.status).toBe(400);
|
|
348
|
+
expect(response.bodyJSON).toEqual({
|
|
349
|
+
title: 'Bad request',
|
|
350
|
+
status: 400,
|
|
351
|
+
instance: mockTool.endpoint
|
|
352
|
+
});
|
|
353
|
+
expect(response.bodyJSON).not.toHaveProperty('detail');
|
|
354
|
+
expect(response.headers.get('content-type')).toBe('application/problem+json');
|
|
355
|
+
});
|
|
356
|
+
it('should default to 500 when ToolError is created without status', async () => {
|
|
357
|
+
const toolError = new ToolError_1.ToolError('Database error');
|
|
358
|
+
jest.mocked(mockTool.handler).mockRejectedValueOnce(toolError);
|
|
359
|
+
const mockRequest = createMockRequest();
|
|
360
|
+
const response = await Service_1.toolsService.processRequest(mockRequest, mockToolFunction);
|
|
361
|
+
expect(response.status).toBe(500);
|
|
362
|
+
expect(response.bodyJSON).toEqual({
|
|
363
|
+
title: 'Database error',
|
|
364
|
+
status: 500,
|
|
365
|
+
instance: mockTool.endpoint
|
|
366
|
+
});
|
|
367
|
+
expect(response.headers.get('content-type')).toBe('application/problem+json');
|
|
278
368
|
});
|
|
279
369
|
});
|
|
280
370
|
describe('interaction execution', () => {
|
|
@@ -340,7 +430,7 @@ describe('ToolsService', () => {
|
|
|
340
430
|
auth: authData
|
|
341
431
|
}, authData);
|
|
342
432
|
});
|
|
343
|
-
it('should return 500 error when interaction handler throws
|
|
433
|
+
it('should return 500 error in RFC 9457 format when interaction handler throws a regular error', async () => {
|
|
344
434
|
const errorMessage = 'Interaction execution failed';
|
|
345
435
|
jest.mocked(mockInteraction.handler).mockRejectedValueOnce(new Error(errorMessage));
|
|
346
436
|
const interactionRequest = createMockRequest({
|
|
@@ -349,8 +439,33 @@ describe('ToolsService', () => {
|
|
|
349
439
|
});
|
|
350
440
|
const response = await Service_1.toolsService.processRequest(interactionRequest, mockToolFunction);
|
|
351
441
|
expect(response.status).toBe(500);
|
|
442
|
+
expect(response.bodyJSON).toEqual({
|
|
443
|
+
title: 'Internal Server Error',
|
|
444
|
+
status: 500,
|
|
445
|
+
detail: errorMessage,
|
|
446
|
+
instance: mockInteraction.endpoint
|
|
447
|
+
});
|
|
448
|
+
expect(response.headers.get('content-type')).toBe('application/problem+json');
|
|
352
449
|
expect(app_sdk_1.logger.error).toHaveBeenCalledWith(`Error in function ${mockInteraction.name}:`, expect.any(Error));
|
|
353
450
|
});
|
|
451
|
+
it('should return custom status code when interaction handler throws ToolError', async () => {
|
|
452
|
+
const toolError = new ToolError_1.ToolError('Webhook validation failed', 400, 'Invalid signature');
|
|
453
|
+
jest.mocked(mockInteraction.handler).mockRejectedValueOnce(toolError);
|
|
454
|
+
const interactionRequest = createMockRequest({
|
|
455
|
+
path: '/test-interaction',
|
|
456
|
+
bodyJSON: { data: { param1: 'test-value' } }
|
|
457
|
+
});
|
|
458
|
+
const response = await Service_1.toolsService.processRequest(interactionRequest, mockToolFunction);
|
|
459
|
+
expect(response.status).toBe(400);
|
|
460
|
+
expect(response.bodyJSON).toEqual({
|
|
461
|
+
title: 'Webhook validation failed',
|
|
462
|
+
status: 400,
|
|
463
|
+
detail: 'Invalid signature',
|
|
464
|
+
instance: mockInteraction.endpoint
|
|
465
|
+
});
|
|
466
|
+
expect(response.headers.get('content-type')).toBe('application/problem+json');
|
|
467
|
+
expect(app_sdk_1.logger.error).toHaveBeenCalledWith(`Error in function ${mockInteraction.name}:`, expect.any(ToolError_1.ToolError));
|
|
468
|
+
});
|
|
354
469
|
});
|
|
355
470
|
describe('error cases', () => {
|
|
356
471
|
it('should return 404 when no matching tool or interaction is found', async () => {
|
|
@@ -531,5 +646,318 @@ describe('ToolsService', () => {
|
|
|
531
646
|
});
|
|
532
647
|
});
|
|
533
648
|
});
|
|
649
|
+
describe('Override functionality', () => {
|
|
650
|
+
beforeEach(() => {
|
|
651
|
+
// Reset KV store mocks
|
|
652
|
+
mockKvStore.get.mockReset();
|
|
653
|
+
mockKvStore.put.mockReset();
|
|
654
|
+
mockKvStore.patch.mockReset();
|
|
655
|
+
mockKvStore.delete.mockReset();
|
|
656
|
+
// Register some test tools for override testing
|
|
657
|
+
Service_1.toolsService.registerTool('search_tool', 'Search for information', jest.fn().mockResolvedValue({ result: 'search success' }), [new Models_1.Parameter('query', Models_1.ParameterType.String, 'Search query', true)], '/search');
|
|
658
|
+
Service_1.toolsService.registerTool('calculator', 'Perform calculations', jest.fn().mockResolvedValue({ result: 'calculation success' }), [
|
|
659
|
+
new Models_1.Parameter('operation', Models_1.ParameterType.String, 'Math operation', true),
|
|
660
|
+
new Models_1.Parameter('numbers', Models_1.ParameterType.String, 'Numbers to calculate', true)
|
|
661
|
+
], '/calculate');
|
|
662
|
+
// Mock ToolFunction constructor name for getAppVersionId and getFunctionName
|
|
663
|
+
Object.defineProperty(mockToolFunction.constructor, 'name', {
|
|
664
|
+
value: 'TestFunction',
|
|
665
|
+
configurable: true
|
|
666
|
+
});
|
|
667
|
+
});
|
|
668
|
+
describe('Discovery endpoint with overrides', () => {
|
|
669
|
+
it('should return original functions when no overrides exist', async () => {
|
|
670
|
+
// Mock KV store to return no overrides
|
|
671
|
+
mockKvStore.get.mockResolvedValue(null);
|
|
672
|
+
const request = createMockRequest({ path: '/discovery', method: 'GET' });
|
|
673
|
+
const response = await Service_1.toolsService.processRequest(request, mockToolFunction);
|
|
674
|
+
expect(response.status).toBe(200);
|
|
675
|
+
expect(mockKvStore.get).toHaveBeenCalledWith('app-v1:TestFunction:opal-tools-overrides');
|
|
676
|
+
const responseData = response.bodyJSON;
|
|
677
|
+
expect(responseData.functions).toHaveLength(2);
|
|
678
|
+
const searchTool = responseData.functions.find((f) => f.name === 'search_tool');
|
|
679
|
+
expect(searchTool.description).toBe('Search for information');
|
|
680
|
+
const calculator = responseData.functions.find((f) => f.name === 'calculator');
|
|
681
|
+
expect(calculator.description).toBe('Perform calculations');
|
|
682
|
+
});
|
|
683
|
+
it('should apply overrides when they exist', async () => {
|
|
684
|
+
// Mock KV store to return overrides in the new map format
|
|
685
|
+
const storedOverrides = {
|
|
686
|
+
search_tool: {
|
|
687
|
+
name: 'search_tool',
|
|
688
|
+
description: 'Enhanced search with AI capabilities',
|
|
689
|
+
parameters: [
|
|
690
|
+
{
|
|
691
|
+
name: 'query',
|
|
692
|
+
description: 'AI-powered search query'
|
|
693
|
+
}
|
|
694
|
+
]
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
mockKvStore.get.mockResolvedValue(storedOverrides);
|
|
698
|
+
const request = createMockRequest({ path: '/discovery', method: 'GET' });
|
|
699
|
+
const response = await Service_1.toolsService.processRequest(request, mockToolFunction);
|
|
700
|
+
expect(response.status).toBe(200);
|
|
701
|
+
const responseData = response.bodyJSON;
|
|
702
|
+
expect(responseData.functions).toHaveLength(2);
|
|
703
|
+
// Check that search_tool has overridden description and parameters
|
|
704
|
+
const searchTool = responseData.functions.find((f) => f.name === 'search_tool');
|
|
705
|
+
expect(searchTool.description).toBe('Enhanced search with AI capabilities');
|
|
706
|
+
expect(searchTool.parameters).toHaveLength(1); // Only the original 'query' parameter
|
|
707
|
+
expect(searchTool.parameters[0].description).toBe('AI-powered search query');
|
|
708
|
+
// Check that calculator remains unchanged
|
|
709
|
+
const calculator = responseData.functions.find((f) => f.name === 'calculator');
|
|
710
|
+
expect(calculator.description).toBe('Perform calculations');
|
|
711
|
+
expect(calculator.parameters).toHaveLength(2);
|
|
712
|
+
});
|
|
713
|
+
it('should handle KV store errors gracefully', async () => {
|
|
714
|
+
// Mock KV store to throw an error
|
|
715
|
+
mockKvStore.get.mockRejectedValue(new Error('KV store unavailable'));
|
|
716
|
+
const request = createMockRequest({ path: '/discovery', method: 'GET' });
|
|
717
|
+
const response = await Service_1.toolsService.processRequest(request, mockToolFunction);
|
|
718
|
+
expect(response.status).toBe(200);
|
|
719
|
+
// Should return original functions despite the error
|
|
720
|
+
const responseData = response.bodyJSON;
|
|
721
|
+
expect(responseData.functions).toHaveLength(2);
|
|
722
|
+
});
|
|
723
|
+
it('should handle KV store errors gracefully when getting overrides fails', async () => {
|
|
724
|
+
// Mock KV store to throw an error when getting overrides
|
|
725
|
+
mockKvStore.get.mockRejectedValue(new Error('KV store connection failed'));
|
|
726
|
+
const request = createMockRequest({ path: '/discovery', method: 'GET' });
|
|
727
|
+
const response = await Service_1.toolsService.processRequest(request, mockToolFunction);
|
|
728
|
+
expect(response.status).toBe(200);
|
|
729
|
+
// Should return original functions despite the error
|
|
730
|
+
const responseData = response.bodyJSON;
|
|
731
|
+
expect(responseData.functions).toHaveLength(2);
|
|
732
|
+
});
|
|
733
|
+
});
|
|
734
|
+
describe('PATCH /overrides endpoint', () => {
|
|
735
|
+
it('should save new overrides successfully', async () => {
|
|
736
|
+
const overrideData = {
|
|
737
|
+
functions: [
|
|
738
|
+
{
|
|
739
|
+
name: 'search_tool',
|
|
740
|
+
description: 'Enhanced search functionality',
|
|
741
|
+
parameters: [
|
|
742
|
+
{
|
|
743
|
+
name: 'query',
|
|
744
|
+
type: 'string',
|
|
745
|
+
description: 'Search query',
|
|
746
|
+
required: true
|
|
747
|
+
}
|
|
748
|
+
]
|
|
749
|
+
}
|
|
750
|
+
]
|
|
751
|
+
};
|
|
752
|
+
const request = createMockRequest({
|
|
753
|
+
path: '/overrides',
|
|
754
|
+
method: 'PATCH',
|
|
755
|
+
bodyJSON: overrideData,
|
|
756
|
+
body: JSON.stringify(overrideData)
|
|
757
|
+
});
|
|
758
|
+
const response = await Service_1.toolsService.processRequest(request, mockToolFunction);
|
|
759
|
+
expect(response.status).toBe(200);
|
|
760
|
+
expect(mockKvStore.patch).toHaveBeenCalledWith('app-v1:TestFunction:opal-tools-overrides', {
|
|
761
|
+
search_tool: {
|
|
762
|
+
name: 'search_tool',
|
|
763
|
+
description: 'Enhanced search functionality',
|
|
764
|
+
parameters: [
|
|
765
|
+
{
|
|
766
|
+
name: 'query',
|
|
767
|
+
type: 'string',
|
|
768
|
+
description: 'Search query',
|
|
769
|
+
required: true
|
|
770
|
+
}
|
|
771
|
+
]
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
it('should merge with existing overrides', async () => {
|
|
776
|
+
const newOverrideData = {
|
|
777
|
+
functions: [
|
|
778
|
+
{
|
|
779
|
+
name: 'new_tool',
|
|
780
|
+
description: 'New tool',
|
|
781
|
+
parameters: []
|
|
782
|
+
}
|
|
783
|
+
]
|
|
784
|
+
};
|
|
785
|
+
const request = createMockRequest({
|
|
786
|
+
path: '/overrides',
|
|
787
|
+
method: 'PATCH',
|
|
788
|
+
bodyJSON: newOverrideData,
|
|
789
|
+
body: JSON.stringify(newOverrideData)
|
|
790
|
+
});
|
|
791
|
+
const response = await Service_1.toolsService.processRequest(request, mockToolFunction);
|
|
792
|
+
expect(response.status).toBe(200);
|
|
793
|
+
// Verify that the new tool is saved using patch (which handles merging automatically)
|
|
794
|
+
expect(mockKvStore.patch).toHaveBeenCalledWith('app-v1:TestFunction:opal-tools-overrides', {
|
|
795
|
+
new_tool: {
|
|
796
|
+
name: 'new_tool',
|
|
797
|
+
description: 'New tool',
|
|
798
|
+
parameters: []
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
});
|
|
802
|
+
it('should update existing tool when saving override with same name', async () => {
|
|
803
|
+
const updatedOverrideData = {
|
|
804
|
+
functions: [
|
|
805
|
+
{
|
|
806
|
+
name: 'search_tool',
|
|
807
|
+
description: 'Updated description',
|
|
808
|
+
parameters: [
|
|
809
|
+
{
|
|
810
|
+
name: 'query',
|
|
811
|
+
type: 'string',
|
|
812
|
+
description: 'Search query',
|
|
813
|
+
required: true
|
|
814
|
+
}
|
|
815
|
+
]
|
|
816
|
+
}
|
|
817
|
+
]
|
|
818
|
+
};
|
|
819
|
+
const request = createMockRequest({
|
|
820
|
+
path: '/overrides',
|
|
821
|
+
method: 'PATCH',
|
|
822
|
+
bodyJSON: updatedOverrideData,
|
|
823
|
+
body: JSON.stringify(updatedOverrideData)
|
|
824
|
+
});
|
|
825
|
+
const response = await Service_1.toolsService.processRequest(request, mockToolFunction);
|
|
826
|
+
expect(response.status).toBe(200);
|
|
827
|
+
// Verify that the existing tool was updated using patch
|
|
828
|
+
expect(mockKvStore.patch).toHaveBeenCalledWith('app-v1:TestFunction:opal-tools-overrides', {
|
|
829
|
+
search_tool: {
|
|
830
|
+
name: 'search_tool',
|
|
831
|
+
description: 'Updated description',
|
|
832
|
+
parameters: [
|
|
833
|
+
{
|
|
834
|
+
name: 'query',
|
|
835
|
+
type: 'string',
|
|
836
|
+
description: 'Search query',
|
|
837
|
+
required: true
|
|
838
|
+
}
|
|
839
|
+
]
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
it('should handle KV store errors during save', async () => {
|
|
844
|
+
mockKvStore.patch.mockRejectedValue(new Error('KV store write error'));
|
|
845
|
+
const request = createMockRequest({
|
|
846
|
+
path: '/overrides',
|
|
847
|
+
method: 'PATCH',
|
|
848
|
+
bodyJSON: { functions: [] }
|
|
849
|
+
});
|
|
850
|
+
const response = await Service_1.toolsService.processRequest(request, mockToolFunction);
|
|
851
|
+
expect(response.status).toBe(500);
|
|
852
|
+
expect(app_sdk_1.logger.error).toHaveBeenCalledWith('Error saving tool overrides:', expect.any(Error));
|
|
853
|
+
});
|
|
854
|
+
it('should return 400 for invalid request body format', async () => {
|
|
855
|
+
const invalidRequest = createMockRequest({
|
|
856
|
+
path: '/overrides',
|
|
857
|
+
method: 'PATCH',
|
|
858
|
+
bodyJSON: { tools: [] } // Wrong property name, should be 'functions'
|
|
859
|
+
});
|
|
860
|
+
const response = await Service_1.toolsService.processRequest(invalidRequest, mockToolFunction);
|
|
861
|
+
expect(response.status).toBe(400);
|
|
862
|
+
expect(response.bodyJSON).toEqual({ error: 'Invalid request body. Expected functions array.' });
|
|
863
|
+
});
|
|
864
|
+
it('should return 400 when functions is not an array', async () => {
|
|
865
|
+
const invalidRequest = createMockRequest({
|
|
866
|
+
path: '/overrides',
|
|
867
|
+
method: 'PATCH',
|
|
868
|
+
bodyJSON: { functions: {} } // Should be array, not object
|
|
869
|
+
});
|
|
870
|
+
const response = await Service_1.toolsService.processRequest(invalidRequest, mockToolFunction);
|
|
871
|
+
expect(response.status).toBe(400);
|
|
872
|
+
expect(response.bodyJSON).toEqual({ error: 'Invalid request body. Expected functions array.' });
|
|
873
|
+
});
|
|
874
|
+
});
|
|
875
|
+
describe('DELETE /overrides endpoint', () => {
|
|
876
|
+
it('should delete overrides successfully', async () => {
|
|
877
|
+
const request = createMockRequest({
|
|
878
|
+
path: '/overrides',
|
|
879
|
+
method: 'DELETE'
|
|
880
|
+
});
|
|
881
|
+
const response = await Service_1.toolsService.processRequest(request, mockToolFunction);
|
|
882
|
+
expect(response.status).toBe(200);
|
|
883
|
+
expect(mockKvStore.delete).toHaveBeenCalledWith('app-v1:TestFunction:opal-tools-overrides');
|
|
884
|
+
expect(response.bodyJSON).toEqual({ success: true });
|
|
885
|
+
});
|
|
886
|
+
it('should handle KV store errors during delete', async () => {
|
|
887
|
+
mockKvStore.delete.mockRejectedValue(new Error('KV store delete error'));
|
|
888
|
+
const request = createMockRequest({
|
|
889
|
+
path: '/overrides',
|
|
890
|
+
method: 'DELETE'
|
|
891
|
+
});
|
|
892
|
+
const response = await Service_1.toolsService.processRequest(request, mockToolFunction);
|
|
893
|
+
expect(response.status).toBe(500);
|
|
894
|
+
expect(app_sdk_1.logger.error).toHaveBeenCalledWith('Error deleting tool overrides:', expect.any(Error));
|
|
895
|
+
});
|
|
896
|
+
});
|
|
897
|
+
describe('Data format transformation', () => {
|
|
898
|
+
it('should transform API format (array) to storage format (map)', async () => {
|
|
899
|
+
const apiFormatData = {
|
|
900
|
+
functions: [
|
|
901
|
+
{
|
|
902
|
+
name: 'search_tool',
|
|
903
|
+
description: 'Search functionality',
|
|
904
|
+
parameters: [
|
|
905
|
+
{
|
|
906
|
+
name: 'query',
|
|
907
|
+
type: 'string',
|
|
908
|
+
description: 'Search query',
|
|
909
|
+
required: true
|
|
910
|
+
}
|
|
911
|
+
]
|
|
912
|
+
}
|
|
913
|
+
]
|
|
914
|
+
};
|
|
915
|
+
const request = createMockRequest({
|
|
916
|
+
path: '/overrides',
|
|
917
|
+
method: 'PATCH',
|
|
918
|
+
bodyJSON: apiFormatData
|
|
919
|
+
});
|
|
920
|
+
await Service_1.toolsService.processRequest(request, mockToolFunction);
|
|
921
|
+
// Verify that data was transformed to storage format (map instead of array)
|
|
922
|
+
const expectedStorageFormat = {
|
|
923
|
+
search_tool: {
|
|
924
|
+
name: 'search_tool',
|
|
925
|
+
description: 'Search functionality',
|
|
926
|
+
parameters: [
|
|
927
|
+
{
|
|
928
|
+
name: 'query',
|
|
929
|
+
type: 'string',
|
|
930
|
+
description: 'Search query',
|
|
931
|
+
required: true
|
|
932
|
+
}
|
|
933
|
+
]
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
expect(mockKvStore.patch).toHaveBeenCalledWith('app-v1:TestFunction:opal-tools-overrides', expectedStorageFormat);
|
|
937
|
+
});
|
|
938
|
+
});
|
|
939
|
+
describe('getToolsWithOverrides method', () => {
|
|
940
|
+
it('should return tools with overrides applied', async () => {
|
|
941
|
+
const storedOverrides = {
|
|
942
|
+
search_tool: {
|
|
943
|
+
name: 'search_tool',
|
|
944
|
+
description: 'Enhanced search functionality'
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
mockKvStore.get.mockResolvedValue(storedOverrides);
|
|
948
|
+
const toolsWithOverrides = await Service_1.toolsService.getToolsWithOverrides('app-v1', 'TestFunction');
|
|
949
|
+
expect(toolsWithOverrides).toHaveLength(2);
|
|
950
|
+
const searchTool = toolsWithOverrides.find((t) => t.name === 'search_tool');
|
|
951
|
+
expect(searchTool?.description).toBe('Enhanced search functionality');
|
|
952
|
+
});
|
|
953
|
+
it('should return original tools when no overrides exist', async () => {
|
|
954
|
+
mockKvStore.get.mockResolvedValue(null);
|
|
955
|
+
const toolsWithOverrides = await Service_1.toolsService.getToolsWithOverrides('app-v1', 'TestFunction');
|
|
956
|
+
expect(toolsWithOverrides).toHaveLength(2);
|
|
957
|
+
const searchTool = toolsWithOverrides.find((t) => t.name === 'search_tool');
|
|
958
|
+
expect(searchTool?.description).toBe('Search for information');
|
|
959
|
+
});
|
|
960
|
+
});
|
|
961
|
+
});
|
|
534
962
|
});
|
|
535
963
|
//# sourceMappingURL=Service.test.js.map
|