import { describe, it, expect } from 'vitest'; import { SessionManager, selectOldestTaskSessionId, findIdleTaskSessions, CAPTCHA_POOL_SESSION_ID, type BrowserSession, } from './browser-session.js'; function fakeSession(partial: Partial & Pick): BrowserSession { return { id: partial.id, kind: partial.kind, taskId: partial.taskId, userId: partial.userId, browser: null, context: null, vncPort: 0, novncPort: 0, userDataDir: '', state: 'ready', xvfbProcess: null, x11vncProcess: null, websockifyProcess: null, display: ':99', createdAt: partial.createdAt ?? partial.lastActiveAt, lastActiveAt: partial.lastActiveAt, lockedByJobId: partial.lockedByJobId ?? null, captchaPending: partial.captchaPending, }; } describe('SessionManager', () => { it('should report availability based on system deps', () => { const result = SessionManager.isAvailable(); expect(typeof result).toBe('boolean'); }); }); describe('selectOldestTaskSessionId', () => { it('returns null when no task sessions exist', () => { const sessions = [ fakeSession({ id: CAPTCHA_POOL_SESSION_ID, kind: 'pool', lastActiveAt: new Date(0) }), ]; expect(selectOldestTaskSessionId(sessions)).toBeNull(); }); it('picks the task session with the smallest lastActiveAt', () => { const t0 = new Date('2026-01-01T00:00:00Z'); const t1 = new Date('2026-01-01T01:00:00Z'); const t2 = new Date('2026-01-01T02:00:00Z'); const sessions = [ fakeSession({ id: 'pool', kind: 'pool', lastActiveAt: t0 }), // pool excluded fakeSession({ id: 'newer', kind: 'task', taskId: 'a', lastActiveAt: t2 }), fakeSession({ id: 'oldest', kind: 'task', taskId: 'b', lastActiveAt: t0 }), fakeSession({ id: 'middle', kind: 'task', taskId: 'c', lastActiveAt: t1 }), ]; expect(selectOldestTaskSessionId(sessions)).toBe('oldest'); }); it('skips locked task sessions', () => { const t0 = new Date('2026-01-01T00:00:00Z'); const t1 = new Date('2026-01-01T01:00:00Z'); const sessions = [ fakeSession({ id: 'oldest-locked', kind: 'task', taskId: 'a', lastActiveAt: t0, lockedByJobId: 'job-1' }), fakeSession({ id: 'newer-unlocked', kind: 'task', taskId: 'b', lastActiveAt: t1 }), ]; expect(selectOldestTaskSessionId(sessions)).toBe('newer-unlocked'); }); it('returns null when all task sessions are locked', () => { const sessions = [ fakeSession({ id: 'a', kind: 'task', taskId: 't1', lastActiveAt: new Date(0), lockedByJobId: 'j1' }), fakeSession({ id: 'b', kind: 'task', taskId: 't2', lastActiveAt: new Date(1000), lockedByJobId: 'j2' }), ]; expect(selectOldestTaskSessionId(sessions)).toBeNull(); }); }); describe('findIdleTaskSessions', () => { const now = new Date('2026-01-01T01:00:00Z'); it('returns sessions older than ttl', () => { const sessions = [ fakeSession({ id: 'old', kind: 'task', taskId: 'a', lastActiveAt: new Date('2026-01-01T00:50:00Z') }), // 10 min ago fakeSession({ id: 'fresh', kind: 'task', taskId: 'b', lastActiveAt: new Date('2026-01-01T00:59:00Z') }), // 1 min ago ]; expect(findIdleTaskSessions(sessions, 300, now)).toEqual(['old']); // ttl=5min }); it('excludes pool sessions even when idle', () => { const sessions = [ fakeSession({ id: CAPTCHA_POOL_SESSION_ID, kind: 'pool', lastActiveAt: new Date('2026-01-01T00:00:00Z') }), ]; expect(findIdleTaskSessions(sessions, 300, now)).toEqual([]); }); it('excludes locked task sessions even when idle', () => { const sessions = [ fakeSession({ id: 'locked-old', kind: 'task', taskId: 'a', lastActiveAt: new Date('2026-01-01T00:00:00Z'), lockedByJobId: 'job-1', }), ]; expect(findIdleTaskSessions(sessions, 300, now)).toEqual([]); }); it('returns empty when all sessions are fresh', () => { const sessions = [ fakeSession({ id: 'a', kind: 'task', taskId: 'a', lastActiveAt: new Date('2026-01-01T00:59:00Z') }), fakeSession({ id: 'b', kind: 'task', taskId: 'b', lastActiveAt: new Date('2026-01-01T00:58:30Z') }), ]; expect(findIdleTaskSessions(sessions, 300, now)).toEqual([]); }); }); describe('storageState injection', () => { it('createLoginSession returns a fresh session with kind=login and bound profileId', async () => { if (!SessionManager.isAvailable()) return; // skip if Xvfb / x11vnc / websockify not installed const sm = new SessionManager({ vncBasePort: 5900, sessionDataDir: '/tmp/bs-test', maxSessions: 5 }); try { const s = await sm.createLoginSession({ ownerId: 'u1', profileId: 7 }); expect(s.kind).toBe('login'); expect(s.profileId).toBe(7); } finally { await sm.destroyAll(); } }); });