Files
UHD_Media_Tool/apps/server/test/acquisition-organize/manual-acquisition.test.ts
T
rootandClaude Opus 5.5 b61335eaf6 test: 测试按行为命名、server 测试统一到 test/、PG 测试分组并行
- server 的 src/modules/*/__tests__ 挪到 test/<模块>/;e2e 去掉 eN- 前缀,
  w5a2 / v2 / search2 / *-dh1 / *-md1 / *-md2、web 的 agui3 / search2、domain 的 id2 / id3 改为按行为命名
- vitest 分三组:unit、pg(真实 PG + 假引擎,文件并行,最多 6 个)、serial(真实引擎 / worker / e2e,串行);
  pg 组用例超时放宽到 60s(迁移类用例并行时排队等 schema 锁)。server 测试 615s → 约 350s
- CLAUDE.md 补测试约定;待办补 harness 去重与 continuous-series 偶发超时(疑似 Slot 评估竞争)
- ADR-004 补充:维持 Deno,记录实际只用到 --allow-run 白名单

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-02 16:54:04 +08:00

154 lines
6.6 KiB
TypeScript

import { ManualAcquisitionRequest } from "@umt/contracts";
import { createMockQb, createTempTree, makeTorrent } from "@umt/testkit";
import { describe, expect, it } from "vitest";
import { evaluateAcquisition } from "../../src/modules/acquisition-organize/services/acquisition.ts";
import { submitDownload } from "../../src/modules/acquisition-organize/services/downloads.ts";
import { planManualAcquisition } from "../../src/modules/acquisition-organize/services/manual-acquisition.ts";
import { createHarness, fakeDisk, pgAvailable, seedBase, seedEntity, seedRelease, seedSlot } from "./harness.ts";
const hasPg = await pgAvailable();
describe.skipIf(!hasPg)("独立手动下载", () => {
it("无订阅下载,未知资源确认,同hash沿用原路径", async () => {
const qb = await createMockQb({ addVisibleAfterPolls: 0 });
const cache = await createTempTree({});
const lib = await createTempTree({}, { sentinel: true });
const torrents = new Map<string, Uint8Array>();
const disk = fakeDisk(10_000_000);
disk.setPct(20);
const h = await createHarness("manual_acquisition", {
diskUsage: disk.fn,
torrentSource: async ({ release }) => ({ kind: "file", bytes: torrents.get(release.id)! }),
});
try {
const base = await seedBase(h.db, { libraryPath: lib.root, cachePath: cache.root, qbUrl: qb.url });
const entityId = await seedEntity(h.db, { kind: "movie", title: "Manual Movie", year: 2026 });
const torrent = makeTorrent({ name: "Manual.Movie.2026.1080p.mkv", length: 300_000 });
const relId = await seedRelease(h.db, {
siteId: base.siteId,
entityId,
title: torrent.name,
size: 300_000,
hash: torrent.hash,
});
torrents.set(relId, torrent.bytes);
const request = (id: string, opts = {}) => ManualAcquisitionRequest.parse({ sourceReleaseId: id, ...opts });
const first = await planManualAcquisition(h.ctx, request(relId));
expect(first).toMatchObject({ submitted: true, slotId: null });
expect(await h.db.selectFrom("subscription").selectAll().execute()).toHaveLength(0);
expect(await h.db.selectFrom("acquisition_slot").selectAll().execute()).toHaveLength(0);
const sub = await submitDownload(h.d, first.plan!.id);
expect(sub.status).toBe("submitted");
const rel2 = await seedRelease(h.db, {
siteId: base.siteId,
entityId,
title: "Other.Release.2160p.mkv",
size: 300_000,
hash: torrent.hash,
});
torrents.set(rel2, torrent.bytes);
const second = await planManualAcquisition(h.ctx, request(rel2, { savePath: `${cache.root}/other` }));
const reused = await submitDownload(h.d, second.plan!.id);
expect(reused).toMatchObject({ reason: "already_exists", downloadTaskId: sub.downloadTaskId });
const dt = await h.db
.selectFrom("download_task")
.selectAll()
.where("id", "=", sub.downloadTaskId!)
.executeTakeFirstOrThrow();
expect(dt.save_path).toBe(cache.root);
expect(dt.media_entity_id).toBe(entityId);
const unknown = makeTorrent({ name: "Unknown.1080p.mkv", length: 300_000, pieceSeed: 42 });
const uid = await seedRelease(h.db, {
siteId: base.siteId,
entityId,
title: unknown.name,
size: 300_000,
hash: unknown.hash,
});
await h.db.updateTable("source_release").set({ media_entity_id: null }).where("id", "=", uid).execute();
torrents.set(uid, unknown.bytes);
expect(await planManualAcquisition(h.ctx, request(uid))).toMatchObject({
submitted: false,
requiresConfirmation: true,
});
const confirmed = await planManualAcquisition(h.ctx, request(uid, { allowUnrecognized: true }));
const submitted = await submitDownload(h.d, confirmed.plan!.id);
expect(submitted.status).toBe("submitted");
expect(
(
await h.db
.selectFrom("download_task")
.select("media_entity_id")
.where("id", "=", submitted.downloadTaskId!)
.executeTakeFirstOrThrow()
).media_entity_id,
).toBeNull();
const autoEntity = await seedEntity(h.db, { kind: "movie", title: "Auto Movie", year: 2026 });
const slotId = await seedSlot(h.db, { entityId: autoEntity });
const autoTorrent = makeTorrent({
name: "Auto.Movie.2026.1080p.BluRay.x264-GRP.mkv",
length: 300_000,
pieceSeed: 100,
});
const autoRelease = await seedRelease(h.db, {
siteId: base.siteId,
entityId: autoEntity,
title: autoTorrent.name,
size: 300_000,
hash: autoTorrent.hash,
});
torrents.set(autoRelease, autoTorrent.bytes);
const auto = await evaluateAcquisition(h.d, { sourceReleaseId: autoRelease });
expect(auto.plans).toHaveLength(1);
const autoTask = await submitDownload(h.d, auto.plans[0]!);
const oldAsset = await h.db
.insertInto("media_asset")
.values({ media_entity_id: autoEntity, kind: "video", size_bytes: 300_000, status: "active" })
.returning("id")
.executeTakeFirstOrThrow();
await h.db
.updateTable("download_task")
.set({ status: "imported" })
.where("id", "=", autoTask.downloadTaskId!)
.execute();
const unchanged = await planManualAcquisition(h.ctx, request(autoRelease));
expect(unchanged.plan!.id).toBe(auto.plans[0]);
const wash = await planManualAcquisition(
h.ctx,
request(autoRelease, { importMode: "replace", replacementAssetIds: [oldAsset.id] }),
);
const washed = await submitDownload(h.d, wash.plan!.id);
expect(washed.downloadTaskId).toBe(autoTask.downloadTaskId);
const linked = await h.db
.selectFrom("download_task")
.selectAll()
.where("id", "=", autoTask.downloadTaskId!)
.executeTakeFirstOrThrow();
expect(linked).toMatchObject({
acquisition_plan_id: auto.plans[0],
import_plan_id: wash.plan!.id,
status: "completed",
});
expect(
(
await h.db
.selectFrom("acquisition_plan")
.select("slot_id")
.where("id", "=", auto.plans[0]!)
.executeTakeFirstOrThrow()
).slot_id,
).toBe(slotId);
expect(h.engine.enqueued).toContainEqual(
expect.objectContaining({
task: "verifyAndImport",
input: expect.objectContaining({ downloadTaskId: autoTask.downloadTaskId }),
}),
);
} finally {
await h.close();
await qb.close();
await cache.cleanup();
await lib.cleanup();
}
});
});