Files
UHD_Media_Tool/packages/mapping/test/enums.test.ts
T
rootandClaude Opus 5.5 42ecc93fa5 feat(mapping): centralize domain <-> contracts mappings (B5)
New @umt/mapping (depends on domain + contracts only): download status + pause reason,
evidence level / media kind, identity resolution status, slot status / spec columns /
evaluation, ParsedRelease <-> source_release.parsed, SiteRelease <-> source_release,
PathBinding, OrganizePlan -> organization_record / file results / organize.plan output.
Tables are Record<union, ...> so adding a value on either side fails typecheck or tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 19:20:30 +08:00

142 lines
6.7 KiB
TypeScript

import * as C from "@umt/contracts";
import { describe, expect, it } from "vitest";
import * as M from "../src/index.ts";
// 枚举全覆盖:
// - domain 侧是 TS 联合类型,映射表写成 Record<DomainX, …>,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"));
});
});
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"]));
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"]));
});
});