import type { CoverageCell, Subscription, SubscriptionSlotSummary } from "@umt/contracts"; import { describe, expect, it } from "vitest"; import { cellKind, draftFromSubscription, draftToParams, emptyDraft, entriesByDate, foldNames, monthGrid, monthKey, monthStart, moveItem, progressText, seasonLabel, shiftMonth, slotBadge, } from "./model.ts"; const slot = (x: Partial): SubscriptionSlotSummary => ({ slotId: "s", name: "4K", primary: true, status: "empty", have: 0, inflight: 0, missing: 0, total: null, upgrading: false, minPri: null, targetPri: null, autoFill: true, ruleGroupIds: [], ...x, }); const cell = (x: Partial): CoverageCell => ({ unitKey: "s1e1", state: "missing", assetId: null, quality: null, pri: null, planId: null, planStage: null, reason: null, upgradable: false, deferred: false, ...x, }); describe("订阅总览文案", () => { it("进度总未知显示 ?", () => { expect(progressText({ have: 3, total: null, inflight: 0 })).toBe("3/?"); expect(progressText({ have: 3, total: 10, inflight: 1 })).toBe("3/10"); }); it("季标签:特别篇 / 接续 / 电影", () => { expect(seasonLabel({ mediaKind: "series", seasonNumber: 0, seasonScope: "current_season" })).toBe("特别篇"); expect(seasonLabel({ mediaKind: "series", seasonNumber: 2, seasonScope: "following_seasons" })).toBe("S2 起"); expect(seasonLabel({ mediaKind: "series", seasonNumber: 1, seasonScope: "current_season" })).toBe("S1"); expect(seasonLabel({ mediaKind: "movie", seasonNumber: null, seasonScope: "current_season" })).toBeNull(); }); it("版本徽标按 洗版 → 下载中 → 缺 → 已齐 判定", () => { expect(slotBadge(slot({ upgrading: true, minPri: 97, targetPri: 100, missing: 2 })).label).toBe("洗版中 97→100"); expect(slotBadge(slot({ inflight: 2, missing: 3 }))).toEqual({ label: "下载中 2", tone: "neutral" }); expect(slotBadge(slot({ missing: 3 })).tone).toBe("destructive"); expect(slotBadge(slot({ have: 10, total: 10 }))).toEqual({ label: "已齐", tone: "success" }); expect(slotBadge(slot({})).label).toBe("未开始"); }); it("名称折叠", () => { expect(foldNames([])).toBe("全部站点"); expect(foldNames(["a", "b", "c", "d"])).toBe("a、b +2"); }); }); describe("覆盖矩阵格", () => { it("state + 附带字段 → 显示类别", () => { expect(cellKind(cell({ state: "have" }))).toBe("have"); expect(cellKind(cell({ state: "have", upgradable: true }))).toBe("upgradable"); expect(cellKind(cell({ state: "missing", reason: "suspected" }))).toBe("suspected"); expect(cellKind(cell({ state: "missing", deferred: true }))).toBe("deferred"); expect(cellKind(cell({ state: "skipped", reason: "not_aired" }))).toBe("not_aired"); expect(cellKind(cell({ state: "skipped", reason: "out_of_range" }))).toBe("skipped"); expect(cellKind(cell({ state: "inflight" }))).toBe("inflight"); }); it("拖动排序", () => { expect(moveItem(["a", "b", "c"], 2, 0)).toEqual(["c", "a", "b"]); expect(moveItem(["a", "b"], 0, 5)).toEqual(["a", "b"]); }); }); describe("表单 ↔ params", () => { it("普通编辑不重建补全配置;只改补全开关也保留预算精度,明确修改才更新", () => { const params = { autoFill: "on", fillBudget: { unitsPerDay: 2, bytesPerDay: Math.round(1.25 * 1024 ** 3), future: true }, }; const sub = { mediaKind: "movie", seasonScope: "current_season", mode: "once", priority: "tracking", seasonUnitId: null, policyRevisionId: null, params, } as unknown as Subscription; const d = draftFromSubscription(sub); const ordinary = draftToParams({ ...d, searchIntervalHours: "6" }, params, { totalChanged: false, autoFillChanged: false, }); expect(ordinary.params).toEqual({ ...params, searchIntervalHours: 6 }); const modeOnly = draftToParams({ ...d, autoFill: "off" }, params, { totalChanged: false, autoFillChanged: true }); expect(modeOnly.params).toEqual({ ...params, autoFill: "off" }); const changed = draftToParams({ ...d, gbPerDay: "2", unitsPerDay: "" }, params, { totalChanged: false, autoFillChanged: true, }); expect(changed.params.fillBudget).toEqual({ bytesPerDay: 2 * 1024 ** 3, future: true }); const cleared = draftToParams( { ...d, gbPerDay: "", unitsPerDay: "" }, { ...params, fillBudget: { unitsPerDay: 2, bytesPerDay: params.fillBudget.bytesPerDay } }, { totalChanged: false, autoFillChanged: true }, ); expect(cleared.params.fillBudget).toBeUndefined(); }); it("保留未知键,空值不写,只在改动时带 total_episode", () => { const d = { ...emptyDraft("series"), siteIds: ["x"], episodeStart: "3", include: "中字\n\n 4K ", autoFill: "off" as const, unitsPerDay: "2", gbPerDay: "1.5", total: "12", }; const { params, errors } = draftToParams( d, { custom: 1, keywords: ["旧"] }, { totalChanged: true, autoFillChanged: true }, ); expect(errors).toEqual({}); expect(params).toEqual({ custom: 1, siteIds: ["x"], include: ["中字", "4K"], episodeRange: { start: 3 }, total_episode: 12, manual_total_episode: false, autoFill: "off", fillBudget: { unitsPerDay: 2, bytesPerDay: Math.round(1.5 * 1024 ** 3) }, }); const same = draftToParams(d, {}, { totalChanged: false, autoFillChanged: true }).params; expect(same.best_version).toBeUndefined(); expect(same.total_episode).toBeUndefined(); }); it("校验:结束集 < 开始集", () => { const { errors } = draftToParams( { ...emptyDraft("series"), episodeStart: "5", episodeEnd: "2" }, {}, { totalChanged: false, autoFillChanged: true }, ); expect(errors.episodeEnd).toBeTruthy(); }); it("从订阅回填", () => { const sub = { mediaKind: "series", seasonUnitId: null, seasonScope: "current_season", mode: "continuous", priority: "tracking", policyRevisionId: null, params: { episodeRange: { start: 2 }, autoFill: "on", fillBudget: { bytesPerDay: 2 * 1024 ** 3 }, keywords: ["a", "b"], }, } as unknown as Subscription; const d = draftFromSubscription(sub); expect(d.episodeStart).toBe("2"); expect(d.autoFill).toBe("on"); expect(d.gbPerDay).toBe("2"); expect(d.keywords).toBe("a\nb"); }); it("编辑跟踪与下载信息保留旧收录规则,不再生成统一洗版指令", () => { const legacy = { resolution: "4k", source: "uhd", effect: "atmos", ruleGroupIds: ["quality"], future: { keep: true }, }; const sub = { mediaKind: "movie", seasonScope: "current_season", mode: "once", priority: "tracking", seasonUnitId: null, policyRevisionId: null, params: legacy, } as unknown as Subscription; const draft = draftFromSubscription(sub); const { params, errors } = draftToParams({ ...draft, siteIds: ["site"], searchIntervalHours: "6" }, legacy, { totalChanged: false, autoFillChanged: true, }); expect(errors).toEqual({}); expect(params).toEqual({ ...legacy, siteIds: ["site"], searchIntervalHours: 6 }); expect(params.best_version).toBeUndefined(); expect(params.best_version_full).toBeUndefined(); const cleared = draftToParams(draft, legacy, { totalChanged: false, autoFillChanged: false, clearQualityPreset: true, }); expect(cleared.params).toEqual({ ruleGroupIds: ["quality"], future: { keep: true } }); expect(draftToParams(emptyDraft("movie"), {}, { totalChanged: false, autoFillChanged: true }).params).toEqual({}); }); }); describe("日历", () => { it("月网格从周一开始,整周铺满;跨年切月", () => { const first = monthStart("2026-09"); const g = monthGrid(first); expect(g[0]).toEqual({ date: "2026-08-31", inMonth: false }); expect(g.length % 7).toBe(0); expect(g.at(-1)).toEqual({ date: "2026-10-04", inMonth: false }); expect(g.filter((d) => d.inMonth)).toHaveLength(30); expect(monthKey(shiftMonth(monthStart("2026-12"), 1))).toBe("2027-01"); expect(monthKey(monthStart("bad", new Date(2026, 4, 9)))).toBe("2026-05"); }); it("按日期分桶", () => { const e = (date: string) => ({ date, subscriptionId: date }) as never; const m = entriesByDate([e("2026-09-01"), e("2026-09-01"), e("2026-09-02")]); expect(m.get("2026-09-01")).toHaveLength(2); expect(m.get("2026-09-03")).toBeUndefined(); }); });