- 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]>
154 lines
6.6 KiB
TypeScript
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();
|
|
}
|
|
});
|
|
});
|