import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { createHarness, type Harness, pgAvailable, seedEntity, } from "../../src/modules/acquisition-organize/__tests__/harness.ts"; import { MEDIA_SERVER_LIMITS } from "../../src/modules/acquisition-organize/routes/library.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 = {}; const saved = { ...MEDIA_SERVER_LIMITS }; const inflight: Record = {}; 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); }); });