import { describe, expect, it } from "vitest"; import { planPageVersionPrune, versionRetentionFromSettings, VERSION_RETENTION_DEFAULTS, type VersionRow } from "./version-retention.js"; const NOW = Date.parse("2026-10-11T00:00:00Z"); const day = 86_400_000; const row = (version: number, daysAgo: number, id?: string): VersionRow => ({ id: id ?? `v${version}`, version, createdAt: new Date(NOW - daysAgo * day) }); describe("planPageVersionPrune (T-152)", () => { it("at or under the cap: nothing is pruned, even when ancient", () => { const rows = [row(1, 30), row(2, 10), row(3, 2)]; expect(planPageVersionPrune(rows, { now: NOW })).toEqual([]); // and the idempotency invariant: survivors are exactly the keep set expect(planPageVersionPrune(rows.slice(2), { now: NOW })).toEqual([]); }); it("within maxVersions, all recent → nothing pruned", () => { const rows = [row(1, 1), row(2, 2), row(3, 3)]; expect(planPageVersionPrune(rows, { now: NOW })).toEqual([]); }); it("above maxVersions: newest N protected even when ancient, rest pruned per grace", () => { // 10 versions, maxVersions 5: v6..v10 protected. v1..v5 are all >7 d old → all prunable. const rows = Array.from({ length: 10 }, (_, i) => row(i + 1, 100 - i)); // v1=99d … v10=90d (all old) const ids = planPageVersionPrune(rows, { maxVersions: 5, now: NOW }); expect(ids.sort()).toEqual(["v1", "v2", "v3", "v4", "v5"].sort()); }); it("above maxVersions: a RECENT row outside the newest N is still grace-protected", () => { // maxVersions 2: newest = v4, v5. v3 is recent (2 d) but outside the top 2 → must NOT be pruned. const rows = [row(1, 40), row(2, 30), row(3, 2), row(4, 1), row(5, 1)]; const ids = planPageVersionPrune(rows, { maxVersions: 2, now: NOW }); expect(ids).toEqual(["v2", "v1"]); // version-desc }); it("maxVersions=1 keeps only the newest version", () => { const rows = [row(1, 100), row(2, 90), row(3, 1)]; expect(planPageVersionPrune(rows, { maxVersions: 1, now: NOW })).toEqual(["v2", "v1"]); }); it("keepRecentDays=0 → grace disabled, only the maxVersions cap applies", () => { const rows = [row(1, 30), row(2, 1), row(3, 1)]; expect(planPageVersionPrune(rows, { maxVersions: 2, keepRecentDays: 0, now: NOW })).toEqual(["v1"]); }); it("empty page → empty plan", () => { expect(planPageVersionPrune([], { now: NOW })).toEqual([]); }); it("deterministic regardless of input order", () => { const rows = [row(1, 30), row(2, 10), row(3, 2), row(4, 1)]; const a = planPageVersionPrune(rows, { now: NOW }); const b = planPageVersionPrune([...rows].reverse(), { now: NOW }); expect(a).toEqual(b); }); }); describe("versionRetentionFromSettings (T-152)", () => { it("defaults when settings absent/malformed", () => { expect(versionRetentionFromSettings(null)).toMatchObject(VERSION_RETENTION_DEFAULTS); expect(versionRetentionFromSettings({})).toMatchObject(VERSION_RETENTION_DEFAULTS); expect(versionRetentionFromSettings({ versions: { maxVersions: "many", keepRecentDays: -5 } })).toMatchObject(VERSION_RETENTION_DEFAULTS); }); it("accepts valid overrides", () => { expect(versionRetentionFromSettings({ versions: { maxVersions: 5, keepRecentDays: 1 } })).toEqual({ maxVersions: 5, keepRecentDays: 1 }); }); });