importing / imported 只由整理侧推进:checkingResumeData / queuedDL / checkingUP / moving 等中间态只更新原始字段,不改回 queued / downloading / completed,completed_at 不重写;这期间也不做无进展判定。 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
147 lines
7.1 KiB
TypeScript
147 lines
7.1 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"));
|
|
// 已进入整理侧: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"]));
|
|
});
|
|
});
|