@optimizely-opal/opal-tool-ocp-sdk 1.0.0-OCP-1449.1 → 1.0.0-beta.10
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 +13 -3
- package/dist/function/GlobalToolFunction.d.ts +3 -2
- package/dist/function/GlobalToolFunction.d.ts.map +1 -1
- package/dist/function/GlobalToolFunction.js +7 -4
- package/dist/function/GlobalToolFunction.js.map +1 -1
- package/dist/function/GlobalToolFunction.test.js +16 -4
- package/dist/function/GlobalToolFunction.test.js.map +1 -1
- package/dist/function/ToolFunction.d.ts +3 -2
- package/dist/function/ToolFunction.d.ts.map +1 -1
- package/dist/function/ToolFunction.js +7 -4
- package/dist/function/ToolFunction.js.map +1 -1
- package/dist/function/ToolFunction.test.js +15 -3
- package/dist/function/ToolFunction.test.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/logging/ToolLogger.d.ts +11 -3
- package/dist/logging/ToolLogger.d.ts.map +1 -1
- package/dist/logging/ToolLogger.js +114 -13
- package/dist/logging/ToolLogger.js.map +1 -1
- package/dist/logging/ToolLogger.test.js +177 -71
- package/dist/logging/ToolLogger.test.js.map +1 -1
- package/dist/types/Models.d.ts +7 -1
- package/dist/types/Models.d.ts.map +1 -1
- package/dist/types/Models.js +5 -1
- package/dist/types/Models.js.map +1 -1
- package/dist/types/ToolError.d.ts +13 -0
- package/dist/types/ToolError.d.ts.map +1 -1
- package/dist/types/ToolError.js +30 -2
- package/dist/types/ToolError.js.map +1 -1
- package/dist/types/ToolError.test.js +27 -3
- package/dist/types/ToolError.test.js.map +1 -1
- package/dist/validation/ParameterValidator.test.js +2 -1
- package/dist/validation/ParameterValidator.test.js.map +1 -1
- package/package.json +3 -3
- package/src/function/GlobalToolFunction.test.ts +21 -6
- package/src/function/GlobalToolFunction.ts +9 -5
- package/src/function/ToolFunction.test.ts +21 -5
- package/src/function/ToolFunction.ts +9 -5
- package/src/index.ts +1 -1
- package/src/logging/ToolLogger.test.ts +225 -74
- package/src/logging/ToolLogger.ts +129 -15
- package/src/types/Models.ts +8 -1
- package/src/types/ToolError.test.ts +33 -3
- package/src/types/ToolError.ts +32 -2
- package/src/validation/ParameterValidator.test.ts +4 -1
|
@@ -53,16 +53,16 @@ jest.mock('@zaiusinc/app-sdk', () => ({
|
|
|
53
53
|
|
|
54
54
|
// Create a concrete implementation for testing
|
|
55
55
|
class TestToolFunction extends ToolFunction {
|
|
56
|
-
private mockReady: jest.MockedFunction<() => Promise<boolean>>;
|
|
56
|
+
private mockReady: jest.MockedFunction<() => Promise<{ ready: boolean; reason?: string }>>;
|
|
57
57
|
|
|
58
58
|
public constructor(request?: any) {
|
|
59
59
|
super(request || {});
|
|
60
60
|
(this as any).request = request;
|
|
61
|
-
this.mockReady = jest.fn().mockResolvedValue(true);
|
|
61
|
+
this.mockReady = jest.fn().mockResolvedValue({ ready: true });
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
// Override the ready method with mock implementation for testing
|
|
65
|
-
protected ready(): Promise<boolean> {
|
|
65
|
+
protected ready(): Promise<{ ready: boolean; reason?: string }> {
|
|
66
66
|
return this.mockReady();
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -174,7 +174,7 @@ describe('ToolFunction', () => {
|
|
|
174
174
|
const readyRequest = createReadyRequestWithAuth();
|
|
175
175
|
|
|
176
176
|
toolFunction = new TestToolFunction(readyRequest);
|
|
177
|
-
toolFunction.getMockReady().mockResolvedValue(true);
|
|
177
|
+
toolFunction.getMockReady().mockResolvedValue({ ready: true });
|
|
178
178
|
|
|
179
179
|
// Act
|
|
180
180
|
const result = await toolFunction.perform();
|
|
@@ -190,7 +190,7 @@ describe('ToolFunction', () => {
|
|
|
190
190
|
const readyRequest = createReadyRequestWithAuth();
|
|
191
191
|
|
|
192
192
|
toolFunction = new TestToolFunction(readyRequest);
|
|
193
|
-
toolFunction.getMockReady().mockResolvedValue(false);
|
|
193
|
+
toolFunction.getMockReady().mockResolvedValue({ ready: false });
|
|
194
194
|
|
|
195
195
|
// Act
|
|
196
196
|
const result = await toolFunction.perform();
|
|
@@ -201,6 +201,22 @@ describe('ToolFunction', () => {
|
|
|
201
201
|
expect(mockProcessRequest).not.toHaveBeenCalled(); // Should not call service
|
|
202
202
|
});
|
|
203
203
|
|
|
204
|
+
it('should return ready: false with reason when ready method returns false with reason', async () => {
|
|
205
|
+
// Arrange
|
|
206
|
+
const readyRequest = createReadyRequestWithAuth();
|
|
207
|
+
|
|
208
|
+
toolFunction = new TestToolFunction(readyRequest);
|
|
209
|
+
toolFunction.getMockReady().mockResolvedValue({ ready: false, reason: 'Database connection failed' });
|
|
210
|
+
|
|
211
|
+
// Act
|
|
212
|
+
const result = await toolFunction.perform();
|
|
213
|
+
|
|
214
|
+
// Assert
|
|
215
|
+
expect(toolFunction.getMockReady()).toHaveBeenCalledTimes(1);
|
|
216
|
+
expect(result).toEqual(new Response(200, { ready: false, reason: 'Database connection failed' }));
|
|
217
|
+
expect(mockProcessRequest).not.toHaveBeenCalled(); // Should not call service
|
|
218
|
+
});
|
|
219
|
+
|
|
204
220
|
it('should handle ready method throwing an error', async () => {
|
|
205
221
|
// Arrange
|
|
206
222
|
const readyRequest = createReadyRequestWithAuth();
|
|
@@ -3,6 +3,7 @@ import { authenticateRegularRequest, authenticateInternalRequest } from '../auth
|
|
|
3
3
|
import { toolsService } from '../service/Service';
|
|
4
4
|
import { ToolLogger } from '../logging/ToolLogger';
|
|
5
5
|
import { ToolError } from '../types/ToolError';
|
|
6
|
+
import { ReadyResponse } from '../types/Models';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Abstract base class for tool-based function execution
|
|
@@ -14,10 +15,10 @@ export abstract class ToolFunction extends Function {
|
|
|
14
15
|
* Override this method to implement any required credentials and/or other configuration
|
|
15
16
|
* exist and are valid. Reasonable caching should be utilized to prevent excessive requests to external resources.
|
|
16
17
|
* @async
|
|
17
|
-
* @returns
|
|
18
|
+
* @returns ReadyResponse containing ready status and optional reason when not ready
|
|
18
19
|
*/
|
|
19
|
-
protected ready(): Promise<boolean> {
|
|
20
|
-
return Promise.resolve(true);
|
|
20
|
+
protected ready(): Promise<ReadyResponse | boolean> {
|
|
21
|
+
return Promise.resolve({ ready: true });
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -67,8 +68,11 @@ export abstract class ToolFunction extends Function {
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
if (this.request.path === '/ready') {
|
|
70
|
-
const
|
|
71
|
-
|
|
71
|
+
const readyResult = await this.ready();
|
|
72
|
+
const readyResponse = typeof readyResult === 'boolean'
|
|
73
|
+
? { ready: readyResult }
|
|
74
|
+
: readyResult;
|
|
75
|
+
return new Response(200, readyResponse);
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
// Pass 'this' as context so decorated methods can use the existing instance
|
package/src/index.ts
CHANGED
|
@@ -4,4 +4,4 @@ export * from './types/Models';
|
|
|
4
4
|
export * from './types/ToolError';
|
|
5
5
|
export * from './decorator/Decorator';
|
|
6
6
|
export * from './auth/TokenVerifier';
|
|
7
|
-
export { Tool, Interaction, InteractionResult } from './service/Service';
|
|
7
|
+
export { Tool, Interaction, InteractionResult, NestedInteractions } from './service/Service';
|
|
@@ -88,6 +88,30 @@ describe('ToolLogger', () => {
|
|
|
88
88
|
return response;
|
|
89
89
|
};
|
|
90
90
|
|
|
91
|
+
const createMockResponseWithBody = (
|
|
92
|
+
status: number,
|
|
93
|
+
bodyData: Uint8Array | undefined,
|
|
94
|
+
contentType: string
|
|
95
|
+
): App.Response => {
|
|
96
|
+
const mockHeaders = {
|
|
97
|
+
get: jest.fn().mockReturnValue(contentType)
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const response = {
|
|
101
|
+
status,
|
|
102
|
+
headers: mockHeaders
|
|
103
|
+
} as any;
|
|
104
|
+
|
|
105
|
+
Object.defineProperty(response, 'bodyAsU8Array', {
|
|
106
|
+
get() {
|
|
107
|
+
return bodyData;
|
|
108
|
+
},
|
|
109
|
+
enumerable: true
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return response;
|
|
113
|
+
};
|
|
114
|
+
|
|
91
115
|
describe('logRequest', () => {
|
|
92
116
|
it('should log request with parameters', () => {
|
|
93
117
|
const req = createMockRequest();
|
|
@@ -240,7 +264,7 @@ describe('ToolLogger', () => {
|
|
|
240
264
|
path: '/test-tool',
|
|
241
265
|
method: 'POST',
|
|
242
266
|
parameters: {
|
|
243
|
-
description: `${'a'.repeat(
|
|
267
|
+
description: `${'a'.repeat(118)}...[22 truncated]...${'a'.repeat(10)}`,
|
|
244
268
|
short_field: 'normal'
|
|
245
269
|
}
|
|
246
270
|
});
|
|
@@ -265,8 +289,8 @@ describe('ToolLogger', () => {
|
|
|
265
289
|
method: 'POST',
|
|
266
290
|
parameters: {
|
|
267
291
|
items: [
|
|
268
|
-
...largeArray.slice(0,
|
|
269
|
-
'... (
|
|
292
|
+
...largeArray.slice(0, 2),
|
|
293
|
+
'... (13 more items truncated)'
|
|
270
294
|
],
|
|
271
295
|
small_array: ['a', 'b']
|
|
272
296
|
}
|
|
@@ -420,6 +444,21 @@ describe('ToolLogger', () => {
|
|
|
420
444
|
// Should not throw error or cause infinite recursion
|
|
421
445
|
expect(() => ToolLogger.logRequest(req)).not.toThrow();
|
|
422
446
|
expect(mockLogger.info).toHaveBeenCalled();
|
|
447
|
+
|
|
448
|
+
// Verify that deeply nested parts are replaced with placeholder
|
|
449
|
+
const logCall = mockLogger.info.mock.calls[0];
|
|
450
|
+
const loggedData = JSON.parse(logCall[1]);
|
|
451
|
+
|
|
452
|
+
// Navigate to a deeply nested level that should be truncated
|
|
453
|
+
// At maxDepth=5, objects beyond depth 5 should be replaced
|
|
454
|
+
const nested1 = loggedData.parameters.deep.nested;
|
|
455
|
+
expect(nested1).toBeDefined(); // depth 2
|
|
456
|
+
const nested2 = nested1.nested;
|
|
457
|
+
expect(nested2).toBeDefined(); // depth 3
|
|
458
|
+
const nested3 = nested2.nested;
|
|
459
|
+
expect(nested3).toBeDefined(); // depth 4
|
|
460
|
+
const nested4 = nested3.nested;
|
|
461
|
+
expect(nested4).toBe('[MAX_DEPTH_EXCEEDED]'); // depth 5, should be truncated
|
|
423
462
|
});
|
|
424
463
|
|
|
425
464
|
it('should replace deeply nested objects with MAX_DEPTH_EXCEEDED placeholder', () => {
|
|
@@ -465,6 +504,7 @@ describe('ToolLogger', () => {
|
|
|
465
504
|
level1: {
|
|
466
505
|
items: [
|
|
467
506
|
{
|
|
507
|
+
shallow: 'data',
|
|
468
508
|
level2: {
|
|
469
509
|
level3: {
|
|
470
510
|
level4: {
|
|
@@ -504,9 +544,12 @@ describe('ToolLogger', () => {
|
|
|
504
544
|
const loggedData = JSON.parse(logCall[1]);
|
|
505
545
|
|
|
506
546
|
const items = loggedData.parameters.level1.items;
|
|
547
|
+
expect(items.length).toBe(3);
|
|
507
548
|
|
|
508
549
|
// First item: deeply nested object with inner parts replaced by placeholder
|
|
550
|
+
// shallow object should be processed normally
|
|
509
551
|
expect(items[0]).toEqual({
|
|
552
|
+
shallow: 'data',
|
|
510
553
|
level2: {
|
|
511
554
|
level3: {
|
|
512
555
|
level4: '[MAX_DEPTH_EXCEEDED]'
|
|
@@ -516,18 +559,6 @@ describe('ToolLogger', () => {
|
|
|
516
559
|
|
|
517
560
|
// Second item: simple string should remain unchanged
|
|
518
561
|
expect(items[1]).toBe('simple-item');
|
|
519
|
-
|
|
520
|
-
// Third item: shallow object should be processed normally
|
|
521
|
-
expect(items[2]).toEqual({ shallow: 'data' });
|
|
522
|
-
|
|
523
|
-
// Fourth item: another deeply nested object with inner parts replaced by placeholder
|
|
524
|
-
expect(items[3]).toEqual({
|
|
525
|
-
level2: {
|
|
526
|
-
level3: {
|
|
527
|
-
level4: '[MAX_DEPTH_EXCEEDED]'
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
});
|
|
531
562
|
});
|
|
532
563
|
});
|
|
533
564
|
|
|
@@ -545,7 +576,8 @@ describe('ToolLogger', () => {
|
|
|
545
576
|
status: 200,
|
|
546
577
|
contentType: 'application/json',
|
|
547
578
|
contentLength: 34, // JSON.stringify({ result: 'success', data: 'test' }).length
|
|
548
|
-
success: true
|
|
579
|
+
success: true,
|
|
580
|
+
responseBody: { result: 'success', data: 'test' }
|
|
549
581
|
};
|
|
550
582
|
|
|
551
583
|
expect(mockLogger.info).toHaveBeenCalledWith(
|
|
@@ -567,14 +599,14 @@ describe('ToolLogger', () => {
|
|
|
567
599
|
status: 400,
|
|
568
600
|
contentType: 'application/json',
|
|
569
601
|
contentLength: 23,
|
|
570
|
-
success: false
|
|
602
|
+
success: false,
|
|
603
|
+
responseBody: { error: 'Bad request' }
|
|
571
604
|
});
|
|
572
605
|
});
|
|
573
606
|
|
|
574
|
-
it('should handle response without
|
|
607
|
+
it('should handle response without body data', () => {
|
|
575
608
|
const req = createMockRequest();
|
|
576
|
-
const response =
|
|
577
|
-
response.bodyJSON = undefined;
|
|
609
|
+
const response = createMockResponseWithBody(204, undefined, 'application/json');
|
|
578
610
|
|
|
579
611
|
ToolLogger.logResponse(req, response);
|
|
580
612
|
|
|
@@ -600,11 +632,12 @@ describe('ToolLogger', () => {
|
|
|
600
632
|
status: 200,
|
|
601
633
|
contentType: 'application/json',
|
|
602
634
|
contentLength: 15,
|
|
603
|
-
success: true
|
|
635
|
+
success: true,
|
|
636
|
+
responseBody: { data: 'test' }
|
|
604
637
|
});
|
|
605
638
|
});
|
|
606
639
|
|
|
607
|
-
it('should handle unknown content type', () => {
|
|
640
|
+
it('should handle unknown content type - response body not logged', () => {
|
|
608
641
|
const req = createMockRequest();
|
|
609
642
|
const response = createMockResponse(200, { data: 'test' });
|
|
610
643
|
response.headers.get = jest.fn().mockReturnValue(null);
|
|
@@ -623,21 +656,35 @@ describe('ToolLogger', () => {
|
|
|
623
656
|
|
|
624
657
|
it('should handle content length calculation error', () => {
|
|
625
658
|
const req = createMockRequest();
|
|
626
|
-
const circularObj: any = { name: 'test' };
|
|
627
|
-
circularObj.self = circularObj; // Create circular reference
|
|
628
659
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
660
|
+
// Simulate a response that will cause errors when trying to calculate content length
|
|
661
|
+
// by providing a Uint8Array but the underlying data causes issues
|
|
662
|
+
const mockHeaders = {
|
|
663
|
+
get: jest.fn().mockReturnValue('application/json')
|
|
664
|
+
};
|
|
632
665
|
|
|
633
|
-
|
|
634
|
-
event: 'opal_tool_response',
|
|
635
|
-
path: '/test-tool',
|
|
666
|
+
const response = {
|
|
636
667
|
status: 200,
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
668
|
+
headers: mockHeaders
|
|
669
|
+
} as any;
|
|
670
|
+
|
|
671
|
+
// Create a getter that throws when accessed (simulating serialization error)
|
|
672
|
+
Object.defineProperty(response, 'bodyAsU8Array', {
|
|
673
|
+
get() {
|
|
674
|
+
throw new Error('Circular structure');
|
|
675
|
+
},
|
|
676
|
+
enumerable: true
|
|
640
677
|
});
|
|
678
|
+
|
|
679
|
+
ToolLogger.logResponse(req, response);
|
|
680
|
+
|
|
681
|
+
// The error causes both contentLength and responseBody to fail gracefully
|
|
682
|
+
const logCall = mockLogger.info.mock.calls[0];
|
|
683
|
+
const loggedData = JSON.parse(logCall[1]);
|
|
684
|
+
|
|
685
|
+
expect(loggedData.event).toBe('opal_tool_response');
|
|
686
|
+
expect(loggedData.contentLength).toBe('unknown');
|
|
687
|
+
expect(loggedData.responseBody).toBeUndefined();
|
|
641
688
|
});
|
|
642
689
|
|
|
643
690
|
it('should correctly identify success status codes', () => {
|
|
@@ -659,14 +706,16 @@ describe('ToolLogger', () => {
|
|
|
659
706
|
const response = createMockResponse(status);
|
|
660
707
|
ToolLogger.logResponse(req, response);
|
|
661
708
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
709
|
+
const logCall = mockLogger.info.mock.calls[0];
|
|
710
|
+
const loggedData = JSON.parse(logCall[1]);
|
|
711
|
+
|
|
712
|
+
expect(loggedData.event).toBe('opal_tool_response');
|
|
713
|
+
expect(loggedData.path).toBe('/test-tool');
|
|
714
|
+
expect(loggedData.status).toBe(status);
|
|
715
|
+
expect(loggedData.contentType).toBe('application/json');
|
|
716
|
+
expect(loggedData.contentLength).toBe(2);
|
|
717
|
+
expect(loggedData.success).toBe(expected);
|
|
718
|
+
expect(loggedData.responseBody).toEqual({});
|
|
670
719
|
});
|
|
671
720
|
});
|
|
672
721
|
|
|
@@ -674,29 +723,148 @@ describe('ToolLogger', () => {
|
|
|
674
723
|
const req = createMockRequest();
|
|
675
724
|
|
|
676
725
|
const testCases = [
|
|
677
|
-
'application/json',
|
|
678
|
-
'text/plain',
|
|
679
|
-
'
|
|
680
|
-
'text/html'
|
|
726
|
+
{ contentType: 'application/json', expectedBody: { data: 'test' } },
|
|
727
|
+
{ contentType: 'text/plain', expectedBody: '{"data":"test"}' },
|
|
728
|
+
{ contentType: 'text/html', expectedBody: '{"data":"test"}' }
|
|
681
729
|
];
|
|
682
730
|
|
|
683
|
-
testCases.forEach((contentType) => {
|
|
731
|
+
testCases.forEach(({ contentType, expectedBody }) => {
|
|
684
732
|
mockLogger.info.mockClear();
|
|
685
733
|
const response = createMockResponse(200, { data: 'test' });
|
|
686
734
|
response.headers.get = jest.fn().mockReturnValue(contentType);
|
|
687
735
|
|
|
688
736
|
ToolLogger.logResponse(req, response);
|
|
689
737
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
738
|
+
const logCall = mockLogger.info.mock.calls[0];
|
|
739
|
+
const loggedData = JSON.parse(logCall[1]);
|
|
740
|
+
|
|
741
|
+
expect(loggedData.event).toBe('opal_tool_response');
|
|
742
|
+
expect(loggedData.path).toBe('/test-tool');
|
|
743
|
+
expect(loggedData.status).toBe(200);
|
|
744
|
+
expect(loggedData.contentType).toBe(contentType);
|
|
745
|
+
expect(loggedData.contentLength).toBe(15);
|
|
746
|
+
expect(loggedData.success).toBe(true);
|
|
747
|
+
expect(loggedData.responseBody).toEqual(expectedBody);
|
|
748
|
+
});
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
it('should log short successful response body', () => {
|
|
752
|
+
const req = createMockRequest();
|
|
753
|
+
const response = createMockResponse(200, { result: 'success', data: 'test' });
|
|
754
|
+
|
|
755
|
+
ToolLogger.logResponse(req, response);
|
|
756
|
+
|
|
757
|
+
const logCall = mockLogger.info.mock.calls[0];
|
|
758
|
+
const loggedData = JSON.parse(logCall[1]);
|
|
759
|
+
|
|
760
|
+
expect(loggedData.responseBody).toEqual({ result: 'success', data: 'test' });
|
|
761
|
+
expect(loggedData.success).toBe(true);
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
it('should truncate long successful response body to 256 chars', () => {
|
|
765
|
+
const req = createMockRequest();
|
|
766
|
+
const longData = 'a'.repeat(300);
|
|
767
|
+
const response = createMockResponse(200, { message: longData });
|
|
768
|
+
|
|
769
|
+
ToolLogger.logResponse(req, response);
|
|
770
|
+
|
|
771
|
+
const logCall = mockLogger.info.mock.calls[0];
|
|
772
|
+
const loggedData = JSON.parse(logCall[1]);
|
|
773
|
+
|
|
774
|
+
// The response body should be truncated when stringified
|
|
775
|
+
expect(loggedData.responseBody.message).toContain(' truncated]...');
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
it('truncates long properties of failed responses', () => {
|
|
779
|
+
const req = createMockRequest();
|
|
780
|
+
const longData = 'a'.repeat(150);
|
|
781
|
+
const response = createMockResponse(400, { error: 'Bad request', details: longData });
|
|
782
|
+
|
|
783
|
+
ToolLogger.logResponse(req, response);
|
|
784
|
+
|
|
785
|
+
const logCall = mockLogger.info.mock.calls[0];
|
|
786
|
+
const loggedData = JSON.parse(logCall[1]);
|
|
787
|
+
|
|
788
|
+
// Failed responses should include full body, not truncated
|
|
789
|
+
expect(loggedData.responseBody.error).toEqual('Bad request');
|
|
790
|
+
expect(loggedData.responseBody.details).toContain(' truncated]...');
|
|
791
|
+
expect(loggedData.success).toBe(false);
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
it('should redact sensitive data in response body', () => {
|
|
795
|
+
const req = createMockRequest();
|
|
796
|
+
const response = createMockResponse(200, {
|
|
797
|
+
user: 'john',
|
|
798
|
+
password: 'secret123',
|
|
799
|
+
api_key: 'key456',
|
|
800
|
+
data: 'public'
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
ToolLogger.logResponse(req, response);
|
|
804
|
+
|
|
805
|
+
const logCall = mockLogger.info.mock.calls[0];
|
|
806
|
+
const loggedData = JSON.parse(logCall[1]);
|
|
807
|
+
|
|
808
|
+
expect(loggedData.responseBody).toEqual({
|
|
809
|
+
user: 'john',
|
|
810
|
+
password: '[REDACTED]',
|
|
811
|
+
api_key: '[REDACTED]',
|
|
812
|
+
data: 'public'
|
|
698
813
|
});
|
|
699
814
|
});
|
|
815
|
+
|
|
816
|
+
it('should handle response with no body', () => {
|
|
817
|
+
const req = createMockRequest();
|
|
818
|
+
const response = createMockResponseWithBody(204, undefined, 'application/json');
|
|
819
|
+
|
|
820
|
+
ToolLogger.logResponse(req, response);
|
|
821
|
+
|
|
822
|
+
const logCall = mockLogger.info.mock.calls[0];
|
|
823
|
+
const loggedData = JSON.parse(logCall[1]);
|
|
824
|
+
|
|
825
|
+
expect(loggedData.responseBody).toBeUndefined();
|
|
826
|
+
expect(loggedData.success).toBe(true);
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
it('should handle plain text response body', () => {
|
|
830
|
+
const req = createMockRequest();
|
|
831
|
+
const plainText = 'This is a plain text response';
|
|
832
|
+
const response = createMockResponseWithBody(200, new Uint8Array(Buffer.from(plainText)), 'text/plain');
|
|
833
|
+
|
|
834
|
+
ToolLogger.logResponse(req, response);
|
|
835
|
+
|
|
836
|
+
const logCall = mockLogger.info.mock.calls[0];
|
|
837
|
+
const loggedData = JSON.parse(logCall[1]);
|
|
838
|
+
|
|
839
|
+
expect(loggedData.responseBody).toBe(plainText);
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
it('should truncate long plain text successful responses', () => {
|
|
843
|
+
const req = createMockRequest();
|
|
844
|
+
const longText = 'a'.repeat(300);
|
|
845
|
+
const response = createMockResponseWithBody(200, new Uint8Array(Buffer.from(longText)), 'text/plain');
|
|
846
|
+
|
|
847
|
+
ToolLogger.logResponse(req, response);
|
|
848
|
+
|
|
849
|
+
const logCall = mockLogger.info.mock.calls[0];
|
|
850
|
+
const loggedData = JSON.parse(logCall[1]);
|
|
851
|
+
|
|
852
|
+
expect(loggedData.responseBody).toBe(`${'a'.repeat(256)}... (truncated)`);
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
it('should not truncate long plain text failed responses', () => {
|
|
856
|
+
const req = createMockRequest();
|
|
857
|
+
const longText = 'a'.repeat(150);
|
|
858
|
+
const response = createMockResponseWithBody(500, new Uint8Array(Buffer.from(longText)), 'text/plain');
|
|
859
|
+
|
|
860
|
+
ToolLogger.logResponse(req, response);
|
|
861
|
+
|
|
862
|
+
const logCall = mockLogger.info.mock.calls[0];
|
|
863
|
+
const loggedData = JSON.parse(logCall[1]);
|
|
864
|
+
|
|
865
|
+
expect(loggedData.responseBody).toBe(longText);
|
|
866
|
+
expect(loggedData.success).toBe(false);
|
|
867
|
+
});
|
|
700
868
|
});
|
|
701
869
|
|
|
702
870
|
describe('edge cases', () => {
|
|
@@ -743,7 +911,7 @@ describe('ToolLogger', () => {
|
|
|
743
911
|
string: 'text',
|
|
744
912
|
number: 42,
|
|
745
913
|
boolean: true,
|
|
746
|
-
array: [1, 2
|
|
914
|
+
array: [1, 2],
|
|
747
915
|
object: { nested: 'value' },
|
|
748
916
|
nullValue: null,
|
|
749
917
|
password: 'secret'
|
|
@@ -761,7 +929,7 @@ describe('ToolLogger', () => {
|
|
|
761
929
|
string: 'text',
|
|
762
930
|
number: 42,
|
|
763
931
|
boolean: true,
|
|
764
|
-
array: [1, 2
|
|
932
|
+
array: [1, 2],
|
|
765
933
|
object: { nested: 'value' },
|
|
766
934
|
nullValue: null,
|
|
767
935
|
password: '[REDACTED]'
|
|
@@ -841,24 +1009,7 @@ describe('ToolLogger', () => {
|
|
|
841
1009
|
description: 'OVERRIDDEN: Enhanced minimum detectable effect calculation',
|
|
842
1010
|
required: true
|
|
843
1011
|
},
|
|
844
|
-
|
|
845
|
-
name: 'sigLevel',
|
|
846
|
-
type: 'number',
|
|
847
|
-
description: 'OVERRIDDEN: Enhanced statistical significance level',
|
|
848
|
-
required: true
|
|
849
|
-
},
|
|
850
|
-
{
|
|
851
|
-
name: 'numVariations',
|
|
852
|
-
type: 'number',
|
|
853
|
-
description: 'OVERRIDDEN: Enhanced number of variations handling',
|
|
854
|
-
required: true
|
|
855
|
-
},
|
|
856
|
-
{
|
|
857
|
-
name: 'dailyVisitors',
|
|
858
|
-
type: 'number',
|
|
859
|
-
description: 'OVERRIDDEN: Enhanced daily visitor count with forecasting',
|
|
860
|
-
required: true
|
|
861
|
-
}
|
|
1012
|
+
'... (3 more items truncated)'
|
|
862
1013
|
]
|
|
863
1014
|
}
|
|
864
1015
|
]
|