Files
UHD_Media_Tool/apps/server/test/inventory/media-servers.test.ts
T
rootandClaude Opus 5.5 c9e8120fc6 test: server 测试共用建库与假引擎,sse-pg 归入串行组
- 新增 apps/server/test/support/{pg,engine}.ts:测试库地址、可用性探测、schema 咨询锁、
  建 schema + 加锁迁移 + 删除、可选去重的假引擎;6 份 harness 改用它(acquisition-organize 与
  file-manager 原先迁移不加锁,并行下有冲突隐患,一并修正)
- sse-pg 起子进程,并行下子进程启动超时,归入 serial
- 修正 b61335e 挪动 / 改名测试后遗留的 import 排序与格式(当时 lint 检查经管道被吞掉退出码)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-02 17:40:43 +08:00

100 lines
4.2 KiB
TypeScript

import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { MEDIA_SERVER_LIMITS } from "../../src/modules/acquisition-organize/routes/library.ts";
import { createHarness, type Harness, pgAvailable, seedEntity } from "../acquisition-organize/harness.ts";
// C1-D:GET /inventory/media-servers 按实例并行(allSettled)+ 实例内并发上限 + 整实例超时;部分失败 partial=true。
const hasPg = await pgAvailable();
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
describe.skipIf(!hasPg)("GET /inventory/media-servers 并行 + 超时 + partial", () => {
let h: Harness;
let entityId: string;
const ids: Record<string, string> = {};
const saved = { ...MEDIA_SERVER_LIMITS };
const inflight: Record<string, { now: number; max: number }> = {};
beforeAll(async () => {
h = await createHarness("msinv");
MEDIA_SERVER_LIMITS.timeoutMs = 400;
MEDIA_SERVER_LIMITS.perInstanceConcurrency = 2;
entityId = await seedEntity(h.db, { kind: "movie", title: "并行库存", year: 2020 });
await h.db
.insertInto("external_identity")
.values(
["tmdb", "imdb", "tvdb"].map((provider, i) => ({
media_entity_id: entityId,
provider,
entity_type: "movie",
external_id: `x${i}`,
status: "accepted",
})) as never,
)
.execute();
for (const name of ["a-ok", "b-slow", "c-boom"]) {
const r = await h.db
.insertInto("media_server_instance")
.values({ name, base_url: `http://${name}.invalid` })
.returning("id")
.executeTakeFirstOrThrow();
ids[name] = r.id;
inflight[name] = { now: 0, max: 0 };
}
h.d.ports.mediaServer = (async (inst: { name: string }) => {
if (inst.name === "c-boom") throw new Error("connect refused");
return {
async findByProviderId(provider: string) {
const f = inflight[inst.name]!;
f.now++;
f.max = Math.max(f.max, f.now);
try {
await sleep(inst.name === "b-slow" ? 5_000 : 60);
return provider === "Tmdb"
? {
status: "present" as const,
items: [{ itemId: "i1", type: "Movie", name: "并行库存", year: 2020, path: "/m", versions: [] }],
}
: { status: "not_found" as const };
} finally {
f.now--;
}
},
};
}) as never;
}, 60_000);
afterAll(async () => {
Object.assign(MEDIA_SERVER_LIMITS, saved);
await h?.close();
});
it("实例并行、慢实例超时、异常实例 unknown,整体 partial", async () => {
const t0 = Date.now();
const r = await h.api("GET", `/inventory/media-servers?mediaEntityId=${entityId}`);
const took = Date.now() - t0;
expect(r.status).toBe(200);
type Inst = { name: string; status: string; reason: string | null; items: unknown[] };
const by = Object.fromEntries((r.body.data.instances as Inst[]).map((x) => [x.name, x]));
expect(by["a-ok"]).toMatchObject({ status: "present", reason: null });
expect(by["a-ok"]!.items).toHaveLength(1);
expect(by["b-slow"]).toMatchObject({ status: "unknown" });
expect(by["b-slow"]!.reason).toMatch(/^timeout/);
expect(by["c-boom"]).toMatchObject({ status: "unknown", reason: "connect refused" });
expect(r.body.data.partial).toBe(true);
// 串行时至少 0.4s(b 超时)+ 3×60ms;并行应接近单个超时
expect(took).toBeLessThan(2_000);
// 实例内并发上限 2(3 个外部 ID)
expect(inflight["a-ok"]!.max).toBe(2);
});
it("全部成功时 partial=false;无外部 ID 的 unknown 不算失败", async () => {
await h.db.updateTable("media_server_instance").set({ enabled: false }).where("name", "!=", "a-ok").execute();
const r = await h.api("GET", `/inventory/media-servers?mediaEntityId=${entityId}`);
expect(r.body.data.partial).toBe(false);
const bareId = await seedEntity(h.db, { kind: "movie", title: "无外部ID", year: 2021 });
const r2 = await h.api("GET", `/inventory/media-servers?mediaEntityId=${bareId}`);
expect(r2.body.data.instances[0]).toMatchObject({ status: "unknown", reason: "no_external_id" });
expect(r2.body.data.partial).toBe(false);
});
});