refactor(core): migrate to Bun and converge acquisition policies and Agent boundaries
This commit is contained in:
1 parent
3f1c6d02f6
commit
27c0f27434
268 files changed
+7468
-7852
No files matched your search
@@ -0,0 +1,334 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { AcquisitionPolicyScope, SlotTemplate, SubscriptionParams } from "@umt/contracts";
|
||||
import type { AcquisitionSlotRow } from "@umt/db";
|
||||
import { type QualityFacts, type SlotSoftPreferences, type SlotSpec, seasonReleaseKey } from "@umt/domain";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { ensureSubscriptionSlot } from "../../src/modules/acquisition-organize/services/collection-strategy.ts";
|
||||
import { seasonReleaseTarget } from "../../src/modules/acquisition-organize/services/season-release.ts";
|
||||
import { slotComplete } from "../../src/modules/acquisition-organize/services/subscriptions.ts";
|
||||
import { applySlotPreset } from "../../src/modules/acquisition-organize/services/subscriptions-params.ts";
|
||||
import { createHarness, type Harness, pgAvailable, seedEntity, seedRelease } from "./harness.ts";
|
||||
|
||||
const hasPg = await pgAvailable();
|
||||
|
||||
describe.skipIf(!hasPg)("收录策略与成套洗版边界(真实 PG)", () => {
|
||||
let h: Harness;
|
||||
let siteId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
h = await createHarness("collection_strategy");
|
||||
siteId = (
|
||||
await h.db
|
||||
.insertInto("site")
|
||||
.values({
|
||||
key: "collection-test",
|
||||
name: "收录策略测试站",
|
||||
adapter: "nexusphp",
|
||||
base_url: "https://pt.example.test",
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow()
|
||||
).id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await h?.close();
|
||||
});
|
||||
|
||||
async function policy(scope: AcquisitionPolicyScope, slots: SlotTemplate[], publishedAt = "2026-01-01T00:00:00Z") {
|
||||
return (
|
||||
await h.db
|
||||
.insertInto("policy_revision")
|
||||
.values({
|
||||
policy_key: randomUUID(),
|
||||
revision: 1,
|
||||
kind: "acquisition",
|
||||
status: "published",
|
||||
title: "收录策略",
|
||||
scope: JSON.stringify(scope),
|
||||
rules: JSON.stringify({ defaultSlots: slots }),
|
||||
published_at: new Date(publishedAt),
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow()
|
||||
).id;
|
||||
}
|
||||
|
||||
async function subscription(
|
||||
kind: "movie" | "series",
|
||||
policyId: string | null = null,
|
||||
params: SubscriptionParams = {},
|
||||
) {
|
||||
const entityId = await seedEntity(h.db, { kind, title: `Collection ${randomUUID()}`, year: 2026 });
|
||||
const sub = await h.db
|
||||
.insertInto("subscription")
|
||||
.values({
|
||||
media_entity_id: entityId,
|
||||
media_kind: kind,
|
||||
mode: "once",
|
||||
season_scope: "current_season",
|
||||
policy_revision_id: policyId,
|
||||
params: JSON.stringify(params),
|
||||
})
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow();
|
||||
return { entityId, sub };
|
||||
}
|
||||
|
||||
const slotsOf = (subId: string) =>
|
||||
h.db.selectFrom("acquisition_slot").selectAll().where("subscription_id", "=", subId).orderBy("position").execute();
|
||||
|
||||
async function series(opts: { resolutions?: string[]; wash?: boolean; soft?: SlotSoftPreferences } = {}) {
|
||||
const { entityId, sub } = await subscription("series");
|
||||
await h.db
|
||||
.insertInto("content_unit")
|
||||
.values({ media_entity_id: entityId, kind: "season", season_number: 1, status: "confirmed", episode_total: 2 })
|
||||
.execute();
|
||||
const eps = new Map<number, string>();
|
||||
for (const ep of [1, 2]) {
|
||||
const unit = await h.db
|
||||
.insertInto("content_unit")
|
||||
.values({
|
||||
media_entity_id: entityId,
|
||||
kind: "episode",
|
||||
season_number: 1,
|
||||
episode_number: ep,
|
||||
status: "confirmed",
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
eps.set(ep, unit.id);
|
||||
}
|
||||
const slot = await h.db
|
||||
.insertInto("acquisition_slot")
|
||||
.values({
|
||||
media_entity_id: entityId,
|
||||
subscription_id: sub.id,
|
||||
name: "保留版本",
|
||||
hard_conditions: JSON.stringify({ resolutions: opts.resolutions ?? ["1080p"] }),
|
||||
soft_preferences: JSON.stringify(opts.soft ?? {}),
|
||||
upgrade_threshold: JSON.stringify(
|
||||
opts.wash === false
|
||||
? {}
|
||||
: { enabled: true, basis: "priority", consistency: "season_release", targetPri: 100 },
|
||||
),
|
||||
})
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow();
|
||||
return { entityId, sub, slot, eps };
|
||||
}
|
||||
|
||||
const quality = (group: string, resolution: QualityFacts["resolution"] = "1080p"): QualityFacts => ({
|
||||
resolution,
|
||||
source: "WEB-DL",
|
||||
videoCodec: "H.264",
|
||||
releaseGroup: { name: group, source: "suffix" },
|
||||
hdr: [],
|
||||
pri: 100,
|
||||
});
|
||||
|
||||
async function asset(f: Awaited<ReturnType<typeof series>>, episodes: number[], q: QualityFacts, ledger = true) {
|
||||
const a = await h.db
|
||||
.insertInto("media_asset")
|
||||
.values({
|
||||
media_entity_id: f.entityId,
|
||||
kind: "video",
|
||||
status: "active",
|
||||
media_info: JSON.stringify({ _umt: { quality: q } }),
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
for (const ep of episodes) {
|
||||
await h.db
|
||||
.insertInto("asset_unit_coverage")
|
||||
.values({ media_asset_id: a.id, content_unit_id: f.eps.get(ep)!, coverage: "full" })
|
||||
.execute();
|
||||
if (ledger)
|
||||
await h.db
|
||||
.insertInto("slot_unit")
|
||||
.values({
|
||||
slot_id: f.slot.id,
|
||||
content_unit_id: f.eps.get(ep)!,
|
||||
media_asset_id: a.id,
|
||||
quality: JSON.stringify(q),
|
||||
pri: q.pri,
|
||||
})
|
||||
.onConflict((oc) =>
|
||||
oc
|
||||
.columns(["slot_id", "content_unit_id"])
|
||||
.doUpdateSet({ media_asset_id: a.id, quality: JSON.stringify(q), pri: q.pri }),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
return a.id;
|
||||
}
|
||||
|
||||
async function plan(
|
||||
slot: AcquisitionSlotRow,
|
||||
q: QualityFacts,
|
||||
units: string[],
|
||||
status: "submitted" | "reserved" = "submitted",
|
||||
) {
|
||||
const relId = await seedRelease(h.db, {
|
||||
siteId,
|
||||
entityId: slot.media_entity_id,
|
||||
title: `Collection.S01.${q.resolution}.WEB-DL.x264-${q.releaseGroup!.name}`,
|
||||
size: 1_000_000,
|
||||
hash: randomUUID().replaceAll("-", "").padEnd(40, "0"),
|
||||
});
|
||||
const p = await h.db
|
||||
.insertInto("acquisition_plan")
|
||||
.values({
|
||||
slot_id: slot.id,
|
||||
source_release_id: relId,
|
||||
status,
|
||||
status_reason: status === "submitted" ? "download_accepted" : null,
|
||||
purpose: "upgrade",
|
||||
unit_keys: units,
|
||||
payload_hash: randomUUID(),
|
||||
idempotency_key: randomUUID(),
|
||||
evaluation: JSON.stringify({ pri: q.pri, quality: q, season: 1, seasonRelease: seasonReleaseKey(q) }),
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
return { planId: p.id, relId };
|
||||
}
|
||||
|
||||
it("显式选择优先;分类按 movie/tv 匹配,未命中分类时使用媒体类型默认模板", async () => {
|
||||
const allId = await policy({}, [{ name: "通用" }], "2026-06-01T00:00:00Z");
|
||||
const movieId = await policy({ mediaKinds: ["movie"] }, [{ name: "电影默认" }]);
|
||||
const tvId = await policy({ mediaKinds: ["series"] }, [{ name: "剧集默认" }]);
|
||||
const movieCategoryId = await policy({ categories: { movie: ["精选"] } }, [{ name: "电影精选" }]);
|
||||
const tvCategoryId = await policy({ categories: { tv: ["精选"] } }, [{ name: "剧集精选" }]);
|
||||
for (const [kind, category, explicitId, expectedId, name] of [
|
||||
["series", "精选", allId, allId, "通用"],
|
||||
["movie", "精选", null, movieCategoryId, "电影精选"],
|
||||
["series", "精选", null, tvCategoryId, "剧集精选"],
|
||||
["movie", "其他", null, movieId, "电影默认"],
|
||||
["series", "其他", null, tvId, "剧集默认"],
|
||||
] as const) {
|
||||
const { sub } = await subscription(kind, explicitId);
|
||||
expect(await ensureSubscriptionSlot(h.db, sub, { mediaKind: kind, category })).toBe(1);
|
||||
expect(await slotsOf(sub.id)).toMatchObject([{ name, policy_revision_id: expectedId, position: 0 }]);
|
||||
expect(await ensureSubscriptionSlot(h.db, sub, { mediaKind: kind, category })).toBe(0);
|
||||
expect(await slotsOf(sub.id)).toHaveLength(1);
|
||||
}
|
||||
});
|
||||
|
||||
it("显式选择没有模板的策略时报告错误,不静默回退到分类或通用策略", async () => {
|
||||
const policyId = await policy({}, []);
|
||||
const { sub } = await subscription("series", policyId);
|
||||
await expect(ensureSubscriptionSlot(h.db, sub, { mediaKind: "series", category: "精选" })).rejects.toThrow(
|
||||
"所选收录策略",
|
||||
);
|
||||
expect(await slotsOf(sub.id)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("双分辨率模板保留各自范围,统一预设即使与某模板相同也不能覆盖它", async () => {
|
||||
const policyId = await policy({}, [
|
||||
{ name: "HD", hardConditions: { resolutions: ["1080p"] } },
|
||||
{ name: "UHD", hardConditions: { resolutions: ["2160p"] } },
|
||||
]);
|
||||
const { sub } = await subscription("series", policyId, { resolution: "1080p" });
|
||||
expect(await ensureSubscriptionSlot(h.db, sub)).toBe(2);
|
||||
const expected = [
|
||||
{ name: "HD", hard_conditions: { resolutions: ["1080p"] } },
|
||||
{ name: "UHD", hard_conditions: { resolutions: ["2160p"] } },
|
||||
];
|
||||
expect(await slotsOf(sub.id)).toMatchObject(expected);
|
||||
await applySlotPreset(h.db, {
|
||||
subscriptionId: sub.id,
|
||||
prev: { resolution: "1080p" },
|
||||
next: { resolution: "720p" },
|
||||
});
|
||||
expect(await slotsOf(sub.id)).toMatchObject(expected);
|
||||
});
|
||||
|
||||
it("单保留4K版本时,作品已有完整720p不能填补4K覆盖", async () => {
|
||||
const f = await series({ resolutions: ["2160p"], wash: false });
|
||||
await asset(f, [1, 2], quality("LOW", "720p"), false);
|
||||
expect(await slotComplete(h.db, f.slot, "tv", [])).toMatchObject({ complete: false, atTarget: false });
|
||||
await asset(f, [1, 2], quality("UHD", "2160p"));
|
||||
expect(await slotComplete(h.db, f.slot, "tv", [])).toMatchObject({ complete: true, atTarget: true });
|
||||
});
|
||||
|
||||
it("已接收的旧720p计划同样不能填补改为4K后的范围", async () => {
|
||||
const f = await series({ resolutions: ["2160p"], wash: false });
|
||||
await plan(f.slot, quality("LOW", "720p"), ["s1e1", "s1e2"]);
|
||||
expect(await slotComplete(h.db, f.slot, "tv", [], { includeSubmitted: true })).toMatchObject({
|
||||
complete: false,
|
||||
atTarget: false,
|
||||
});
|
||||
await plan(f.slot, quality("UHD", "2160p"), ["s1e1", "s1e2"]);
|
||||
expect(await slotComplete(h.db, f.slot, "tv", [], { includeSubmitted: true })).toMatchObject({
|
||||
complete: true,
|
||||
atTarget: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("HD/UHD分别完成,另一个保留分辨率的资产不能借来填缺", async () => {
|
||||
const f = await series({ resolutions: ["1080p"], wash: false });
|
||||
const uhd = await h.db
|
||||
.insertInto("acquisition_slot")
|
||||
.values({
|
||||
media_entity_id: f.entityId,
|
||||
subscription_id: f.sub.id,
|
||||
name: "UHD",
|
||||
hard_conditions: JSON.stringify({ resolutions: ["2160p"] }),
|
||||
})
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow();
|
||||
await asset(f, [1, 2], quality("HD"));
|
||||
expect(await slotComplete(h.db, f.slot, "tv", [])).toMatchObject({ complete: true });
|
||||
expect(await slotComplete(h.db, uhd, "tv", [])).toMatchObject({ complete: false });
|
||||
await asset({ ...f, slot: uhd }, [1, 2], quality("UHD", "2160p"));
|
||||
expect(await slotComplete(h.db, uhd, "tv", [])).toMatchObject({ complete: true });
|
||||
expect(await slotComplete(h.db, f.slot, "tv", [])).toMatchObject({ complete: true });
|
||||
});
|
||||
|
||||
it("各集pri100但组不同仍未到成套目标;同组的分集资源合齐后到目标", async () => {
|
||||
const f = await series({ soft: { groups: ["B", "A"] } });
|
||||
await asset(f, [1], quality("B"));
|
||||
await asset(f, [2], quality("A"));
|
||||
expect(await slotComplete(h.db, f.slot, "tv", [])).toMatchObject({ complete: true, atTarget: false });
|
||||
await asset(f, [2], quality("B"));
|
||||
expect(await slotComplete(h.db, f.slot, "tv", [])).toMatchObject({ complete: true, atTarget: true });
|
||||
});
|
||||
|
||||
it("完整A不能盖过更优但只有部分集的B,成套洗版继续以B为目标", async () => {
|
||||
const f = await series({ soft: { groups: ["B", "A"] } });
|
||||
await asset(f, [1, 2], quality("A"));
|
||||
expect(await slotComplete(h.db, f.slot, "tv", [])).toMatchObject({ complete: true, atTarget: true });
|
||||
await asset(f, [1], quality("B"));
|
||||
expect(await slotComplete(h.db, f.slot, "tv", [])).toMatchObject({ complete: true, atTarget: false });
|
||||
await asset(f, [2], quality("B"));
|
||||
expect(await slotComplete(h.db, f.slot, "tv", [])).toMatchObject({ complete: true, atTarget: true });
|
||||
});
|
||||
|
||||
it("在途季包冻结版本事实,发布属性修改不漂移锚点,失败后才释放", async () => {
|
||||
const f = await series({ soft: { groups: ["C", "A", "B"] } });
|
||||
const frozen = quality("A");
|
||||
const p = await plan(f.slot, frozen, ["s1*"], "reserved");
|
||||
const spec: SlotSpec = {
|
||||
name: "成套目标",
|
||||
hard: { resolutions: ["1080p"] },
|
||||
soft: { groups: ["C", "A", "B"] },
|
||||
upgrade: { enabled: true, basis: "priority", consistency: "season_release" },
|
||||
};
|
||||
expect(seasonReleaseKey((await seasonReleaseTarget(h.db, f.slot, 1, spec, quality("B")))!)).toBe(
|
||||
seasonReleaseKey(frozen),
|
||||
);
|
||||
await h.db
|
||||
.updateTable("source_release")
|
||||
.set({ title: "Collection.S01.1080p.WEB-DL.x264-C", parsed: JSON.stringify({}), parser_version: null })
|
||||
.where("id", "=", p.relId)
|
||||
.execute();
|
||||
expect(seasonReleaseKey((await seasonReleaseTarget(h.db, f.slot, 1, spec, quality("B")))!)).toBe(
|
||||
seasonReleaseKey(frozen),
|
||||
);
|
||||
await h.db.updateTable("acquisition_plan").set({ status: "failed" }).where("id", "=", p.planId).execute();
|
||||
expect(seasonReleaseKey((await seasonReleaseTarget(h.db, f.slot, 1, spec, quality("B")))!)).toBe(
|
||||
seasonReleaseKey(quality("B")),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user