Implements three-tier garbage collection for agent sessions: - SessionGCService.collect() for immediate per-session cleanup on destroySession() - SessionGCService.sweepOrphans() for daily cron sweep of orphaned Valkey keys - SessionGCService.fullCollect() for cold-start aggressive cleanup via OnModuleInit - /gc slash command wired into CommandExecutorService + registered in CommandRegistryService - SESSION_GC_CRON (daily 4am) added to CronService - GCModule provides Valkey (ioredis via @mosaic/queue) and is imported by AgentModule, LogModule, CommandsModule, AppModule - 8 Vitest unit tests covering all three GC tiers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
98 lines
3.2 KiB
TypeScript
98 lines
3.2 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { Logger } from '@nestjs/common';
|
|
import type { QueueHandle } from '@mosaic/queue';
|
|
import type { LogService } from '@mosaic/log';
|
|
import { SessionGCService } from './session-gc.service.js';
|
|
|
|
type MockRedis = {
|
|
keys: ReturnType<typeof vi.fn>;
|
|
del: ReturnType<typeof vi.fn>;
|
|
};
|
|
|
|
describe('SessionGCService', () => {
|
|
let service: SessionGCService;
|
|
let mockRedis: MockRedis;
|
|
let mockLogService: { logs: { promoteToWarm: ReturnType<typeof vi.fn> } };
|
|
|
|
beforeEach(() => {
|
|
mockRedis = {
|
|
keys: vi.fn().mockResolvedValue([]),
|
|
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.keys.mockResolvedValue(['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.keys.mockResolvedValue([]);
|
|
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.keys.mockResolvedValue(['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.keys.mockResolvedValue([]);
|
|
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 () => {
|
|
mockRedis.keys.mockResolvedValue([
|
|
'mosaic:session:abc:system',
|
|
'mosaic:session:abc:messages',
|
|
'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.keys.mockResolvedValue([]);
|
|
const result = await service.sweepOrphans();
|
|
expect(result.orphanedSessions).toBe(0);
|
|
expect(result.totalCleaned).toHaveLength(0);
|
|
});
|
|
});
|