import { describe, expect, it } from "vitest"; import { checkBudget, DEFAULT_PRIORITY_ORDER, determineMode, evaluateCacheVolume, type InflightTask, type ModeInput, priorityOf, sortByPriority, validatePriorityOrder, } from "../src/index.ts"; const GiB = 1024 ** 3; const base: ModeInput = { entity: null, releaseUnits: [], satisfiedUnits: [], slotSatisfied: false, now: "2026-09-27T00:00:00Z", newTitleWindowDays: 90, }; describe("三种模式判定", () => { it("新作品按发行日期,不按站点上传日期", () => { expect(determineMode({ ...base, firstReleaseDate: "2026-08-01" })).toEqual({ mode: "new_title", reason: "released_within_window", }); expect(determineMode({ ...base, firstReleaseDate: "1999-03-31" }).reason).toBe("not_new_release"); expect(determineMode(base).reason).toBe("release_date_unknown"); expect(determineMode({ ...base, firstReleaseDate: "bad" }).reason).toBe("release_date_unknown"); expect(determineMode({ ...base, firstReleaseDate: "2027-01-01" }).reason).toBe("not_yet_released"); }); it("追更补缺:覆盖未满足的单元", () => { const tv = { ...base, entity: { tracked: true, kind: "tv" as const } }; expect(determineMode({ ...tv, releaseUnits: ["e1", "e2"], satisfiedUnits: ["e1"] }).mode).toBe("catch_up"); expect(determineMode({ ...tv, releaseUnits: ["e1"], satisfiedUnits: ["e1"] }).reason).toBe("already_satisfied"); expect(determineMode({ ...tv, releaseUnits: ["e1"], satisfiedUnits: ["e1"], upgradeCandidate: true }).mode).toBe( "upgrade", ); expect(determineMode({ ...tv }).reason).toBe("no_units_resolved"); }); it("已有电影:Slot 缺口/更优版本 → 升级;只登记的作品不自动下载", () => { const mv = { ...base, entity: { tracked: true, kind: "movie" as const } }; expect(determineMode(mv)).toEqual({ mode: "upgrade", reason: "slot_gap" }); expect(determineMode({ ...mv, slotSatisfied: true }).mode).toBeNull(); expect(determineMode({ ...mv, slotSatisfied: true, upgradeCandidate: true }).reason).toBe("better_version"); expect(determineMode({ ...mv, entity: { tracked: false, kind: "movie" } }).reason).toBe("not_tracked"); }); }); describe("优先级", () => { const items = [ { id: "u", priorityClass: priorityOf("upgrade"), enqueuedAt: "2026-01-01T00:00:00Z" }, { id: "n", priorityClass: priorityOf("new_title"), enqueuedAt: "2026-01-01T00:00:00Z" }, { id: "c2", priorityClass: priorityOf("catch_up"), enqueuedAt: "2026-01-02T00:00:00Z" }, { id: "c1", priorityClass: priorityOf("catch_up"), enqueuedAt: "2026-01-01T00:00:00Z" }, { id: "i", priorityClass: priorityOf("upgrade", true), enqueuedAt: "2026-01-03T00:00:00Z" }, ]; it("默认 即时 > 追更 > 新作品 > 升级,同级先进先出", () => { expect(sortByPriority(items).map((x) => x.id)).toEqual(["i", "c1", "c2", "n", "u"]); expect(DEFAULT_PRIORITY_ORDER[0]).toBe("immediate"); }); it("可配置", () => { expect(sortByPriority(items, ["immediate", "new_title", "catch_up", "upgrade"]).map((x) => x.id)).toEqual([ "i", "n", "c1", "c2", "u", ]); }); it("配置必须完整", () => { expect(() => validatePriorityOrder(["immediate", "catch_up"])).toThrow(); expect(() => validatePriorityOrder(["immediate", "catch_up", "catch_up", "upgrade"])).toThrow(); }); }); describe("预算", () => { const budget = { maxNewTitlesPerDay: 3, maxBytesPerDay: 100 * GiB, maxConcurrent: 2 }; it("放行", () => { const r = checkBudget( budget, { newTitlesToday: 2, bytesToday: 10 * GiB, activeDownloads: 1 }, { countsAsNewTitle: true, bytes: GiB }, ); expect(r).toEqual({ decision: "allow", violations: [], unknown: [] }); }); it("超限列出全部违规,预留字节计入", () => { const r = checkBudget( budget, { newTitlesToday: 3, bytesToday: 60 * GiB, reservedBytesToday: 30 * GiB, activeDownloads: 2 }, { countsAsNewTitle: true, bytes: 20 * GiB }, ); expect(r.decision).toBe("deny"); expect(r.violations.map((v) => v.kind)).toEqual(["new_titles", "bytes", "concurrency"]); expect(r.violations[1]!.current).toBe(90 * GiB); }); it("体积未知 → unknown,不放行;追更不占新作品额度", () => { const r = checkBudget( budget, { newTitlesToday: 3, bytesToday: 0, activeDownloads: 0 }, { countsAsNewTitle: false }, ); expect(r).toEqual({ decision: "unknown", violations: [], unknown: ["bytes"] }); expect( checkBudget({}, { newTitlesToday: 99, bytesToday: 0, activeDownloads: 99 }, { countsAsNewTitle: true }).decision, ).toBe("allow"); }); }); describe("缓存卷硬阈值", () => { const cfg = { hardPct: 90, resumePct: 85 }; const vol = (usedPct: number) => ({ volumeId: "v1", totalBytes: 1000 * GiB, usedBytes: usedPct * 10 * GiB }); const task = (over: Partial): InflightTask => ({ taskId: "t", volumeId: "v1", sizeBytes: 50 * GiB, onDiskBytes: 0, managed: true, status: "downloading", ...over, }); it("读不到磁盘状态 → 保守阻止", () => { const r = evaluateCacheVolume({ volume: null, config: cfg, inflight: [], reservations: [], pausedByThreshold: false, }); expect(r.decision).toBe("block"); expect(r.reasons).toEqual(["volume_state_unknown"]); }); it("配置校验", () => { expect(() => evaluateCacheVolume({ volume: vol(1), config: { hardPct: 80, resumePct: 85 }, inflight: [], reservations: [], pausedByThreshold: false, }), ).toThrow(); }); it("达到硬阈值 → 暂停纳管的下载中任务", () => { const r = evaluateCacheVolume({ volume: vol(91), config: cfg, inflight: [ task({ taskId: "a" }), task({ taskId: "b", managed: false }), task({ taskId: "c", status: "paused" }), task({ taskId: "d", volumeId: "v2" }), ], reservations: [], pausedByThreshold: false, }); expect(r.decision).toBe("pause_managed"); expect(r.pauseTaskIds).toEqual(["a"]); expect(r.unmanagedGrowthRisk).toBe(true); }); it("在途剩余需求 + 预留:已预分配不重复扣,已提交计划不重复算预留", () => { const r = evaluateCacheVolume({ volume: vol(70), config: cfg, inflight: [ task({ taskId: "a", planId: "p1", sizeBytes: 100 * GiB, onDiskBytes: 100 * GiB }), task({ taskId: "b", planId: "p2", sizeBytes: 100 * GiB, onDiskBytes: 40 * GiB }), task({ taskId: "c", status: "completed", sizeBytes: 500 * GiB }), ], reservations: [ { planId: "p2", volumeId: "v1", bytes: 100 * GiB }, { planId: "p3", volumeId: "v1", bytes: 50 * GiB }, { planId: "p4", volumeId: "v2", bytes: 999 * GiB }, ], request: { bytes: 60 * GiB }, pausedByThreshold: false, }); expect(r.pendingBytes).toBe(60 * GiB); expect(r.reservedBytes).toBe(50 * GiB); expect(r.projectedPct).toBeCloseTo(87); expect(r.decision).toBe("allow"); const over = evaluateCacheVolume({ volume: vol(70), config: cfg, inflight: [task({ sizeBytes: 150 * GiB })], reservations: [], request: { bytes: 60 * GiB }, pausedByThreshold: false, }); expect(over.decision).toBe("block"); expect(over.reasons).toContain("projected_over_threshold"); }); it("体积未知 → 阻止新提交", () => { const r = evaluateCacheVolume({ volume: vol(10), config: cfg, inflight: [task({ sizeBytes: undefined, taskId: "m" })], reservations: [], request: {}, pausedByThreshold: false, }); expect(r.decision).toBe("block"); expect(r.reasons).toEqual(["request_size_unknown", "inflight_size_unknown"]); expect(r.unknownSizeTaskIds).toEqual(["m"]); }); it("恢复水位滞回:85%~90% 之间保持暂停,≤85% 恢复阈值暂停的任务", () => { const mid = evaluateCacheVolume({ volume: vol(87), config: cfg, inflight: [], reservations: [], pausedByThreshold: true, }); expect(mid.decision).toBe("block"); expect(mid.resumeThresholdPaused).toBe(false); const low = evaluateCacheVolume({ volume: vol(80), config: cfg, inflight: [], reservations: [], pausedByThreshold: true, }); expect(low.decision).toBe("allow"); expect(low.resumeThresholdPaused).toBe(true); expect(low.reasons).toEqual(["below_resume_watermark"]); }); });