import * as C from "@umt/contracts"; import { describe, expect, it } from "vitest"; import * as M from "../src/index.ts"; // 枚举全覆盖: // - domain 侧是 TS 联合类型,映射表写成 Record,domain 增删值时 typecheck 直接失败; // 这里再在运行时比对 DOMAIN_* 清单与期望清单,防止有人用类型断言绕过。 // - contracts 侧是 z.enum,逐个 .options 断言都有映射,新增值时测试失败。 const sorted = (xs: readonly string[]) => [...xs].sort(); describe("下载状态", () => { it("domain 取值清单固定", () => { expect(sorted(M.DOMAIN_DOWNLOAD_STATUSES)).toEqual( sorted([ "queued", "downloading", "paused_by_threshold", "paused_by_user", "completed", "error", "missing_files", "removed", ]), ); }); it("domain → contracts → domain 往返无损,pauseReason 只在 paused 时出现", () => { for (const s of M.DOMAIN_DOWNLOAD_STATUSES) { const c = M.toContractDownloadState(s); expect(C.DownloadStatus.options).toContain(c.status); expect(c.pauseReason !== null).toBe(c.status === "paused"); expect(M.toDomainDownloadStatus(c.status, c.pauseReason), s).toBe(s); } }); it("contracts 每个状态、每个暂停原因都能折回 domain", () => { for (const s of C.DownloadStatus.options) { for (const r of [null, ...C.PauseReason.options]) { expect(M.DOMAIN_DOWNLOAD_STATUSES, `${s}/${r}`).toContain(M.toDomainDownloadStatus(s, r)); } } expect(M.toDomainDownloadStatus("paused", "threshold")).toBe("paused_by_threshold"); expect(M.toDomainDownloadStatus("paused", "agent")).toBe("paused_by_user"); expect(M.toDomainDownloadStatus("imported", null)).toBe("completed"); }); it("合并观察:业务叠加态在底层状态不变时保留,否则以观察为准", () => { const st = (status: C.DownloadStatus, pauseReason: C.PauseReason | null = null) => ({ status, pauseReason }); expect(M.mergeObservedDownloadState(st("importing"), "completed")).toEqual(st("importing")); expect(M.mergeObservedDownloadState(st("imported"), "completed")).toEqual(st("imported")); expect(M.mergeObservedDownloadState(st("stalled"), "downloading")).toEqual(st("stalled")); expect(M.mergeObservedDownloadState(st("stalled"), "completed")).toEqual(st("completed")); expect(M.mergeObservedDownloadState(st("submitting"), "queued")).toEqual(st("submitting")); expect(M.mergeObservedDownloadState(st("paused", "agent"), "paused_by_user")).toEqual(st("paused", "agent")); expect(M.mergeObservedDownloadState(st("paused", "user"), "paused_by_threshold")).toEqual( st("paused", "threshold"), ); expect(M.mergeObservedDownloadState(st("imported"), "removed")).toEqual(st("removed")); // 已进入整理侧:qB 重启 / 重新校验 / 移动的中间态不把状态改回下载阶段 for (const o of ["queued", "downloading", "paused_by_user", "error", "missing_files"] as const) { expect(M.mergeObservedDownloadState(st("imported"), o), o).toEqual(st("imported")); expect(M.mergeObservedDownloadState(st("importing"), o), o).toEqual(st("importing")); } }); }); describe("证据等级 / 媒体类型", () => { it("domain 匹配等级清单固定;rejected 不形成证据", () => { expect(sorted(M.DOMAIN_MATCH_LEVELS)).toEqual( sorted(["rule_confirmed", "multi_evidence", "needs_confirm", "rejected"]), ); expect(M.toContractEvidenceLevel("needs_confirm")).toBe("needs_review"); expect(M.toContractEvidenceLevel("rejected")).toBeNull(); for (const l of M.DOMAIN_MATCH_LEVELS) { const c = M.toContractEvidenceLevel(l); if (c) expect(M.toDomainConfidence(c)).toBe(l); } }); it("contracts 每个证据等级都能折回 domain;manual 视为最强", () => { for (const l of C.EvidenceLevel.options) expect(M.DOMAIN_MATCH_LEVELS).toContain(M.toDomainConfidence(l)); expect(M.toDomainConfidence("manual")).toBe("rule_confirmed"); }); it("媒体类型与外部实体类型", () => { for (const k of C.MediaKind.options) { const d = M.toDomainMediaType(k); if (d) expect(M.toDomainMediaType(M.toContractMediaKind(d, { variety: k === "variety" }))).toBe(d); } expect(M.toContractMediaKind("tv")).toBe("series"); expect(M.toContractMediaKind("tv", { variety: true })).toBe("variety"); for (const t of C.ExternalEntityType.options) { const d = M.toDomainEntityType(t); if (d) expect(M.toContractEntityType(d)).toBe(t); } expect(M.toDomainEntityType("season")).toBeNull(); }); }); describe("身份解析状态", () => { it("domain 状态清单固定,映射到能力输出与 source_release 状态", () => { expect(sorted(M.DOMAIN_RESOLUTION_STATUSES)).toEqual(sorted(["auto", "ambiguous", "no_match"])); const out = C.IdentityResolveOutput.shape.status.options; for (const s of M.DOMAIN_RESOLUTION_STATUSES) { for (const autoAccept of [true, false]) expect(out).toContain(M.toContractResolveStatus(s, { autoAccept })); } expect(M.toContractResolveStatus("auto", { autoAccept: true })).toBe("resolved"); expect(M.toContractResolveStatus("auto", { autoAccept: false })).toBe("proposed"); expect(M.toContractResolveStatus("no_match", { autoAccept: true })).toBe("unidentifiable"); }); it("能力输出每个状态都能映射到 ResolutionStatus", () => { for (const s of C.IdentityResolveOutput.shape.status.options) { expect(C.ResolutionStatus.options).toContain(M.toReleaseResolutionStatus(s)); } }); }); describe("Slot / 内容类型 / 分段 / 整理动作", () => { it("Slot 状态两侧一一对应", () => { expect(sorted(M.DOMAIN_SLOT_STATUSES)).toEqual(sorted(C.SlotStatus.options)); for (const s of C.SlotStatus.options) expect(M.toContractSlotStatus(M.toDomainSlotStatus(s))).toBe(s); }); it("综艺内容类型两侧一一对应;分段 unknown ↔ 缺失", () => { expect(sorted(M.DOMAIN_CONTENT_TYPES)).toEqual(sorted(C.ContentType.options)); for (const t of C.ContentType.options) expect(M.toContractContentType(M.toDomainContentType(t))).toBe(t); expect(sorted([...M.DOMAIN_SEGMENTS, "unknown"])).toEqual(sorted(C.Segment.options)); for (const s of C.Segment.options) expect(M.toContractSegment(M.toDomainSegment(s))).toBe(s); }); it("整理操作、文件类型、计划动作都有映射", () => { expect(sorted(M.DOMAIN_ORGANIZE_OPERATIONS)).toEqual(sorted(["copy", "move", "rename", "in_place"])); for (const op of M.DOMAIN_ORGANIZE_OPERATIONS) expect(M.toOrganizeOperation(M.toOrganizationAction(op))).toBe(op); for (const a of C.OrganizationAction.options) { const op = M.toOrganizeOperation(a); if (op) expect(M.toOrganizationAction(op)).toBe(a); } expect(sorted(M.DOMAIN_FILE_KINDS)).toEqual(sorted(["video", "subtitle", "audio", "nfo", "other"])); for (const k of M.DOMAIN_FILE_KINDS) { expect(C.FileRole.options).toContain(M.toFileRole({ role: "sidecar", kind: k })); } expect(sorted(M.DOMAIN_PLANNED_ACTIONS)).toEqual(sorted(["transfer", "replace", "skip", "hold"])); }); });