- 定时主动搜索默认关(DEFAULT_SEARCH_INTERVAL_HOURS = 0),新建 / 手动照常各搜一次,之后靠 RSS 与站点快照 - 多版本自动补全系统缺省改为关 - 表单:基础页 = 作品与范围、下载、洗版;收录版本、优先级、自动补全折叠到进阶页「多版本」 - 对照文档 §7.3 记录落实范围 Co-Authored-By: Claude Opus 5.5 <[email protected]>
121 lines
5.2 KiB
TypeScript
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);
|
|
});
|
|
});
|