1import { describe, expect, it } from "vitest";
2import { planPageVersionPrune, versionRetentionFromSettings, VERSION_RETENTION_DEFAULTS, type VersionRow } from "./version-retention.js";
3
4const NOW = Date.parse("2026-10-11T00:00:00Z");
5const day = 86_400_000;
6const row = (version: number, daysAgo: number, id?: string): VersionRow => ({
7 id: id ?? `v${version}`,
8 version,
9 createdAt: new Date(NOW - daysAgo * day)
10});
11
12describe("planPageVersionPrune (T-152)", () => {
13 it("at or under the cap: nothing is pruned, even when ancient", () => {
14 const rows = [row(1, 30), row(2, 10), row(3, 2)];
15 expect(planPageVersionPrune(rows, { now: NOW })).toEqual([]);
16
17 expect(planPageVersionPrune(rows.slice(2), { now: NOW })).toEqual([]);
18 });
19
20 it("within maxVersions, all recent → nothing pruned", () => {
21 const rows = [row(1, 1), row(2, 2), row(3, 3)];
22 expect(planPageVersionPrune(rows, { now: NOW })).toEqual([]);
23 });
24
25 it("above maxVersions: newest N protected even when ancient, rest pruned per grace", () => {
26
27 const rows = Array.from({ length: 10 }, (_, i) => row(i + 1, 100 - i));
28 const ids = planPageVersionPrune(rows, { maxVersions: 5, now: NOW });
29 expect(ids.sort()).toEqual(["v1", "v2", "v3", "v4", "v5"].sort());
30 });
31
32 it("above maxVersions: a RECENT row outside the newest N is still grace-protected", () => {
33
34 const rows = [row(1, 40), row(2, 30), row(3, 2), row(4, 1), row(5, 1)];
35 const ids = planPageVersionPrune(rows, { maxVersions: 2, now: NOW });
36 expect(ids).toEqual(["v2", "v1"]);
37 });
38
39 it("maxVersions=1 keeps only the newest version", () => {
40 const rows = [row(1, 100), row(2, 90), row(3, 1)];
41 expect(planPageVersionPrune(rows, { maxVersions: 1, now: NOW })).toEqual(["v2", "v1"]);
42 });
43
44 it("keepRecentDays=0 → grace disabled, only the maxVersions cap applies", () => {
45 const rows = [row(1, 30), row(2, 1), row(3, 1)];
46 expect(planPageVersionPrune(rows, { maxVersions: 2, keepRecentDays: 0, now: NOW })).toEqual(["v1"]);
47 });
48
49 it("empty page → empty plan", () => {
50 expect(planPageVersionPrune([], { now: NOW })).toEqual([]);
51 });
52
53 it("deterministic regardless of input order", () => {
54 const rows = [row(1, 30), row(2, 10), row(3, 2), row(4, 1)];
55 const a = planPageVersionPrune(rows, { now: NOW });
56 const b = planPageVersionPrune([...rows].reverse(), { now: NOW });
57 expect(a).toEqual(b);
58 });
59});
60
61describe("versionRetentionFromSettings (T-152)", () => {
62 it("defaults when settings absent/malformed", () => {
63 expect(versionRetentionFromSettings(null)).toMatchObject(VERSION_RETENTION_DEFAULTS);
64 expect(versionRetentionFromSettings({})).toMatchObject(VERSION_RETENTION_DEFAULTS);
65 expect(versionRetentionFromSettings({ versions: { maxVersions: "many", keepRecentDays: -5 } })).toMatchObject(VERSION_RETENTION_DEFAULTS);
66 });
67
68 it("accepts valid overrides", () => {
69 expect(versionRetentionFromSettings({ versions: { maxVersions: 5, keepRecentDays: 1 } })).toEqual({
70 maxVersions: 5,
71 keepRecentDays: 1
72 });
73 });
74});
75