import type { Decision, EpisodeMappingPreview } from "@umt/contracts"; import { createElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { describe, expect, it } from "vitest"; import { agentSimulationText, EpisodeDecisionBody, episodeDecisionView, simulationText } from "./decision.tsx"; const ENTITY = "7d3f7f5e-8a8c-4b8e-9f0a-1b2c3d4e5f60"; const decision = (over: Partial = {}): 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 条已对上的资源", ); }); });