import { describe, expect, it } from "vitest"; import { emptyDecisions, setNote, setVerdict } from "@/lib/review/decisions"; import type { JobView } from "@/lib/jobs/job"; import { buildResumeRequest, chainPhase, chainResultView, } from "./chain"; function job(result: Record | null): JobView { return { id: "j1", kind: "chain", status: "running", progress: 0, result, error: null, }; } describe("chainResultView", () => { it("narrows a completed chain result", () => { const view = chainResultView( job({ chain_key: "draft_volume", written: [1, 2, 3], completed: true, awaiting_chapter: null, }), ); expect(view).toEqual({ chainKey: "draft_volume", written: [1, 2, 3], awaitingChapter: null, completed: true, }); }); it("narrows an awaiting result (interrupt on a chapter)", () => { const view = chainResultView( job({ chain_key: "draft_volume", written: [1], completed: false, awaiting_chapter: 2, }), ); expect(view.awaitingChapter).toBe(2); expect(view.completed).toBe(false); expect(view.written).toEqual([1]); }); it("returns safe defaults for null/garbage result", () => { expect(chainResultView(job(null))).toEqual({ chainKey: null, written: [], awaitingChapter: null, completed: false, }); expect( chainResultView(job({ written: "nope", awaiting_chapter: "x" })).written, ).toEqual([]); }); it("keeps only finite integer chapter numbers in written", () => { const view = chainResultView( job({ written: [1, "2", 3.5, null, 4] }), ); expect(view.written).toEqual([1, 4]); }); }); describe("chainPhase", () => { it("maps polling with no awaiting chapter to running", () => { expect(chainPhase("polling", null)).toBe("running"); }); it("maps any awaiting chapter to awaiting (interrupt signal is authoritative)", () => { // 后端 interrupt 时置 status=awaiting_input(被 narrowJob 抹平成 queued→仍 polling), // 但 result.awaiting_chapter 已置数;运行中该值恒 null,故它是暂停的权威信号。 expect(chainPhase("polling", 2)).toBe("awaiting"); expect(chainPhase("done", 2)).toBe("awaiting"); }); it("maps done + no awaiting to completed", () => { expect(chainPhase("done", null)).toBe("completed"); }); it("maps error to failed (even if an awaiting chapter lingers)", () => { expect(chainPhase("error", null)).toBe("failed"); expect(chainPhase("error", 2)).toBe("failed"); }); }); describe("buildResumeRequest", () => { it("includes only resolved decisions and trims notes (reuses accept decision logic)", () => { let drafts = emptyDecisions(3); drafts = setVerdict(drafts, 0, "accept"); drafts = setVerdict(drafts, 2, "manual"); drafts = setNote(drafts, 2, " 改成后天淬炼 "); const req = buildResumeRequest(drafts); expect(req.decisions).toEqual([ { conflict_index: 0, verdict: "accept" }, { conflict_index: 2, verdict: "manual", note: "改成后天淬炼" }, ]); }); it("empty drafts → empty decisions list", () => { expect(buildResumeRequest([])).toEqual({ decisions: [] }); }); });