Files
UHD_Media_Tool/packages/mapping/test/enums.test.ts
T
rootandClaude Opus 5.5 ceb66f8c77 feat: v0.3.8–v0.3.14 工作区汇总(依赖升级、整理来源、手动入库、原地入库等)
生产 v0.3.8~v0.3.13 均由 8013196 加工作区修改构建部署,此前未提交;本提交补齐源码,
并含未部署的 v0.3.14 原地入库。中间各版本源码状态未保存,无法逐版拆分,详见
docs/plan/17-full-upgrade.md 与 docs/plan/18-organize-release.md。

- v0.3.8 全面升级:Deno workspace 依赖、Router 8 / Vite 8、camofox 1.18.0 / Node 24、
  PostgreSQL 18.6、备份客户端 18.6、演示 qB 5.2.4
- v0.3.9 媒体整理:分类规则、整理来源与目录快照(0059)、文件管理整理入口
- v0.3.10 手动下载与入库:独立手动下载、详情页资源下载、TMDB / Local 元数据兜底
  (metadata.resolve_organize)、正常多版本 / 一次性洗版(0060)
- v0.3.11 搜索详情与桌面黑边修复
- v0.3.12 列表分页、搜索历史、首页概况
- v0.3.13 统一整理弹窗与队列
- v0.3.14 原地入库(0061,未部署)

同批审计修复:
- 刮削入队:确认原记录存在后才删重放产生的新记录;写回 run 关联失败时保持 queued,
  由处理器补关联(对齐 MP 先登记、只在入队失败时记失败)
- 识别升级链只在结果更确定时采用,不再用 no_match 顶替原有歧义候选
- bluray_dir 资产以原盘目录本身为刮削根(MP 同做法)
- metadata.resolve_organize 登记到入库排障 Skill 与能力地图
- db/src/types.ts 按迁移重新生成;contracts 整理动作枚举顺序与数据库对齐
- 测试:原地入库用例改为直接查记录表;domain 测试模板补 version
- 前端:通知铃铛弹层与用户菜单统一样式;站点卡片共用卡头、底栏贴底、分页间距

依赖升级证据只提交摘要文件,registry-snapshot.json 移至 .local/(SHA-256 记于文档)。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-02 15:47:17 +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", "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"]));
});
});