import { checkFillBudget, coverageRow, resolveAutoFill, subscriptionGroup } from "@umt/domain"; import { describe, expect, it } from "vitest"; // W9-E 纯函数:总览分组顺序、矩阵一行的格状态、自动补全取值、补全预算。 describe("subscriptionGroup", () => { const base = { status: "active" as const, mediaKind: "series" as const, mode: "continuous" as const, totalsKnown: true, anyProgress: false, allSlotsComplete: false, }; it("按 暂停 → 待定 → 已完成 → 追更中 → 待搜索 判定;cancelled 不进总览", () => { expect(subscriptionGroup({ ...base, status: "paused", totalsKnown: false }).group).toBe("paused"); expect(subscriptionGroup({ ...base, status: "pending" }).group).toBe("pending"); expect(subscriptionGroup({ ...base, totalsKnown: false, anyProgress: true }).group).toBe("pending"); // 电影不看总集数 expect(subscriptionGroup({ ...base, mediaKind: "movie", totalsKnown: false }).group).toBe("waiting"); expect(subscriptionGroup({ ...base, status: "completed" })).toEqual({ group: "completed", awaitingNewSeason: false, }); expect(subscriptionGroup({ ...base, anyProgress: true, allSlotsComplete: true })).toEqual({ group: "completed", awaitingNewSeason: true, }); expect(subscriptionGroup({ ...base, mode: "once", allSlotsComplete: true }).awaitingNewSeason).toBe(false); expect(subscriptionGroup({ ...base, anyProgress: true }).group).toBe("following"); expect(subscriptionGroup(base).group).toBe("waiting"); expect(subscriptionGroup({ ...base, status: "cancelled" }).group).toBeNull(); }); }); describe("coverageRow", () => { const cols = [1, 2, 3, 4, 5, 6].map((e) => ({ unitKey: `s1e${e}`, episode: e, airDate: e === 6 ? "2099-01-01" : null, totalKnown: true, })); it("已入库 > 在途(含整季通配)> 范围外/未播出 > 缺失;可升级与延后标记", () => { const r = coverageRow({ columns: [...cols, { unitKey: "s2e1", episode: 1, totalKnown: false }], covered: new Map([ ["s1e1", { assetId: "a1", quality: { resolution: "2160p", source: "REMUX", label: "2160p REMUX" }, pri: 100 }], ["s1e2", { assetId: "a1", pri: 97 }], ]), inflight: [ { id: "p1", stage: "downloading", unitKeys: ["s1e3"] }, { id: "p2", stage: "reserved", unitKeys: ["s2*"] }, ], deferredKeys: ["s1e4"], range: { end: 4 }, today: "2026-09-28", upgrade: { enabled: true, targetPri: 100 }, }); const by = Object.fromEntries(r.cells.map((c) => [c.unitKey, c])); expect(by.s1e1).toMatchObject({ state: "have", assetId: "a1", pri: 100, upgradable: false }); expect(by.s1e1!.quality?.label).toBe("2160p REMUX"); expect(by.s1e2).toMatchObject({ state: "have", upgradable: true }); expect(by.s1e3).toMatchObject({ state: "inflight", planId: "p1", planStage: "downloading" }); expect(by.s1e4).toMatchObject({ state: "missing", deferred: true, reason: null }); expect(by.s1e5).toMatchObject({ state: "skipped", reason: "out_of_range" }); expect(by.s1e6).toMatchObject({ state: "skipped", reason: "out_of_range" }); expect(by.s2e1).toMatchObject({ state: "inflight", planId: "p2" }); expect(r.summary).toEqual({ have: 2, inflight: 2, missing: 1, skipped: 2 }); expect(r.minPri).toBe(97); }); it("未播出跳过;总集数未知的缺失标 suspected", () => { const r = coverageRow({ columns: [ { unitKey: "s1e1", episode: 1, airDate: "2099-01-01", totalKnown: true }, { unitKey: "s1e2", episode: 2, totalKnown: false }, ], covered: new Map(), inflight: [], today: "2026-09-28", }); expect(r.cells.map((c) => [c.state, c.reason])).toEqual([ ["skipped", "not_aired"], ["missing", "suspected"], ]); }); }); describe("自动补全开关与补全预算", () => { it("订阅显式值优先;inherit 取规则链上最后一个设了 autoFill 的组;都没设为开", () => { expect(resolveAutoFill("off", [{ id: "g", autoFill: true }])).toMatchObject({ enabled: false, source: "subscription", }); expect( resolveAutoFill("inherit", [ { id: "a", autoFill: true }, { id: "b", autoFill: false }, { id: "c", autoFill: null }, ]), ).toEqual({ enabled: false, mode: "inherit", source: "rule_group", ruleGroupId: "b" }); expect(resolveAutoFill(undefined, [{ id: "a", autoFill: null }])).toEqual({ enabled: true, mode: "inherit", source: "default", }); }); it("单元数与字节按日累计;未设维度不限;体积未知只看单元数", () => { expect(checkFillBudget(undefined, { unitsToday: 99, bytesToday: 1e12 }, { units: 5, bytes: 1e9 }).allow).toBe(true); const d = checkFillBudget( { unitsPerDay: 3, bytesPerDay: 100 }, { unitsToday: 2, bytesToday: 50 }, { units: 2, bytes: 60 }, ); expect(d.allow).toBe(false); expect(d.violations.map((v) => v.kind)).toEqual(["units", "bytes"]); expect( checkFillBudget({ bytesPerDay: 100 }, { unitsToday: 0, bytesToday: 99 }, { units: 1, bytes: null }).allow, ).toBe(true); }); });