131 lines
4.8 KiB
TypeScript
131 lines
4.8 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
SessionManager,
|
|
selectOldestTaskSessionId,
|
|
findIdleTaskSessions,
|
|
CAPTCHA_POOL_SESSION_ID,
|
|
type BrowserSession,
|
|
} from './browser-session.js';
|
|
|
|
function fakeSession(partial: Partial<BrowserSession> & Pick<BrowserSession, 'id' | 'kind' | 'lastActiveAt'>): 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();
|
|
}
|
|
});
|
|
});
|