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)
This commit is contained in:
69
packages/server/test/session-archive.test.ts
Normal file
69
packages/server/test/session-archive.test.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
// 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 n’archive 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
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user