__tests__/session-revive.test.js
Rama main · réplica de lectura del repositorio original.
/*
* A SESSION THAT TIMED OUT COMES BACK WHEN IT IS USED AGAIN (2026-11-07). 74 sessions in 40 hours closed
* for being idle and the AI using them read "no such session" as the browser restarting. These keep the
* reopening honest: same id, same owner only, same profile and page, same device, and it says so.
*/
import { describe, it, expect } from 'vitest';
import { createRequire } from 'module';
import express from 'express';
const require = createRequire(import.meta.url);
const { makeReviver, revivedNote } = require('../src/sessionRevive');
const { BrowserPool } = require('../src/pool');
function fakePool({ failCreate = false } = {}) {
const pool = { sessions: new Map(), resting: new Map(), created: [], visited: [] };
pool.putResting = BrowserPool.prototype.putResting.bind(pool);
pool.takeResting = BrowserPool.prototype.takeResting.bind(pool);
pool.createSession = async (o) => {
if (failCreate) throw Object.assign(new Error('profile "facebook" is in use'), { status: 409 });
pool.created.push(o);
pool.sessions.set(o.id, { id: o.id, owner: o.owner, profile: o.profile, page: { goto: async (u) => { pool.visited.push(u); } } });
return { sessionId: o.id };
};
pool.get = (id) => { const s = pool.sessions.get(id); if (!s) throw Object.assign(new Error('no such session'), { status: 404 }); return s; };
return pool;
}
/* A tiny stand-in for supertest (not a dependency here): listen, call, close. */
function request(a) {
const call = (method, path, body) => new Promise((resolve, reject) => {
const srv = a.listen(0, async () => {
try {
const r = await fetch(`http://127.0.0.1:${srv.address().port}${path}`, { method, headers: { 'content-type': 'application/json' }, ...(body ? { body: JSON.stringify(body) } : {}) });
const text = await r.text(); let json = {}; try { json = JSON.parse(text); } catch {}
resolve({ status: r.status, body: json, headers: Object.fromEntries(r.headers) });
} catch (e) { reject(e); } finally { srv.close(); }
});
});
return { post: (p) => ({ send: (b) => call('POST', p, b) }), delete: (p) => call('DELETE', p) };
}
function app(pool, owner = 'carla') {
const a = express();
a.use((req, _res, next) => { req.client = { owner, maxConcurrent: 6, console: false }; next(); });
a.use('/v1/sessions/:id', makeReviver({ pool, log: { info() {}, warn() {} } }));
a.post('/v1/sessions/:id/navigate', (req, res) => { const s = pool.get(req.params.id); res.json({ ok: true, ring: s.ring || null }); });
a.delete('/v1/sessions/:id', (req, res) => { try { pool.get(req.params.id); res.json({ closed: true }); } catch { res.status(404).json({ error: 'no such session' }); } });
return a;
}
const rest = (o = {}) => ({ owner: 'carla', profile: 'facebook', url: 'https://www.facebook.com/profile.php?id=1', ring: null, closedAt: Date.now() - 7 * 60000, unusedMs: 20 * 60000, why: 'idle', ...o });
describe('a timed-out session is reopened by its next call', () => {
it('under the same id, in the same profile, back on the same page — and the reply says so', async () => {
const pool = fakePool(); pool.putResting('s-1', rest());
const r = await request(app(pool)).post('/v1/sessions/s-1/navigate').send({});
expect(r.status).toBe(200);
expect(pool.created[0]).toMatchObject({ id: 's-1', owner: 'carla', profile: 'facebook' });
expect(pool.visited).toEqual(['https://www.facebook.com/profile.php?id=1']);
const note = revivedNote(r.headers['x-gb-revived']);
expect(note).toMatch(/was closed 7 minutes ago after 20 minutes without use and was reopened with the same id in the "facebook" profile/);
expect(revivedNote(encodeURIComponent(JSON.stringify({ unusedMinutes: 20, closedMinutesAgo: 1 })))).toMatch(/closed 1 minute ago after 20 minutes without use/);
expect(note).toMatch(/anything typed or uploaded but not saved is gone/);
});
it('a session that ran on a device goes back to that device', async () => {
const pool = fakePool(); pool.putResting('s-2', rest({ ring: { deviceId: 'd1', name: 'SM-F926B', profile: '' } }));
const r = await request(app(pool)).post('/v1/sessions/s-2/navigate').send({});
expect(r.body.ring).toMatchObject({ deviceId: 'd1', name: 'SM-F926B', elements: [] });
expect(pool.visited).toEqual([]); // the device holds its own page
});
it('only for its own owner, once, and not after two hours', async () => {
const pool = fakePool(); pool.putResting('s-3', rest());
expect((await request(app(pool, 'someone-else')).post('/v1/sessions/s-3/navigate').send({})).status).toBe(404); // the route's own 'no such session'
expect(pool.created).toEqual([]);
pool.putResting('s-4', rest({ closedAt: Date.now() - 3 * 60 * 60 * 1000 }));
expect(pool.takeResting('s-4', 'carla')).toBeNull();
});
it('closing a session that already closed opens no browser', async () => {
const pool = fakePool(); pool.putResting('s-5', rest());
const r = await request(app(pool)).delete('/v1/sessions/s-5');
expect(r.body).toMatchObject({ closed: true });
expect(pool.created).toEqual([]);
});
it('when it cannot be reopened, says why and keeps it for another try', async () => {
const pool = fakePool({ failCreate: true }); pool.putResting('s-6', rest());
const r = await request(app(pool)).post('/v1/sessions/s-6/navigate').send({});
expect(r.status).toBe(409);
expect(r.body.error).toMatch(/closed after inactivity and could not be reopened: profile "facebook" is in use/);
expect(pool.resting.has('s-6')).toBe(true);
});
it('a live session is left alone', async () => {
const pool = fakePool(); pool.sessions.set('s-7', { id: 's-7', owner: 'carla' });
const r = await request(app(pool)).post('/v1/sessions/s-7/navigate').send({});
expect(r.status).toBe(200);
expect(r.headers['x-gb-revived']).toBeUndefined();
expect(pool.created).toEqual([]);
});
});