Files

126 lines
5.1 KiB
TypeScript

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");
});
});