import * as C from "@umt/contracts"; import { DEFAULT_TEMPLATES, evaluateCandidate, type IdentityCandidate, parseRelease, planOrganize, resolveIdentity, resolveTargetDirectory, type SlotSpec, } from "@umt/domain"; import { describe, expect, it } from "vitest"; import * as M from "../src/index.ts"; const U = (n: number) => `00000000-0000-4000-8000-${String(n).padStart(12, "0")}`; const AT = "2026-09-27T00:00:00.000Z"; const GiB = 1024 ** 3; describe("ParsedRelease ↔ source_release.parsed", () => { it("往返无损;版本不符或形状不对返回 null(调用方重解析)", () => { const p = parseRelease("Dune.Part.Two.2024.2160p.UHD.BluRay.REMUX.DV.HDR.HEVC.TrueHD.7.1-FGT"); const cols = M.toParsedColumns(p); expect(C.SourceReleaseCreate.shape.parserVersion.safeParse(cols.parserVersion).success).toBe(true); expect(M.fromParsedColumns(cols)).toEqual(JSON.parse(JSON.stringify(p))); expect(M.fromParsedColumns({ ...cols, parserVersion: "0" })).toBeNull(); expect( M.fromParsedColumns({ parsed: { ...cols.parsed, audio: "x" }, parserVersion: cols.parserVersion }), ).toBeNull(); }); }); describe("身份", () => { const p = parseRelease("Dune.Part.Two.2024.2160p.WEB-DL.DDP5.1.H.265-FLUX"); const tmdb: IdentityCandidate = { provider: "tmdb", entityType: "movie", externalId: "693134", titles: ["Dune: Part Two", "Dune Part Two"], year: 2024, aliases: [], }; it("排名候选 → ExternalIdentityCreate(schema 合法);本地候选不生成外部身份", () => { const res = resolveIdentity(p, [tmdb]); const create = M.rankedToIdentityCreate(res.ranked[0]!, { mediaEntityId: U(1) }, "accepted"); expect(create).not.toBeNull(); expect(C.ExternalIdentityCreate.safeParse(create).success).toBe(true); expect(create).toMatchObject({ provider: "tmdb", externalId: "693134", status: "accepted" }); const local = M.localIdentityCandidate( { id: U(2), kind: "movie", title: "沙丘2", originalTitle: "Dune: Part Two", year: 2024 }, ["Dune Part Two"], [{ provider: "tmdb", externalId: "693134", status: "accepted" }], ); expect(local).toMatchObject({ provider: "local", localEntityId: U(2), linkedExternalIds: [{ provider: "tmdb", id: "693134" }], }); const both = resolveIdentity(p, [tmdb, local!]); // 本地作品与 TMDB 是同一身份(linkedExternalIds),不构成歧义 expect(both.status).toBe("auto"); const localRanked = both.ranked.find((r) => r.candidate.provider === "local")!; expect(M.rankedToIdentityCreate(localRanked, { mediaEntityId: U(2) })).toBeNull(); expect( M.localIdentityCandidate({ id: U(3), kind: "collection", title: "x", originalTitle: null, year: null }, []), ).toBeNull(); }); }); describe("PathBinding", () => { const row = (over: Partial): C.PathBinding => ({ id: U(10), createdAt: AT, updatedAt: AT, mediaEntityId: U(1), contentUnitId: null, seasonNumber: null, bindingKind: "entity", libraryRootId: U(20), relativePath: "最后生还者 (2023)", templateVersion: "1", status: "active", boundAt: AT, boundBy: null, supersededById: null, note: null, ...over, }); it("行 → domain 后参与目标目录决策:active 优先,非 active 忽略", () => { const bindings = [ row({}), row({ id: U(11), bindingKind: "season", seasonNumber: 1, relativePath: "最后生还者 (2023)/S1" }), row({ id: U(12), relativePath: "旧目录", status: "superseded", templateVersion: "legacy" }), ].map(M.toDomainPathBinding); expect(bindings[2]).toMatchObject({ active: false, templateVersion: 0 }); const r = resolveTargetDirectory({ entity: { id: U(1), kind: "tv" }, naming: { title: "最后的生还者", year: 2023 }, season: 1, bindings, libraryRoot: { id: U(20), hostPath: "/media/tv" }, templates: DEFAULT_TEMPLATES, }); expect(r).toMatchObject({ kind: "existing", relativePath: "最后生还者 (2023)/S1", bindingIds: [U(11)] }); }); it("新绑定 → PathBindingCreate(schema 合法,season 绑定带 seasonNumber)", () => { const r = resolveTargetDirectory({ entity: { id: U(1), kind: "tv" }, naming: { title: "最后生还者", year: 2023 }, season: 2, bindings: [], libraryRoot: { id: U(20), hostPath: "/media/tv" }, templates: DEFAULT_TEMPLATES, }); expect(r.kind).toBe("new"); if (r.kind !== "new") return; const creates = r.newBindings.map((nb) => M.toPathBindingCreate(nb, { mediaEntityId: U(1) })); for (const c of creates) expect(C.PathBindingCreate.safeParse(c).success, JSON.stringify(c)).toBe(true); expect(creates.map((c) => [c.bindingKind, c.seasonNumber ?? null, c.templateVersion])).toEqual([ ["entity", null, "1"], ["season", 2, "1"], ]); }); }); describe("整理计划 → 整理记录", () => { const ROOT = "/downloads/tv/The.Last.of.Us.S01.2160p.WEB-DL-FLUX"; const plan = planOrganize({ operation: "copy", mediaType: "tv", sourceRoot: ROOT, files: [ { path: `${ROOT}/The.Last.of.Us.S01E01.2160p.mkv`, size: 5 * GiB, device: 1 }, { path: `${ROOT}/The.Last.of.Us.S01E01.2160p.chs.srt`, size: 50_000, device: 1 }, { path: `${ROOT}/Sample/the.last.of.us.s01e01.sample.mkv`, size: 80 * 1024 ** 2, device: 1 }, ], entity: { title: "最后生还者", year: 2023 }, target: { seasonDirs: { 1: "/media/tv/最后生还者 (2023)/Season 1" }, device: 2 }, existingTargets: [], }); it("记录与逐文件草稿都通过 contracts schema;排除文件记为 skipped", () => { const rec = M.organizePlanToRecord(plan, { trigger: "auto", actorType: "system", downloadTaskId: U(5), sourcePath: ROOT, planFingerprint: "h1", }); expect(C.OrganizationRecordCreate.safeParse(rec).success).toBe(true); expect(rec).toMatchObject({ action: "copy", sourcePathView: "local", downloadTaskId: U(5) }); expect(rec.plan).toMatchObject({ operation: "copy" }); const files = M.organizePlanToFileResults(plan, { targetLibraryRootId: U(20) }); for (const f of files) { expect(C.OrganizationFileResultCreate.safeParse({ ...f, organizationRecordId: U(9) }).success).toBe(true); expect(C.FileResultStatus.options).toContain(f.status); } expect(files.map((f) => [f.ordinal, f.role, f.status, f.skipReason])).toEqual([ [0, "main", "queued", null], [1, "subtitle", "queued", null], [2, "main", "skipped", "excluded:sample"], ]); }); it("能力输出合法;整理记录状态按文件结果汇总", () => { const out = M.organizePlanToCapabilityOutput(plan, { planId: U(7), payloadHash: "h1" }); expect(C.OrganizePlanOutput.safeParse(out).success).toBe(true); expect(out.files).toHaveLength(3); expect(M.organizationStatusFromFiles(["succeeded", "skipped"])).toBe("succeeded"); expect(M.organizationStatusFromFiles(["succeeded", "failed"])).toBe("partial"); expect(M.organizationStatusFromFiles(["failed", "skipped"])).toBe("failed"); expect(M.organizationStatusFromFiles(["succeeded", "running"])).toBe("running"); expect(M.organizationStatusFromFiles(["cancelled"])).toBe("cancelled"); expect(M.organizationStatusFromFiles([])).toBe("succeeded"); }); }); describe("Slot 规则与评估", () => { const spec: SlotSpec = { id: U(30), name: "archival", hard: { resolutions: ["2160p"], maxSizeBytes: 40 * GiB }, unknownPolicy: { size: "accept" }, soft: { hdr: ["DV"] }, upgrade: { enabled: true }, }; it("SlotSpec → 规则列 → SlotSpec 往返", () => { const cols = M.slotSpecToColumns(spec); expect(C.AcquisitionSlotCreate.safeParse({ ...cols, mediaEntityId: U(1) }).success).toBe(true); const back = M.slotSpecFromRow({ id: U(30), ...cols, completion: cols.completion ?? {} } as Parameters< typeof M.slotSpecFromRow >[0]); expect(back).toEqual(spec); }); it("评估结果 → 扁平表示(rejected 与 DownloadPlanOutput.rejected 同形)", () => { const ok = M.toContractSlotEvaluation(evaluateCandidate(spec, parseRelease("Dune.2021.2160p.BluRay.x265.DV-CHD"))); expect(ok).toMatchObject({ decision: "accept", acceptable: true, rejected: [] }); const bad = M.toContractSlotEvaluation(evaluateCandidate(spec, parseRelease("Dune.2021.1080p.HDTV.x264-GRP"))); expect(bad.acceptable).toBe(false); expect(bad.rejected[0]?.code).toMatch(/^resolution\./); const shape = C.DownloadPlanOutput.shape.rejected; expect(shape.safeParse(bad.rejected).success).toBe(true); }); }); describe("SiteRelease ↔ source_release", () => { const site: M.SiteReleaseLike = { siteId: "audiences", siteReleaseId: "123", title: "Dune.Part.Two.2024.2160p.WEB-DL-FLUX", subtitle: "沙丘2", category: { id: "401", name: "电影" }, sizeBytes: 20 * GiB, seeders: 10, leechers: 1, snatched: 30, promotion: { type: "free", downloadFactor: 0, uploadFactor: 1, expiresAt: "2026-09-28T00:00:00.000Z" }, hr: "unknown", uploadedAt: "2026-09-26T00:00:00.000Z", detailUrl: "https://pt.example/details.php?id=123", downloadUrl: "https://pt.example/download.php?id=123", infoHash: "A".repeat(40), imdbId: "tt15239678", tags: ["中字"], raw: { small_descr: "沙丘2" }, observedAt: AT, }; it("转换结果通过 SourceReleaseCreate;downloadUrl 不入库;siteId 用 site 行 UUID", () => { const create = M.siteReleaseToSourceRelease(site, { siteId: U(40), parsed: parseRelease(site.title) }); expect(C.SourceReleaseCreate.safeParse(create).success).toBe(true); expect(create).toMatchObject({ siteId: U(40), infohashV1: "a".repeat(40), infohashV2: null, categoryRaw: "401" }); expect(JSON.stringify(create)).not.toContain("download.php"); expect(create.parserVersion).toBe("1"); }); it("source_release 行还原为 SiteReleaseLike(下载入口除外)", () => { const create = M.siteReleaseToSourceRelease(site, { siteId: U(40) }); const row: C.SourceRelease = { ...(create as Required), id: U(41), createdAt: AT, updatedAt: AT, firstSeenAt: AT, lastSeenAt: AT, fileList: null, detailFetchedAt: null, parsed: {}, parserVersion: null, mediaEntityId: null, contentUnitId: null, resolutionStatus: "unresolved", resolutionEvidence: {}, availability: "active", }; expect(C.SourceRelease.safeParse(row).success).toBe(true); const { downloadUrl: _d, ...expected } = site; expect(M.sourceReleaseToSiteRelease(row)).toEqual({ ...expected, infoHash: "a".repeat(40) }); }); });