mcp-rubber-duck 1.5.1 → 1.5.2

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.
@@ -0,0 +1,240 @@
1
+ import { describe, it, expect, jest, beforeEach } from '@jest/globals';
2
+ import { ResponseCache } from '../src/services/cache.js';
3
+
4
+ // Mock logger to avoid console noise during tests
5
+ jest.mock('../src/utils/logger');
6
+
7
+ describe('ResponseCache', () => {
8
+ let cache: ResponseCache;
9
+
10
+ beforeEach(() => {
11
+ cache = new ResponseCache(300); // 5 minute TTL
12
+ });
13
+
14
+ describe('basic operations', () => {
15
+ it('should store and retrieve values', () => {
16
+ cache.set('key1', 'value1');
17
+ expect(cache.get('key1')).toBe('value1');
18
+ });
19
+
20
+ it('should return undefined for missing keys', () => {
21
+ expect(cache.get('nonexistent')).toBeUndefined();
22
+ });
23
+
24
+ it('should check if key exists', () => {
25
+ cache.set('key1', 'value1');
26
+ expect(cache.has('key1')).toBe(true);
27
+ expect(cache.has('nonexistent')).toBe(false);
28
+ });
29
+
30
+ it('should delete keys', () => {
31
+ cache.set('key1', 'value1');
32
+ expect(cache.has('key1')).toBe(true);
33
+
34
+ const deleted = cache.delete('key1');
35
+ expect(deleted).toBe(1);
36
+ expect(cache.has('key1')).toBe(false);
37
+ });
38
+
39
+ it('should return 0 when deleting nonexistent key', () => {
40
+ const deleted = cache.delete('nonexistent');
41
+ expect(deleted).toBe(0);
42
+ });
43
+
44
+ it('should store complex objects', () => {
45
+ const obj = { foo: 'bar', nested: { value: 123 } };
46
+ cache.set('obj', obj);
47
+ expect(cache.get('obj')).toEqual(obj);
48
+ });
49
+ });
50
+
51
+ describe('key generation', () => {
52
+ it('should generate consistent keys for same input', () => {
53
+ const key1 = cache.generateKey('openai', 'test prompt', { temp: 0.7 });
54
+ const key2 = cache.generateKey('openai', 'test prompt', { temp: 0.7 });
55
+ expect(key1).toBe(key2);
56
+ });
57
+
58
+ it('should generate different keys for different providers', () => {
59
+ const key1 = cache.generateKey('openai', 'test prompt');
60
+ const key2 = cache.generateKey('groq', 'test prompt');
61
+ expect(key1).not.toBe(key2);
62
+ });
63
+
64
+ it('should generate different keys for different prompts', () => {
65
+ const key1 = cache.generateKey('openai', 'prompt 1');
66
+ const key2 = cache.generateKey('openai', 'prompt 2');
67
+ expect(key1).not.toBe(key2);
68
+ });
69
+
70
+ it('should generate different keys for different options', () => {
71
+ const key1 = cache.generateKey('openai', 'test', { temp: 0.7 });
72
+ const key2 = cache.generateKey('openai', 'test', { temp: 0.5 });
73
+ expect(key1).not.toBe(key2);
74
+ });
75
+
76
+ it('should generate valid hex hashes', () => {
77
+ const key = cache.generateKey('openai', 'test');
78
+ expect(key).toMatch(/^[a-f0-9]{64}$/); // SHA-256 produces 64 hex chars
79
+ });
80
+ });
81
+
82
+ describe('TTL handling', () => {
83
+ it('should respect custom TTL on set', () => {
84
+ const shortTTLCache = new ResponseCache(1); // 1 second default TTL
85
+ shortTTLCache.set('key', 'value', 10); // 10 second TTL
86
+ expect(shortTTLCache.get('key')).toBe('value');
87
+ });
88
+
89
+ it('should use default TTL when not specified', () => {
90
+ cache.set('key', 'value');
91
+ expect(cache.get('key')).toBe('value');
92
+ });
93
+ });
94
+
95
+ describe('flush', () => {
96
+ it('should clear all entries', () => {
97
+ cache.set('key1', 'value1');
98
+ cache.set('key2', 'value2');
99
+ cache.set('key3', 'value3');
100
+
101
+ expect(cache.has('key1')).toBe(true);
102
+ expect(cache.has('key2')).toBe(true);
103
+
104
+ cache.flush();
105
+
106
+ expect(cache.has('key1')).toBe(false);
107
+ expect(cache.has('key2')).toBe(false);
108
+ expect(cache.has('key3')).toBe(false);
109
+ });
110
+
111
+ it('should allow new entries after flush', () => {
112
+ cache.set('key1', 'value1');
113
+ cache.flush();
114
+ cache.set('key2', 'value2');
115
+
116
+ expect(cache.get('key2')).toBe('value2');
117
+ });
118
+ });
119
+
120
+ describe('stats', () => {
121
+ it('should track cache statistics', () => {
122
+ cache.set('key1', 'value1');
123
+
124
+ // Generate a hit
125
+ cache.get('key1');
126
+
127
+ // Generate a miss
128
+ cache.get('nonexistent');
129
+
130
+ const stats = cache.getStats();
131
+ expect(stats.keys).toBe(1);
132
+ expect(stats.hits).toBe(1);
133
+ expect(stats.misses).toBe(1);
134
+ expect(stats.hitRate).toBe(0.5);
135
+ });
136
+
137
+ it('should handle zero hits and misses', () => {
138
+ const stats = cache.getStats();
139
+ expect(stats.keys).toBe(0);
140
+ expect(stats.hitRate).toBe(0); // NaN protection check
141
+ });
142
+ });
143
+
144
+ describe('getOrSet', () => {
145
+ it('should return cached value on hit', async () => {
146
+ cache.set('key', 'cached-value');
147
+ const fetcher = jest.fn<() => Promise<string>>().mockResolvedValue('fetched-value');
148
+
149
+ const result = await cache.getOrSet('key', fetcher);
150
+
151
+ expect(result.value).toBe('cached-value');
152
+ expect(result.cached).toBe(true);
153
+ expect(fetcher).not.toHaveBeenCalled();
154
+ });
155
+
156
+ it('should fetch and cache on miss', async () => {
157
+ const fetcher = jest.fn<() => Promise<string>>().mockResolvedValue('fetched-value');
158
+
159
+ const result = await cache.getOrSet('key', fetcher);
160
+
161
+ expect(result.value).toBe('fetched-value');
162
+ expect(result.cached).toBe(false);
163
+ expect(fetcher).toHaveBeenCalledTimes(1);
164
+
165
+ // Verify it was cached
166
+ expect(cache.get('key')).toBe('fetched-value');
167
+ });
168
+
169
+ it('should use custom TTL when provided', async () => {
170
+ const fetcher = jest.fn<() => Promise<string>>().mockResolvedValue('value');
171
+
172
+ await cache.getOrSet('key', fetcher, 600);
173
+
174
+ expect(cache.get('key')).toBe('value');
175
+ });
176
+
177
+ it('should handle async fetcher errors', async () => {
178
+ const fetcher = jest.fn<() => Promise<string>>().mockRejectedValue(new Error('Fetch failed'));
179
+
180
+ await expect(cache.getOrSet('key', fetcher)).rejects.toThrow('Fetch failed');
181
+
182
+ // Should not cache failed result
183
+ expect(cache.has('key')).toBe(false);
184
+ });
185
+
186
+ it('should cache complex objects', async () => {
187
+ const complexValue = { data: [1, 2, 3], nested: { key: 'value' } };
188
+ const fetcher = jest.fn<() => Promise<typeof complexValue>>().mockResolvedValue(complexValue);
189
+
190
+ const result = await cache.getOrSet('key', fetcher);
191
+
192
+ expect(result.value).toEqual(complexValue);
193
+ expect(cache.get('key')).toEqual(complexValue);
194
+ });
195
+ });
196
+
197
+ describe('edge cases', () => {
198
+ it('should handle empty string values', () => {
199
+ cache.set('key', '');
200
+ expect(cache.get('key')).toBe('');
201
+ expect(cache.has('key')).toBe(true);
202
+ });
203
+
204
+ it('should handle null values', () => {
205
+ cache.set('key', null);
206
+ expect(cache.get('key')).toBe(null);
207
+ expect(cache.has('key')).toBe(true);
208
+ });
209
+
210
+ it('should handle zero values', () => {
211
+ cache.set('key', 0);
212
+ expect(cache.get('key')).toBe(0);
213
+ expect(cache.has('key')).toBe(true);
214
+ });
215
+
216
+ it('should handle false boolean values', () => {
217
+ cache.set('key', false);
218
+ expect(cache.get('key')).toBe(false);
219
+ expect(cache.has('key')).toBe(true);
220
+ });
221
+
222
+ it('should handle undefined values by not storing', () => {
223
+ // node-cache doesn't store undefined values
224
+ cache.set('key', undefined);
225
+ expect(cache.has('key')).toBe(true);
226
+ });
227
+
228
+ it('should handle very long keys', () => {
229
+ const longKey = 'a'.repeat(10000);
230
+ cache.set(longKey, 'value');
231
+ expect(cache.get(longKey)).toBe('value');
232
+ });
233
+
234
+ it('should handle special characters in keys', () => {
235
+ const specialKey = 'key:with/special\\chars!@#$%^&*()';
236
+ cache.set(specialKey, 'value');
237
+ expect(cache.get(specialKey)).toBe('value');
238
+ });
239
+ });
240
+ });