mcp-rubber-duck 1.8.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +158 -1
- package/audit-ci.json +2 -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 +1 -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
|
@@ -0,0 +1,911 @@
|
|
|
1
|
+
import { describe, it, expect, jest, beforeEach } from '@jest/globals';
|
|
2
|
+
import {
|
|
3
|
+
GuardrailsService,
|
|
4
|
+
createGuardrailContext,
|
|
5
|
+
cloneContext,
|
|
6
|
+
GuardrailContext,
|
|
7
|
+
GuardrailPhase,
|
|
8
|
+
GuardrailResult,
|
|
9
|
+
GuardrailPlugin,
|
|
10
|
+
} from '../../src/guardrails';
|
|
11
|
+
|
|
12
|
+
// Mock logger to avoid console noise during tests
|
|
13
|
+
jest.mock('../../src/utils/logger');
|
|
14
|
+
|
|
15
|
+
describe('GuardrailsService', () => {
|
|
16
|
+
describe('initialization', () => {
|
|
17
|
+
it('should be disabled by default', async () => {
|
|
18
|
+
const service = new GuardrailsService();
|
|
19
|
+
await service.initialize();
|
|
20
|
+
|
|
21
|
+
expect(service.isEnabled()).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should be disabled when config.enabled is false', async () => {
|
|
25
|
+
const service = new GuardrailsService({ enabled: false });
|
|
26
|
+
await service.initialize();
|
|
27
|
+
|
|
28
|
+
expect(service.isEnabled()).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should have no plugins when disabled', async () => {
|
|
32
|
+
const service = new GuardrailsService({ enabled: false });
|
|
33
|
+
await service.initialize();
|
|
34
|
+
|
|
35
|
+
expect(service.getPlugins()).toHaveLength(0);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('context creation', () => {
|
|
40
|
+
it('should create context with defaults', () => {
|
|
41
|
+
const service = new GuardrailsService();
|
|
42
|
+
const context = service.createContext({});
|
|
43
|
+
|
|
44
|
+
expect(context.requestId).toBeDefined();
|
|
45
|
+
expect(context.provider).toBe('unknown');
|
|
46
|
+
expect(context.model).toBe('unknown');
|
|
47
|
+
expect(context.timestamp).toBeInstanceOf(Date);
|
|
48
|
+
expect(context.messages).toEqual([]);
|
|
49
|
+
expect(context.metadata).toBeInstanceOf(Map);
|
|
50
|
+
expect(context.violations).toEqual([]);
|
|
51
|
+
expect(context.modifications).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should create context with provided values', () => {
|
|
55
|
+
const service = new GuardrailsService();
|
|
56
|
+
const context = service.createContext({
|
|
57
|
+
requestId: 'test-123',
|
|
58
|
+
provider: 'openai',
|
|
59
|
+
model: 'gpt-4',
|
|
60
|
+
prompt: 'Hello world',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(context.requestId).toBe('test-123');
|
|
64
|
+
expect(context.provider).toBe('openai');
|
|
65
|
+
expect(context.model).toBe('gpt-4');
|
|
66
|
+
expect(context.prompt).toBe('Hello world');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('execute', () => {
|
|
71
|
+
it('should allow requests when disabled', async () => {
|
|
72
|
+
const service = new GuardrailsService({ enabled: false });
|
|
73
|
+
await service.initialize();
|
|
74
|
+
|
|
75
|
+
const context = createGuardrailContext({ prompt: 'test' });
|
|
76
|
+
const result = await service.execute('pre_request', context);
|
|
77
|
+
|
|
78
|
+
expect(result.action).toBe('allow');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should allow requests when no plugins match phase', async () => {
|
|
82
|
+
const service = new GuardrailsService({ enabled: true });
|
|
83
|
+
await service.initialize();
|
|
84
|
+
|
|
85
|
+
const context = createGuardrailContext({ prompt: 'test' });
|
|
86
|
+
const result = await service.execute('pre_request', context);
|
|
87
|
+
|
|
88
|
+
expect(result.action).toBe('allow');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('shutdown', () => {
|
|
93
|
+
it('should disable service on shutdown', async () => {
|
|
94
|
+
const service = new GuardrailsService({ enabled: true });
|
|
95
|
+
await service.initialize();
|
|
96
|
+
await service.shutdown();
|
|
97
|
+
|
|
98
|
+
expect(service.isEnabled()).toBe(false);
|
|
99
|
+
expect(service.getPlugins()).toHaveLength(0);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('createGuardrailContext', () => {
|
|
105
|
+
it('should generate unique request IDs', () => {
|
|
106
|
+
const context1 = createGuardrailContext({});
|
|
107
|
+
const context2 = createGuardrailContext({});
|
|
108
|
+
|
|
109
|
+
expect(context1.requestId).not.toBe(context2.requestId);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should use provided request ID', () => {
|
|
113
|
+
const context = createGuardrailContext({ requestId: 'custom-id' });
|
|
114
|
+
expect(context.requestId).toBe('custom-id');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should initialize empty violations and modifications', () => {
|
|
118
|
+
const context = createGuardrailContext({});
|
|
119
|
+
|
|
120
|
+
expect(context.violations).toEqual([]);
|
|
121
|
+
expect(context.modifications).toEqual([]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should initialize empty metadata map', () => {
|
|
125
|
+
const context = createGuardrailContext({});
|
|
126
|
+
|
|
127
|
+
expect(context.metadata).toBeInstanceOf(Map);
|
|
128
|
+
expect(context.metadata.size).toBe(0);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('cloneContext', () => {
|
|
133
|
+
let originalContext: GuardrailContext;
|
|
134
|
+
|
|
135
|
+
beforeEach(() => {
|
|
136
|
+
originalContext = createGuardrailContext({
|
|
137
|
+
requestId: 'test-id',
|
|
138
|
+
provider: 'openai',
|
|
139
|
+
model: 'gpt-4',
|
|
140
|
+
prompt: 'Hello',
|
|
141
|
+
messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
|
|
142
|
+
toolArgs: { key: 'value' },
|
|
143
|
+
});
|
|
144
|
+
originalContext.metadata.set('testKey', 'testValue');
|
|
145
|
+
originalContext.violations.push({
|
|
146
|
+
pluginName: 'test',
|
|
147
|
+
phase: 'pre_request',
|
|
148
|
+
rule: 'test-rule',
|
|
149
|
+
severity: 'warning',
|
|
150
|
+
message: 'Test violation',
|
|
151
|
+
});
|
|
152
|
+
originalContext.modifications.push({
|
|
153
|
+
pluginName: 'test',
|
|
154
|
+
phase: 'pre_request',
|
|
155
|
+
field: 'prompt',
|
|
156
|
+
reason: 'Test modification',
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should create a new object (not same reference)', () => {
|
|
161
|
+
const cloned = cloneContext(originalContext);
|
|
162
|
+
|
|
163
|
+
expect(cloned).not.toBe(originalContext);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should copy all primitive fields', () => {
|
|
167
|
+
const cloned = cloneContext(originalContext);
|
|
168
|
+
|
|
169
|
+
expect(cloned.requestId).toBe(originalContext.requestId);
|
|
170
|
+
expect(cloned.provider).toBe(originalContext.provider);
|
|
171
|
+
expect(cloned.model).toBe(originalContext.model);
|
|
172
|
+
expect(cloned.prompt).toBe(originalContext.prompt);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should deep copy messages array', () => {
|
|
176
|
+
const cloned = cloneContext(originalContext);
|
|
177
|
+
|
|
178
|
+
expect(cloned.messages).toEqual(originalContext.messages);
|
|
179
|
+
expect(cloned.messages).not.toBe(originalContext.messages);
|
|
180
|
+
|
|
181
|
+
// Modifying cloned messages should not affect original
|
|
182
|
+
cloned.messages.push({ role: 'assistant', content: 'Hi', timestamp: new Date() });
|
|
183
|
+
expect(originalContext.messages).toHaveLength(1);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should deep copy toolArgs object', () => {
|
|
187
|
+
const cloned = cloneContext(originalContext);
|
|
188
|
+
|
|
189
|
+
expect(cloned.toolArgs).toEqual(originalContext.toolArgs);
|
|
190
|
+
expect(cloned.toolArgs).not.toBe(originalContext.toolArgs);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should deep copy metadata Map', () => {
|
|
194
|
+
const cloned = cloneContext(originalContext);
|
|
195
|
+
|
|
196
|
+
expect(cloned.metadata.get('testKey')).toBe('testValue');
|
|
197
|
+
expect(cloned.metadata).not.toBe(originalContext.metadata);
|
|
198
|
+
|
|
199
|
+
// Modifying cloned metadata should not affect original
|
|
200
|
+
cloned.metadata.set('newKey', 'newValue');
|
|
201
|
+
expect(originalContext.metadata.has('newKey')).toBe(false);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should copy violations array', () => {
|
|
205
|
+
const cloned = cloneContext(originalContext);
|
|
206
|
+
|
|
207
|
+
expect(cloned.violations).toEqual(originalContext.violations);
|
|
208
|
+
expect(cloned.violations).not.toBe(originalContext.violations);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should copy modifications array', () => {
|
|
212
|
+
const cloned = cloneContext(originalContext);
|
|
213
|
+
|
|
214
|
+
expect(cloned.modifications).toEqual(originalContext.modifications);
|
|
215
|
+
expect(cloned.modifications).not.toBe(originalContext.modifications);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should handle undefined toolArgs', () => {
|
|
219
|
+
const contextWithoutToolArgs = createGuardrailContext({ prompt: 'test' });
|
|
220
|
+
const cloned = cloneContext(contextWithoutToolArgs);
|
|
221
|
+
|
|
222
|
+
expect(cloned.toolArgs).toBeUndefined();
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('GuardrailsService with real plugins', () => {
|
|
227
|
+
describe('plugin loading', () => {
|
|
228
|
+
it('should load rate_limiter plugin from config', async () => {
|
|
229
|
+
const service = new GuardrailsService({
|
|
230
|
+
enabled: true,
|
|
231
|
+
plugins: {
|
|
232
|
+
rate_limiter: {
|
|
233
|
+
enabled: true,
|
|
234
|
+
requests_per_minute: 10,
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
await service.initialize();
|
|
240
|
+
|
|
241
|
+
expect(service.isEnabled()).toBe(true);
|
|
242
|
+
const plugins = service.getPlugins();
|
|
243
|
+
expect(plugins).toHaveLength(1);
|
|
244
|
+
expect(plugins[0].name).toBe('rate_limiter');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should load token_limiter plugin from config', async () => {
|
|
248
|
+
const service = new GuardrailsService({
|
|
249
|
+
enabled: true,
|
|
250
|
+
plugins: {
|
|
251
|
+
token_limiter: {
|
|
252
|
+
enabled: true,
|
|
253
|
+
max_input_tokens: 1000,
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
await service.initialize();
|
|
259
|
+
|
|
260
|
+
const plugins = service.getPlugins();
|
|
261
|
+
expect(plugins).toHaveLength(1);
|
|
262
|
+
expect(plugins[0].name).toBe('token_limiter');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should load pattern_blocker plugin from config', async () => {
|
|
266
|
+
const service = new GuardrailsService({
|
|
267
|
+
enabled: true,
|
|
268
|
+
plugins: {
|
|
269
|
+
pattern_blocker: {
|
|
270
|
+
enabled: true,
|
|
271
|
+
blocked_patterns: ['secret'],
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
await service.initialize();
|
|
277
|
+
|
|
278
|
+
const plugins = service.getPlugins();
|
|
279
|
+
expect(plugins).toHaveLength(1);
|
|
280
|
+
expect(plugins[0].name).toBe('pattern_blocker');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('should load pii_redactor plugin from config', async () => {
|
|
284
|
+
const service = new GuardrailsService({
|
|
285
|
+
enabled: true,
|
|
286
|
+
plugins: {
|
|
287
|
+
pii_redactor: {
|
|
288
|
+
enabled: true,
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
await service.initialize();
|
|
294
|
+
|
|
295
|
+
const plugins = service.getPlugins();
|
|
296
|
+
expect(plugins).toHaveLength(1);
|
|
297
|
+
expect(plugins[0].name).toBe('pii_redactor');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('should load multiple plugins and sort by priority', async () => {
|
|
301
|
+
const service = new GuardrailsService({
|
|
302
|
+
enabled: true,
|
|
303
|
+
plugins: {
|
|
304
|
+
rate_limiter: {
|
|
305
|
+
enabled: true,
|
|
306
|
+
priority: 30,
|
|
307
|
+
},
|
|
308
|
+
token_limiter: {
|
|
309
|
+
enabled: true,
|
|
310
|
+
priority: 10,
|
|
311
|
+
},
|
|
312
|
+
pattern_blocker: {
|
|
313
|
+
enabled: true,
|
|
314
|
+
priority: 20,
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
await service.initialize();
|
|
320
|
+
|
|
321
|
+
const plugins = service.getPlugins();
|
|
322
|
+
expect(plugins).toHaveLength(3);
|
|
323
|
+
// Should be sorted by priority
|
|
324
|
+
expect(plugins[0].name).toBe('token_limiter');
|
|
325
|
+
expect(plugins[1].name).toBe('pattern_blocker');
|
|
326
|
+
expect(plugins[2].name).toBe('rate_limiter');
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('should skip plugins that are disabled in config', async () => {
|
|
330
|
+
const service = new GuardrailsService({
|
|
331
|
+
enabled: true,
|
|
332
|
+
plugins: {
|
|
333
|
+
rate_limiter: {
|
|
334
|
+
enabled: true,
|
|
335
|
+
},
|
|
336
|
+
token_limiter: {
|
|
337
|
+
enabled: false,
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
await service.initialize();
|
|
343
|
+
|
|
344
|
+
const plugins = service.getPlugins();
|
|
345
|
+
expect(plugins).toHaveLength(1);
|
|
346
|
+
expect(plugins[0].name).toBe('rate_limiter');
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('should remain disabled if no plugins are enabled', async () => {
|
|
350
|
+
const service = new GuardrailsService({
|
|
351
|
+
enabled: true,
|
|
352
|
+
plugins: {
|
|
353
|
+
rate_limiter: {
|
|
354
|
+
enabled: false,
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
await service.initialize();
|
|
360
|
+
|
|
361
|
+
expect(service.isEnabled()).toBe(false);
|
|
362
|
+
expect(service.getPlugins()).toHaveLength(0);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('should handle unknown plugin names gracefully', async () => {
|
|
366
|
+
// Access private method via type assertion to test error path
|
|
367
|
+
const service = new GuardrailsService({ enabled: true });
|
|
368
|
+
|
|
369
|
+
// The loadPlugin method throws for unknown plugins, but this is caught
|
|
370
|
+
// in loadPluginsFromConfig. We test indirectly by checking that unknown
|
|
371
|
+
// plugins in config don't crash initialization.
|
|
372
|
+
await service.initialize();
|
|
373
|
+
expect(service.isEnabled()).toBe(false);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('should continue loading other plugins when one fails to initialize', async () => {
|
|
377
|
+
// We can't easily make a real plugin fail, but we can verify the error
|
|
378
|
+
// handling path exists by checking that valid plugins still load even
|
|
379
|
+
// when the config has issues
|
|
380
|
+
const service = new GuardrailsService({
|
|
381
|
+
enabled: true,
|
|
382
|
+
plugins: {
|
|
383
|
+
rate_limiter: {
|
|
384
|
+
enabled: true,
|
|
385
|
+
},
|
|
386
|
+
token_limiter: {
|
|
387
|
+
enabled: true,
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
await service.initialize();
|
|
393
|
+
|
|
394
|
+
// Both plugins should load successfully
|
|
395
|
+
expect(service.getPlugins()).toHaveLength(2);
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
describe('violation logging', () => {
|
|
400
|
+
it('should log violations when pattern blocker detects blocked content', async () => {
|
|
401
|
+
const service = new GuardrailsService({
|
|
402
|
+
enabled: true,
|
|
403
|
+
log_violations: true,
|
|
404
|
+
plugins: {
|
|
405
|
+
pattern_blocker: {
|
|
406
|
+
enabled: true,
|
|
407
|
+
blocked_patterns: ['forbidden'],
|
|
408
|
+
action_on_match: 'block',
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
await service.initialize();
|
|
414
|
+
|
|
415
|
+
const context = createGuardrailContext({ prompt: 'This contains forbidden content' });
|
|
416
|
+
const result = await service.execute('pre_request', context);
|
|
417
|
+
|
|
418
|
+
expect(result.action).toBe('block');
|
|
419
|
+
expect(result.context.violations.length).toBeGreaterThan(0);
|
|
420
|
+
expect(result.context.violations[0].pluginName).toBe('pattern_blocker');
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
describe('modification logging', () => {
|
|
425
|
+
it('should log modifications when pii_redactor redacts content', async () => {
|
|
426
|
+
const service = new GuardrailsService({
|
|
427
|
+
enabled: true,
|
|
428
|
+
log_modifications: true,
|
|
429
|
+
plugins: {
|
|
430
|
+
pii_redactor: {
|
|
431
|
+
enabled: true,
|
|
432
|
+
detect_emails: true,
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
await service.initialize();
|
|
438
|
+
|
|
439
|
+
const context = createGuardrailContext({ prompt: 'Contact me at test@example.com' });
|
|
440
|
+
const result = await service.execute('pre_request', context);
|
|
441
|
+
|
|
442
|
+
// Service returns 'modify' when content was modified
|
|
443
|
+
expect(result.action).toBe('modify');
|
|
444
|
+
expect(result.context.modifications.length).toBeGreaterThan(0);
|
|
445
|
+
expect(result.context.modifications[0].pluginName).toBe('pii_redactor');
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
describe('real plugin execution flow', () => {
|
|
450
|
+
it('should block request when rate limit exceeded', async () => {
|
|
451
|
+
const service = new GuardrailsService({
|
|
452
|
+
enabled: true,
|
|
453
|
+
plugins: {
|
|
454
|
+
rate_limiter: {
|
|
455
|
+
enabled: true,
|
|
456
|
+
requests_per_minute: 2,
|
|
457
|
+
burst_allowance: 0, // No bursting - strict limit
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
await service.initialize();
|
|
463
|
+
|
|
464
|
+
// First two requests should be allowed
|
|
465
|
+
const context1 = createGuardrailContext({ prompt: 'Request 1' });
|
|
466
|
+
const result1 = await service.execute('pre_request', context1);
|
|
467
|
+
expect(result1.action).toBe('allow');
|
|
468
|
+
|
|
469
|
+
const context2 = createGuardrailContext({ prompt: 'Request 2' });
|
|
470
|
+
const result2 = await service.execute('pre_request', context2);
|
|
471
|
+
expect(result2.action).toBe('allow');
|
|
472
|
+
|
|
473
|
+
// Third request should be blocked
|
|
474
|
+
const context3 = createGuardrailContext({ prompt: 'Request 3' });
|
|
475
|
+
const result3 = await service.execute('pre_request', context3);
|
|
476
|
+
expect(result3.action).toBe('block');
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('should block request when token limit exceeded', async () => {
|
|
480
|
+
const service = new GuardrailsService({
|
|
481
|
+
enabled: true,
|
|
482
|
+
plugins: {
|
|
483
|
+
token_limiter: {
|
|
484
|
+
enabled: true,
|
|
485
|
+
max_input_tokens: 10,
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
await service.initialize();
|
|
491
|
+
|
|
492
|
+
// Long prompt that exceeds token limit
|
|
493
|
+
const context = createGuardrailContext({
|
|
494
|
+
prompt: 'This is a very long prompt that should definitely exceed the token limit we set',
|
|
495
|
+
});
|
|
496
|
+
const result = await service.execute('pre_request', context);
|
|
497
|
+
|
|
498
|
+
expect(result.action).toBe('block');
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('should redact PII and allow request', async () => {
|
|
502
|
+
const service = new GuardrailsService({
|
|
503
|
+
enabled: true,
|
|
504
|
+
plugins: {
|
|
505
|
+
pii_redactor: {
|
|
506
|
+
enabled: true,
|
|
507
|
+
detect_emails: true,
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
await service.initialize();
|
|
513
|
+
|
|
514
|
+
const context = createGuardrailContext({
|
|
515
|
+
prompt: 'My email is user@domain.com',
|
|
516
|
+
});
|
|
517
|
+
const result = await service.execute('pre_request', context);
|
|
518
|
+
|
|
519
|
+
// Service returns 'modify' when content was modified
|
|
520
|
+
expect(result.action).toBe('modify');
|
|
521
|
+
expect(result.context.prompt).not.toContain('user@domain.com');
|
|
522
|
+
expect(result.context.prompt).toContain('[EMAIL_');
|
|
523
|
+
expect(result.context.modifications.length).toBeGreaterThan(0);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it('should run multiple plugins in order', async () => {
|
|
527
|
+
const service = new GuardrailsService({
|
|
528
|
+
enabled: true,
|
|
529
|
+
plugins: {
|
|
530
|
+
rate_limiter: {
|
|
531
|
+
enabled: true,
|
|
532
|
+
requests_per_minute: 100,
|
|
533
|
+
priority: 10,
|
|
534
|
+
},
|
|
535
|
+
pii_redactor: {
|
|
536
|
+
enabled: true,
|
|
537
|
+
detect_emails: true,
|
|
538
|
+
priority: 20,
|
|
539
|
+
},
|
|
540
|
+
},
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
await service.initialize();
|
|
544
|
+
|
|
545
|
+
const context = createGuardrailContext({
|
|
546
|
+
prompt: 'Contact: test@example.com',
|
|
547
|
+
});
|
|
548
|
+
const result = await service.execute('pre_request', context);
|
|
549
|
+
|
|
550
|
+
// Rate limiter runs first (priority 10), then PII redactor (priority 20)
|
|
551
|
+
// Service returns 'modify' when content was modified
|
|
552
|
+
expect(result.action).toBe('modify');
|
|
553
|
+
expect(result.context.prompt).not.toContain('test@example.com');
|
|
554
|
+
expect(result.context.modifications.length).toBeGreaterThan(0);
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
describe('GuardrailsService integration', () => {
|
|
560
|
+
// Helper to create a mock plugin
|
|
561
|
+
function createMockPlugin(
|
|
562
|
+
name: string,
|
|
563
|
+
phases: GuardrailPhase[],
|
|
564
|
+
executeFn: (phase: GuardrailPhase, context: GuardrailContext) => Promise<GuardrailResult>
|
|
565
|
+
): GuardrailPlugin {
|
|
566
|
+
return {
|
|
567
|
+
name,
|
|
568
|
+
enabled: true,
|
|
569
|
+
priority: 50,
|
|
570
|
+
phases,
|
|
571
|
+
initialize: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
|
572
|
+
execute: jest.fn(executeFn),
|
|
573
|
+
shutdown: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
describe('plugin execution', () => {
|
|
578
|
+
it('should execute plugins in priority order', async () => {
|
|
579
|
+
const executionOrder: string[] = [];
|
|
580
|
+
|
|
581
|
+
const plugin1 = createMockPlugin('first', ['pre_request'], async (_phase, context) => {
|
|
582
|
+
executionOrder.push('first');
|
|
583
|
+
return { action: 'allow', context };
|
|
584
|
+
});
|
|
585
|
+
plugin1.priority = 10;
|
|
586
|
+
|
|
587
|
+
const plugin2 = createMockPlugin('second', ['pre_request'], async (_phase, context) => {
|
|
588
|
+
executionOrder.push('second');
|
|
589
|
+
return { action: 'allow', context };
|
|
590
|
+
});
|
|
591
|
+
plugin2.priority = 20;
|
|
592
|
+
|
|
593
|
+
const plugin3 = createMockPlugin('third', ['pre_request'], async (_phase, context) => {
|
|
594
|
+
executionOrder.push('third');
|
|
595
|
+
return { action: 'allow', context };
|
|
596
|
+
});
|
|
597
|
+
plugin3.priority = 5; // Lowest priority number = runs first
|
|
598
|
+
|
|
599
|
+
const service = new GuardrailsService({ enabled: true });
|
|
600
|
+
await service.initialize();
|
|
601
|
+
|
|
602
|
+
// Manually add plugins (simulating plugin loading)
|
|
603
|
+
(service as unknown as { plugins: GuardrailPlugin[] }).plugins = [plugin1, plugin2, plugin3];
|
|
604
|
+
(service as unknown as { plugins: GuardrailPlugin[] }).plugins.sort((a, b) => a.priority - b.priority);
|
|
605
|
+
(service as unknown as { enabled: boolean }).enabled = true;
|
|
606
|
+
|
|
607
|
+
const context = createGuardrailContext({ prompt: 'test' });
|
|
608
|
+
await service.execute('pre_request', context);
|
|
609
|
+
|
|
610
|
+
expect(executionOrder).toEqual(['third', 'first', 'second']);
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
it('should only execute plugins that handle the requested phase', async () => {
|
|
614
|
+
const executedPlugins: string[] = [];
|
|
615
|
+
|
|
616
|
+
const preRequestPlugin = createMockPlugin('pre_only', ['pre_request'], async (_phase, context) => {
|
|
617
|
+
executedPlugins.push('pre_only');
|
|
618
|
+
return { action: 'allow', context };
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
const postResponsePlugin = createMockPlugin('post_only', ['post_response'], async (_phase, context) => {
|
|
622
|
+
executedPlugins.push('post_only');
|
|
623
|
+
return { action: 'allow', context };
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
const service = new GuardrailsService({ enabled: true });
|
|
627
|
+
await service.initialize();
|
|
628
|
+
|
|
629
|
+
(service as unknown as { plugins: GuardrailPlugin[] }).plugins = [preRequestPlugin, postResponsePlugin];
|
|
630
|
+
(service as unknown as { enabled: boolean }).enabled = true;
|
|
631
|
+
|
|
632
|
+
const context = createGuardrailContext({ prompt: 'test' });
|
|
633
|
+
await service.execute('pre_request', context);
|
|
634
|
+
|
|
635
|
+
expect(executedPlugins).toEqual(['pre_only']);
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it('should stop execution and return block when plugin blocks', async () => {
|
|
639
|
+
const executedPlugins: string[] = [];
|
|
640
|
+
|
|
641
|
+
const blockingPlugin = createMockPlugin('blocker', ['pre_request'], async (_phase, context) => {
|
|
642
|
+
executedPlugins.push('blocker');
|
|
643
|
+
return {
|
|
644
|
+
action: 'block',
|
|
645
|
+
context,
|
|
646
|
+
blockedBy: 'blocker',
|
|
647
|
+
blockReason: 'Content not allowed',
|
|
648
|
+
};
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
const afterPlugin = createMockPlugin('after', ['pre_request'], async (_phase, context) => {
|
|
652
|
+
executedPlugins.push('after');
|
|
653
|
+
return { action: 'allow', context };
|
|
654
|
+
});
|
|
655
|
+
afterPlugin.priority = 100;
|
|
656
|
+
|
|
657
|
+
const service = new GuardrailsService({ enabled: true });
|
|
658
|
+
await service.initialize();
|
|
659
|
+
|
|
660
|
+
(service as unknown as { plugins: GuardrailPlugin[] }).plugins = [blockingPlugin, afterPlugin];
|
|
661
|
+
(service as unknown as { enabled: boolean }).enabled = true;
|
|
662
|
+
|
|
663
|
+
const context = createGuardrailContext({ prompt: 'test' });
|
|
664
|
+
const result = await service.execute('pre_request', context);
|
|
665
|
+
|
|
666
|
+
expect(result.action).toBe('block');
|
|
667
|
+
expect(result.blockedBy).toBe('blocker');
|
|
668
|
+
expect(result.blockReason).toBe('Content not allowed');
|
|
669
|
+
expect(executedPlugins).toEqual(['blocker']); // 'after' should not run
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it('should pass modified context to subsequent plugins', async () => {
|
|
673
|
+
const plugin1 = createMockPlugin('modifier', ['pre_request'], async (_phase, context) => {
|
|
674
|
+
context.prompt = 'modified prompt';
|
|
675
|
+
return { action: 'modify', context };
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
let receivedPrompt = '';
|
|
679
|
+
const plugin2 = createMockPlugin('receiver', ['pre_request'], async (_phase, context) => {
|
|
680
|
+
receivedPrompt = context.prompt || '';
|
|
681
|
+
return { action: 'allow', context };
|
|
682
|
+
});
|
|
683
|
+
plugin2.priority = 100;
|
|
684
|
+
|
|
685
|
+
const service = new GuardrailsService({ enabled: true });
|
|
686
|
+
await service.initialize();
|
|
687
|
+
|
|
688
|
+
(service as unknown as { plugins: GuardrailPlugin[] }).plugins = [plugin1, plugin2];
|
|
689
|
+
(service as unknown as { enabled: boolean }).enabled = true;
|
|
690
|
+
|
|
691
|
+
const context = createGuardrailContext({ prompt: 'original' });
|
|
692
|
+
await service.execute('pre_request', context);
|
|
693
|
+
|
|
694
|
+
expect(receivedPrompt).toBe('modified prompt');
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
describe('error handling', () => {
|
|
699
|
+
it('should block on plugin error when fail_open is false', async () => {
|
|
700
|
+
const errorPlugin = createMockPlugin('error_plugin', ['pre_request'], async () => {
|
|
701
|
+
throw new Error('Plugin crashed');
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
const service = new GuardrailsService({ enabled: true, fail_open: false });
|
|
705
|
+
await service.initialize();
|
|
706
|
+
|
|
707
|
+
(service as unknown as { plugins: GuardrailPlugin[] }).plugins = [errorPlugin];
|
|
708
|
+
(service as unknown as { enabled: boolean }).enabled = true;
|
|
709
|
+
|
|
710
|
+
const context = createGuardrailContext({ prompt: 'test' });
|
|
711
|
+
const result = await service.execute('pre_request', context);
|
|
712
|
+
|
|
713
|
+
expect(result.action).toBe('block');
|
|
714
|
+
expect(result.blockedBy).toBe('error_plugin');
|
|
715
|
+
expect(result.blockReason).toContain('Plugin error');
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it('should continue on plugin error when fail_open is true', async () => {
|
|
719
|
+
const executedPlugins: string[] = [];
|
|
720
|
+
|
|
721
|
+
const errorPlugin = createMockPlugin('error_plugin', ['pre_request'], async () => {
|
|
722
|
+
executedPlugins.push('error_plugin');
|
|
723
|
+
throw new Error('Plugin crashed');
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
const afterPlugin = createMockPlugin('after', ['pre_request'], async (_phase, context) => {
|
|
727
|
+
executedPlugins.push('after');
|
|
728
|
+
return { action: 'allow', context };
|
|
729
|
+
});
|
|
730
|
+
afterPlugin.priority = 100;
|
|
731
|
+
|
|
732
|
+
const service = new GuardrailsService({ enabled: true, fail_open: true });
|
|
733
|
+
await service.initialize();
|
|
734
|
+
|
|
735
|
+
(service as unknown as { plugins: GuardrailPlugin[] }).plugins = [errorPlugin, afterPlugin];
|
|
736
|
+
(service as unknown as { enabled: boolean }).enabled = true;
|
|
737
|
+
|
|
738
|
+
const context = createGuardrailContext({ prompt: 'test' });
|
|
739
|
+
const result = await service.execute('pre_request', context);
|
|
740
|
+
|
|
741
|
+
expect(result.action).toBe('allow');
|
|
742
|
+
expect(executedPlugins).toEqual(['error_plugin', 'after']);
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
it('should handle non-Error thrown values', async () => {
|
|
746
|
+
const errorPlugin = createMockPlugin('string_error', ['pre_request'], async () => {
|
|
747
|
+
throw 'String error message';
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
const service = new GuardrailsService({ enabled: true, fail_open: false });
|
|
751
|
+
await service.initialize();
|
|
752
|
+
|
|
753
|
+
(service as unknown as { plugins: GuardrailPlugin[] }).plugins = [errorPlugin];
|
|
754
|
+
(service as unknown as { enabled: boolean }).enabled = true;
|
|
755
|
+
|
|
756
|
+
const context = createGuardrailContext({ prompt: 'test' });
|
|
757
|
+
const result = await service.execute('pre_request', context);
|
|
758
|
+
|
|
759
|
+
expect(result.action).toBe('block');
|
|
760
|
+
expect(result.blockReason).toContain('String error message');
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
describe('shutdown', () => {
|
|
765
|
+
it('should call shutdown on all plugins', async () => {
|
|
766
|
+
const plugin1 = createMockPlugin('plugin1', ['pre_request'], async (_phase, context) => {
|
|
767
|
+
return { action: 'allow', context };
|
|
768
|
+
});
|
|
769
|
+
const plugin2 = createMockPlugin('plugin2', ['pre_request'], async (_phase, context) => {
|
|
770
|
+
return { action: 'allow', context };
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
const service = new GuardrailsService({ enabled: true });
|
|
774
|
+
await service.initialize();
|
|
775
|
+
|
|
776
|
+
(service as unknown as { plugins: GuardrailPlugin[] }).plugins = [plugin1, plugin2];
|
|
777
|
+
(service as unknown as { enabled: boolean }).enabled = true;
|
|
778
|
+
|
|
779
|
+
await service.shutdown();
|
|
780
|
+
|
|
781
|
+
expect(plugin1.shutdown).toHaveBeenCalled();
|
|
782
|
+
expect(plugin2.shutdown).toHaveBeenCalled();
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
it('should handle plugin shutdown errors gracefully', async () => {
|
|
786
|
+
const errorPlugin = createMockPlugin('error_plugin', ['pre_request'], async (_phase, context) => {
|
|
787
|
+
return { action: 'allow', context };
|
|
788
|
+
});
|
|
789
|
+
(errorPlugin.shutdown as jest.MockedFunction<typeof errorPlugin.shutdown>).mockRejectedValue(
|
|
790
|
+
new Error('Shutdown failed')
|
|
791
|
+
);
|
|
792
|
+
|
|
793
|
+
const normalPlugin = createMockPlugin('normal', ['pre_request'], async (_phase, context) => {
|
|
794
|
+
return { action: 'allow', context };
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
const service = new GuardrailsService({ enabled: true });
|
|
798
|
+
await service.initialize();
|
|
799
|
+
|
|
800
|
+
(service as unknown as { plugins: GuardrailPlugin[] }).plugins = [errorPlugin, normalPlugin];
|
|
801
|
+
(service as unknown as { enabled: boolean }).enabled = true;
|
|
802
|
+
|
|
803
|
+
// Should not throw
|
|
804
|
+
await expect(service.shutdown()).resolves.toBeUndefined();
|
|
805
|
+
|
|
806
|
+
// Should still try to shutdown all plugins
|
|
807
|
+
expect(errorPlugin.shutdown).toHaveBeenCalled();
|
|
808
|
+
expect(normalPlugin.shutdown).toHaveBeenCalled();
|
|
809
|
+
|
|
810
|
+
// Service should be disabled after shutdown
|
|
811
|
+
expect(service.isEnabled()).toBe(false);
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
it('should clear plugins list after shutdown', async () => {
|
|
815
|
+
const plugin = createMockPlugin('plugin', ['pre_request'], async (_phase, context) => {
|
|
816
|
+
return { action: 'allow', context };
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
const service = new GuardrailsService({ enabled: true });
|
|
820
|
+
await service.initialize();
|
|
821
|
+
|
|
822
|
+
(service as unknown as { plugins: GuardrailPlugin[] }).plugins = [plugin];
|
|
823
|
+
(service as unknown as { enabled: boolean }).enabled = true;
|
|
824
|
+
|
|
825
|
+
await service.shutdown();
|
|
826
|
+
|
|
827
|
+
expect(service.getPlugins()).toHaveLength(0);
|
|
828
|
+
});
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
describe('disabled plugins', () => {
|
|
832
|
+
it('should skip disabled plugins', async () => {
|
|
833
|
+
const executedPlugins: string[] = [];
|
|
834
|
+
|
|
835
|
+
const enabledPlugin = createMockPlugin('enabled', ['pre_request'], async (_phase, context) => {
|
|
836
|
+
executedPlugins.push('enabled');
|
|
837
|
+
return { action: 'allow', context };
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
const disabledPlugin = createMockPlugin('disabled', ['pre_request'], async (_phase, context) => {
|
|
841
|
+
executedPlugins.push('disabled');
|
|
842
|
+
return { action: 'allow', context };
|
|
843
|
+
});
|
|
844
|
+
disabledPlugin.enabled = false;
|
|
845
|
+
|
|
846
|
+
const service = new GuardrailsService({ enabled: true });
|
|
847
|
+
await service.initialize();
|
|
848
|
+
|
|
849
|
+
(service as unknown as { plugins: GuardrailPlugin[] }).plugins = [enabledPlugin, disabledPlugin];
|
|
850
|
+
(service as unknown as { enabled: boolean }).enabled = true;
|
|
851
|
+
|
|
852
|
+
const context = createGuardrailContext({ prompt: 'test' });
|
|
853
|
+
await service.execute('pre_request', context);
|
|
854
|
+
|
|
855
|
+
expect(executedPlugins).toEqual(['enabled']);
|
|
856
|
+
});
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
describe('error handling edge cases', () => {
|
|
860
|
+
it('should throw error for unknown plugin name', async () => {
|
|
861
|
+
const service = new GuardrailsService({
|
|
862
|
+
enabled: true,
|
|
863
|
+
plugins: {
|
|
864
|
+
unknown_plugin: { enabled: true },
|
|
865
|
+
},
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
// The service logs error but doesn't throw during initialize
|
|
869
|
+
await service.initialize();
|
|
870
|
+
|
|
871
|
+
// Plugin should not be loaded
|
|
872
|
+
expect(service.getPlugins()).toHaveLength(0);
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
it('should handle plugin initialization failure gracefully', async () => {
|
|
876
|
+
// Create a service with a valid plugin name but cause initialization to fail
|
|
877
|
+
const service = new GuardrailsService({
|
|
878
|
+
enabled: true,
|
|
879
|
+
plugins: {
|
|
880
|
+
// rate_limiter with invalid config that will cause internal errors
|
|
881
|
+
rate_limiter: {
|
|
882
|
+
enabled: true,
|
|
883
|
+
// These are all valid, but let's test that a broken plugin doesn't crash the service
|
|
884
|
+
},
|
|
885
|
+
},
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
// Should not throw
|
|
889
|
+
await service.initialize();
|
|
890
|
+
|
|
891
|
+
// Plugin should be loaded (if config is valid)
|
|
892
|
+
expect(service.getPlugins().length).toBeGreaterThanOrEqual(0);
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
it('should continue loading other plugins when one fails', async () => {
|
|
896
|
+
const service = new GuardrailsService({
|
|
897
|
+
enabled: true,
|
|
898
|
+
plugins: {
|
|
899
|
+
unknown_plugin: { enabled: true }, // This will fail
|
|
900
|
+
rate_limiter: { enabled: true }, // This should succeed
|
|
901
|
+
},
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
await service.initialize();
|
|
905
|
+
|
|
906
|
+
// Only rate_limiter should be loaded
|
|
907
|
+
expect(service.getPlugins()).toHaveLength(1);
|
|
908
|
+
expect(service.getPlugins()[0].name).toBe('rate_limiter');
|
|
909
|
+
});
|
|
910
|
+
});
|
|
911
|
+
});
|