import { describe, expect, it } from "vitest"; import { draftFromJson, draftToJson, type SlotRulesDraft, type SlotRulesJson } from "./slot-rules.tsx"; const existingRules = (): SlotRulesJson => ({ hardConditions: { resolutions: ["1080p", "2160p"], sources: ["WEB-DL", "BluRay"], requireEncode: true, minSizeBytes: 1024 ** 3, maxSizeBytes: 10 * 1024 ** 3, requiredSubtitles: ["zh", "en"], cuts: ["default", "IMAX"], excludeGroups: ["LOW", "BAD"], contentTypes: ["main"], codecs: ["H.265"], hdr: { anyOf: ["HDR10"], noneOf: ["DV"] }, }, softPreferences: { hdr: ["DV", "HDR10"], groups: ["CHD", "ADE"], sources: ["BluRay", "WEB-DL"], codecs: ["H.265", "H.264"], preferredSizeBytes: { min: 2 * 1024 ** 3, max: 8 * 1024 ** 3 }, tiers: [ { name: "优先蓝光", sources: ["BluRay"], codecs: ["H.265"] }, { name: "追更", sources: ["WEB-DL"], resolutions: ["1080p"] }, ], tiersRequired: true, }, fallback: { unknownPolicy: "hold", retryLimit: 3, investigation: { enabled: true, fields: ["codec", "hdr"] }, }, upgradeThreshold: { enabled: true, ruleGroupIds: ["quality", "release-group"], consistency: "season_release", minTierSteps: 2, allowResolutionUpgrade: true, encodeGroupAllowlist: ["CHD", "ADE"], ceilingTier: 4, mode: "full_pack", basis: "priority", targetPri: 98, sourceTiers: { BluRay: 3, "WEB-DL": 2 }, }, completion: { kind: "movie", cuts: ["default", "IMAX"] }, }); function save(draft: SlotRulesDraft): SlotRulesJson { const result = draftToJson(draft); if (!result.ok) throw new Error(JSON.stringify(result.errors)); return result.value; } describe("收录规则编辑回写", () => { it("逐版本修改洗版方式,关闭再开启保留模式,明确选默认才清除模式", () => { const original = existingRules(); const disabled = save({ ...draftFromJson(original), upgradeEnabled: false }); expect(disabled.upgradeThreshold).toEqual({ ...original.upgradeThreshold, enabled: false }); const reopened = save({ ...draftFromJson(disabled), upgradeEnabled: true }); expect(reopened).toEqual(original); const perUnit = save({ ...draftFromJson(original), upgradeMode: "per_unit" }); expect(perUnit.upgradeThreshold).toEqual({ ...original.upgradeThreshold, mode: "per_unit" }); const { mode: _mode, ...rest } = original.upgradeThreshold; expect(save({ ...draftFromJson(original), upgradeMode: "" }).upgradeThreshold).toEqual(rest); expect(save(draftFromJson({})).upgradeThreshold).toEqual({ enabled: false }); }); it("未修改保存时保留表单没有展示的各层 JSON 字段", () => { const original = existingRules(); expect(save(draftFromJson(original))).toEqual(original); }); it.each([ { name: "分辨率", edit: { resolutions: ["2160p"] }, expected: (original: SlotRulesJson) => ({ ...original, hardConditions: { ...original.hardConditions, resolutions: ["2160p"] }, }), }, { name: "编码偏好", edit: { softCodecs: ["AV1", "H.265"] }, expected: (original: SlotRulesJson) => ({ ...original, softPreferences: { ...original.softPreferences, codecs: ["AV1", "H.265"] }, }), }, ])("编辑$name只改写对应字段,不改硬编码限制或其他规则", ({ edit, expected }) => { const original = existingRules(); const snapshot = structuredClone(original); expect(save({ ...draftFromJson(original), ...edit })).toEqual(expected(original)); expect(original).toEqual(snapshot); }); it("同时更新分辨率和编码偏好仍保留阶梯、洗版口径、完成版本及回退扩展", () => { const original = existingRules(); const value = save({ ...draftFromJson(original), resolutions: ["720p", "1080p"], softCodecs: ["AV1"], }); expect(value).toEqual({ ...original, hardConditions: { ...original.hardConditions, resolutions: ["720p", "1080p"] }, softPreferences: { ...original.softPreferences, codecs: ["AV1"] }, }); }); it.each([false, true])("清空洗版规则和阈值不会从原 JSON 恢复,升级开关为 %s", (upgradeEnabled) => { const original = existingRules(); const { ruleGroupIds: _groups, consistency: _consistency, minTierSteps: _steps, ...remaining } = original.upgradeThreshold; const value = save({ ...draftFromJson(original), upgradeEnabled, upgradeGroupIds: [], seasonConsistency: false, minTierSteps: "", }); expect(value).toEqual({ ...original, upgradeThreshold: { ...remaining, enabled: upgradeEnabled }, }); expect(value.upgradeThreshold).not.toHaveProperty("ruleGroupIds"); expect(value.upgradeThreshold).not.toHaveProperty("consistency"); expect(value.upgradeThreshold).not.toHaveProperty("minTierSteps"); }); it("关闭升级并清空配置后,再加载并打开升级也不会复活旧规则", () => { const disabled = save({ ...draftFromJson(existingRules()), upgradeEnabled: false, upgradeGroupIds: [], seasonConsistency: false, minTierSteps: "", }); const reopened = draftFromJson(disabled); expect(reopened.upgradeGroupIds).toEqual([]); expect(reopened.seasonConsistency).toBe(false); expect(reopened.minTierSteps).toBe(""); const enabled = save({ ...reopened, upgradeEnabled: true }); expect(enabled.upgradeThreshold).toEqual({ ...disabled.upgradeThreshold, enabled: true }); expect(enabled.upgradeThreshold).not.toHaveProperty("ruleGroupIds"); expect(enabled.upgradeThreshold).not.toHaveProperty("consistency"); expect(enabled.upgradeThreshold).not.toHaveProperty("minTierSteps"); }); }); describe("按字段配置的未知值回退", () => { const fieldFallback = () => ({ ...existingRules(), fallback: { unknownPolicy: { codec: "hold", resolution: "reject", hdr: "accept" }, investigation: { enabled: true, fields: ["codec"] }, }, }); it("按字段对象回填为保留状态,未修改保存仍保留原配置", () => { const original = fieldFallback(); const draft = draftFromJson(original); expect(draft.unknownPolicy).toBe("fields"); expect(save(draft)).toEqual(original); }); it("仅修改其他规则时也不会清除按字段回退对象及其扩展字段", () => { const original = fieldFallback(); expect(save({ ...draftFromJson(original), resolutions: ["2160p"] })).toEqual({ ...original, hardConditions: { ...original.hardConditions, resolutions: ["2160p"] }, }); }); it("明确选择全局回退后替换原按字段对象,保留其他回退字段", () => { const original = fieldFallback(); expect(save({ ...draftFromJson(original), unknownPolicy: "reject" })).toEqual({ ...original, fallback: { ...original.fallback, unknownPolicy: "reject" }, }); }); it("原按字段配置先改为全局拒绝,再明确选系统默认时清空回退,不恢复原对象", () => { const original = fieldFallback(); const draft = draftFromJson(original); const selectedGlobal = { ...draft, unknownPolicy: "reject" as const }; const cleared = { ...selectedGlobal, unknownPolicy: "" as const }; const { unknownPolicy: _policy, ...remaining } = original.fallback; const value = save(cleared); expect(value).toEqual({ ...original, fallback: remaining }); expect(value.fallback).not.toHaveProperty("unknownPolicy"); }); it("已有全局回退可以清空,保存后不会恢复原选择", () => { const original = existingRules(); const { unknownPolicy: _policy, ...remaining } = original.fallback; const value = save({ ...draftFromJson(original), unknownPolicy: "" }); expect(value).toEqual({ ...original, fallback: remaining }); expect(value.fallback).not.toHaveProperty("unknownPolicy"); }); });