95 lines
3.0 KiB
TypeScript
95 lines
3.0 KiB
TypeScript
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<typeof vi.fn>;
|
|
del: ReturnType<typeof vi.fn>;
|
|
};
|
|
|
|
describe('SessionGCService', () => {
|
|
let service: SessionGCService;
|
|
let mockRedis: MockRedis;
|
|
let mockLogService: {
|
|
logs: {
|
|
promoteSessionToWarm: ReturnType<typeof vi.fn>;
|
|
promoteToWarm: ReturnType<typeof vi.fn>;
|
|
};
|
|
};
|
|
|
|
/**
|
|
* 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<typeof vi.fn> {
|
|
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);
|
|
});
|
|
});
|