Files
rootandClaude Opus 5.5 ed083fd138 feat(subscriptions): 按 MP 收敛订阅默认值与表单
- 定时主动搜索默认关(DEFAULT_SEARCH_INTERVAL_HOURS = 0),新建 / 手动照常各搜一次,之后靠 RSS 与站点快照
- 多版本自动补全系统缺省改为关
- 表单:基础页 = 作品与范围、下载、洗版;收录版本、优先级、自动补全折叠到进阶页「多版本」
- 对照文档 §7.3 记录落实范围

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-02 22:34:48 +08:00

121 lines
5.2 KiB
TypeScript

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 的组;都没设为关(MP 没有多版本补全)", () => {
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: false,
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);
});
});