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: { promoteSessionToWarm: ReturnType; 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: { promoteSessionToWarm: vi.fn().mockResolvedValue(0), 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() only scans its session namespace and cannot reach durable approvals', async () => { await service.collect('abc'); expect(mockRedis.scan).toHaveBeenCalledWith('0', 'MATCH', 'mosaic:session:abc:*', 'COUNT', 100); expect(mockRedis.del).not.toHaveBeenCalledWith(expect.stringContaining('command-approval')); }); it('collect() returns sessionId in result', async () => { const result = await service.collect('test-session-id'); expect(result.sessionId).toBe('test-session-id'); }); it('collect() demotes logs only for the requested session', async () => { await service.collect('owned-session'); expect(mockLogService.logs.promoteSessionToWarm).toHaveBeenCalledWith( 'owned-session', expect.any(Date), ); expect(mockLogService.logs.promoteToWarm).not.toHaveBeenCalled(); }); it('does not expose automatic global GC entry points', () => { expect('fullCollect' in service).toBe(false); expect('sweepOrphans' in service).toBe(false); }); });