import { describe, expect, it } from "vitest"; import type { ForeshadowView } from "@/lib/api/types"; import { chapterForeshadowTotal, classifyChapterForeshadow, foreshadowStatusLabel, } from "./chapterForeshadow"; const view = (over: Partial): ForeshadowView => ({ code: "F-001", title: "线索", status: "OPEN", ...over, }); describe("classifyChapterForeshadow", () => { it("puts items whose window covers the current chapter into 可回收", () => { const groups = classifyChapterForeshadow( [ view({ code: "F-A", status: "OPEN", expected_close_from: 40, expected_close_to: 60 }), view({ code: "F-B", status: "PARTIAL", expected_close_to: 50 }), ], 50, ); expect(groups.recyclable.map((v) => v.code)).toEqual(["F-A", "F-B"]); expect(groups.pending).toEqual([]); expect(groups.overdue).toEqual([]); }); it("puts OPEN/PARTIAL with a later or missing window into 待回收", () => { const groups = classifyChapterForeshadow( [ view({ code: "F-LATER", status: "OPEN", expected_close_from: 80, expected_close_to: 90 }), view({ code: "F-NOWIN", status: "PARTIAL" }), ], 50, ); expect(groups.pending.map((v) => v.code)).toEqual(["F-LATER", "F-NOWIN"]); expect(groups.recyclable).toEqual([]); }); it("puts OVERDUE into 已逾期 even when a window would otherwise cover the chapter", () => { const groups = classifyChapterForeshadow( [view({ code: "F-OD", status: "OVERDUE", expected_close_from: 40, expected_close_to: 60 })], 50, ); expect(groups.overdue.map((v) => v.code)).toEqual(["F-OD"]); expect(groups.recyclable).toEqual([]); }); it("drops CLOSED and unknown statuses from every group", () => { const groups = classifyChapterForeshadow( [ view({ code: "F-CLOSED", status: "CLOSED", expected_close_to: 60 }), view({ code: "F-WEIRD", status: "WEIRD" }), ], 50, ); expect(chapterForeshadowTotal(groups)).toBe(0); }); it("handles the window boundary: inclusive at from and to, excluded just outside", () => { const win = (code: string): ForeshadowView => view({ code, status: "OPEN", expected_close_from: 40, expected_close_to: 60 }); expect(classifyChapterForeshadow([win("lo")], 40).recyclable).toHaveLength(1); expect(classifyChapterForeshadow([win("hi")], 60).recyclable).toHaveLength(1); expect(classifyChapterForeshadow([win("before")], 39).pending).toHaveLength(1); expect(classifyChapterForeshadow([win("after")], 61).pending).toHaveLength(1); }); it("returns empty groups for empty / undefined input without mutating a shared object", () => { const a = classifyChapterForeshadow(undefined, 1); const b = classifyChapterForeshadow([], 1); expect(chapterForeshadowTotal(a)).toBe(0); expect(chapterForeshadowTotal(b)).toBe(0); // 不可变:两次调用返回相互独立的空组 a.pending.push(view({})); expect(b.pending).toEqual([]); }); }); describe("chapterForeshadowTotal", () => { it("sums the three lanes", () => { const groups = classifyChapterForeshadow( [ view({ code: "r", status: "OPEN", expected_close_to: 50 }), view({ code: "p", status: "OPEN" }), view({ code: "o", status: "OVERDUE" }), ], 50, ); expect(chapterForeshadowTotal(groups)).toBe(3); }); }); describe("foreshadowStatusLabel", () => { it("maps known statuses to author-facing labels and falls back for unknown", () => { expect(foreshadowStatusLabel("OPEN")).toBe("待推进"); expect(foreshadowStatusLabel("PARTIAL")).toBe("推进中"); expect(foreshadowStatusLabel("OVERDUE")).toBe("已逾期"); expect(foreshadowStatusLabel("WEIRD")).toBe("WEIRD"); }); });