import { describe, it, expect, vi, beforeEach } from 'vitest'; import { Logger } from '@nestjs/common'; import type { QueueHandle } from '@mosaicstack/queue'; import type { LogService } from '@mosaicstack/log'; import { SessionGCService } from './session-gc.service.js'; type MockRedis = { scan: ReturnType; del: ReturnType; }; describe('SessionGCService', () => { let service: SessionGCService; let mockRedis: MockRedis; let mockLogService: { logs: { promoteToWarm: ReturnType } }; /** * Helper: build a scan mock that returns all provided keys in a single * cursor iteration (cursor '0' in → ['0', keys] out). */ function makeScanMock(keys: string[]): ReturnType { return vi.fn().mockResolvedValue(['0', keys]); } beforeEach(() => { mockRedis = { scan: makeScanMock([]), del: vi.fn().mockResolvedValue(0), }; mockLogService = { logs: { promoteToWarm: vi.fn().mockResolvedValue(0), }, }; // Suppress logger output in tests vi.spyOn(Logger.prototype, 'log').mockImplementation(() => {}); service = new SessionGCService( mockRedis as unknown as QueueHandle['redis'], mockLogService as unknown as LogService, ); }); it('collect() deletes Valkey keys for session', async () => { mockRedis.scan = makeScanMock(['mosaic:session:abc:system', 'mosaic:session:abc:foo']); const result = await service.collect('abc'); expect(mockRedis.del).toHaveBeenCalledWith( 'mosaic:session:abc:system', 'mosaic:session:abc:foo', ); expect(result.cleaned.valkeyKeys).toBe(2); }); it('collect() with no keys returns empty cleaned valkeyKeys', async () => { mockRedis.scan = makeScanMock([]); const result = await service.collect('abc'); expect(result.cleaned.valkeyKeys).toBeUndefined(); }); it('collect() returns sessionId in result', async () => { const result = await service.collect('test-session-id'); expect(result.sessionId).toBe('test-session-id'); }); it('fullCollect() deletes all session keys', async () => { mockRedis.scan = makeScanMock(['mosaic:session:abc:system', 'mosaic:session:xyz:foo']); const result = await service.fullCollect(); expect(mockRedis.del).toHaveBeenCalled(); expect(result.valkeyKeys).toBe(2); }); it('fullCollect() with no keys returns 0 valkeyKeys', async () => { mockRedis.scan = makeScanMock([]); const result = await service.fullCollect(); expect(result.valkeyKeys).toBe(0); expect(mockRedis.del).not.toHaveBeenCalled(); }); it('fullCollect() returns duration', async () => { const result = await service.fullCollect(); expect(result.duration).toBeGreaterThanOrEqual(0); }); it('sweepOrphans() extracts unique session IDs and collects them', async () => { // First scan call returns the global session list; subsequent calls return // per-session keys during collect(). mockRedis.scan = vi .fn() .mockResolvedValueOnce([ '0', ['mosaic:session:abc:system', 'mosaic:session:abc:messages', 'mosaic:session:xyz:system'], ]) // collect('abc') scan .mockResolvedValueOnce(['0', ['mosaic:session:abc:system', 'mosaic:session:abc:messages']]) // collect('xyz') scan .mockResolvedValueOnce(['0', ['mosaic:session:xyz:system']]); mockRedis.del.mockResolvedValue(1); const result = await service.sweepOrphans(); expect(result.orphanedSessions).toBeGreaterThanOrEqual(0); expect(result.duration).toBeGreaterThanOrEqual(0); }); it('sweepOrphans() returns empty when no session keys', async () => { mockRedis.scan = makeScanMock([]); const result = await service.sweepOrphans(); expect(result.orphanedSessions).toBe(0); expect(result.totalCleaned).toHaveLength(0); }); });