231 lines
9.1 KiB
TypeScript
231 lines
9.1 KiB
TypeScript
import type { Decision, EpisodeMappingPreview } from "@umt/contracts";
|
|
import { createElement } from "react";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
import { MemoryRouter } from "react-router";
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
agentSimulationText,
|
|
EpisodeDecisionBody,
|
|
EpisodeDecisionStatus,
|
|
episodeDecisionView,
|
|
simulationText,
|
|
} from "./decision.tsx";
|
|
|
|
const ENTITY = "7d3f7f5e-8a8c-4b8e-9f0a-1b2c3d4e5f60";
|
|
const decision = (over: Partial<Decision> = {}): Decision =>
|
|
({
|
|
id: "11111111-1111-4111-8111-111111111111",
|
|
kind: "episode_mapping",
|
|
status: "open",
|
|
severity: "warning",
|
|
title: "集数待确认:药屋少女的呢喃 第 1 季 · Lilith-Raws",
|
|
summary: "来源 S01E41:集数超出作品季表,也换算不出对应的季集(共 2 条资源)",
|
|
subjectType: "media_entity",
|
|
subjectId: ENTITY,
|
|
dedupeKey: `episode_mapping:${ENTITY}:1:lilith-raws`,
|
|
evidence: {
|
|
mediaEntityId: ENTITY,
|
|
season: 1,
|
|
releaseGroup: "Lilith-Raws",
|
|
count: 2,
|
|
releases: [
|
|
{
|
|
sourceReleaseId: "a",
|
|
title: "[Lilith-Raws] X - 40",
|
|
source: { season: 1, episodes: { start: 40, end: 40 } },
|
|
},
|
|
{
|
|
sourceReleaseId: "b",
|
|
title: "[Lilith-Raws] X - 41",
|
|
source: { season: 1, episodes: { start: 41, end: 41 } },
|
|
},
|
|
],
|
|
suggestion: {
|
|
expression: "EP-30",
|
|
targetSeason: 2,
|
|
samples: [
|
|
{
|
|
source: { start: 40, end: 40 },
|
|
target: { season: 2, episode: 10 },
|
|
publishedAt: "x",
|
|
airDate: "2025-03-14",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
options: [
|
|
{
|
|
key: "confirm_suggestion",
|
|
label: "确认建议规则",
|
|
description: "把「S01 @Lilith-Raws >> S02 EP-30」写进作品识别词",
|
|
input: { line: "S01 @Lilith-Raws >> S02 EP-30" },
|
|
},
|
|
{ key: "ignore_rule", label: "忽略并写忽略规则", input: { line: "S01 @Lilith-Raws >> 忽略" } },
|
|
],
|
|
...over,
|
|
}) as Decision;
|
|
|
|
describe("待处理「集数待确认」", () => {
|
|
it("从证据取范围、来源季集、建议规则与可用操作", () => {
|
|
const v = episodeDecisionView(decision());
|
|
expect(v).toMatchObject({ entityId: ENTITY, season: 1, releaseGroup: "Lilith-Raws", count: 2 });
|
|
expect(v.releases.map((r) => r.source)).toEqual(["S01E40", "S01E41"]);
|
|
expect(v.suggestion?.expression).toBe("EP-30");
|
|
expect(v.suggestionLine).toBe("S01 @Lilith-Raws >> S02 EP-30");
|
|
expect(v.confirm?.key).toBe("confirm_suggestion");
|
|
expect(v.ignoreRule?.key).toBe("ignore_rule");
|
|
const none = episodeDecisionView(decision({ options: [], evidence: { mediaEntityId: ENTITY, suggestion: null } }));
|
|
expect(none).toMatchObject({ confirm: null, ignoreRule: null, suggestion: null, season: null });
|
|
});
|
|
|
|
it("渲染来源季集、建议规则样本与模拟结果", () => {
|
|
const html = renderToStaticMarkup(
|
|
createElement(EpisodeDecisionBody, { view: episodeDecisionView(decision()), simulation: "套用后 2 条都对得上" }),
|
|
);
|
|
expect(html).toContain("第 1 季 · Lilith-Raws");
|
|
expect(html).toContain("S01E40");
|
|
expect(html).toContain("S01 @Lilith-Raws >> S02 EP-30");
|
|
expect(html).toContain("S02E10");
|
|
expect(html).toContain("套用后 2 条都对得上");
|
|
});
|
|
|
|
it("没有建议时提示在作品识别词里手动写", () => {
|
|
const html = renderToStaticMarkup(
|
|
createElement(EpisodeDecisionBody, {
|
|
view: episodeDecisionView(decision({ evidence: { mediaEntityId: ENTITY, suggestion: null } })),
|
|
simulation: null,
|
|
}),
|
|
);
|
|
expect(html).toContain("展开作品识别词后手动写");
|
|
});
|
|
|
|
it("模拟结果一句话", () => {
|
|
const p = (vals: string[]) =>
|
|
({ items: vals.map((validation) => ({ validation })) }) as unknown as EpisodeMappingPreview;
|
|
expect(simulationText(p(["pass", "pass"]))).toBe("套用后 2 条都对得上");
|
|
expect(simulationText(p(["pass", "fail"]))).toBe("套用后 1 条对得上,1 条仍对不上");
|
|
expect(simulationText(p([]))).toBeNull();
|
|
});
|
|
|
|
const agentSuggestion = {
|
|
line: "S01 @Lilith-Raws >> S02 EP-30",
|
|
reasons: ["40、41 的发布时间对上第 2 季第 10、11 集的播出日期"],
|
|
simulation: { samples: 2, resolved: 2, passed: 2, missing: 0, regressions: 0, changed: 2, scanned: 5, ok: true },
|
|
samples: [
|
|
{
|
|
title: "[Lilith-Raws] X - 40",
|
|
source: { season: 1, episodes: { start: 40, end: 40 } },
|
|
after: { method: "rule", canonical: { season: 2, episodes: { start: 10, end: 10 } }, special: null },
|
|
},
|
|
],
|
|
runId: null,
|
|
at: "2026-10-03T00:00:00Z",
|
|
};
|
|
const withAgent = () =>
|
|
decision({
|
|
evidence: { ...decision().evidence, agentSuggestion },
|
|
options: [
|
|
{
|
|
key: "confirm_agent_suggestion",
|
|
label: "确认智能体建议",
|
|
description: "按 EP-30 换算到第 2 季,建规则前按当前资源再模拟一次,通过才写入并重算",
|
|
},
|
|
...decision().options,
|
|
],
|
|
});
|
|
|
|
it("智能体建议:取规则、理由、模拟结果与「确认智能体建议」", () => {
|
|
const v = episodeDecisionView(withAgent());
|
|
expect(v.agentSuggestion?.line).toBe("S01 @Lilith-Raws >> S02 EP-30");
|
|
expect(v.agentSuggestion?.reasons).toHaveLength(1);
|
|
expect(v.confirmAgent?.key).toBe("confirm_agent_suggestion");
|
|
expect(v.confirm?.key).toBe("confirm_suggestion");
|
|
// 没有建议的事项不出这一块
|
|
expect(episodeDecisionView(decision())).toMatchObject({ agentSuggestion: null, confirmAgent: null });
|
|
});
|
|
|
|
it("渲染智能体建议:那一行作品识别词、理由、样本套用后的季集与模拟结果", () => {
|
|
const html = renderToStaticMarkup(
|
|
createElement(EpisodeDecisionBody, { view: episodeDecisionView(withAgent()), simulation: null }),
|
|
);
|
|
expect(html).toContain("智能体建议");
|
|
expect(html).toContain("S01 @Lilith-Raws >> S02 EP-30");
|
|
expect(html).toContain("对上第 2 季第 10、11 集的播出日期");
|
|
expect(html).toContain("S01E40 → S02E10");
|
|
expect(html).toContain("模拟:这 2 条都对得上,不影响已对上的资源");
|
|
});
|
|
|
|
it("智能体建议的模拟结果一句话", () => {
|
|
const s = agentSuggestion.simulation;
|
|
expect(agentSimulationText({ ...s, resolved: 1, regressions: 2 })).toBe(
|
|
"模拟:2 条里 1 条对得上,会影响 2 条已对上的资源",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("季集映射调查状态", () => {
|
|
const renderStatus = (d: Decision) =>
|
|
renderToStaticMarkup(createElement(MemoryRouter, null, createElement(EpisodeDecisionStatus, { d })));
|
|
it("调查中显示状态与已有会话入口,没有会话也能显示进度", () => {
|
|
const html = renderStatus(decision({ status: "investigating", agentRunId: "agent-run" }));
|
|
expect(html).toContain("智能体正在核对季集");
|
|
expect(html).toContain("/console/agent/agent-run");
|
|
expect(renderStatus(decision({ status: "investigating", agentRunId: null }))).toContain("智能体正在核对季集");
|
|
});
|
|
it("交还人工显示恢复建议,并保留排查入口", () => {
|
|
const html = renderStatus(
|
|
decision({
|
|
status: "awaiting_user",
|
|
agentRunId: "agent-run",
|
|
evidence: { autoRecover: { suggestion: "请补充资源站分季详情" } },
|
|
}),
|
|
);
|
|
expect(html).toContain("需要你确认");
|
|
expect(html).toContain("请补充资源站分季详情");
|
|
expect(html).toContain("查看排查过程");
|
|
expect(renderStatus(decision())).toBe("");
|
|
});
|
|
});
|
|
|
|
it("文件事项显示原号、文件路径与已对上的校验样本,不虚构资源数量", () => {
|
|
const sample = (ep: number, pending: boolean) => ({
|
|
recordId: "record",
|
|
path: `/anime/E${ep}.mkv`,
|
|
parsed: { season: 1, episodes: { start: ep, end: ep } },
|
|
publishedAt: null,
|
|
resolution: {
|
|
method: pending ? "unresolved" : "raw",
|
|
source: { season: 1, episodes: { start: ep, end: ep } },
|
|
canonical: pending ? null : { season: 1, episodes: { start: ep, end: ep } },
|
|
},
|
|
});
|
|
const view = episodeDecisionView(
|
|
decision({
|
|
evidence: {
|
|
mediaEntityId: ENTITY,
|
|
count: 1,
|
|
sourceReleaseIds: [],
|
|
fileSamples: [sample(13, true), sample(1, false)],
|
|
},
|
|
}),
|
|
);
|
|
expect(view.files.map((f) => f.pending)).toEqual([true, false]);
|
|
const out = renderToStaticMarkup(createElement(EpisodeDecisionBody, { view, simulation: null }));
|
|
expect(out).toContain("待确认文件");
|
|
expect(out).toContain("S01E13");
|
|
expect(out).toContain("/anime/E13.mkv");
|
|
expect(out).toContain("已对上的 1 个文件");
|
|
expect(out).toContain("S01E01 → S01E01");
|
|
expect(out).not.toContain("来源季集");
|
|
expect(out).not.toContain("另有");
|
|
const mixed = episodeDecisionView(
|
|
decision({
|
|
evidence: { ...decision().evidence, count: 3, sourceReleaseIds: ["a", "b"], fileSamples: [sample(13, true)] },
|
|
}),
|
|
);
|
|
expect(mixed.releaseCount).toBe(2);
|
|
expect(renderToStaticMarkup(createElement(EpisodeDecisionBody, { view: mixed, simulation: null }))).not.toContain(
|
|
"另有",
|
|
);
|
|
});
|