mcp-rubber-duck 1.8.0 → 1.9.3
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/.github/workflows/semantic-release.yml +12 -1
- package/.releaserc.json +6 -1
- package/CHANGELOG.md +30 -0
- package/README.md +158 -1
- package/audit-ci.json +3 -1
- package/dist/config/config.d.ts +2 -0
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +144 -1
- package/dist/config/config.js.map +1 -1
- package/dist/config/types.d.ts +1084 -2
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +59 -0
- package/dist/config/types.js.map +1 -1
- package/dist/guardrails/context.d.ts +10 -0
- package/dist/guardrails/context.d.ts.map +1 -0
- package/dist/guardrails/context.js +35 -0
- package/dist/guardrails/context.js.map +1 -0
- package/dist/guardrails/errors.d.ts +26 -0
- package/dist/guardrails/errors.d.ts.map +1 -0
- package/dist/guardrails/errors.js +42 -0
- package/dist/guardrails/errors.js.map +1 -0
- package/dist/guardrails/index.d.ts +6 -0
- package/dist/guardrails/index.d.ts.map +1 -0
- package/dist/guardrails/index.js +11 -0
- package/dist/guardrails/index.js.map +1 -0
- package/dist/guardrails/plugins/base-plugin.d.ts +35 -0
- package/dist/guardrails/plugins/base-plugin.d.ts.map +1 -0
- package/dist/guardrails/plugins/base-plugin.js +70 -0
- package/dist/guardrails/plugins/base-plugin.js.map +1 -0
- package/dist/guardrails/plugins/index.d.ts +6 -0
- package/dist/guardrails/plugins/index.d.ts.map +1 -0
- package/dist/guardrails/plugins/index.js +6 -0
- package/dist/guardrails/plugins/index.js.map +1 -0
- package/dist/guardrails/plugins/pattern-blocker.d.ts +27 -0
- package/dist/guardrails/plugins/pattern-blocker.d.ts.map +1 -0
- package/dist/guardrails/plugins/pattern-blocker.js +140 -0
- package/dist/guardrails/plugins/pattern-blocker.js.map +1 -0
- package/dist/guardrails/plugins/pii-redactor/detectors.d.ts +40 -0
- package/dist/guardrails/plugins/pii-redactor/detectors.d.ts.map +1 -0
- package/dist/guardrails/plugins/pii-redactor/detectors.js +134 -0
- package/dist/guardrails/plugins/pii-redactor/detectors.js.map +1 -0
- package/dist/guardrails/plugins/pii-redactor/index.d.ts +28 -0
- package/dist/guardrails/plugins/pii-redactor/index.d.ts.map +1 -0
- package/dist/guardrails/plugins/pii-redactor/index.js +157 -0
- package/dist/guardrails/plugins/pii-redactor/index.js.map +1 -0
- package/dist/guardrails/plugins/pii-redactor/pseudonymizer.d.ts +33 -0
- package/dist/guardrails/plugins/pii-redactor/pseudonymizer.d.ts.map +1 -0
- package/dist/guardrails/plugins/pii-redactor/pseudonymizer.js +70 -0
- package/dist/guardrails/plugins/pii-redactor/pseudonymizer.js.map +1 -0
- package/dist/guardrails/plugins/rate-limiter.d.ts +28 -0
- package/dist/guardrails/plugins/rate-limiter.d.ts.map +1 -0
- package/dist/guardrails/plugins/rate-limiter.js +91 -0
- package/dist/guardrails/plugins/rate-limiter.js.map +1 -0
- package/dist/guardrails/plugins/token-limiter.d.ts +30 -0
- package/dist/guardrails/plugins/token-limiter.d.ts.map +1 -0
- package/dist/guardrails/plugins/token-limiter.js +98 -0
- package/dist/guardrails/plugins/token-limiter.js.map +1 -0
- package/dist/guardrails/service.d.ts +38 -0
- package/dist/guardrails/service.d.ts.map +1 -0
- package/dist/guardrails/service.js +183 -0
- package/dist/guardrails/service.js.map +1 -0
- package/dist/guardrails/types.d.ts +96 -0
- package/dist/guardrails/types.d.ts.map +1 -0
- package/dist/guardrails/types.js +2 -0
- package/dist/guardrails/types.js.map +1 -0
- package/dist/providers/duck-provider-enhanced.d.ts +2 -1
- package/dist/providers/duck-provider-enhanced.d.ts.map +1 -1
- package/dist/providers/duck-provider-enhanced.js +55 -6
- package/dist/providers/duck-provider-enhanced.js.map +1 -1
- package/dist/providers/enhanced-manager.d.ts +2 -1
- package/dist/providers/enhanced-manager.d.ts.map +1 -1
- package/dist/providers/enhanced-manager.js +3 -3
- package/dist/providers/enhanced-manager.js.map +1 -1
- package/dist/providers/manager.d.ts +3 -1
- package/dist/providers/manager.d.ts.map +1 -1
- package/dist/providers/manager.js +4 -2
- package/dist/providers/manager.js.map +1 -1
- package/dist/providers/provider.d.ts +3 -1
- package/dist/providers/provider.d.ts.map +1 -1
- package/dist/providers/provider.js +43 -3
- package/dist/providers/provider.js.map +1 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +28 -6
- package/dist/server.js.map +1 -1
- package/dist/services/function-bridge.d.ts +3 -1
- package/dist/services/function-bridge.d.ts.map +1 -1
- package/dist/services/function-bridge.js +40 -1
- package/dist/services/function-bridge.js.map +1 -1
- package/package.json +5 -1
- package/src/config/config.ts +187 -1
- package/src/config/types.ts +73 -0
- package/src/guardrails/context.ts +37 -0
- package/src/guardrails/errors.ts +46 -0
- package/src/guardrails/index.ts +20 -0
- package/src/guardrails/plugins/base-plugin.ts +103 -0
- package/src/guardrails/plugins/index.ts +5 -0
- package/src/guardrails/plugins/pattern-blocker.ts +190 -0
- package/src/guardrails/plugins/pii-redactor/detectors.ts +200 -0
- package/src/guardrails/plugins/pii-redactor/index.ts +203 -0
- package/src/guardrails/plugins/pii-redactor/pseudonymizer.ts +91 -0
- package/src/guardrails/plugins/rate-limiter.ts +142 -0
- package/src/guardrails/plugins/token-limiter.ts +155 -0
- package/src/guardrails/service.ts +209 -0
- package/src/guardrails/types.ts +120 -0
- package/src/providers/duck-provider-enhanced.ts +76 -7
- package/src/providers/enhanced-manager.ts +5 -3
- package/src/providers/manager.ts +6 -3
- package/src/providers/provider.ts +57 -6
- package/src/server.ts +32 -6
- package/src/services/function-bridge.ts +53 -2
- package/tests/guardrails/config.test.ts +267 -0
- package/tests/guardrails/errors.test.ts +109 -0
- package/tests/guardrails/plugins/pattern-blocker.test.ts +309 -0
- package/tests/guardrails/plugins/pii-redactor.test.ts +1004 -0
- package/tests/guardrails/plugins/rate-limiter.test.ts +310 -0
- package/tests/guardrails/plugins/token-limiter.test.ts +216 -0
- package/tests/guardrails/service.test.ts +911 -0
- package/tests/mcp-bridge.test.ts +248 -0
- package/tests/providers.test.ts +739 -0
package/tests/providers.test.ts
CHANGED
|
@@ -857,4 +857,743 @@ describe('DuckProvider Error Handling', () => {
|
|
|
857
857
|
expect(callArgs.messages[0].role).toBe('system');
|
|
858
858
|
expect(callArgs.messages[0].content).toBe('You are a helpful assistant');
|
|
859
859
|
});
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
describe('DuckProvider with Guardrails', () => {
|
|
863
|
+
let mockGuardrailsService: {
|
|
864
|
+
isEnabled: jest.Mock;
|
|
865
|
+
createContext: jest.Mock;
|
|
866
|
+
execute: jest.Mock;
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
beforeEach(() => {
|
|
870
|
+
jest.clearAllMocks();
|
|
871
|
+
|
|
872
|
+
// Setup mock OpenAI response
|
|
873
|
+
mockCreate.mockResolvedValue({
|
|
874
|
+
choices: [{
|
|
875
|
+
message: { content: 'Mocked response' },
|
|
876
|
+
finish_reason: 'stop',
|
|
877
|
+
}],
|
|
878
|
+
usage: {
|
|
879
|
+
prompt_tokens: 10,
|
|
880
|
+
completion_tokens: 20,
|
|
881
|
+
total_tokens: 30,
|
|
882
|
+
},
|
|
883
|
+
model: 'test-model',
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
// Create mock guardrails service
|
|
887
|
+
mockGuardrailsService = {
|
|
888
|
+
isEnabled: jest.fn().mockReturnValue(true),
|
|
889
|
+
createContext: jest.fn().mockImplementation((params) => ({
|
|
890
|
+
requestId: 'test-request-id',
|
|
891
|
+
provider: params.provider,
|
|
892
|
+
model: params.model,
|
|
893
|
+
messages: params.messages || [],
|
|
894
|
+
prompt: params.prompt,
|
|
895
|
+
violations: [],
|
|
896
|
+
modifications: [],
|
|
897
|
+
metadata: new Map(),
|
|
898
|
+
})),
|
|
899
|
+
execute: jest.fn().mockResolvedValue({ action: 'allow', context: {} }),
|
|
900
|
+
};
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
it('should execute pre_request guardrails before chat', async () => {
|
|
904
|
+
const provider = new DuckProvider(
|
|
905
|
+
'test',
|
|
906
|
+
'Test Duck',
|
|
907
|
+
{ apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
|
|
908
|
+
mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
|
|
909
|
+
);
|
|
910
|
+
provider['client'].chat.completions.create = mockCreate;
|
|
911
|
+
|
|
912
|
+
await provider.chat({
|
|
913
|
+
messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
expect(mockGuardrailsService.isEnabled).toHaveBeenCalled();
|
|
917
|
+
expect(mockGuardrailsService.createContext).toHaveBeenCalledWith(
|
|
918
|
+
expect.objectContaining({
|
|
919
|
+
provider: 'test',
|
|
920
|
+
model: 'test-model',
|
|
921
|
+
prompt: 'Hello',
|
|
922
|
+
})
|
|
923
|
+
);
|
|
924
|
+
expect(mockGuardrailsService.execute).toHaveBeenCalledWith('pre_request', expect.any(Object));
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
it('should execute post_response guardrails after chat', async () => {
|
|
928
|
+
const provider = new DuckProvider(
|
|
929
|
+
'test',
|
|
930
|
+
'Test Duck',
|
|
931
|
+
{ apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
|
|
932
|
+
mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
|
|
933
|
+
);
|
|
934
|
+
provider['client'].chat.completions.create = mockCreate;
|
|
935
|
+
|
|
936
|
+
await provider.chat({
|
|
937
|
+
messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
// Should be called twice: pre_request and post_response
|
|
941
|
+
expect(mockGuardrailsService.execute).toHaveBeenCalledTimes(2);
|
|
942
|
+
expect(mockGuardrailsService.execute).toHaveBeenNthCalledWith(1, 'pre_request', expect.any(Object));
|
|
943
|
+
expect(mockGuardrailsService.execute).toHaveBeenNthCalledWith(2, 'post_response', expect.any(Object));
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
it('should block request when pre_request guardrails return block', async () => {
|
|
947
|
+
mockGuardrailsService.execute.mockResolvedValueOnce({
|
|
948
|
+
action: 'block',
|
|
949
|
+
blockedBy: 'rate_limiter',
|
|
950
|
+
blockReason: 'Too many requests',
|
|
951
|
+
context: {},
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
const provider = new DuckProvider(
|
|
955
|
+
'test',
|
|
956
|
+
'Test Duck',
|
|
957
|
+
{ apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
|
|
958
|
+
mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
|
|
959
|
+
);
|
|
960
|
+
provider['client'].chat.completions.create = mockCreate;
|
|
961
|
+
|
|
962
|
+
await expect(
|
|
963
|
+
provider.chat({
|
|
964
|
+
messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
|
|
965
|
+
})
|
|
966
|
+
).rejects.toThrow("Request blocked by guardrail 'rate_limiter': Too many requests");
|
|
967
|
+
|
|
968
|
+
// Should NOT call the LLM when blocked
|
|
969
|
+
expect(mockCreate).not.toHaveBeenCalled();
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
it('should block response when post_response guardrails return block', async () => {
|
|
973
|
+
// Pre-request allows, post-response blocks
|
|
974
|
+
mockGuardrailsService.execute
|
|
975
|
+
.mockResolvedValueOnce({ action: 'allow', context: {} })
|
|
976
|
+
.mockResolvedValueOnce({
|
|
977
|
+
action: 'block',
|
|
978
|
+
blockedBy: 'content_filter',
|
|
979
|
+
blockReason: 'Inappropriate content detected',
|
|
980
|
+
context: {},
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
const provider = new DuckProvider(
|
|
984
|
+
'test',
|
|
985
|
+
'Test Duck',
|
|
986
|
+
{ apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
|
|
987
|
+
mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
|
|
988
|
+
);
|
|
989
|
+
provider['client'].chat.completions.create = mockCreate;
|
|
990
|
+
|
|
991
|
+
await expect(
|
|
992
|
+
provider.chat({
|
|
993
|
+
messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
|
|
994
|
+
})
|
|
995
|
+
).rejects.toThrow("Request blocked by guardrail 'content_filter': Inappropriate content detected");
|
|
996
|
+
|
|
997
|
+
// LLM was called but response was blocked
|
|
998
|
+
expect(mockCreate).toHaveBeenCalled();
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
it('should modify messages when pre_request guardrails return modify', async () => {
|
|
1002
|
+
const modifiedMessages = [
|
|
1003
|
+
{ role: 'user' as const, content: 'Hello [EMAIL_1]', timestamp: new Date() },
|
|
1004
|
+
];
|
|
1005
|
+
|
|
1006
|
+
mockGuardrailsService.execute.mockImplementation((phase) => {
|
|
1007
|
+
if (phase === 'pre_request') {
|
|
1008
|
+
return Promise.resolve({
|
|
1009
|
+
action: 'modify',
|
|
1010
|
+
context: { messages: modifiedMessages },
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
return Promise.resolve({ action: 'allow', context: {} });
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
mockGuardrailsService.createContext.mockReturnValue({
|
|
1017
|
+
requestId: 'test-id',
|
|
1018
|
+
provider: 'test',
|
|
1019
|
+
model: 'test-model',
|
|
1020
|
+
messages: modifiedMessages,
|
|
1021
|
+
prompt: 'Hello [EMAIL_1]',
|
|
1022
|
+
violations: [],
|
|
1023
|
+
modifications: [],
|
|
1024
|
+
metadata: new Map(),
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
const provider = new DuckProvider(
|
|
1028
|
+
'test',
|
|
1029
|
+
'Test Duck',
|
|
1030
|
+
{ apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
|
|
1031
|
+
mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
|
|
1032
|
+
);
|
|
1033
|
+
provider['client'].chat.completions.create = mockCreate;
|
|
1034
|
+
|
|
1035
|
+
await provider.chat({
|
|
1036
|
+
messages: [{ role: 'user', content: 'Hello test@example.com', timestamp: new Date() }],
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
// The LLM should receive the modified (redacted) message
|
|
1040
|
+
const callArgs = mockCreate.mock.calls[0][0];
|
|
1041
|
+
expect(callArgs.messages[0].content).toBe('Hello [EMAIL_1]');
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
it('should modify response when post_response guardrails return modify', async () => {
|
|
1045
|
+
// Create a shared context object that gets modified by execute
|
|
1046
|
+
const sharedContext = {
|
|
1047
|
+
requestId: 'test-id',
|
|
1048
|
+
provider: 'test',
|
|
1049
|
+
model: 'test-model',
|
|
1050
|
+
messages: [] as Array<{ role: string; content: string; timestamp: Date }>,
|
|
1051
|
+
prompt: 'Hello',
|
|
1052
|
+
response: '',
|
|
1053
|
+
violations: [],
|
|
1054
|
+
modifications: [],
|
|
1055
|
+
metadata: new Map(),
|
|
1056
|
+
};
|
|
1057
|
+
|
|
1058
|
+
mockGuardrailsService.createContext.mockReturnValue(sharedContext);
|
|
1059
|
+
|
|
1060
|
+
mockGuardrailsService.execute.mockImplementation((phase) => {
|
|
1061
|
+
if (phase === 'post_response') {
|
|
1062
|
+
// Modify the response in the shared context
|
|
1063
|
+
sharedContext.response = 'Modified response with restored PII';
|
|
1064
|
+
return Promise.resolve({
|
|
1065
|
+
action: 'modify',
|
|
1066
|
+
context: sharedContext,
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
return Promise.resolve({ action: 'allow', context: sharedContext });
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
const provider = new DuckProvider(
|
|
1073
|
+
'test',
|
|
1074
|
+
'Test Duck',
|
|
1075
|
+
{ apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
|
|
1076
|
+
mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
|
|
1077
|
+
);
|
|
1078
|
+
provider['client'].chat.completions.create = mockCreate;
|
|
1079
|
+
|
|
1080
|
+
const response = await provider.chat({
|
|
1081
|
+
messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
expect(response.content).toBe('Modified response with restored PII');
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
it('should skip guardrails when service is disabled', async () => {
|
|
1088
|
+
mockGuardrailsService.isEnabled.mockReturnValue(false);
|
|
1089
|
+
|
|
1090
|
+
const provider = new DuckProvider(
|
|
1091
|
+
'test',
|
|
1092
|
+
'Test Duck',
|
|
1093
|
+
{ apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
|
|
1094
|
+
mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
|
|
1095
|
+
);
|
|
1096
|
+
provider['client'].chat.completions.create = mockCreate;
|
|
1097
|
+
|
|
1098
|
+
const response = await provider.chat({
|
|
1099
|
+
messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
expect(response.content).toBe('Mocked response');
|
|
1103
|
+
expect(mockGuardrailsService.createContext).not.toHaveBeenCalled();
|
|
1104
|
+
expect(mockGuardrailsService.execute).not.toHaveBeenCalled();
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
it('should work without guardrails service (undefined)', async () => {
|
|
1108
|
+
const provider = new DuckProvider(
|
|
1109
|
+
'test',
|
|
1110
|
+
'Test Duck',
|
|
1111
|
+
{ apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' }
|
|
1112
|
+
// No guardrails service passed
|
|
1113
|
+
);
|
|
1114
|
+
provider['client'].chat.completions.create = mockCreate;
|
|
1115
|
+
|
|
1116
|
+
const response = await provider.chat({
|
|
1117
|
+
messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
expect(response.content).toBe('Mocked response');
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
it('should re-throw GuardrailBlockError without wrapping', async () => {
|
|
1124
|
+
mockGuardrailsService.execute.mockRejectedValueOnce(
|
|
1125
|
+
new (await import('../src/guardrails/errors')).GuardrailBlockError('test_plugin', 'Test block reason')
|
|
1126
|
+
);
|
|
1127
|
+
|
|
1128
|
+
const provider = new DuckProvider(
|
|
1129
|
+
'test',
|
|
1130
|
+
'Test Duck',
|
|
1131
|
+
{ apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
|
|
1132
|
+
mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
|
|
1133
|
+
);
|
|
1134
|
+
provider['client'].chat.completions.create = mockCreate;
|
|
1135
|
+
|
|
1136
|
+
await expect(
|
|
1137
|
+
provider.chat({
|
|
1138
|
+
messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
|
|
1139
|
+
})
|
|
1140
|
+
).rejects.toThrow("Request blocked by guardrail 'test_plugin': Test block reason");
|
|
1141
|
+
});
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
describe('EnhancedDuckProvider with Guardrails', () => {
|
|
1145
|
+
let mockGuardrailsService: {
|
|
1146
|
+
isEnabled: jest.Mock;
|
|
1147
|
+
createContext: jest.Mock;
|
|
1148
|
+
execute: jest.Mock;
|
|
1149
|
+
};
|
|
1150
|
+
|
|
1151
|
+
let mockFunctionBridge: {
|
|
1152
|
+
getFunctionDefinitions: jest.Mock;
|
|
1153
|
+
handleFunctionCall: jest.Mock;
|
|
1154
|
+
getStats: jest.Mock;
|
|
1155
|
+
};
|
|
1156
|
+
|
|
1157
|
+
beforeEach(() => {
|
|
1158
|
+
jest.clearAllMocks();
|
|
1159
|
+
|
|
1160
|
+
mockGuardrailsService = {
|
|
1161
|
+
isEnabled: jest.fn().mockReturnValue(true),
|
|
1162
|
+
createContext: jest.fn().mockImplementation((params) => ({
|
|
1163
|
+
requestId: 'test-request-id',
|
|
1164
|
+
provider: params.provider,
|
|
1165
|
+
model: params.model,
|
|
1166
|
+
messages: params.messages || [],
|
|
1167
|
+
prompt: params.prompt,
|
|
1168
|
+
response: '',
|
|
1169
|
+
violations: [],
|
|
1170
|
+
modifications: [],
|
|
1171
|
+
metadata: new Map(),
|
|
1172
|
+
})),
|
|
1173
|
+
execute: jest.fn().mockResolvedValue({ action: 'allow', context: {} }),
|
|
1174
|
+
};
|
|
1175
|
+
|
|
1176
|
+
mockFunctionBridge = {
|
|
1177
|
+
getFunctionDefinitions: jest.fn().mockResolvedValue([]),
|
|
1178
|
+
handleFunctionCall: jest.fn(),
|
|
1179
|
+
getStats: jest.fn().mockReturnValue({ totalFunctions: 0, serverCount: 0, trustedToolCount: 0, connectedServers: [] }),
|
|
1180
|
+
};
|
|
1181
|
+
|
|
1182
|
+
// Setup mock response for regular chat
|
|
1183
|
+
mockCreate.mockResolvedValue({
|
|
1184
|
+
choices: [{
|
|
1185
|
+
message: { content: 'Mocked response' },
|
|
1186
|
+
finish_reason: 'stop',
|
|
1187
|
+
}],
|
|
1188
|
+
usage: {
|
|
1189
|
+
prompt_tokens: 10,
|
|
1190
|
+
completion_tokens: 20,
|
|
1191
|
+
total_tokens: 30,
|
|
1192
|
+
},
|
|
1193
|
+
model: 'test-model',
|
|
1194
|
+
});
|
|
1195
|
+
});
|
|
1196
|
+
|
|
1197
|
+
it('should execute pre_request guardrails before making request', async () => {
|
|
1198
|
+
const { EnhancedDuckProvider } = await import('../src/providers/duck-provider-enhanced');
|
|
1199
|
+
|
|
1200
|
+
const provider = new EnhancedDuckProvider(
|
|
1201
|
+
'test',
|
|
1202
|
+
'Test Duck',
|
|
1203
|
+
{ apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
|
|
1204
|
+
mockFunctionBridge as unknown as import('../src/services/function-bridge').FunctionBridge,
|
|
1205
|
+
false, // mcpEnabled
|
|
1206
|
+
mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
|
|
1207
|
+
);
|
|
1208
|
+
provider['client'].chat.completions.create = mockCreate;
|
|
1209
|
+
|
|
1210
|
+
await provider.chat({
|
|
1211
|
+
messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
expect(mockGuardrailsService.isEnabled).toHaveBeenCalled();
|
|
1215
|
+
expect(mockGuardrailsService.createContext).toHaveBeenCalledWith(
|
|
1216
|
+
expect.objectContaining({
|
|
1217
|
+
provider: 'test',
|
|
1218
|
+
model: 'test-model',
|
|
1219
|
+
})
|
|
1220
|
+
);
|
|
1221
|
+
expect(mockGuardrailsService.execute).toHaveBeenCalledWith('pre_request', expect.any(Object));
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
it('should execute post_response guardrails after receiving response', async () => {
|
|
1225
|
+
const { EnhancedDuckProvider } = await import('../src/providers/duck-provider-enhanced');
|
|
1226
|
+
|
|
1227
|
+
const provider = new EnhancedDuckProvider(
|
|
1228
|
+
'test',
|
|
1229
|
+
'Test Duck',
|
|
1230
|
+
{ apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
|
|
1231
|
+
mockFunctionBridge as unknown as import('../src/services/function-bridge').FunctionBridge,
|
|
1232
|
+
false,
|
|
1233
|
+
mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
|
|
1234
|
+
);
|
|
1235
|
+
provider['client'].chat.completions.create = mockCreate;
|
|
1236
|
+
|
|
1237
|
+
await provider.chat({
|
|
1238
|
+
messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
// Should be called twice: pre_request and post_response
|
|
1242
|
+
expect(mockGuardrailsService.execute).toHaveBeenCalledTimes(2);
|
|
1243
|
+
expect(mockGuardrailsService.execute).toHaveBeenNthCalledWith(1, 'pre_request', expect.any(Object));
|
|
1244
|
+
expect(mockGuardrailsService.execute).toHaveBeenNthCalledWith(2, 'post_response', expect.any(Object));
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
it('should block request when pre_request guardrails return block', async () => {
|
|
1248
|
+
const { EnhancedDuckProvider } = await import('../src/providers/duck-provider-enhanced');
|
|
1249
|
+
|
|
1250
|
+
mockGuardrailsService.execute.mockResolvedValueOnce({
|
|
1251
|
+
action: 'block',
|
|
1252
|
+
blockedBy: 'pattern_blocker',
|
|
1253
|
+
blockReason: 'Blocked pattern detected',
|
|
1254
|
+
context: {},
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
const provider = new EnhancedDuckProvider(
|
|
1258
|
+
'test',
|
|
1259
|
+
'Test Duck',
|
|
1260
|
+
{ apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
|
|
1261
|
+
mockFunctionBridge as unknown as import('../src/services/function-bridge').FunctionBridge,
|
|
1262
|
+
false,
|
|
1263
|
+
mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
|
|
1264
|
+
);
|
|
1265
|
+
provider['client'].chat.completions.create = mockCreate;
|
|
1266
|
+
|
|
1267
|
+
await expect(
|
|
1268
|
+
provider.chat({
|
|
1269
|
+
messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
|
|
1270
|
+
})
|
|
1271
|
+
).rejects.toThrow("Request blocked by guardrail 'pattern_blocker': Blocked pattern detected");
|
|
1272
|
+
|
|
1273
|
+
// API should NOT have been called
|
|
1274
|
+
expect(mockCreate).not.toHaveBeenCalled();
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
it('should block response when post_response guardrails return block', async () => {
|
|
1278
|
+
const { EnhancedDuckProvider } = await import('../src/providers/duck-provider-enhanced');
|
|
1279
|
+
|
|
1280
|
+
// Pre-request allows, post-response blocks
|
|
1281
|
+
mockGuardrailsService.execute
|
|
1282
|
+
.mockResolvedValueOnce({ action: 'allow', context: {} })
|
|
1283
|
+
.mockResolvedValueOnce({
|
|
1284
|
+
action: 'block',
|
|
1285
|
+
blockedBy: 'pii_redactor',
|
|
1286
|
+
blockReason: 'PII detected in response',
|
|
1287
|
+
context: {},
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
const provider = new EnhancedDuckProvider(
|
|
1291
|
+
'test',
|
|
1292
|
+
'Test Duck',
|
|
1293
|
+
{ apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
|
|
1294
|
+
mockFunctionBridge as unknown as import('../src/services/function-bridge').FunctionBridge,
|
|
1295
|
+
false,
|
|
1296
|
+
mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
|
|
1297
|
+
);
|
|
1298
|
+
provider['client'].chat.completions.create = mockCreate;
|
|
1299
|
+
|
|
1300
|
+
await expect(
|
|
1301
|
+
provider.chat({
|
|
1302
|
+
messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
|
|
1303
|
+
})
|
|
1304
|
+
).rejects.toThrow("Request blocked by guardrail 'pii_redactor': PII detected in response");
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
it('should modify messages when pre_request guardrails return modify', async () => {
|
|
1308
|
+
const { EnhancedDuckProvider } = await import('../src/providers/duck-provider-enhanced');
|
|
1309
|
+
|
|
1310
|
+
const modifiedMessages = [
|
|
1311
|
+
{ role: 'user' as const, content: '[REDACTED]', timestamp: new Date() }
|
|
1312
|
+
];
|
|
1313
|
+
|
|
1314
|
+
const sharedContext = {
|
|
1315
|
+
requestId: 'test-id',
|
|
1316
|
+
provider: 'test',
|
|
1317
|
+
model: 'test-model',
|
|
1318
|
+
messages: modifiedMessages,
|
|
1319
|
+
prompt: 'Hello',
|
|
1320
|
+
response: '',
|
|
1321
|
+
violations: [],
|
|
1322
|
+
modifications: [],
|
|
1323
|
+
metadata: new Map(),
|
|
1324
|
+
};
|
|
1325
|
+
|
|
1326
|
+
mockGuardrailsService.createContext.mockReturnValue(sharedContext);
|
|
1327
|
+
mockGuardrailsService.execute.mockImplementation((phase) => {
|
|
1328
|
+
if (phase === 'pre_request') {
|
|
1329
|
+
return Promise.resolve({ action: 'modify', context: sharedContext });
|
|
1330
|
+
}
|
|
1331
|
+
return Promise.resolve({ action: 'allow', context: sharedContext });
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
const provider = new EnhancedDuckProvider(
|
|
1335
|
+
'test',
|
|
1336
|
+
'Test Duck',
|
|
1337
|
+
{ apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
|
|
1338
|
+
mockFunctionBridge as unknown as import('../src/services/function-bridge').FunctionBridge,
|
|
1339
|
+
false,
|
|
1340
|
+
mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
|
|
1341
|
+
);
|
|
1342
|
+
provider['client'].chat.completions.create = mockCreate;
|
|
1343
|
+
|
|
1344
|
+
await provider.chat({
|
|
1345
|
+
messages: [{ role: 'user', content: 'sensitive data', timestamp: new Date() }],
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
// The API should receive the modified messages
|
|
1349
|
+
const callArgs = mockCreate.mock.calls[0][0] as { messages: Array<{ content: string }> };
|
|
1350
|
+
expect(callArgs.messages[0].content).toBe('[REDACTED]');
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
it('should modify response when post_response guardrails return modify', async () => {
|
|
1354
|
+
const { EnhancedDuckProvider } = await import('../src/providers/duck-provider-enhanced');
|
|
1355
|
+
|
|
1356
|
+
const sharedContext = {
|
|
1357
|
+
requestId: 'test-id',
|
|
1358
|
+
provider: 'test',
|
|
1359
|
+
model: 'test-model',
|
|
1360
|
+
messages: [],
|
|
1361
|
+
prompt: 'Hello',
|
|
1362
|
+
response: '',
|
|
1363
|
+
violations: [],
|
|
1364
|
+
modifications: [],
|
|
1365
|
+
metadata: new Map(),
|
|
1366
|
+
};
|
|
1367
|
+
|
|
1368
|
+
mockGuardrailsService.createContext.mockReturnValue(sharedContext);
|
|
1369
|
+
mockGuardrailsService.execute.mockImplementation((phase) => {
|
|
1370
|
+
if (phase === 'post_response') {
|
|
1371
|
+
sharedContext.response = '[REDACTED RESPONSE]';
|
|
1372
|
+
return Promise.resolve({ action: 'modify', context: sharedContext });
|
|
1373
|
+
}
|
|
1374
|
+
return Promise.resolve({ action: 'allow', context: sharedContext });
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
const provider = new EnhancedDuckProvider(
|
|
1378
|
+
'test',
|
|
1379
|
+
'Test Duck',
|
|
1380
|
+
{ apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
|
|
1381
|
+
mockFunctionBridge as unknown as import('../src/services/function-bridge').FunctionBridge,
|
|
1382
|
+
false,
|
|
1383
|
+
mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
|
|
1384
|
+
);
|
|
1385
|
+
provider['client'].chat.completions.create = mockCreate;
|
|
1386
|
+
|
|
1387
|
+
const result = await provider.chat({
|
|
1388
|
+
messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
expect(result.content).toBe('[REDACTED RESPONSE]');
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
it('should skip guardrails when service is disabled', async () => {
|
|
1395
|
+
const { EnhancedDuckProvider } = await import('../src/providers/duck-provider-enhanced');
|
|
1396
|
+
|
|
1397
|
+
mockGuardrailsService.isEnabled.mockReturnValue(false);
|
|
1398
|
+
|
|
1399
|
+
const provider = new EnhancedDuckProvider(
|
|
1400
|
+
'test',
|
|
1401
|
+
'Test Duck',
|
|
1402
|
+
{ apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
|
|
1403
|
+
mockFunctionBridge as unknown as import('../src/services/function-bridge').FunctionBridge,
|
|
1404
|
+
false,
|
|
1405
|
+
mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
|
|
1406
|
+
);
|
|
1407
|
+
provider['client'].chat.completions.create = mockCreate;
|
|
1408
|
+
|
|
1409
|
+
await provider.chat({
|
|
1410
|
+
messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
expect(mockGuardrailsService.createContext).not.toHaveBeenCalled();
|
|
1414
|
+
expect(mockGuardrailsService.execute).not.toHaveBeenCalled();
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
it('should work without guardrails service (undefined)', async () => {
|
|
1418
|
+
const { EnhancedDuckProvider } = await import('../src/providers/duck-provider-enhanced');
|
|
1419
|
+
|
|
1420
|
+
const provider = new EnhancedDuckProvider(
|
|
1421
|
+
'test',
|
|
1422
|
+
'Test Duck',
|
|
1423
|
+
{ apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
|
|
1424
|
+
mockFunctionBridge as unknown as import('../src/services/function-bridge').FunctionBridge,
|
|
1425
|
+
false
|
|
1426
|
+
// No guardrails service
|
|
1427
|
+
);
|
|
1428
|
+
provider['client'].chat.completions.create = mockCreate;
|
|
1429
|
+
|
|
1430
|
+
const result = await provider.chat({
|
|
1431
|
+
messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
|
|
1432
|
+
});
|
|
1433
|
+
|
|
1434
|
+
expect(result.content).toBe('Mocked response');
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1437
|
+
it('should re-throw GuardrailBlockError without wrapping', async () => {
|
|
1438
|
+
const { EnhancedDuckProvider } = await import('../src/providers/duck-provider-enhanced');
|
|
1439
|
+
const { GuardrailBlockError } = await import('../src/guardrails/errors');
|
|
1440
|
+
|
|
1441
|
+
mockGuardrailsService.execute.mockRejectedValueOnce(
|
|
1442
|
+
new GuardrailBlockError('custom_plugin', 'Custom block reason')
|
|
1443
|
+
);
|
|
1444
|
+
|
|
1445
|
+
const provider = new EnhancedDuckProvider(
|
|
1446
|
+
'test',
|
|
1447
|
+
'Test Duck',
|
|
1448
|
+
{ apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
|
|
1449
|
+
mockFunctionBridge as unknown as import('../src/services/function-bridge').FunctionBridge,
|
|
1450
|
+
false,
|
|
1451
|
+
mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
|
|
1452
|
+
);
|
|
1453
|
+
provider['client'].chat.completions.create = mockCreate;
|
|
1454
|
+
|
|
1455
|
+
await expect(
|
|
1456
|
+
provider.chat({
|
|
1457
|
+
messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
|
|
1458
|
+
})
|
|
1459
|
+
).rejects.toThrow("Request blocked by guardrail 'custom_plugin': Custom block reason");
|
|
1460
|
+
});
|
|
1461
|
+
|
|
1462
|
+
it('should apply guardrails with tool calls - blocking post_response after tool execution', async () => {
|
|
1463
|
+
const { EnhancedDuckProvider } = await import('../src/providers/duck-provider-enhanced');
|
|
1464
|
+
|
|
1465
|
+
// First call returns tool_calls, second returns final response
|
|
1466
|
+
mockCreate
|
|
1467
|
+
.mockResolvedValueOnce({
|
|
1468
|
+
choices: [{
|
|
1469
|
+
message: {
|
|
1470
|
+
content: null,
|
|
1471
|
+
tool_calls: [{
|
|
1472
|
+
id: 'call_123',
|
|
1473
|
+
type: 'function',
|
|
1474
|
+
function: {
|
|
1475
|
+
name: 'mcp__test__tool',
|
|
1476
|
+
arguments: JSON.stringify({ arg: 'value' }),
|
|
1477
|
+
},
|
|
1478
|
+
}],
|
|
1479
|
+
},
|
|
1480
|
+
finish_reason: 'tool_calls',
|
|
1481
|
+
}],
|
|
1482
|
+
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
|
1483
|
+
model: 'test-model',
|
|
1484
|
+
})
|
|
1485
|
+
.mockResolvedValueOnce({
|
|
1486
|
+
choices: [{
|
|
1487
|
+
message: { content: 'Final response with sensitive data' },
|
|
1488
|
+
finish_reason: 'stop',
|
|
1489
|
+
}],
|
|
1490
|
+
usage: { prompt_tokens: 20, completion_tokens: 30, total_tokens: 50 },
|
|
1491
|
+
model: 'test-model',
|
|
1492
|
+
});
|
|
1493
|
+
|
|
1494
|
+
mockFunctionBridge.handleFunctionCall.mockResolvedValue({
|
|
1495
|
+
success: true,
|
|
1496
|
+
data: { result: 'tool result' },
|
|
1497
|
+
});
|
|
1498
|
+
|
|
1499
|
+
// Pre-request allows, post-response blocks
|
|
1500
|
+
mockGuardrailsService.execute
|
|
1501
|
+
.mockResolvedValueOnce({ action: 'allow', context: {} }) // pre_request
|
|
1502
|
+
.mockResolvedValueOnce({
|
|
1503
|
+
action: 'block',
|
|
1504
|
+
blockedBy: 'pii_redactor',
|
|
1505
|
+
blockReason: 'Sensitive data in final response',
|
|
1506
|
+
context: {},
|
|
1507
|
+
}); // post_response
|
|
1508
|
+
|
|
1509
|
+
const provider = new EnhancedDuckProvider(
|
|
1510
|
+
'test',
|
|
1511
|
+
'Test Duck',
|
|
1512
|
+
{ apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
|
|
1513
|
+
mockFunctionBridge as unknown as import('../src/services/function-bridge').FunctionBridge,
|
|
1514
|
+
true, // mcpEnabled
|
|
1515
|
+
mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
|
|
1516
|
+
);
|
|
1517
|
+
provider['client'].chat.completions.create = mockCreate;
|
|
1518
|
+
|
|
1519
|
+
await expect(
|
|
1520
|
+
provider.chat({
|
|
1521
|
+
messages: [{ role: 'user', content: 'Run the tool', timestamp: new Date() }],
|
|
1522
|
+
})
|
|
1523
|
+
).rejects.toThrow("Request blocked by guardrail 'pii_redactor': Sensitive data in final response");
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1526
|
+
it('should modify tool result when post_response guardrails return modify after tool calls', async () => {
|
|
1527
|
+
const { EnhancedDuckProvider } = await import('../src/providers/duck-provider-enhanced');
|
|
1528
|
+
|
|
1529
|
+
mockCreate
|
|
1530
|
+
.mockResolvedValueOnce({
|
|
1531
|
+
choices: [{
|
|
1532
|
+
message: {
|
|
1533
|
+
content: null,
|
|
1534
|
+
tool_calls: [{
|
|
1535
|
+
id: 'call_123',
|
|
1536
|
+
type: 'function',
|
|
1537
|
+
function: {
|
|
1538
|
+
name: 'mcp__test__tool',
|
|
1539
|
+
arguments: JSON.stringify({ arg: 'value' }),
|
|
1540
|
+
},
|
|
1541
|
+
}],
|
|
1542
|
+
},
|
|
1543
|
+
finish_reason: 'tool_calls',
|
|
1544
|
+
}],
|
|
1545
|
+
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
|
1546
|
+
model: 'test-model',
|
|
1547
|
+
})
|
|
1548
|
+
.mockResolvedValueOnce({
|
|
1549
|
+
choices: [{
|
|
1550
|
+
message: { content: 'Final response with SSN: 123-45-6789' },
|
|
1551
|
+
finish_reason: 'stop',
|
|
1552
|
+
}],
|
|
1553
|
+
usage: { prompt_tokens: 20, completion_tokens: 30, total_tokens: 50 },
|
|
1554
|
+
model: 'test-model',
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
mockFunctionBridge.handleFunctionCall.mockResolvedValue({
|
|
1558
|
+
success: true,
|
|
1559
|
+
data: { result: 'tool result' },
|
|
1560
|
+
});
|
|
1561
|
+
|
|
1562
|
+
const sharedContext = {
|
|
1563
|
+
requestId: 'test-id',
|
|
1564
|
+
provider: 'test',
|
|
1565
|
+
model: 'test-model',
|
|
1566
|
+
messages: [],
|
|
1567
|
+
prompt: 'Run the tool',
|
|
1568
|
+
response: '',
|
|
1569
|
+
violations: [],
|
|
1570
|
+
modifications: [],
|
|
1571
|
+
metadata: new Map(),
|
|
1572
|
+
};
|
|
1573
|
+
|
|
1574
|
+
mockGuardrailsService.createContext.mockReturnValue(sharedContext);
|
|
1575
|
+
mockGuardrailsService.execute.mockImplementation((phase) => {
|
|
1576
|
+
if (phase === 'post_response') {
|
|
1577
|
+
sharedContext.response = 'Final response with SSN: [REDACTED]';
|
|
1578
|
+
return Promise.resolve({ action: 'modify', context: sharedContext });
|
|
1579
|
+
}
|
|
1580
|
+
return Promise.resolve({ action: 'allow', context: sharedContext });
|
|
1581
|
+
});
|
|
1582
|
+
|
|
1583
|
+
const provider = new EnhancedDuckProvider(
|
|
1584
|
+
'test',
|
|
1585
|
+
'Test Duck',
|
|
1586
|
+
{ apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
|
|
1587
|
+
mockFunctionBridge as unknown as import('../src/services/function-bridge').FunctionBridge,
|
|
1588
|
+
true,
|
|
1589
|
+
mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
|
|
1590
|
+
);
|
|
1591
|
+
provider['client'].chat.completions.create = mockCreate;
|
|
1592
|
+
|
|
1593
|
+
const result = await provider.chat({
|
|
1594
|
+
messages: [{ role: 'user', content: 'Run the tool', timestamp: new Date() }],
|
|
1595
|
+
});
|
|
1596
|
+
|
|
1597
|
+
expect(result.content).toBe('Final response with SSN: [REDACTED]');
|
|
1598
|
+
});
|
|
860
1599
|
});
|