test: 测试按行为命名、server 测试统一到 test/、PG 测试分组并行
- 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]>
This commit is contained in:
1 parent
b343dddbf6
commit
b61335eaf6
75 files changed
+159
-115
No files matched your search
@@ -0,0 +1,148 @@
|
||||
import { createTmdbClient } from "@umt/adapters";
|
||||
import { PgCache } from "@umt/db";
|
||||
import { createMockTmdb, HEART_SIGNAL_FIXTURE, type MockTmdb, TMDB_FIXTURES } from "@umt/testkit";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { createEntityFromTmdbCandidate } from "../../src/modules/sources-identity/service/entity-from-candidate.ts";
|
||||
import { refreshMetadata } from "../../src/modules/sources-identity/service/metadata.ts";
|
||||
import {
|
||||
applyOverviewRecompute,
|
||||
planOverviewRecompute,
|
||||
} from "../../src/modules/sources-identity/service/overview-recompute.ts";
|
||||
import { CACHE_REGIONS } from "../../src/shared-cache.ts";
|
||||
import { createHarness, type Harness, pgAvailable } from "./harness.ts";
|
||||
|
||||
const up = await pgAvailable();
|
||||
type Final = { value: unknown; source: string; language: string | null };
|
||||
const CHAIN = ["zh-CN", "zh-SG", "zh-TW", "zh-HK", "en-US"];
|
||||
|
||||
describe.skipIf(!up)("MD-2 简介只回退到中文变体 + 存量重算", () => {
|
||||
let h: Harness;
|
||||
let tmdb: MockTmdb;
|
||||
let id: string;
|
||||
const actor = { type: "user" as const, id: "tester" };
|
||||
const now = () => new Date("2026-01-01T00:00:00Z");
|
||||
|
||||
beforeAll(async () => {
|
||||
tmdb = await createMockTmdb({ fixtures: [...TMDB_FIXTURES, HEART_SIGNAL_FIXTURE] });
|
||||
// 走 PG 请求缓存(kv_cache tmdb.request),与生产一致:存量重算只读这里
|
||||
const cache = {
|
||||
get: (k: string) => new PgCache(h.db).get(CACHE_REGIONS.tmdbRequest, k),
|
||||
set: (k: string, v: unknown, ttlMs: number) =>
|
||||
new PgCache(h.db).set(CACHE_REGIONS.tmdbRequest, k, v, { ttl: ttlMs / 1000 }),
|
||||
};
|
||||
h = await createHarness("md2", {
|
||||
now,
|
||||
tmdb: () =>
|
||||
createTmdbClient({ apiKey: "test-tmdb-key", baseUrl: `${tmdb.url}/3`, retries: 0, sharedCache: cache }),
|
||||
});
|
||||
});
|
||||
afterAll(async () => {
|
||||
await h?.close();
|
||||
await tmdb?.close();
|
||||
});
|
||||
|
||||
const setMeta = async (v: Record<string, unknown> | null) => {
|
||||
await h.db.deleteFrom("settings").where("key", "=", "metadata").execute();
|
||||
if (v)
|
||||
await h.db
|
||||
.insertInto("settings")
|
||||
.values({ key: "metadata", value: JSON.stringify(v) } as never)
|
||||
.execute();
|
||||
};
|
||||
const entityMeta = async () =>
|
||||
(await h.db.selectFrom("media_entity").select("metadata").where("id", "=", id).executeTakeFirstOrThrow())
|
||||
.metadata as Record<string, Final>;
|
||||
const eps = async () =>
|
||||
(
|
||||
await h.db
|
||||
.selectFrom("content_unit")
|
||||
.select(["episode_number", "title", "metadata"])
|
||||
.where("media_entity_id", "=", id)
|
||||
.where("kind", "=", "episode")
|
||||
.orderBy("episode_number")
|
||||
.execute()
|
||||
).map(
|
||||
(u) =>
|
||||
({ ...(u.metadata as Record<string, Final | undefined>), ep: u.episode_number, title: u.title }) as {
|
||||
ep: number | null;
|
||||
title: string | null;
|
||||
overview?: Final;
|
||||
name?: Final;
|
||||
},
|
||||
);
|
||||
|
||||
it("允许回退到非中文时(旧行为):英文简介进库", async () => {
|
||||
await setMeta({ languages: CHAIN, overviewNonChineseFallback: true });
|
||||
const r = await createEntityFromTmdbCandidate(h.db, () => h.rt.tmdb(), { externalId: "103147", kind: "tv" }, actor);
|
||||
id = r.mediaEntityId;
|
||||
expect((await refreshMetadata(h.rt, id, { full: true })).status).toBe("refreshed");
|
||||
expect((await entityMeta()).overview).toMatchObject({ value: "A Chinese dating reality show.", language: "en-US" });
|
||||
const [e1, e2] = await eps();
|
||||
expect(e1!.overview).toMatchObject({
|
||||
value: "If Your Heart Skips a Beat, Play Your Cards Openly!",
|
||||
language: "en-US",
|
||||
});
|
||||
expect(e2!.overview).toMatchObject({ value: "Who Is the One?", language: "en-US" });
|
||||
});
|
||||
|
||||
it("存量重算:dry-run 只列清单,不请求 TMDB、不改库;--apply 按缓存清空,标题不动", async () => {
|
||||
await setMeta({ languages: CHAIN });
|
||||
const calls = tmdb.calls.length;
|
||||
const plan = await planOverviewRecompute(h.db, { languages: CHAIN, now: now() });
|
||||
expect(tmdb.calls.length).toBe(calls);
|
||||
expect(plan.overviewLanguages).toEqual(["zh-CN", "zh-SG", "zh-TW", "zh-HK"]);
|
||||
expect(plan.scanned).toEqual({ entities: 1, units: 2 });
|
||||
expect(plan.cache.misses).toBe(0);
|
||||
const brief = plan.changes.map((c) => [c.target, c.episode, c.field, c.from.language, c.to.value, c.basis]);
|
||||
expect(brief).toEqual([
|
||||
["entity", null, "overview", "en-US", null, "cache"],
|
||||
["episode", 1, "overview", "en-US", null, "cache"],
|
||||
["episode", 2, "overview", "en-US", null, "cache"],
|
||||
]);
|
||||
// dry-run 不改库
|
||||
expect((await entityMeta()).overview?.language).toBe("en-US");
|
||||
|
||||
const r = await h.db.transaction().execute((trx) => applyOverviewRecompute(trx, plan, { now: now() }));
|
||||
expect(r).toEqual({ entities: 1, units: 2 });
|
||||
expect(tmdb.calls.length).toBe(calls);
|
||||
expect((await entityMeta()).overview).toMatchObject({ value: null, language: null, source: "tmdb" });
|
||||
const [e1, e2, e3] = await eps();
|
||||
expect(e1!.overview?.value).toBeNull();
|
||||
expect(e1!.title).toBe("心动不如行动,大胆亮出底牌!");
|
||||
expect(e2!.overview?.value).toBeNull();
|
||||
// 标题仍可回退到英文
|
||||
expect(e2!.name).toMatchObject({ value: "Who Is the One?", language: "en-US" });
|
||||
expect(e3!.overview).toMatchObject({ value: "約會之夜。", language: "zh-TW" });
|
||||
// 旧语言行删掉,只留一行 tmdb 来源
|
||||
const rows = await h.db
|
||||
.selectFrom("metadata_field")
|
||||
.select(["language", "value"])
|
||||
.where("media_entity_id", "=", id)
|
||||
.where("field", "=", "overview")
|
||||
.execute();
|
||||
expect(rows).toEqual([{ language: null, value: null }]);
|
||||
// 再跑一次没有要改的
|
||||
expect((await planOverviewRecompute(h.db, { languages: CHAIN, now: now() })).changes).toEqual([]);
|
||||
});
|
||||
|
||||
it("缓存被淘汰时不请求 TMDB,按「当时中文为空」清空;刷新按新规则也不再写英文", async () => {
|
||||
await setMeta({ languages: CHAIN, overviewNonChineseFallback: true });
|
||||
await refreshMetadata(h.rt, id, { full: true });
|
||||
expect((await eps())[0]!.overview?.language).toBe("en-US");
|
||||
await setMeta({ languages: CHAIN });
|
||||
await h.db.deleteFrom("kv_cache" as never).execute();
|
||||
const calls = tmdb.calls.length;
|
||||
const plan = await planOverviewRecompute(h.db, { languages: CHAIN, now: now() });
|
||||
expect(tmdb.calls.length).toBe(calls);
|
||||
expect(plan.changes.every((c) => c.basis === "no_cache" && c.to.value === null)).toBe(true);
|
||||
expect(plan.changes).toHaveLength(3);
|
||||
|
||||
// 不跑重算,直接刷新:按新规则清掉英文简介
|
||||
await refreshMetadata(h.rt, id, { full: true });
|
||||
expect((await entityMeta()).overview?.value).toBeNull();
|
||||
const [e1, e2] = await eps();
|
||||
expect(e1!.overview?.value).toBeNull();
|
||||
expect(e2!.overview?.value).toBeNull();
|
||||
expect(e2!.name?.value).toBe("Who Is the One?");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user