import { mkdtempSync, rmSync } from "node:fs"; import { Readable } from "node:stream"; import os from "node:os"; import path from "node:path"; import type { IncomingMessage, ServerResponse } from "node:http"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { savePreset } from "../src/preset/store.js"; import { handlePresetLabApi } from "../src/server/preset-lab-handlers.js"; import type { PresetPackage } from "../src/types/preset.js"; function preset(id = "lab"): PresetPackage { return { id, name: "工作台预设", source: "native", prompts: [ { id: "sys", name: "规则", enabled: true, role: "system", content: "\u4f60\u662f{{char}}", marker: false, sourceIdentifier: "sys" }, { id: "chatHistory", name: "历史", enabled: true, role: "system", content: "", marker: true, sourceIdentifier: "chatHistory" }, ], promptOrder: [ { promptId: "sys", enabled: true, orderIndex: 0 }, { promptId: "chatHistory", enabled: true, orderIndex: 1 }, ], generation: {}, unsupported: [], importedAt: new Date().toISOString(), }; } type Captured = { status: number; body: any }; async function call( pathname: string, init: { method?: string; body?: unknown; query?: string } = {}, ): Promise { const raw = init.body === undefined ? "" : JSON.stringify(init.body); const req = Readable.from(raw ? [Buffer.from(raw, "utf8")] : []) as unknown as IncomingMessage; (req as any).method = init.method ?? "GET"; let status = 0; let payload = ""; const res = { writeHead(code: number) { status = code; }, end(chunk: string) { payload = chunk ?? ""; }, } as unknown as ServerResponse; const handled = await handlePresetLabApi(req, res, pathname, new URLSearchParams(init.query ?? "")); expect(handled).toBe(true); return { status, body: payload ? JSON.parse(payload) : null }; } let dir = ""; beforeEach(() => { dir = mkdtempSync(path.join(os.tmpdir(), "preset-lab-api-")); process.env.WRITING_AGENT_DATA_DIR = dir; savePreset(preset()); }); afterEach(() => { delete process.env.WRITING_AGENT_DATA_DIR; rmSync(dir, { recursive: true, force: true }); }); describe("预设工作台 API", () => { it("非本模块路径不接管", async () => { const req = Readable.from([]) as unknown as IncomingMessage; (req as any).method = "GET"; const res = { writeHead() {}, end() {} } as unknown as ServerResponse; expect(await handlePresetLabApi(req, res, "/api/settings/x", new URLSearchParams())).toBe(false); }); it("列出预设", async () => { const { status, body } = await call("/api/preset-lab/presets"); expect(status).toBe(200); expect(body.presets.some((p: any) => p.id === "lab")).toBe(true); }); it("读取单个预设与结构检查", async () => { const one = await call("/api/preset-lab/preset", { query: "presetId=lab" }); expect(one.body.preset.id).toBe("lab"); const checked = await call("/api/preset-lab/check", { method: "POST", body: { preset: one.body.preset } }); expect(checked.body.issues.filter((i: any) => i.level === "error")).toHaveLength(0); }); it("提交版本后能读回版本树", async () => { const committed = await call("/api/preset-lab/versions/commit", { method: "POST", body: { preset: preset(), note: "第一次" }, }); expect(committed.body.nodes).toHaveLength(1); const tree = await call("/api/preset-lab/versions", { query: "presetId=lab" }); expect(tree.body.currentId).toBe(committed.body.nodes[0].id); }); it("保存至当前版本只覆盖节点,不新增", async () => { const first = await call("/api/preset-lab/versions/commit", { method: "POST", body: { preset: preset(), note: "建根" }, }); const nodeId = first.body.currentId; const edited = { ...preset(), name: "改过的名字" }; const amended = await call("/api/preset-lab/versions/amend", { method: "POST", body: { preset: edited, note: "就地改" }, }); expect(amended.body.nodes).toHaveLength(1); expect(amended.body.currentId).toBe(nodeId); const node = amended.body.nodes[0]; expect(node.id).toBe(nodeId); expect(node.parentId).toBe(null); expect(node.preset.name).toBe("改过的名字"); expect(node.note).toBe("就地改"); expect(typeof node.updatedAt).toBe("string"); }); it("预览按用例组装消息,{{char}} 被替换", async () => { const { status, body } = await call("/api/preset-lab/preview", { method: "POST", body: { preset: preset(), testCase: { id: "c", name: "c", charName: "林", charDescription: "", worldBook: "", userInput: "开门", history: [] }, }, }); expect(status).toBe(200); const text = body.messages.map((m: any) => m.content).join("\n"); expect(text).toContain("你是林"); expect(text).toContain("开门"); }); it("测试组保存与列出", async () => { const saved = await call("/api/preset-lab/groups/save", { method: "POST", body: { presetId: "lab", name: "组一", cases: [{ name: "a", userInput: "hi" }] }, }); expect(saved.body.cases[0].id).toBeTruthy(); const groups = await call("/api/preset-lab/groups", { query: "presetId=lab" }); expect(groups.body.groups).toHaveLength(1); }); it("缺少 preset 时返回 400 而不是崩溃", async () => { const { status, body } = await call("/api/preset-lab/check", { method: "POST", body: {} }); expect(status).toBe(400); expect(body.error).toContain("preset"); }); });