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.
Files changed (118) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +158 -1
  3. package/audit-ci.json +2 -1
  4. package/dist/config/config.d.ts +2 -0
  5. package/dist/config/config.d.ts.map +1 -1
  6. package/dist/config/config.js +144 -1
  7. package/dist/config/config.js.map +1 -1
  8. package/dist/config/types.d.ts +1084 -2
  9. package/dist/config/types.d.ts.map +1 -1
  10. package/dist/config/types.js +59 -0
  11. package/dist/config/types.js.map +1 -1
  12. package/dist/guardrails/context.d.ts +10 -0
  13. package/dist/guardrails/context.d.ts.map +1 -0
  14. package/dist/guardrails/context.js +35 -0
  15. package/dist/guardrails/context.js.map +1 -0
  16. package/dist/guardrails/errors.d.ts +26 -0
  17. package/dist/guardrails/errors.d.ts.map +1 -0
  18. package/dist/guardrails/errors.js +42 -0
  19. package/dist/guardrails/errors.js.map +1 -0
  20. package/dist/guardrails/index.d.ts +6 -0
  21. package/dist/guardrails/index.d.ts.map +1 -0
  22. package/dist/guardrails/index.js +11 -0
  23. package/dist/guardrails/index.js.map +1 -0
  24. package/dist/guardrails/plugins/base-plugin.d.ts +35 -0
  25. package/dist/guardrails/plugins/base-plugin.d.ts.map +1 -0
  26. package/dist/guardrails/plugins/base-plugin.js +70 -0
  27. package/dist/guardrails/plugins/base-plugin.js.map +1 -0
  28. package/dist/guardrails/plugins/index.d.ts +6 -0
  29. package/dist/guardrails/plugins/index.d.ts.map +1 -0
  30. package/dist/guardrails/plugins/index.js +6 -0
  31. package/dist/guardrails/plugins/index.js.map +1 -0
  32. package/dist/guardrails/plugins/pattern-blocker.d.ts +27 -0
  33. package/dist/guardrails/plugins/pattern-blocker.d.ts.map +1 -0
  34. package/dist/guardrails/plugins/pattern-blocker.js +140 -0
  35. package/dist/guardrails/plugins/pattern-blocker.js.map +1 -0
  36. package/dist/guardrails/plugins/pii-redactor/detectors.d.ts +40 -0
  37. package/dist/guardrails/plugins/pii-redactor/detectors.d.ts.map +1 -0
  38. package/dist/guardrails/plugins/pii-redactor/detectors.js +134 -0
  39. package/dist/guardrails/plugins/pii-redactor/detectors.js.map +1 -0
  40. package/dist/guardrails/plugins/pii-redactor/index.d.ts +28 -0
  41. package/dist/guardrails/plugins/pii-redactor/index.d.ts.map +1 -0
  42. package/dist/guardrails/plugins/pii-redactor/index.js +157 -0
  43. package/dist/guardrails/plugins/pii-redactor/index.js.map +1 -0
  44. package/dist/guardrails/plugins/pii-redactor/pseudonymizer.d.ts +33 -0
  45. package/dist/guardrails/plugins/pii-redactor/pseudonymizer.d.ts.map +1 -0
  46. package/dist/guardrails/plugins/pii-redactor/pseudonymizer.js +70 -0
  47. package/dist/guardrails/plugins/pii-redactor/pseudonymizer.js.map +1 -0
  48. package/dist/guardrails/plugins/rate-limiter.d.ts +28 -0
  49. package/dist/guardrails/plugins/rate-limiter.d.ts.map +1 -0
  50. package/dist/guardrails/plugins/rate-limiter.js +91 -0
  51. package/dist/guardrails/plugins/rate-limiter.js.map +1 -0
  52. package/dist/guardrails/plugins/token-limiter.d.ts +30 -0
  53. package/dist/guardrails/plugins/token-limiter.d.ts.map +1 -0
  54. package/dist/guardrails/plugins/token-limiter.js +98 -0
  55. package/dist/guardrails/plugins/token-limiter.js.map +1 -0
  56. package/dist/guardrails/service.d.ts +38 -0
  57. package/dist/guardrails/service.d.ts.map +1 -0
  58. package/dist/guardrails/service.js +183 -0
  59. package/dist/guardrails/service.js.map +1 -0
  60. package/dist/guardrails/types.d.ts +96 -0
  61. package/dist/guardrails/types.d.ts.map +1 -0
  62. package/dist/guardrails/types.js +2 -0
  63. package/dist/guardrails/types.js.map +1 -0
  64. package/dist/providers/duck-provider-enhanced.d.ts +2 -1
  65. package/dist/providers/duck-provider-enhanced.d.ts.map +1 -1
  66. package/dist/providers/duck-provider-enhanced.js +55 -6
  67. package/dist/providers/duck-provider-enhanced.js.map +1 -1
  68. package/dist/providers/enhanced-manager.d.ts +2 -1
  69. package/dist/providers/enhanced-manager.d.ts.map +1 -1
  70. package/dist/providers/enhanced-manager.js +3 -3
  71. package/dist/providers/enhanced-manager.js.map +1 -1
  72. package/dist/providers/manager.d.ts +3 -1
  73. package/dist/providers/manager.d.ts.map +1 -1
  74. package/dist/providers/manager.js +4 -2
  75. package/dist/providers/manager.js.map +1 -1
  76. package/dist/providers/provider.d.ts +3 -1
  77. package/dist/providers/provider.d.ts.map +1 -1
  78. package/dist/providers/provider.js +43 -3
  79. package/dist/providers/provider.js.map +1 -1
  80. package/dist/server.d.ts +1 -0
  81. package/dist/server.d.ts.map +1 -1
  82. package/dist/server.js +28 -6
  83. package/dist/server.js.map +1 -1
  84. package/dist/services/function-bridge.d.ts +3 -1
  85. package/dist/services/function-bridge.d.ts.map +1 -1
  86. package/dist/services/function-bridge.js +40 -1
  87. package/dist/services/function-bridge.js.map +1 -1
  88. package/package.json +1 -1
  89. package/src/config/config.ts +187 -1
  90. package/src/config/types.ts +73 -0
  91. package/src/guardrails/context.ts +37 -0
  92. package/src/guardrails/errors.ts +46 -0
  93. package/src/guardrails/index.ts +20 -0
  94. package/src/guardrails/plugins/base-plugin.ts +103 -0
  95. package/src/guardrails/plugins/index.ts +5 -0
  96. package/src/guardrails/plugins/pattern-blocker.ts +190 -0
  97. package/src/guardrails/plugins/pii-redactor/detectors.ts +200 -0
  98. package/src/guardrails/plugins/pii-redactor/index.ts +203 -0
  99. package/src/guardrails/plugins/pii-redactor/pseudonymizer.ts +91 -0
  100. package/src/guardrails/plugins/rate-limiter.ts +142 -0
  101. package/src/guardrails/plugins/token-limiter.ts +155 -0
  102. package/src/guardrails/service.ts +209 -0
  103. package/src/guardrails/types.ts +120 -0
  104. package/src/providers/duck-provider-enhanced.ts +76 -7
  105. package/src/providers/enhanced-manager.ts +5 -3
  106. package/src/providers/manager.ts +6 -3
  107. package/src/providers/provider.ts +57 -6
  108. package/src/server.ts +32 -6
  109. package/src/services/function-bridge.ts +53 -2
  110. package/tests/guardrails/config.test.ts +267 -0
  111. package/tests/guardrails/errors.test.ts +109 -0
  112. package/tests/guardrails/plugins/pattern-blocker.test.ts +309 -0
  113. package/tests/guardrails/plugins/pii-redactor.test.ts +1004 -0
  114. package/tests/guardrails/plugins/rate-limiter.test.ts +310 -0
  115. package/tests/guardrails/plugins/token-limiter.test.ts +216 -0
  116. package/tests/guardrails/service.test.ts +911 -0
  117. package/tests/mcp-bridge.test.ts +248 -0
  118. package/tests/providers.test.ts +739 -0
@@ -0,0 +1,310 @@
1
+ import { describe, it, expect, beforeEach, jest } from '@jest/globals';
2
+ import { RateLimiterPlugin } from '../../../src/guardrails/plugins/rate-limiter';
3
+ import { createGuardrailContext } from '../../../src/guardrails/context';
4
+
5
+ // Mock logger to avoid console noise during tests
6
+ jest.mock('../../../src/utils/logger');
7
+
8
+ describe('RateLimiterPlugin', () => {
9
+ let plugin: RateLimiterPlugin;
10
+
11
+ beforeEach(async () => {
12
+ plugin = new RateLimiterPlugin();
13
+ plugin.reset();
14
+ });
15
+
16
+ describe('initialization', () => {
17
+ it('should initialize with default values', async () => {
18
+ await plugin.initialize({ enabled: true });
19
+
20
+ expect(plugin.enabled).toBe(true);
21
+ expect(plugin.name).toBe('rate_limiter');
22
+ expect(plugin.phases).toContain('pre_request');
23
+ });
24
+
25
+ it('should initialize with custom config', async () => {
26
+ await plugin.initialize({
27
+ enabled: true,
28
+ priority: 5,
29
+ requests_per_minute: 30,
30
+ requests_per_hour: 500,
31
+ per_provider: true,
32
+ burst_allowance: 10,
33
+ });
34
+
35
+ expect(plugin.priority).toBe(5);
36
+ });
37
+ });
38
+
39
+ describe('rate limiting - per minute', () => {
40
+ it('should allow requests under the limit', async () => {
41
+ await plugin.initialize({
42
+ enabled: true,
43
+ requests_per_minute: 10,
44
+ requests_per_hour: 1000,
45
+ burst_allowance: 0,
46
+ });
47
+
48
+ const context = createGuardrailContext({ provider: 'openai' });
49
+
50
+ // Make 5 requests - all should be allowed
51
+ for (let i = 0; i < 5; i++) {
52
+ const result = await plugin.execute('pre_request', context);
53
+ expect(result.action).toBe('allow');
54
+ }
55
+
56
+ expect(plugin.getRequestCounts().lastMinute).toBe(5);
57
+ });
58
+
59
+ it('should block requests over the limit', async () => {
60
+ await plugin.initialize({
61
+ enabled: true,
62
+ requests_per_minute: 3,
63
+ requests_per_hour: 1000,
64
+ burst_allowance: 0,
65
+ });
66
+
67
+ const context = createGuardrailContext({ provider: 'openai' });
68
+
69
+ // Make 3 requests - all should be allowed
70
+ for (let i = 0; i < 3; i++) {
71
+ const result = await plugin.execute('pre_request', context);
72
+ expect(result.action).toBe('allow');
73
+ }
74
+
75
+ // 4th request should be blocked
76
+ const result = await plugin.execute('pre_request', context);
77
+ expect(result.action).toBe('block');
78
+ expect(result.blockedBy).toBe('rate_limiter');
79
+ expect(result.blockReason).toContain('Rate limit exceeded');
80
+ });
81
+
82
+ it('should allow burst requests within burst allowance', async () => {
83
+ await plugin.initialize({
84
+ enabled: true,
85
+ requests_per_minute: 3,
86
+ requests_per_hour: 1000,
87
+ burst_allowance: 2,
88
+ });
89
+
90
+ const context = createGuardrailContext({ provider: 'openai' });
91
+
92
+ // Make 5 requests - all should be allowed (3 + 2 burst)
93
+ for (let i = 0; i < 5; i++) {
94
+ const result = await plugin.execute('pre_request', context);
95
+ expect(result.action).toBe('allow');
96
+ }
97
+
98
+ // 6th request should be blocked
99
+ const result = await plugin.execute('pre_request', context);
100
+ expect(result.action).toBe('block');
101
+ });
102
+ });
103
+
104
+ describe('rate limiting - per provider', () => {
105
+ it('should track requests globally by default', async () => {
106
+ await plugin.initialize({
107
+ enabled: true,
108
+ requests_per_minute: 5,
109
+ requests_per_hour: 1000,
110
+ per_provider: false,
111
+ burst_allowance: 0,
112
+ });
113
+
114
+ // Make requests for different providers
115
+ for (let i = 0; i < 5; i++) {
116
+ const provider = i % 2 === 0 ? 'openai' : 'gemini';
117
+ const context = createGuardrailContext({ provider });
118
+ await plugin.execute('pre_request', context);
119
+ }
120
+
121
+ // Global count should be 5
122
+ expect(plugin.getRequestCounts('global').lastMinute).toBe(5);
123
+
124
+ // Next request from any provider should be blocked
125
+ const context = createGuardrailContext({ provider: 'openai' });
126
+ const result = await plugin.execute('pre_request', context);
127
+ expect(result.action).toBe('block');
128
+ });
129
+
130
+ it('should track requests per provider when configured', async () => {
131
+ await plugin.initialize({
132
+ enabled: true,
133
+ requests_per_minute: 3,
134
+ requests_per_hour: 1000,
135
+ per_provider: true,
136
+ burst_allowance: 0,
137
+ });
138
+
139
+ // Make 3 requests for OpenAI
140
+ for (let i = 0; i < 3; i++) {
141
+ const context = createGuardrailContext({ provider: 'openai' });
142
+ const result = await plugin.execute('pre_request', context);
143
+ expect(result.action).toBe('allow');
144
+ }
145
+
146
+ // OpenAI should be blocked
147
+ const openaiContext = createGuardrailContext({ provider: 'openai' });
148
+ const openaiResult = await plugin.execute('pre_request', openaiContext);
149
+ expect(openaiResult.action).toBe('block');
150
+
151
+ // But Gemini should still be allowed
152
+ const geminiContext = createGuardrailContext({ provider: 'gemini' });
153
+ const geminiResult = await plugin.execute('pre_request', geminiContext);
154
+ expect(geminiResult.action).toBe('allow');
155
+
156
+ expect(plugin.getRequestCounts('openai').lastMinute).toBe(3);
157
+ expect(plugin.getRequestCounts('gemini').lastMinute).toBe(1);
158
+ });
159
+ });
160
+
161
+ describe('rate limiting - per hour', () => {
162
+ it('should block requests when hourly limit exceeded', async () => {
163
+ await plugin.initialize({
164
+ enabled: true,
165
+ requests_per_minute: 1000, // High minute limit
166
+ requests_per_hour: 5, // Low hour limit
167
+ burst_allowance: 0,
168
+ });
169
+
170
+ const context = createGuardrailContext({ provider: 'openai' });
171
+
172
+ // Make 5 requests - all should be allowed
173
+ for (let i = 0; i < 5; i++) {
174
+ const result = await plugin.execute('pre_request', context);
175
+ expect(result.action).toBe('allow');
176
+ }
177
+
178
+ // 6th request should be blocked due to hourly limit
179
+ const result = await plugin.execute('pre_request', context);
180
+ expect(result.action).toBe('block');
181
+ expect(result.blockReason).toContain('per hour');
182
+ });
183
+
184
+ it('should add violation for hourly limit exceeded', async () => {
185
+ await plugin.initialize({
186
+ enabled: true,
187
+ requests_per_minute: 1000,
188
+ requests_per_hour: 3,
189
+ burst_allowance: 0,
190
+ });
191
+
192
+ const context = createGuardrailContext({ provider: 'openai' });
193
+
194
+ // Make 3 requests
195
+ for (let i = 0; i < 3; i++) {
196
+ await plugin.execute('pre_request', context);
197
+ }
198
+
199
+ // 4th request - should be blocked with hourly violation
200
+ const result = await plugin.execute('pre_request', context);
201
+
202
+ expect(result.action).toBe('block');
203
+ const hourlyViolation = context.violations.find((v) => v.rule === 'requests_per_hour');
204
+ expect(hourlyViolation).toBeDefined();
205
+ expect(hourlyViolation?.severity).toBe('error');
206
+ });
207
+ });
208
+
209
+ describe('violations', () => {
210
+ it('should add violation when rate limit exceeded', async () => {
211
+ await plugin.initialize({
212
+ enabled: true,
213
+ requests_per_minute: 1,
214
+ requests_per_hour: 1000,
215
+ burst_allowance: 0,
216
+ });
217
+
218
+ const context = createGuardrailContext({ provider: 'openai' });
219
+
220
+ // First request
221
+ await plugin.execute('pre_request', context);
222
+
223
+ // Second request - should be blocked with violation
224
+ const result = await plugin.execute('pre_request', context);
225
+
226
+ expect(result.action).toBe('block');
227
+ expect(context.violations.length).toBeGreaterThan(0);
228
+ expect(context.violations[0].rule).toBe('requests_per_minute');
229
+ expect(context.violations[0].severity).toBe('error');
230
+ });
231
+
232
+ it('should add warning when approaching limit', async () => {
233
+ await plugin.initialize({
234
+ enabled: true,
235
+ requests_per_minute: 10,
236
+ requests_per_hour: 1000,
237
+ burst_allowance: 0,
238
+ });
239
+
240
+ const context = createGuardrailContext({ provider: 'openai' });
241
+
242
+ // Make 9 requests - the 9th sees 8 previous requests (80% of limit)
243
+ // Warning is added when request count before current >= 80%
244
+ for (let i = 0; i < 9; i++) {
245
+ await plugin.execute('pre_request', context);
246
+ }
247
+
248
+ // Check for warning violation
249
+ const warnings = context.violations.filter(
250
+ (v) => v.severity === 'warning' && v.rule === 'requests_per_minute_warning'
251
+ );
252
+ expect(warnings.length).toBeGreaterThan(0);
253
+ });
254
+ });
255
+
256
+ describe('phase handling', () => {
257
+ it('should only process pre_request phase', async () => {
258
+ await plugin.initialize({ enabled: true, requests_per_minute: 1 });
259
+
260
+ const context = createGuardrailContext({ provider: 'openai' });
261
+
262
+ // pre_request should be processed
263
+ const preResult = await plugin.execute('pre_request', context);
264
+ expect(preResult.action).toBe('allow');
265
+
266
+ // post_response should be skipped (not in phases)
267
+ const postResult = await plugin.execute('post_response', context);
268
+ expect(postResult.action).toBe('allow');
269
+
270
+ // Only 1 request counted (pre_request)
271
+ expect(plugin.getRequestCounts().lastMinute).toBe(1);
272
+ });
273
+ });
274
+
275
+ describe('shutdown', () => {
276
+ it('should disable plugin on shutdown', async () => {
277
+ await plugin.initialize({ enabled: true, requests_per_minute: 10 });
278
+
279
+ expect(plugin.enabled).toBe(true);
280
+
281
+ await plugin.shutdown();
282
+
283
+ expect(plugin.enabled).toBe(false);
284
+ });
285
+ });
286
+
287
+ describe('reset', () => {
288
+ it('should clear request history on reset', async () => {
289
+ await plugin.initialize({
290
+ enabled: true,
291
+ requests_per_minute: 10,
292
+ burst_allowance: 0,
293
+ });
294
+
295
+ const context = createGuardrailContext({ provider: 'openai' });
296
+
297
+ // Make some requests
298
+ for (let i = 0; i < 5; i++) {
299
+ await plugin.execute('pre_request', context);
300
+ }
301
+
302
+ expect(plugin.getRequestCounts().lastMinute).toBe(5);
303
+
304
+ // Reset
305
+ plugin.reset();
306
+
307
+ expect(plugin.getRequestCounts().lastMinute).toBe(0);
308
+ });
309
+ });
310
+ });
@@ -0,0 +1,216 @@
1
+ import { describe, it, expect, beforeEach, jest } from '@jest/globals';
2
+ import { TokenLimiterPlugin } from '../../../src/guardrails/plugins/token-limiter';
3
+ import { createGuardrailContext } from '../../../src/guardrails/context';
4
+
5
+ // Mock logger to avoid console noise during tests
6
+ jest.mock('../../../src/utils/logger');
7
+
8
+ describe('TokenLimiterPlugin', () => {
9
+ let plugin: TokenLimiterPlugin;
10
+
11
+ beforeEach(async () => {
12
+ plugin = new TokenLimiterPlugin();
13
+ });
14
+
15
+ describe('initialization', () => {
16
+ it('should initialize with default values', async () => {
17
+ await plugin.initialize({ enabled: true });
18
+
19
+ expect(plugin.enabled).toBe(true);
20
+ expect(plugin.name).toBe('token_limiter');
21
+ expect(plugin.phases).toContain('pre_request');
22
+ expect(plugin.getLimits().maxInputTokens).toBe(8192);
23
+ });
24
+
25
+ it('should initialize with custom config', async () => {
26
+ await plugin.initialize({
27
+ enabled: true,
28
+ priority: 15,
29
+ max_input_tokens: 4096,
30
+ max_output_tokens: 2048,
31
+ warn_at_percentage: 90,
32
+ });
33
+
34
+ expect(plugin.priority).toBe(15);
35
+ const limits = plugin.getLimits();
36
+ expect(limits.maxInputTokens).toBe(4096);
37
+ expect(limits.maxOutputTokens).toBe(2048);
38
+ });
39
+ });
40
+
41
+ describe('token estimation', () => {
42
+ it('should estimate tokens from text', async () => {
43
+ await plugin.initialize({ enabled: true });
44
+
45
+ // ~4 chars per token
46
+ expect(plugin.estimateTokenCount('Hello')).toBeGreaterThan(0);
47
+ expect(plugin.estimateTokenCount('Hello world')).toBeGreaterThan(plugin.estimateTokenCount('Hello'));
48
+ });
49
+
50
+ it('should return 0 for empty text', async () => {
51
+ await plugin.initialize({ enabled: true });
52
+
53
+ expect(plugin.estimateTokenCount('')).toBe(0);
54
+ });
55
+ });
56
+
57
+ describe('token limiting', () => {
58
+ it('should allow prompts under the limit', async () => {
59
+ await plugin.initialize({
60
+ enabled: true,
61
+ max_input_tokens: 1000,
62
+ });
63
+
64
+ const shortPrompt = 'Hello world'; // ~7 tokens
65
+ const context = createGuardrailContext({ prompt: shortPrompt });
66
+ const result = await plugin.execute('pre_request', context);
67
+
68
+ expect(result.action).toBe('allow');
69
+ });
70
+
71
+ it('should block prompts over the limit', async () => {
72
+ await plugin.initialize({
73
+ enabled: true,
74
+ max_input_tokens: 10, // Very small limit
75
+ });
76
+
77
+ // Create a prompt that exceeds the limit (~4 chars per token)
78
+ const longPrompt = 'This is a longer prompt that will definitely exceed our tiny token limit';
79
+ const context = createGuardrailContext({ prompt: longPrompt });
80
+ const result = await plugin.execute('pre_request', context);
81
+
82
+ expect(result.action).toBe('block');
83
+ expect(result.blockedBy).toBe('token_limiter');
84
+ expect(result.blockReason).toContain('Token limit exceeded');
85
+ });
86
+
87
+ it('should include messages in token count', async () => {
88
+ await plugin.initialize({
89
+ enabled: true,
90
+ max_input_tokens: 20,
91
+ });
92
+
93
+ const context = createGuardrailContext({
94
+ prompt: 'Short',
95
+ messages: [
96
+ { role: 'user', content: 'This is a message with many tokens that should push us over the limit', timestamp: new Date() },
97
+ ],
98
+ });
99
+
100
+ const result = await plugin.execute('pre_request', context);
101
+ expect(result.action).toBe('block');
102
+ });
103
+ });
104
+
105
+ describe('warnings', () => {
106
+ it('should add warning when approaching limit', async () => {
107
+ await plugin.initialize({
108
+ enabled: true,
109
+ max_input_tokens: 100,
110
+ warn_at_percentage: 50, // Warn at 50%
111
+ });
112
+
113
+ // Create prompt that's ~60-80% of limit
114
+ const prompt = 'A'.repeat(300); // ~75 tokens
115
+ const context = createGuardrailContext({ prompt });
116
+ await plugin.execute('pre_request', context);
117
+
118
+ const warnings = context.violations.filter((v) => v.severity === 'warning');
119
+ expect(warnings.length).toBeGreaterThan(0);
120
+ });
121
+
122
+ it('should not warn when well under limit', async () => {
123
+ await plugin.initialize({
124
+ enabled: true,
125
+ max_input_tokens: 1000,
126
+ warn_at_percentage: 80,
127
+ });
128
+
129
+ const shortPrompt = 'Hello';
130
+ const context = createGuardrailContext({ prompt: shortPrompt });
131
+ await plugin.execute('pre_request', context);
132
+
133
+ const warnings = context.violations.filter((v) => v.severity === 'warning');
134
+ expect(warnings.length).toBe(0);
135
+ });
136
+ });
137
+
138
+ describe('phase handling', () => {
139
+ it('should process pre_request and post_response phases', async () => {
140
+ await plugin.initialize({ enabled: true });
141
+
142
+ expect(plugin.phases).toContain('pre_request');
143
+ expect(plugin.phases).toContain('post_response');
144
+ });
145
+
146
+ it('should allow other phases', async () => {
147
+ await plugin.initialize({ enabled: true });
148
+
149
+ const context = createGuardrailContext({ prompt: 'test' });
150
+
151
+ // pre_tool_input should pass through
152
+ const result = await plugin.execute('pre_tool_input', context);
153
+ expect(result.action).toBe('allow');
154
+ });
155
+ });
156
+
157
+ describe('output token limiting', () => {
158
+ it('should allow responses under the limit', async () => {
159
+ await plugin.initialize({
160
+ enabled: true,
161
+ max_output_tokens: 1000,
162
+ });
163
+
164
+ const context = createGuardrailContext({ prompt: 'test' });
165
+ context.response = 'Short response';
166
+ const result = await plugin.execute('post_response', context);
167
+
168
+ expect(result.action).toBe('allow');
169
+ });
170
+
171
+ it('should block responses over the limit', async () => {
172
+ await plugin.initialize({
173
+ enabled: true,
174
+ max_output_tokens: 10, // Very small limit
175
+ });
176
+
177
+ const context = createGuardrailContext({ prompt: 'test' });
178
+ context.response = 'This is a longer response that will definitely exceed our tiny token limit for output';
179
+ const result = await plugin.execute('post_response', context);
180
+
181
+ expect(result.action).toBe('block');
182
+ expect(result.blockedBy).toBe('token_limiter');
183
+ expect(result.blockReason).toContain('Output token limit exceeded');
184
+ });
185
+
186
+ it('should skip output check if no max_output_tokens configured', async () => {
187
+ await plugin.initialize({
188
+ enabled: true,
189
+ // No max_output_tokens
190
+ });
191
+
192
+ const context = createGuardrailContext({ prompt: 'test' });
193
+ context.response = 'A'.repeat(10000); // Very long response
194
+ const result = await plugin.execute('post_response', context);
195
+
196
+ expect(result.action).toBe('allow');
197
+ });
198
+
199
+ it('should add warning when response approaches output limit', async () => {
200
+ await plugin.initialize({
201
+ enabled: true,
202
+ max_output_tokens: 100,
203
+ warn_at_percentage: 50, // Warn at 50%
204
+ });
205
+
206
+ // Create response that's ~60-80% of limit
207
+ const context = createGuardrailContext({ prompt: 'test' });
208
+ context.response = 'A'.repeat(300); // ~75 tokens
209
+ await plugin.execute('post_response', context);
210
+
211
+ const warnings = context.violations.filter((v) => v.severity === 'warning');
212
+ expect(warnings.length).toBeGreaterThan(0);
213
+ expect(warnings[0].rule).toBe('max_output_tokens_warning');
214
+ });
215
+ });
216
+ });