import { describe, expect, it } from "vitest"; import type { ToolDescriptorView, ToolInputFieldView } from "@/lib/api/types"; import { buildGenerateRequest, initialFieldValues, isLegacyTool, mapPreview, missingRequiredFields, previewRows, resolveLegacyRoute, templateFillTarget, } from "./toolbox"; const field = (over: Partial = {}): ToolInputFieldView => ({ name: "brief", label: "需求", type: "textarea", required: true, ...over, }); describe("initialFieldValues", () => { it("seeds defaults and empty strings", () => { const values = initialFieldValues([ field({ name: "brief", default: undefined }), field({ name: "count", type: "number", default: "3", required: false }), ]); expect(values).toEqual({ brief: "", count: "3" }); }); it("handles undefined fields", () => { expect(initialFieldValues(undefined)).toEqual({}); }); }); describe("missingRequiredFields", () => { it("reports labels of blank required fields only", () => { const fields = [ field({ name: "brief", label: "需求", required: true }), field({ name: "count", label: "数量", required: false }), ]; expect(missingRequiredFields(fields, { brief: " ", count: "" })).toEqual([ "需求", ]); }); it("passes when all required present", () => { expect( missingRequiredFields([field()], { brief: "一个脑洞" }), ).toEqual([]); }); }); describe("buildGenerateRequest", () => { it("trims brief and maps known number fields", () => { expect( buildGenerateRequest({ brief: " 修真 ", count: "5", chapter_no: "3" }), ).toEqual({ brief: "修真", count: 5, chapter_no: 3 }); }); it("omits blank/invalid numbers and empty kind", () => { expect( buildGenerateRequest({ brief: "x", count: "", chapter_no: "abc", kind: " " }), ).toEqual({ brief: "x" }); }); it("includes kind when present", () => { expect(buildGenerateRequest({ brief: "x", kind: "门派" })).toEqual({ brief: "x", kind: "门派", }); }); it("defaults brief to empty string", () => { expect(buildGenerateRequest({})).toEqual({ brief: "" }); }); it("forwards trimmed text (扩写/降AI/拆书 原文输入)", () => { expect( buildGenerateRequest({ brief: "扩写", text: " 原始正文片段 " }), ).toEqual({ brief: "扩写", text: "原始正文片段" }); }); it("omits blank/whitespace-only text", () => { expect(buildGenerateRequest({ brief: "x", text: " " })).toEqual({ brief: "x", }); }); }); describe("previewRows", () => { it("picks the first array field as records", () => { const rows = previewRows({ ideas: [{ premise: "a" }, { premise: "b" }] }); expect(rows).toHaveLength(2); expect(rows[0]).toEqual({ premise: "a" }); }); it("returns empty for missing/non-array", () => { expect(previewRows(undefined)).toEqual([]); expect(previewRows({ note: "x" })).toEqual([]); }); }); describe("mapPreview", () => { it("maps IdeaListResult to heading + fields", () => { const model = mapPreview("IdeaListResult", { ideas: [{ premise: "废柴逆袭", hook: "系统觉醒", genre_fit: "玄幻" }], }); expect(model.kind).toBe("IdeaListResult"); expect(model.items[0]?.heading).toBe("废柴逆袭"); expect(model.items[0]?.fields).toEqual([ { label: "爽点", value: "系统觉醒" }, { label: "题材契合", value: "玄幻" }, ]); }); it("maps BlurbResult text into body and angle into heading", () => { const model = mapPreview("BlurbResult", { variants: [{ angle: "悬念向", text: "一段简介" }], }); expect(model.items[0]?.heading).toBe("悬念向"); expect(model.items[0]?.body).toBe("一段简介"); }); it("maps GlossaryResult rules into a joined field", () => { const model = mapPreview("GlossaryResult", { terms: [{ name: "灵气", type: "概念", rules: ["不可逆", "有上限"] }], }); const ruleField = model.items[0]?.fields.find((f) => f.label === "硬规则"); expect(ruleField?.value).toBe("不可逆;有上限"); }); it("maps OpeningResult as pure body", () => { const model = mapPreview("OpeningResult", { variants: [{ text: "开篇第一段" }], }); expect(model.items[0]?.heading).toBeNull(); expect(model.items[0]?.body).toBe("开篇第一段"); }); it("falls back gracefully for unknown output_kind", () => { const model = mapPreview("MysteryResult", { things: [{ a: "first", b: "second" }], }); expect(model.items[0]?.heading).toBe("first"); expect(model.items[0]?.fields).toEqual([{ label: "b", value: "second" }]); }); it("yields no items for empty preview", () => { expect(mapPreview("IdeaListResult", undefined).items).toEqual([]); }); it("maps a text-only result (续写/扩写/降AI) into a single body item", () => { for (const kind of ["ContinuationResult", "PolishResult", "DeAiResult"]) { const model = mapPreview(kind, { text: "一段续写/改写后的正文" }); expect(model.items).toHaveLength(1); expect(model.items[0]?.heading).toBeNull(); expect(model.items[0]?.body).toBe("一段续写/改写后的正文"); } }); it("maps BookTeardownResult structured fields into one card", () => { const model = mapPreview("BookTeardownResult", { themes: ["逆袭", "复仇"], archetypes: ["废柴主角"], structure: "三幕递进", hooks: ["开局打脸", "扮猪吃虎"], }); expect(model.items).toHaveLength(1); const item = model.items[0]; expect(item?.heading).toBe("三幕递进"); const byLabel = (label: string) => item?.fields.find((f) => f.label === label)?.value; expect(byLabel("主题")).toBe("逆袭;复仇"); expect(byLabel("人物原型")).toBe("废柴主角"); expect(byLabel("钩子套路")).toBe("开局打脸;扮猪吃虎"); }); it("yields no items for an empty text result", () => { expect(mapPreview("ContinuationResult", { text: "" }).items).toEqual([]); expect(mapPreview("ContinuationResult", undefined).items).toEqual([]); }); }); describe("resolveLegacyRoute", () => { it("substitutes the {id} placeholder", () => { expect(resolveLegacyRoute("/projects/{id}/codex?gen=world", "p9")).toBe( "/projects/p9/codex?gen=world", ); }); }); describe("isLegacyTool", () => { const tool = (over: Partial): ToolDescriptorView => ({ key: "k", title: "t", subtitle: "s", is_legacy: false, ingestable: false, ...over, }); it("is true only with legacy flag and a route", () => { expect( isLegacyTool(tool({ is_legacy: true, legacy_route: "/x/{id}" })), ).toBe(true); expect(isLegacyTool(tool({ is_legacy: true }))).toBe(false); expect( isLegacyTool(tool({ is_legacy: false, legacy_route: "/x/{id}" })), ).toBe(false); }); }); describe("templateFillTarget", () => { it("prefers brief when present", () => { expect( templateFillTarget([field({ name: "brief" }), field({ name: "text" })]), ).toBe("brief"); }); it("falls back to text when brief absent", () => { expect(templateFillTarget([field({ name: "text" })])).toBe("text"); }); it("returns null when no fillable field exists", () => { expect(templateFillTarget([field({ name: "count" })])).toBeNull(); expect(templateFillTarget(undefined)).toBeNull(); }); });