Files
arboretum/packages/server/test/session-archive.test.ts
Johan LEROY c8dd539571 feat(p10): archivage automatique des sessions terminées (rétention configurable)
- DB: migration #10 (sessions.archived_at + index), helpers archive/unarchive/archiveExpired (soft-archive, jamais de DELETE)
- core/retention-settings.ts: session_retention_days (défaut 30, 0=jamais, 1-3650) + session_purge_days (off)
- core/session-archive.ts: scheduler start/stop/sweep (calqué DiscoveryService), câblé dans runDaemon
- pty-manager.list({includeArchived}) + SessionSummary.archived + emitHistoricalUpdate
- routes: GET /sessions?includeArchived, POST/DELETE /sessions/:id/archive, POST /sessions/archive-now
- protocole additif: message WS session_archived (relayé par la gateway, abonnés 'sessions')
- settings: retentionDays/purgeDays exposés et validés dans PATCH /settings
- web: store sessions (showArchived + archive/unarchive), SessionsListView (toggle/badge/actions), SettingsView (slider rétention), i18n EN+FR
- tests: retention-settings + session-archive (vitest) + settings-routes étendu ; acceptance-p10.mjs (sweep, event WS, resume d'une session archivée → 201, rétention=0)
2026-06-27 13:31:28 +02:00

70 lines
2.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Archivage automatique des sessions terminées (P10) : scheduler sweep() sur base :memory:.
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { isSessionArchived, openDb, setSetting, type Db } from '../src/db/index.js';
import { RETENTION_DAYS_KEY } from '../src/core/retention-settings.js';
import { SessionArchiveService } from '../src/core/session-archive.js';
let db: Db;
function insertSession(id: string, endedAt: string | null): void {
db.prepare('INSERT INTO sessions (id, cwd, command, created_at, ended_at) VALUES (?, ?, ?, ?, ?)').run(
id,
'/tmp/x',
'bash',
'2020-01-01T00:00:00.000Z',
endedAt,
);
}
beforeEach(() => {
db = openDb(':memory:');
insertSession('old', '2020-01-01T00:00:00.000Z'); // terminée il y a longtemps
insertSession('recent', new Date().toISOString()); // terminée à l'instant
insertSession('live', null); // vivante (jamais archivée)
});
afterEach(() => db.close());
describe('SessionArchiveService.sweep', () => {
it('archive seulement les sessions terminées plus anciennes que la rétention', () => {
setSetting(db, RETENTION_DAYS_KEY, '30');
const svc = new SessionArchiveService({ db });
const events: string[] = [];
svc.on('session_archived', (e) => events.push(e.id));
const n = svc.sweep();
expect(n).toBe(1);
expect(events).toEqual(['old']);
expect(isSessionArchived(db, 'old')).toBe(true);
expect(isSessionArchived(db, 'recent')).toBe(false);
expect(isSessionArchived(db, 'live')).toBe(false);
});
it('est idempotent : un second balayage narchive rien de plus', () => {
setSetting(db, RETENTION_DAYS_KEY, '30');
const svc = new SessionArchiveService({ db });
expect(svc.sweep()).toBe(1);
const events: string[] = [];
svc.on('session_archived', (e) => events.push(e.id));
expect(svc.sweep()).toBe(0);
expect(events).toEqual([]);
});
it('rétention = 0 → no-op (archivage désactivé)', () => {
setSetting(db, RETENTION_DAYS_KEY, '0');
const svc = new SessionArchiveService({ db });
expect(svc.sweep()).toBe(0);
expect(isSessionArchived(db, 'old')).toBe(false);
});
it('start() déclenche un balayage immédiat puis stop() est sûr', () => {
setSetting(db, RETENTION_DAYS_KEY, '30');
const svc = new SessionArchiveService({ db, intervalMs: 60_000 });
svc.start();
expect(isSessionArchived(db, 'old')).toBe(true);
svc.stop();
svc.stop(); // idempotent
});
});