maestro/src/engine/browser-session.test.ts
2026-06-03 05:08:00 +00:00

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();
}
});
});