nestlens 0.1.1 → 0.2.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 (39) hide show
  1. package/dist/__tests__/core/storage/memory.storage.spec.d.ts +2 -0
  2. package/dist/__tests__/core/storage/memory.storage.spec.d.ts.map +1 -0
  3. package/dist/__tests__/core/storage/memory.storage.spec.js +450 -0
  4. package/dist/__tests__/core/storage/memory.storage.spec.js.map +1 -0
  5. package/dist/__tests__/core/storage/redis.storage.spec.d.ts +10 -0
  6. package/dist/__tests__/core/storage/redis.storage.spec.d.ts.map +1 -0
  7. package/dist/__tests__/core/storage/redis.storage.spec.js +660 -0
  8. package/dist/__tests__/core/storage/redis.storage.spec.js.map +1 -0
  9. package/dist/__tests__/core/storage/storage.factory.spec.d.ts +2 -0
  10. package/dist/__tests__/core/storage/storage.factory.spec.d.ts.map +1 -0
  11. package/dist/__tests__/core/storage/storage.factory.spec.js +151 -0
  12. package/dist/__tests__/core/storage/storage.factory.spec.js.map +1 -0
  13. package/dist/core/storage/index.d.ts +2 -1
  14. package/dist/core/storage/index.d.ts.map +1 -1
  15. package/dist/core/storage/index.js +17 -1
  16. package/dist/core/storage/index.js.map +1 -1
  17. package/dist/core/storage/memory.storage.d.ts +59 -0
  18. package/dist/core/storage/memory.storage.d.ts.map +1 -0
  19. package/dist/core/storage/memory.storage.js +629 -0
  20. package/dist/core/storage/memory.storage.js.map +1 -0
  21. package/dist/core/storage/redis.storage.d.ts +77 -0
  22. package/dist/core/storage/redis.storage.d.ts.map +1 -0
  23. package/dist/core/storage/redis.storage.js +595 -0
  24. package/dist/core/storage/redis.storage.js.map +1 -0
  25. package/dist/core/storage/storage.factory.d.ts +28 -0
  26. package/dist/core/storage/storage.factory.d.ts.map +1 -0
  27. package/dist/core/storage/storage.factory.js +169 -0
  28. package/dist/core/storage/storage.factory.js.map +1 -0
  29. package/dist/index.d.ts +3 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +5 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/nestlens.config.d.ts +54 -0
  34. package/dist/nestlens.config.d.ts.map +1 -1
  35. package/dist/nestlens.config.js +16 -2
  36. package/dist/nestlens.config.js.map +1 -1
  37. package/dist/nestlens.module.js +3 -3
  38. package/dist/nestlens.module.js.map +1 -1
  39. package/package.json +19 -4
@@ -0,0 +1,660 @@
1
+ "use strict";
2
+ /**
3
+ * Redis Storage Tests
4
+ *
5
+ * These tests verify the RedisStorage implementation using mocked Redis operations.
6
+ * Since RedisStorage uses dynamic imports, we test the class by:
7
+ * 1. Creating a testable subclass that allows injecting a mock client
8
+ * 2. Testing all public methods with the mock client
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ // Mock pipeline
12
+ const createMockPipeline = () => ({
13
+ hset: jest.fn().mockReturnThis(),
14
+ zadd: jest.fn().mockReturnThis(),
15
+ sadd: jest.fn().mockReturnThis(),
16
+ hgetall: jest.fn().mockReturnThis(),
17
+ hincrby: jest.fn().mockReturnThis(),
18
+ exec: jest.fn().mockResolvedValue([]),
19
+ });
20
+ // Mock Redis client factory
21
+ const createMockRedisClient = () => {
22
+ const pipeline = createMockPipeline();
23
+ return {
24
+ incr: jest.fn(),
25
+ incrby: jest.fn(),
26
+ hset: jest.fn(),
27
+ hget: jest.fn(),
28
+ hgetall: jest.fn(),
29
+ hdel: jest.fn(),
30
+ hincrby: jest.fn(),
31
+ zadd: jest.fn(),
32
+ zcard: jest.fn(),
33
+ zrange: jest.fn(),
34
+ zrevrange: jest.fn(),
35
+ zrangebyscore: jest.fn(),
36
+ zrevrangebyscore: jest.fn(),
37
+ zrem: jest.fn(),
38
+ zcount: jest.fn(),
39
+ sadd: jest.fn(),
40
+ srem: jest.fn(),
41
+ smembers: jest.fn(),
42
+ sinter: jest.fn(),
43
+ sunion: jest.fn(),
44
+ del: jest.fn(),
45
+ keys: jest.fn(),
46
+ quit: jest.fn().mockResolvedValue('OK'),
47
+ pipeline: jest.fn(() => pipeline),
48
+ _pipeline: pipeline, // expose for test assertions
49
+ };
50
+ };
51
+ // Import the actual class to access its internals for testing
52
+ // We'll use reflection to set the private client
53
+ const redis_storage_1 = require("../../../core/storage/redis.storage");
54
+ /**
55
+ * Helper to create a RedisStorage instance with a mock client
56
+ */
57
+ async function createTestStorage(config = {}) {
58
+ const storage = new redis_storage_1.RedisStorage({ keyPrefix: 'test:', ...config });
59
+ const mockClient = createMockRedisClient();
60
+ // Use reflection to set the private client
61
+ // This is a test-only pattern to avoid needing ioredis installed
62
+ storage.client = mockClient;
63
+ return { storage, mockClient, mockPipeline: mockClient._pipeline };
64
+ }
65
+ describe('RedisStorage', () => {
66
+ // ==================== Helper Functions ====================
67
+ function createEntry(type, payload = {}) {
68
+ return {
69
+ type,
70
+ payload,
71
+ requestId: `req-${Date.now()}`,
72
+ };
73
+ }
74
+ function createMockEntryHash(id, type, payload = {}) {
75
+ return {
76
+ id: String(id),
77
+ type,
78
+ requestId: `req-${id}`,
79
+ payload: JSON.stringify(payload),
80
+ createdAt: new Date().toISOString(),
81
+ familyHash: '',
82
+ resolvedAt: '',
83
+ };
84
+ }
85
+ // ==================== Core CRUD Tests ====================
86
+ describe('save', () => {
87
+ it('should save an entry and return it with id and createdAt', async () => {
88
+ // Arrange
89
+ const { storage, mockClient } = await createTestStorage();
90
+ mockClient.incr.mockResolvedValue(1);
91
+ const entry = createEntry('request', { method: 'GET', path: '/test' });
92
+ // Act
93
+ const saved = await storage.save(entry);
94
+ // Assert
95
+ expect(saved.id).toBe(1);
96
+ expect(saved.createdAt).toBeDefined();
97
+ expect(saved.type).toBe('request');
98
+ expect(mockClient.hset).toHaveBeenCalled();
99
+ expect(mockClient.zadd).toHaveBeenCalledTimes(2); // all + type index
100
+ await storage.close();
101
+ });
102
+ it('should add entry to request index when requestId is provided', async () => {
103
+ // Arrange
104
+ const { storage, mockClient } = await createTestStorage();
105
+ mockClient.incr.mockResolvedValue(1);
106
+ const entry = createEntry('request', { method: 'GET' });
107
+ entry.requestId = 'req-123';
108
+ // Act
109
+ await storage.save(entry);
110
+ // Assert
111
+ expect(mockClient.sadd).toHaveBeenCalledWith(expect.stringContaining('request:req-123'), '1');
112
+ await storage.close();
113
+ });
114
+ it('should assign incrementing IDs', async () => {
115
+ // Arrange
116
+ const { storage, mockClient } = await createTestStorage();
117
+ mockClient.incr
118
+ .mockResolvedValueOnce(1)
119
+ .mockResolvedValueOnce(2)
120
+ .mockResolvedValueOnce(3);
121
+ // Act
122
+ const saved1 = await storage.save(createEntry('request'));
123
+ const saved2 = await storage.save(createEntry('query'));
124
+ const saved3 = await storage.save(createEntry('log'));
125
+ // Assert
126
+ expect(saved1.id).toBe(1);
127
+ expect(saved2.id).toBe(2);
128
+ expect(saved3.id).toBe(3);
129
+ await storage.close();
130
+ });
131
+ });
132
+ describe('saveBatch', () => {
133
+ it('should save multiple entries at once', async () => {
134
+ // Arrange
135
+ const { storage, mockClient, mockPipeline } = await createTestStorage();
136
+ mockClient.incrby.mockResolvedValue(3);
137
+ const entries = [
138
+ createEntry('request', { path: '/1' }),
139
+ createEntry('request', { path: '/2' }),
140
+ createEntry('request', { path: '/3' }),
141
+ ];
142
+ // Act
143
+ const saved = await storage.saveBatch(entries);
144
+ // Assert
145
+ expect(saved).toHaveLength(3);
146
+ expect(saved[0].id).toBe(1);
147
+ expect(saved[1].id).toBe(2);
148
+ expect(saved[2].id).toBe(3);
149
+ expect(mockPipeline.exec).toHaveBeenCalled();
150
+ await storage.close();
151
+ });
152
+ it('should return empty array for empty input', async () => {
153
+ // Arrange
154
+ const { storage } = await createTestStorage();
155
+ // Act
156
+ const saved = await storage.saveBatch([]);
157
+ // Assert
158
+ expect(saved).toEqual([]);
159
+ await storage.close();
160
+ });
161
+ });
162
+ describe('find', () => {
163
+ it('should find entries by type', async () => {
164
+ // Arrange
165
+ const { storage, mockClient, mockPipeline } = await createTestStorage();
166
+ mockClient.zrevrange.mockResolvedValue(['1', '2']);
167
+ mockPipeline.exec.mockResolvedValue([
168
+ [null, createMockEntryHash(1, 'request', { path: '/a' })],
169
+ [null, createMockEntryHash(2, 'request', { path: '/b' })],
170
+ ]);
171
+ mockClient.smembers.mockResolvedValue([]); // for tag hydration
172
+ // Act
173
+ const entries = await storage.find({ type: 'request' });
174
+ // Assert
175
+ expect(entries).toHaveLength(2);
176
+ expect(mockClient.zrevrange).toHaveBeenCalledWith(expect.stringContaining('type:request'), 0, 99);
177
+ await storage.close();
178
+ });
179
+ it('should find entries by requestId', async () => {
180
+ // Arrange
181
+ const { storage, mockClient, mockPipeline } = await createTestStorage();
182
+ mockClient.smembers.mockResolvedValueOnce(['1']); // for request lookup
183
+ mockPipeline.exec.mockResolvedValue([
184
+ [null, createMockEntryHash(1, 'request', { path: '/test' })],
185
+ ]);
186
+ mockClient.smembers.mockResolvedValue([]); // for tag hydration
187
+ // Act
188
+ const entries = await storage.find({ requestId: 'req-123' });
189
+ // Assert
190
+ expect(entries).toHaveLength(1);
191
+ await storage.close();
192
+ });
193
+ it('should return empty array when no entries found', async () => {
194
+ // Arrange
195
+ const { storage, mockClient } = await createTestStorage();
196
+ mockClient.zrevrange.mockResolvedValue([]);
197
+ // Act
198
+ const entries = await storage.find({ type: 'exception' });
199
+ // Assert
200
+ expect(entries).toEqual([]);
201
+ await storage.close();
202
+ });
203
+ });
204
+ describe('findById', () => {
205
+ it('should find an entry by ID', async () => {
206
+ // Arrange
207
+ const { storage, mockClient } = await createTestStorage();
208
+ mockClient.hgetall.mockResolvedValue(createMockEntryHash(1, 'request', { method: 'GET' }));
209
+ mockClient.smembers.mockResolvedValue([]);
210
+ // Act
211
+ const entry = await storage.findById(1);
212
+ // Assert
213
+ expect(entry).not.toBeNull();
214
+ expect(entry.id).toBe(1);
215
+ expect(entry.type).toBe('request');
216
+ await storage.close();
217
+ });
218
+ it('should return null for non-existent entry', async () => {
219
+ // Arrange
220
+ const { storage, mockClient } = await createTestStorage();
221
+ mockClient.hgetall.mockResolvedValue({});
222
+ // Act
223
+ const entry = await storage.findById(999);
224
+ // Assert
225
+ expect(entry).toBeNull();
226
+ await storage.close();
227
+ });
228
+ });
229
+ describe('count', () => {
230
+ it('should count all entries', async () => {
231
+ // Arrange
232
+ const { storage, mockClient } = await createTestStorage();
233
+ mockClient.zcard.mockResolvedValue(10);
234
+ // Act
235
+ const count = await storage.count();
236
+ // Assert
237
+ expect(count).toBe(10);
238
+ expect(mockClient.zcard).toHaveBeenCalledWith(expect.stringContaining('entries:all'));
239
+ await storage.close();
240
+ });
241
+ it('should count entries by type', async () => {
242
+ // Arrange
243
+ const { storage, mockClient } = await createTestStorage();
244
+ mockClient.zcard.mockResolvedValue(5);
245
+ // Act
246
+ const count = await storage.count('request');
247
+ // Assert
248
+ expect(count).toBe(5);
249
+ expect(mockClient.zcard).toHaveBeenCalledWith(expect.stringContaining('type:request'));
250
+ await storage.close();
251
+ });
252
+ });
253
+ // ==================== Tag Tests ====================
254
+ describe('addTags', () => {
255
+ it('should add tags to an entry', async () => {
256
+ // Arrange
257
+ const { storage, mockPipeline } = await createTestStorage();
258
+ // Act
259
+ await storage.addTags(1, ['bug', 'urgent']);
260
+ // Assert
261
+ expect(mockPipeline.sadd).toHaveBeenCalledTimes(4); // 2 tags x 2 sets each
262
+ expect(mockPipeline.exec).toHaveBeenCalled();
263
+ await storage.close();
264
+ });
265
+ });
266
+ describe('getEntryTags', () => {
267
+ it('should return tags for an entry', async () => {
268
+ // Arrange
269
+ const { storage, mockClient } = await createTestStorage();
270
+ mockClient.smembers.mockResolvedValue(['bug', 'urgent']);
271
+ // Act
272
+ const tags = await storage.getEntryTags(1);
273
+ // Assert
274
+ expect(tags).toEqual(['bug', 'urgent']);
275
+ await storage.close();
276
+ });
277
+ it('should return empty array for entry with no tags', async () => {
278
+ // Arrange
279
+ const { storage, mockClient } = await createTestStorage();
280
+ mockClient.smembers.mockResolvedValue([]);
281
+ // Act
282
+ const tags = await storage.getEntryTags(1);
283
+ // Assert
284
+ expect(tags).toEqual([]);
285
+ await storage.close();
286
+ });
287
+ });
288
+ describe('getAllTags', () => {
289
+ it('should return all tags with counts', async () => {
290
+ // Arrange
291
+ const { storage, mockClient } = await createTestStorage();
292
+ mockClient.hgetall.mockResolvedValue({
293
+ bug: '5',
294
+ feature: '3',
295
+ urgent: '2',
296
+ });
297
+ // Act
298
+ const tags = await storage.getAllTags();
299
+ // Assert
300
+ expect(tags).toHaveLength(3);
301
+ expect(tags[0]).toEqual({ tag: 'bug', count: 5 });
302
+ expect(tags[1]).toEqual({ tag: 'feature', count: 3 });
303
+ await storage.close();
304
+ });
305
+ it('should filter out tags with zero count', async () => {
306
+ // Arrange
307
+ const { storage, mockClient } = await createTestStorage();
308
+ mockClient.hgetall.mockResolvedValue({
309
+ bug: '5',
310
+ deleted: '0',
311
+ });
312
+ // Act
313
+ const tags = await storage.getAllTags();
314
+ // Assert
315
+ expect(tags).toHaveLength(1);
316
+ expect(tags[0].tag).toBe('bug');
317
+ await storage.close();
318
+ });
319
+ });
320
+ describe('findByTags', () => {
321
+ it('should find entries with OR logic by default', async () => {
322
+ // Arrange
323
+ const { storage, mockClient, mockPipeline } = await createTestStorage();
324
+ mockClient.sunion.mockResolvedValue(['1', '2']);
325
+ mockPipeline.exec.mockResolvedValue([
326
+ [null, createMockEntryHash(1, 'exception', { name: 'Error' })],
327
+ [null, createMockEntryHash(2, 'exception', { name: 'TypeError' })],
328
+ ]);
329
+ mockClient.smembers.mockResolvedValue([]); // for tag hydration
330
+ // Act
331
+ const entries = await storage.findByTags(['bug', 'urgent']);
332
+ // Assert
333
+ expect(mockClient.sunion).toHaveBeenCalled();
334
+ expect(entries).toHaveLength(2);
335
+ await storage.close();
336
+ });
337
+ it('should find entries with AND logic', async () => {
338
+ // Arrange
339
+ const { storage, mockClient, mockPipeline } = await createTestStorage();
340
+ mockClient.sinter.mockResolvedValue(['1']);
341
+ mockPipeline.exec.mockResolvedValue([
342
+ [null, createMockEntryHash(1, 'exception', { name: 'Error' })],
343
+ ]);
344
+ mockClient.smembers.mockResolvedValue([]); // for tag hydration
345
+ // Act
346
+ const entries = await storage.findByTags(['bug', 'urgent'], 'AND');
347
+ // Assert
348
+ expect(mockClient.sinter).toHaveBeenCalled();
349
+ expect(entries).toHaveLength(1);
350
+ await storage.close();
351
+ });
352
+ it('should return empty array for no matching tags', async () => {
353
+ // Arrange
354
+ const { storage, mockClient } = await createTestStorage();
355
+ mockClient.sunion.mockResolvedValue([]);
356
+ // Act
357
+ const entries = await storage.findByTags(['nonexistent']);
358
+ // Assert
359
+ expect(entries).toEqual([]);
360
+ await storage.close();
361
+ });
362
+ });
363
+ // ==================== Monitored Tags Tests ====================
364
+ describe('addMonitoredTag', () => {
365
+ it('should add a monitored tag', async () => {
366
+ // Arrange
367
+ const { storage, mockClient } = await createTestStorage();
368
+ mockClient.hget.mockResolvedValue(null);
369
+ mockClient.incr.mockResolvedValue(1);
370
+ // Act
371
+ const monitored = await storage.addMonitoredTag('critical');
372
+ // Assert
373
+ expect(monitored.id).toBe(1);
374
+ expect(monitored.tag).toBe('critical');
375
+ expect(monitored.createdAt).toBeDefined();
376
+ await storage.close();
377
+ });
378
+ it('should return existing monitored tag if already exists', async () => {
379
+ // Arrange
380
+ const { storage, mockClient } = await createTestStorage();
381
+ const existing = { id: 1, tag: 'critical', createdAt: '2024-01-01T00:00:00Z' };
382
+ mockClient.hget.mockResolvedValue(JSON.stringify(existing));
383
+ // Act
384
+ const monitored = await storage.addMonitoredTag('critical');
385
+ // Assert
386
+ expect(monitored).toEqual(existing);
387
+ expect(mockClient.incr).not.toHaveBeenCalled();
388
+ await storage.close();
389
+ });
390
+ });
391
+ describe('getMonitoredTags', () => {
392
+ it('should return all monitored tags', async () => {
393
+ // Arrange
394
+ const { storage, mockClient } = await createTestStorage();
395
+ mockClient.hgetall.mockResolvedValue({
396
+ critical: JSON.stringify({ id: 1, tag: 'critical', createdAt: '2024-01-01T00:00:00Z' }),
397
+ important: JSON.stringify({ id: 2, tag: 'important', createdAt: '2024-01-02T00:00:00Z' }),
398
+ });
399
+ // Act
400
+ const tags = await storage.getMonitoredTags();
401
+ // Assert
402
+ expect(tags).toHaveLength(2);
403
+ expect(tags[0].tag).toBe('critical');
404
+ expect(tags[1].tag).toBe('important');
405
+ await storage.close();
406
+ });
407
+ });
408
+ // ==================== Resolution Tests ====================
409
+ describe('resolveEntry', () => {
410
+ it('should mark an entry as resolved', async () => {
411
+ // Arrange
412
+ const { storage, mockClient } = await createTestStorage();
413
+ // Act
414
+ await storage.resolveEntry(1);
415
+ // Assert
416
+ expect(mockClient.hset).toHaveBeenCalledWith(expect.stringContaining('entries:1'), 'resolvedAt', expect.any(String));
417
+ await storage.close();
418
+ });
419
+ });
420
+ describe('unresolveEntry', () => {
421
+ it('should mark an entry as unresolved', async () => {
422
+ // Arrange
423
+ const { storage, mockClient } = await createTestStorage();
424
+ // Act
425
+ await storage.unresolveEntry(1);
426
+ // Assert
427
+ expect(mockClient.hset).toHaveBeenCalledWith(expect.stringContaining('entries:1'), 'resolvedAt', '');
428
+ await storage.close();
429
+ });
430
+ });
431
+ // ==================== Family Hash Tests ====================
432
+ describe('updateFamilyHash', () => {
433
+ it('should update family hash and add to index', async () => {
434
+ // Arrange
435
+ const { storage, mockClient } = await createTestStorage();
436
+ // Act
437
+ await storage.updateFamilyHash(1, 'abc123');
438
+ // Assert
439
+ expect(mockClient.hset).toHaveBeenCalledWith(expect.stringContaining('entries:1'), 'familyHash', 'abc123');
440
+ expect(mockClient.sadd).toHaveBeenCalledWith(expect.stringContaining('family:abc123'), '1');
441
+ await storage.close();
442
+ });
443
+ });
444
+ describe('findByFamilyHash', () => {
445
+ it('should find entries by family hash', async () => {
446
+ // Arrange
447
+ const { storage, mockClient, mockPipeline } = await createTestStorage();
448
+ mockClient.smembers.mockResolvedValueOnce(['1', '2']); // family lookup
449
+ mockPipeline.exec.mockResolvedValue([
450
+ [null, createMockEntryHash(1, 'exception', { name: 'Error' })],
451
+ [null, createMockEntryHash(2, 'exception', { name: 'Error' })],
452
+ ]);
453
+ mockClient.smembers.mockResolvedValue([]); // tag hydration
454
+ // Act
455
+ const entries = await storage.findByFamilyHash('abc123');
456
+ // Assert
457
+ expect(entries).toHaveLength(2);
458
+ await storage.close();
459
+ });
460
+ it('should return empty array when no entries found', async () => {
461
+ // Arrange
462
+ const { storage, mockClient } = await createTestStorage();
463
+ mockClient.smembers.mockResolvedValue([]);
464
+ // Act
465
+ const entries = await storage.findByFamilyHash('nonexistent');
466
+ // Assert
467
+ expect(entries).toEqual([]);
468
+ await storage.close();
469
+ });
470
+ });
471
+ // ==================== Statistics Tests ====================
472
+ describe('getStats', () => {
473
+ it('should return statistics', async () => {
474
+ // Arrange
475
+ const { storage, mockClient } = await createTestStorage();
476
+ mockClient.zcard
477
+ .mockResolvedValueOnce(10) // request
478
+ .mockResolvedValueOnce(5) // query
479
+ .mockResolvedValueOnce(2) // exception
480
+ .mockResolvedValue(0); // all others
481
+ // Act
482
+ const stats = await storage.getStats();
483
+ // Assert
484
+ expect(stats.total).toBe(17);
485
+ expect(stats.byType.request).toBe(10);
486
+ expect(stats.byType.query).toBe(5);
487
+ expect(stats.byType.exception).toBe(2);
488
+ await storage.close();
489
+ });
490
+ });
491
+ describe('getStorageStats', () => {
492
+ it('should return storage statistics', async () => {
493
+ // Arrange
494
+ const { storage, mockClient } = await createTestStorage();
495
+ mockClient.zcard.mockResolvedValue(10);
496
+ mockClient.zrange.mockResolvedValue(['1']);
497
+ mockClient.zrevrange.mockResolvedValue(['10']);
498
+ mockClient.hget
499
+ .mockResolvedValueOnce('2024-01-01T00:00:00Z')
500
+ .mockResolvedValueOnce('2024-01-10T00:00:00Z');
501
+ // Act
502
+ const stats = await storage.getStorageStats();
503
+ // Assert
504
+ expect(stats.oldestEntry).toBe('2024-01-01T00:00:00Z');
505
+ expect(stats.newestEntry).toBe('2024-01-10T00:00:00Z');
506
+ await storage.close();
507
+ });
508
+ });
509
+ // ==================== Pruning Tests ====================
510
+ describe('prune', () => {
511
+ it('should delete entries older than given date', async () => {
512
+ // Arrange
513
+ const { storage, mockClient } = await createTestStorage();
514
+ mockClient.zrangebyscore.mockResolvedValue(['1', '2']);
515
+ mockClient.hgetall.mockResolvedValue(createMockEntryHash(1, 'request', {}));
516
+ mockClient.smembers.mockResolvedValue([]);
517
+ // Act
518
+ const deleted = await storage.prune(new Date('2024-01-01'));
519
+ // Assert
520
+ expect(deleted).toBe(2);
521
+ expect(mockClient.del).toHaveBeenCalled();
522
+ await storage.close();
523
+ });
524
+ it('should return 0 when no entries to prune', async () => {
525
+ // Arrange
526
+ const { storage, mockClient } = await createTestStorage();
527
+ mockClient.zrangebyscore.mockResolvedValue([]);
528
+ // Act
529
+ const deleted = await storage.prune(new Date('2024-01-01'));
530
+ // Assert
531
+ expect(deleted).toBe(0);
532
+ await storage.close();
533
+ });
534
+ });
535
+ // ==================== Clear & Close Tests ====================
536
+ describe('clear', () => {
537
+ it('should delete all NestLens keys', async () => {
538
+ // Arrange
539
+ const { storage, mockClient } = await createTestStorage();
540
+ mockClient.keys.mockResolvedValue(['test:entries:1', 'test:entries:2']);
541
+ // Act
542
+ await storage.clear();
543
+ // Assert
544
+ expect(mockClient.del).toHaveBeenCalledWith('test:entries:1', 'test:entries:2');
545
+ await storage.close();
546
+ });
547
+ it('should handle empty storage', async () => {
548
+ // Arrange
549
+ const { storage, mockClient } = await createTestStorage();
550
+ mockClient.keys.mockResolvedValue([]);
551
+ // Act
552
+ await storage.clear();
553
+ // Assert
554
+ expect(mockClient.del).not.toHaveBeenCalled();
555
+ await storage.close();
556
+ });
557
+ });
558
+ describe('close', () => {
559
+ it('should close the Redis connection', async () => {
560
+ // Arrange
561
+ const { storage, mockClient } = await createTestStorage();
562
+ // Act
563
+ await storage.close();
564
+ // Assert
565
+ expect(mockClient.quit).toHaveBeenCalled();
566
+ });
567
+ });
568
+ // ==================== Sequence Tests ====================
569
+ describe('getLatestSequence', () => {
570
+ it('should return the latest sequence number', async () => {
571
+ // Arrange
572
+ const { storage, mockClient } = await createTestStorage();
573
+ mockClient.zrevrange.mockResolvedValue(['10']);
574
+ // Act
575
+ const sequence = await storage.getLatestSequence();
576
+ // Assert
577
+ expect(sequence).toBe(10);
578
+ await storage.close();
579
+ });
580
+ it('should return null for empty storage', async () => {
581
+ // Arrange
582
+ const { storage, mockClient } = await createTestStorage();
583
+ mockClient.zrevrange.mockResolvedValue([]);
584
+ // Act
585
+ const sequence = await storage.getLatestSequence();
586
+ // Assert
587
+ expect(sequence).toBeNull();
588
+ await storage.close();
589
+ });
590
+ });
591
+ describe('hasEntriesAfter', () => {
592
+ it('should return count of entries after sequence', async () => {
593
+ // Arrange
594
+ const { storage, mockClient } = await createTestStorage();
595
+ mockClient.zcount.mockResolvedValue(5);
596
+ // Act
597
+ const count = await storage.hasEntriesAfter(10);
598
+ // Assert
599
+ expect(count).toBe(5);
600
+ expect(mockClient.zcount).toHaveBeenCalledWith(expect.stringContaining('entries:all'), '(10', '+inf');
601
+ await storage.close();
602
+ });
603
+ });
604
+ // ==================== Cursor Pagination Tests ====================
605
+ describe('findWithCursor', () => {
606
+ it('should return entries with cursor metadata', async () => {
607
+ // Arrange
608
+ const { storage, mockClient, mockPipeline } = await createTestStorage();
609
+ mockClient.zrevrange.mockResolvedValue(['2', '1']);
610
+ mockClient.zcard.mockResolvedValue(2);
611
+ mockPipeline.exec.mockResolvedValue([
612
+ [null, createMockEntryHash(2, 'request', { path: '/b' })],
613
+ [null, createMockEntryHash(1, 'request', { path: '/a' })],
614
+ ]);
615
+ mockClient.smembers.mockResolvedValue([]);
616
+ // Act
617
+ const result = await storage.findWithCursor(undefined, { limit: 10 });
618
+ // Assert
619
+ expect(result.data).toHaveLength(2);
620
+ expect(result.meta.total).toBe(2);
621
+ expect(result.meta.newestSequence).toBe(2);
622
+ expect(result.meta.oldestSequence).toBe(1);
623
+ await storage.close();
624
+ });
625
+ it('should support beforeSequence pagination', async () => {
626
+ // Arrange
627
+ const { storage, mockClient, mockPipeline } = await createTestStorage();
628
+ mockClient.zrevrangebyscore.mockResolvedValue(['1']);
629
+ mockClient.zcard.mockResolvedValue(2);
630
+ mockPipeline.exec.mockResolvedValue([
631
+ [null, createMockEntryHash(1, 'request', { path: '/a' })],
632
+ ]);
633
+ mockClient.smembers.mockResolvedValue([]);
634
+ // Act
635
+ await storage.findWithCursor(undefined, {
636
+ limit: 10,
637
+ beforeSequence: 2,
638
+ });
639
+ // Assert
640
+ expect(mockClient.zrevrangebyscore).toHaveBeenCalledWith(expect.stringContaining('entries:all'), '(2', '-inf', 'LIMIT', '0', '11');
641
+ await storage.close();
642
+ });
643
+ it('should filter by type', async () => {
644
+ // Arrange
645
+ const { storage, mockClient, mockPipeline } = await createTestStorage();
646
+ mockClient.zrevrange.mockResolvedValue(['1']);
647
+ mockClient.zcard.mockResolvedValue(1);
648
+ mockPipeline.exec.mockResolvedValue([
649
+ [null, createMockEntryHash(1, 'query', { sql: 'SELECT 1' })],
650
+ ]);
651
+ mockClient.smembers.mockResolvedValue([]);
652
+ // Act
653
+ await storage.findWithCursor('query', { limit: 10 });
654
+ // Assert
655
+ expect(mockClient.zrevrange).toHaveBeenCalledWith(expect.stringContaining('type:query'), 0, 10);
656
+ await storage.close();
657
+ });
658
+ });
659
+ });
660
+ //# sourceMappingURL=redis.storage.spec.js.map