import { createMockTmdb, type MockTmdb } from "@umt/testkit"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { registerScheduledJob, scheduleRegistry } from "../../src/modules/schedules/registry.ts"; import { syncDefinitions } from "../../src/modules/schedules/store.ts"; import { SourcesRuntime } from "../../src/modules/sources-identity/runtime.ts"; import { BACKFILL_LANGUAGES_JOB, backfillLanguages, } from "../../src/modules/sources-identity/service/backfill-languages.ts"; import { createEntityFromTmdbCandidate } from "../../src/modules/sources-identity/service/entity-from-candidate.ts"; import { createHarness, type Harness, pgAvailable } from "./harness.ts"; // FIX-5 存量元数据回刷(metadata.backfill_languages) const up = await pgAvailable(); describe.skipIf(!up)("元数据语言回刷", () => { let h: Harness; let tmdb: MockTmdb; const ids: string[] = []; const actor = { type: "user" as const, id: "tester" }; beforeAll(async () => { tmdb = await createMockTmdb(); h = await createHarness("backfill", { tmdbUrl: tmdb.url }); for (const [externalId, kind] of [ ["95396", "tv"], ["872585", "movie"], ] as const) { const r = await createEntityFromTmdbCandidate(h.db, () => h.rt.tmdb(), { externalId, kind }, actor); ids.push(r.mediaEntityId); } // 没有 TMDB 身份的作品不参与 await h.db .insertInto("media_entity") .values({ kind: "series", title: "本地短剧", status: "provisional" } as never) .execute(); }); afterAll(async () => { await h?.close(); await tmdb?.close(); }); it("登记为维护组任务,首次登记即到期;跑完按新语言链刷新并自动停用", async () => { registerScheduledJob(h.ctx, { key: BACKFILL_LANGUAGES_JOB, defaults: { name: "元数据语言回刷", group: "maintain", schedule: { type: "interval", everyMs: 86_400_000 }, firstRun: "now", }, run: async () => undefined, }); const now = new Date(); await syncDefinitions(h.db, scheduleRegistry(h.ctx).list(), now); const before = await h.db .selectFrom("scheduled_job") .selectAll() .where("id", "=", BACKFILL_LANGUAGES_JOB) .executeTakeFirstOrThrow(); expect(before).toMatchObject({ enabled: true, board_group: "maintain" }); expect(before.next_run_at!.getTime()).toBeLessThanOrEqual(now.getTime()); const r = await backfillLanguages(h.rt, { jobId: BACKFILL_LANGUAGES_JOB, disableWhenDone: true, minIntervalMs: 0 }); expect(r).toMatchObject({ status: "ok", total: 2, refreshed: 2, failed: 0 }); expect(r.summary).toContain("2 部作品:刷新 2"); const tv = await h.db.selectFrom("media_entity").selectAll().where("id", "=", ids[0]!).executeTakeFirstOrThrow(); expect(tv.title).toBe("人生切割术"); const eps = await h.db .selectFrom("content_unit") .select(["episode_number", "title"]) .where("media_entity_id", "=", ids[0]!) .where("kind", "=", "episode") .where("season_number", "=", 1) .orderBy("episode_number") .execute(); expect(eps.length).toBeGreaterThan(0); const after = await h.db .selectFrom("scheduled_job") .selectAll() .where("id", "=", BACKFILL_LANGUAGES_JOB) .executeTakeFirstOrThrow(); expect(after).toMatchObject({ enabled: false, customized: true }); // 重启后同步定义不会重新打开 await syncDefinitions(h.db, scheduleRegistry(h.ctx).list(), new Date()); const synced = await h.db .selectFrom("scheduled_job") .select(["enabled"]) .where("id", "=", BACKFILL_LANGUAGES_JOB) .executeTakeFirstOrThrow(); expect(synced.enabled).toBe(false); }); it("按设置建的客户端:网络请求限速,走 PG 请求缓存,第二次不再请求 TMDB", async () => { await h.db .insertInto("settings") .values({ key: "metadata", value: JSON.stringify({ tmdbBaseUrl: `${tmdb.url}/3` }) } as never) .onConflict((oc) => oc.column("key").doUpdateSet({ value: JSON.stringify({ tmdbBaseUrl: `${tmdb.url}/3` }) })) .execute(); const prev = Deno.env.get("TMDB_API_KEY"); Deno.env.set("TMDB_API_KEY", "test-tmdb-key"); try { const rt = new SourcesRuntime(h.ctx, {}); const t0 = performance.now(); const first = await backfillLanguages(rt, { minIntervalMs: 100 }); const elapsed = performance.now() - t0; expect(first).toMatchObject({ status: "ok", total: 2, refreshed: 2 }); expect(first.requests).toBeGreaterThan(2); expect(elapsed).toBeGreaterThanOrEqual((first.requests! - 1) * 90); const second = await backfillLanguages(rt, { minIntervalMs: 100 }); expect(second).toMatchObject({ refreshed: 2, requests: 0 }); } finally { if (prev === undefined) Deno.env.delete("TMDB_API_KEY"); else Deno.env.set("TMDB_API_KEY", prev); } }); it("TMDB 未配置:跳过,不停用", async () => { const rt = new SourcesRuntime(h.ctx, { tmdb: () => null }); const r = await backfillLanguages(rt, { jobId: "x", disableWhenDone: true }); expect(r.status).toBe("skipped"); }); });