@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.
Files changed (64) hide show
  1. package/README.md +114 -72
  2. package/dist/auth/AuthUtils.d.ts +12 -5
  3. package/dist/auth/AuthUtils.d.ts.map +1 -1
  4. package/dist/auth/AuthUtils.js +80 -25
  5. package/dist/auth/AuthUtils.js.map +1 -1
  6. package/dist/auth/AuthUtils.test.js +161 -117
  7. package/dist/auth/AuthUtils.test.js.map +1 -1
  8. package/dist/function/GlobalToolFunction.d.ts +1 -1
  9. package/dist/function/GlobalToolFunction.d.ts.map +1 -1
  10. package/dist/function/GlobalToolFunction.js +17 -4
  11. package/dist/function/GlobalToolFunction.js.map +1 -1
  12. package/dist/function/GlobalToolFunction.test.js +54 -8
  13. package/dist/function/GlobalToolFunction.test.js.map +1 -1
  14. package/dist/function/ToolFunction.d.ts +1 -1
  15. package/dist/function/ToolFunction.d.ts.map +1 -1
  16. package/dist/function/ToolFunction.js +24 -4
  17. package/dist/function/ToolFunction.js.map +1 -1
  18. package/dist/function/ToolFunction.test.js +260 -8
  19. package/dist/function/ToolFunction.test.js.map +1 -1
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/logging/ToolLogger.d.ts.map +1 -1
  25. package/dist/logging/ToolLogger.js +2 -1
  26. package/dist/logging/ToolLogger.js.map +1 -1
  27. package/dist/logging/ToolLogger.test.js +114 -2
  28. package/dist/logging/ToolLogger.test.js.map +1 -1
  29. package/dist/service/Service.d.ts +88 -2
  30. package/dist/service/Service.d.ts.map +1 -1
  31. package/dist/service/Service.js +227 -55
  32. package/dist/service/Service.js.map +1 -1
  33. package/dist/service/Service.test.js +464 -36
  34. package/dist/service/Service.test.js.map +1 -1
  35. package/dist/types/ToolError.d.ts +59 -0
  36. package/dist/types/ToolError.d.ts.map +1 -0
  37. package/dist/types/ToolError.js +79 -0
  38. package/dist/types/ToolError.js.map +1 -0
  39. package/dist/types/ToolError.test.d.ts +2 -0
  40. package/dist/types/ToolError.test.d.ts.map +1 -0
  41. package/dist/types/ToolError.test.js +161 -0
  42. package/dist/types/ToolError.test.js.map +1 -0
  43. package/dist/validation/ParameterValidator.d.ts +5 -16
  44. package/dist/validation/ParameterValidator.d.ts.map +1 -1
  45. package/dist/validation/ParameterValidator.js +10 -3
  46. package/dist/validation/ParameterValidator.js.map +1 -1
  47. package/dist/validation/ParameterValidator.test.js +186 -146
  48. package/dist/validation/ParameterValidator.test.js.map +1 -1
  49. package/package.json +1 -1
  50. package/src/auth/AuthUtils.test.ts +176 -157
  51. package/src/auth/AuthUtils.ts +96 -33
  52. package/src/function/GlobalToolFunction.test.ts +54 -8
  53. package/src/function/GlobalToolFunction.ts +26 -6
  54. package/src/function/ToolFunction.test.ts +274 -8
  55. package/src/function/ToolFunction.ts +33 -7
  56. package/src/index.ts +1 -0
  57. package/src/logging/ToolLogger.test.ts +118 -2
  58. package/src/logging/ToolLogger.ts +2 -1
  59. package/src/service/Service.test.ts +577 -34
  60. package/src/service/Service.ts +286 -54
  61. package/src/types/ToolError.test.ts +192 -0
  62. package/src/types/ToolError.ts +95 -0
  63. package/src/validation/ParameterValidator.test.ts +185 -158
  64. 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
- logger: {
10
- error: jest.fn()
11
- },
12
- Function: class {
13
- request;
14
- constructor(request) {
15
- this.request = request;
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
- Headers: class {
19
- constructor(headers = []) {
20
- this.headers = {};
21
- headers.forEach(([name, value]) => {
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
- headers;
26
- set(name, value) {
27
- this.headers[name.toLowerCase()] = value;
28
- }
29
- get(name) {
30
- return this.headers[name.toLowerCase()];
31
- }
32
- has(name) {
33
- return name.toLowerCase() in this.headers;
34
- }
35
- },
36
- Response: jest.fn().mockImplementation((status, data, headers) => ({
37
- status,
38
- data,
39
- bodyJSON: data,
40
- bodyAsU8Array: new Uint8Array(),
41
- headers: headers || { get: jest.fn(), has: jest.fn(), set: jest.fn() }
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 an error', async () => {
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 an error', async () => {
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