import { mkdirSync, mkdtempSync, rmSync, writeFileSync } 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, vi } from "vitest"; import { getPreset } from "../src/preset/store.js"; import { createNativePreset } from "../src/preset/new-preset.js"; import type { PresetPackage } from "../src/types/preset.js"; vi.mock("../src/llm/client.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, OpenAiCompatibleProvider: class { async complete(messages: unknown[], options?: { caller?: string }) { return { content: `mock:${options?.caller ?? "unknown"}`, usage: { promptTokens: 1, completionTokens: 2, totalTokens: 3 }, model: "mock-model", messages, }; } }, }; }); import { handlePresetLabApi } from "../src/server/preset-lab-handlers.js"; 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 }; } function writeProfiles(dir: string): void { mkdirSync(dir, { recursive: true }); const now = new Date().toISOString(); writeFileSync( path.join(dir, "profiles.json"), JSON.stringify({ version: 1, profiles: [ { id: "p1", name: "本地模型", baseUrl: "https://example.invalid/v1", apiKey: "test-key", model: "mock-model", createdAt: now, updatedAt: now, }, ], }), "utf8", ); } let dir = ""; beforeEach(() => { dir = mkdtempSync(path.join(os.tmpdir(), "preset-lab-workbench-")); process.env.WRITING_AGENT_DATA_DIR = dir; writeProfiles(dir); }); afterEach(() => { delete process.env.WRITING_AGENT_DATA_DIR; rmSync(dir, { recursive: true, force: true }); vi.restoreAllMocks(); }); describe("工作台新建与写回", () => { it("读取带 BOM 的预设文件", () => { const presetsDir = path.join(dir, "presets"); mkdirSync(presetsDir, { recursive: true }); const preset = createNativePreset({ id: "bom", name: "BOM" }); writeFileSync(path.join(presetsDir, "bom.json"), `\uFEFF${JSON.stringify(preset)}`, "utf8"); expect(getPreset("bom")?.name).toBe("BOM"); }); it("新建原生预设并生成根版本", async () => { const { status, body } = await call("/api/preset-lab/presets/create", { method: "POST", body: { name: "新预设", id: "fresh" }, }); expect(status).toBe(201); expect(body.preset.id).toBe("fresh"); expect(body.preset.source).toBe("native"); expect(body.tree.nodes).toHaveLength(1); expect(body.preset.prompts.some((p: any) => p.id === "chatHistory" && p.marker)).toBe(true); expect(body.preset.prompts.some((p: any) => p.id === "currentTurn" && p.marker)).toBe(true); expect(getPreset("fresh")?.name).toBe("新预设"); }); it("写回前拦截结构 error,force 才落盘", async () => { const created = await call("/api/preset-lab/presets/create", { method: "POST", body: { name: "写回", id: "apply-me" }, }); const preset = created.body.preset as PresetPackage; preset.prompts[0]!.content = "{{setvar::a::1"; const blocked = await call("/api/preset-lab/preset/apply", { method: "POST", body: { preset }, }); expect(blocked.status).toBe(400); expect(blocked.body.error).toContain("结构检查"); const forced = await call("/api/preset-lab/preset/apply", { method: "POST", body: { preset, force: true }, }); expect(forced.status).toBe(200); expect(getPreset("apply-me")?.prompts[0]!.content).toContain("setvar"); }); it("三花括号是合法的字面量花括号,不算未闭合宏", async () => { const created = await call("/api/preset-lab/presets/create", { method: "POST", body: { name: "三花括号", id: "triple" }, }); const preset = created.body.preset as PresetPackage; preset.prompts[0]!.content = "{{{setvar::SEMOD1::}}正文"; const checked = await call("/api/preset-lab/check", { method: "POST", body: { preset }, }); expect(checked.body.issues.filter((i: any) => i.level === "error")).toHaveLength(0); }); it("重复引用同一模块是 error", async () => { const created = await call("/api/preset-lab/presets/create", { method: "POST", body: { name: "重复", id: "dup-order" }, }); const preset = created.body.preset as PresetPackage; preset.promptOrder.push({ ...preset.promptOrder[0]!, orderIndex: 99 }); const checked = await call("/api/preset-lab/check", { method: "POST", body: { preset }, }); expect(checked.body.issues.some((i: any) => i.level === "error" && i.message.includes("重复"))).toBe(true); }); }); describe("草稿运行", () => { it("run 使用请求里的草稿而不是旧版本文件", async () => { const created = await call("/api/preset-lab/presets/create", { method: "POST", body: { name: "草稿", id: "draft-run" }, }); const preset = created.body.preset as PresetPackage; preset.prompts[0]!.content = "草稿规则 {{char}}"; const group = await call("/api/preset-lab/groups/save", { method: "POST", body: { presetId: preset.id, name: "组", cases: [{ name: "开门", charName: "林", userInput: "我推门" }], }, }); const run = await call("/api/preset-lab/run", { method: "POST", body: { presetId: preset.id, groupId: group.body.id, profileIds: ["p1"], preset, }, }); expect(run.status).toBe(200); expect(run.body.results).toHaveLength(1); expect(run.body.results[0].status).toBe("done"); expect(run.body.results[0].caseName).toBe("开门"); expect(run.body.results[0].content).toContain("mock:preset-test:p1"); expect(run.body.requests[0].messages.map((m: any) => m.content).join("\n")).toContain("草稿规则 林"); expect(run.body.requests[0].messages.map((m: any) => m.content).join("\n")).toContain("我推门"); const runs = await call("/api/preset-lab/runs", { query: `presetId=${preset.id}` }); expect(runs.body.runs).toHaveLength(1); expect(runs.body.runs[0].id).toBe(run.body.id); const one = await call("/api/preset-lab/run", { query: `presetId=${preset.id}&runId=${run.body.id}`, }); expect(one.body.results[0].caseName).toBe("开门"); const marked = await call("/api/preset-lab/verdict", { method: "POST", body: { presetId: preset.id, runId: run.body.id, profileId: "p1", caseId: run.body.results[0].caseId, verdict: "success", }, }); expect(marked.body.results[0].verdict).toBe("success"); }); });