import { describe, expect, it } from "vitest"; import { normalizeSessionCleanupRequest, normalizeSessionCleanupThresholds, planSessionCleanup } from "./sessionCleanup.js"; import type { PiSessionListEntry } from "./piSessionService.js"; import type { ArchivedSessionRecord } from "./sessionArchiveStore.js"; describe("session cleanup planning", () => { it("plans cleanup by strict cutoffs and groups counts by stored cwd", () => { const now = new Date("2026-06-25T00:00:00.000Z"); const archivedRecords: ArchivedSessionRecord[] = [ archivedRecord("already-archived", "/unregistered", "2026-06-20T00:00:00.000Z"), archivedRecord("delete-old", "/other", "2026-06-14T23:59:59.999Z"), archivedRecord("keep-exact", "/other", "2026-06-15T00:00:00.000Z"), ]; const plan = planSessionCleanup({ now, thresholds: { archiveIdleDays: 30, deleteArchivedDays: 10 }, archivedRecords, sessions: [ sessionEntry("archive-old", "/unregistered", "2026-05-25T23:59:59.999Z"), sessionEntry("keep-exact", "/unregistered", "2026-05-26T00:00:00.000Z"), sessionEntry("keep-new", "/unregistered", "2026-05-26T00:00:00.001Z"), sessionEntry("already-archived", "/unregistered", "2026-05-01T00:00:00.000Z"), ], }); expect(plan.archiveInputs.map((input) => input.sessionId)).toEqual(["archive-old"]); expect(plan.deleteRecords.map((record) => record.sessionId)).toEqual(["delete-old"]); expect(plan.projects).toEqual([ { cwd: "/other", archiveCount: 0, deleteCount: 1 }, { cwd: "/unregistered", archiveCount: 1, deleteCount: 0 }, ]); expect(plan.totals).toEqual({ archiveCount: 1, deleteCount: 1 }); }); it("filters cleanup candidates to selected project cwd paths", () => { const plan = planSessionCleanup({ now: new Date("2026-06-25T00:00:00.000Z"), thresholds: { archiveIdleDays: 30, deleteArchivedDays: 30 }, projectCwds: ["/repo-a"], sessions: [ sessionEntry("archive-a", "/repo-a", "2026-05-01T00:00:00.000Z"), sessionEntry("archive-b", "/repo-b", "2026-05-01T00:00:00.000Z"), ], archivedRecords: [ archivedRecord("delete-a", "/repo-a", "2026-05-01T00:00:00.000Z"), archivedRecord("delete-b", "/repo-b", "2026-05-01T00:00:00.000Z"), ], }); expect(plan.archiveInputs.map((input) => input.sessionId)).toEqual(["archive-a"]); expect(plan.deleteRecords.map((record) => record.sessionId)).toEqual(["delete-a"]); expect(plan.projects).toEqual([{ cwd: "/repo-a", archiveCount: 1, deleteCount: 1 }]); expect(plan.totals).toEqual({ archiveCount: 1, deleteCount: 1 }); }); it("skips archive and delete candidates that are busy in memory", () => { const plan = planSessionCleanup({ now: new Date("2026-06-25T00:00:00.000Z"), thresholds: { archiveIdleDays: 1, deleteArchivedDays: 1 }, sessions: [sessionEntry("busy-open", "/repo", "2026-06-01T00:00:00.000Z")], archivedRecords: [archivedRecord("busy-archived", "/repo", "2026-06-01T00:00:00.000Z")], activeSessions: [ { sessionId: "busy-open", hasActiveWork: true }, { sessionId: "busy-archived", hasActiveWork: true }, ], }); expect(plan.archiveInputs).toHaveLength(0); expect(plan.deleteRecords).toHaveLength(0); expect(plan.skippedBusySessionIds).toEqual(["busy-archived", "busy-open"]); expect(plan.totals).toEqual({ archiveCount: 0, deleteCount: 0 }); }); it("validates optional runtime thresholds", () => { expect(normalizeSessionCleanupThresholds({ archiveIdleDays: 30, deleteArchivedDays: null })).toEqual({ archiveIdleDays: 30 }); expect(normalizeSessionCleanupThresholds({})).toEqual({}); expect(() => normalizeSessionCleanupThresholds({ archiveIdleDays: -1 })).toThrow("archiveIdleDays field must be a non-negative integer"); expect(() => normalizeSessionCleanupThresholds({ deleteArchivedDays: 1.5 })).toThrow("deleteArchivedDays field must be a non-negative integer"); expect(() => normalizeSessionCleanupThresholds({ archiveIdleDays: "30" })).toThrow("archiveIdleDays field must be a non-negative integer"); }); it("validates optional selected project cwd paths", () => { expect(normalizeSessionCleanupRequest({ archiveIdleDays: 30, projectCwds: ["/repo", "/repo"] })).toEqual({ thresholds: { archiveIdleDays: 30 }, projectCwds: ["/repo"], }); expect(normalizeSessionCleanupRequest({ projectCwds: null })).toEqual({ thresholds: {} }); expect(() => normalizeSessionCleanupRequest({ projectCwds: ["/repo", 1] })).toThrow("projectCwds field must be an array of strings"); }); }); function sessionEntry(id: string, cwd: string, modified: string): PiSessionListEntry { return { id, cwd, path: `/sessions/${id}.jsonl`, created: new Date("2026-01-01T00:00:00.000Z"), modified: new Date(modified), messageCount: 1, firstMessage: "hello", allMessagesText: "hello", }; } function archivedRecord(sessionId: string, cwd: string, archivedAt: string): ArchivedSessionRecord { return { sessionId, cwd, archivedAt }; }