import { describe, expect, it } from "vitest"; import { initialPollState, isTerminal, narrowJob, reducePoll, styleLearnResult, type JobView, } from "./job"; describe("narrowJob", () => { it("narrows a well-formed loose dict into JobView", () => { const job = narrowJob({ id: "j1", kind: "style_learn", status: "running", progress: 42, result: null, error: null, }); expect(job).toEqual({ id: "j1", kind: "style_learn", status: "running", progress: 42, result: null, error: null, }); }); it("falls back safely for missing/wrong-typed fields", () => { expect(narrowJob({})).toEqual({ id: "", kind: "", status: "queued", progress: 0, result: null, error: null, }); // unknown status → queued; out-of-range progress clamped. expect(narrowJob({ status: "weird", progress: 250 }).status).toBe("queued"); expect(narrowJob({ progress: 250 }).progress).toBe(100); expect(narrowJob({ progress: -5 }).progress).toBe(0); }); it("keeps result only when an object (not array)", () => { expect(narrowJob({ result: { version: 2 } }).result).toEqual({ version: 2 }); expect(narrowJob({ result: [1, 2] }).result).toBeNull(); }); it("narrows non-object input", () => { expect(narrowJob(null).status).toBe("queued"); expect(narrowJob("nope").id).toBe(""); }); }); describe("isTerminal", () => { it("done and failed are terminal", () => { expect(isTerminal("done")).toBe(true); expect(isTerminal("failed")).toBe(true); expect(isTerminal("queued")).toBe(false); expect(isTerminal("running")).toBe(false); }); }); describe("reducePoll", () => { const job = (over: Partial): JobView => ({ id: "j1", kind: "style_learn", status: "running", progress: 0, result: null, error: null, ...over, }); it("stays polling for queued/running and tracks progress", () => { const out = reducePoll(initialPollState, { type: "tick", job: job({ status: "running", progress: 30 }), }); expect(out.status).toBe("polling"); expect(out.progress).toBe(30); }); it("transitions to done with progress 100", () => { const out = reducePoll(initialPollState, { type: "tick", job: job({ status: "done", result: { version: 2, dims_count: 16 } }), }); expect(out.status).toBe("done"); expect(out.progress).toBe(100); expect(out.job?.result).toEqual({ version: 2, dims_count: 16 }); }); it("transitions to error on failed, using job.error", () => { const out = reducePoll(initialPollState, { type: "tick", job: job({ status: "failed", error: "网关爆炸", progress: 20 }), }); expect(out.status).toBe("error"); expect(out.error).toBe("网关爆炸"); expect(out.progress).toBe(20); }); it("defaults error text when failed without error string", () => { const out = reducePoll(initialPollState, { type: "tick", job: job({ status: "failed", error: null }), }); expect(out.error).toBe("任务失败"); }); it("handles a fail action (poll request failure)", () => { const out = reducePoll(initialPollState, { type: "fail", error: "网络断了", }); expect(out.status).toBe("error"); expect(out.error).toBe("网络断了"); }); }); describe("styleLearnResult", () => { it("extracts version + dims_count from done result", () => { const out = styleLearnResult({ id: "j", kind: "style_learn", status: "done", progress: 100, result: { version: 3, dims_count: 16 }, error: null, }); expect(out).toEqual({ version: 3, dimsCount: 16 }); }); it("returns nulls for missing result fields", () => { expect( styleLearnResult({ id: "j", kind: "style_learn", status: "done", progress: 100, result: null, error: null, }), ).toEqual({ version: null, dimsCount: null }); }); });