import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import type { PresetPackage } from "../src/types/preset.js"; import { buildCaseMessages } from "../src/preset/test-run.js"; import { getTestRun, saveTestRun, updateVerdict } from "../src/preset/test-runs.js"; import { deleteTestGroup, getTestGroup, listTestGroups, saveTestGroup } from "../src/preset/test-cases.js"; import { deleteTestCase, listTestCases, saveTestCase } from "../src/preset/test-cases.js"; import { loadPresetLabState, savePresetLabState } from "../src/preset/lab-state.js"; function preset(): PresetPackage { return { id: "pp", name: "pp", source: "native", prompts: [ { id: "sys", name: "规则", enabled: true, role: "system", content: "你是{{char}}的叙事者", marker: false, sourceIdentifier: "sys" }, { id: "worldInfoBefore", name: "世界书", enabled: true, role: "system", content: "", marker: true, sourceIdentifier: "worldInfoBefore" }, { id: "charDescription", name: "角色描述", enabled: true, role: "system", content: "", marker: true, sourceIdentifier: "charDescription" }, { id: "chatHistory", name: "历史", enabled: true, role: "system", content: "", marker: true, sourceIdentifier: "chatHistory" }, { id: "currentTurn", name: "本轮", enabled: true, role: "user", content: "", marker: true, sourceIdentifier: "currentTurn" }, ], promptOrder: [ { promptId: "sys", enabled: true, orderIndex: 0 }, { promptId: "worldInfoBefore", enabled: true, orderIndex: 1 }, { promptId: "charDescription", enabled: true, orderIndex: 2 }, { promptId: "chatHistory", enabled: true, orderIndex: 3 }, { promptId: "currentTurn", enabled: true, orderIndex: 4 }, ], generation: {}, unsupported: [], importedAt: new Date().toISOString(), }; } let dir = ""; beforeEach(() => { dir = mkdtempSync(path.join(os.tmpdir(), "preset-tests-")); process.env.WRITING_AGENT_DATA_DIR = dir; }); afterEach(() => { delete process.env.WRITING_AGENT_DATA_DIR; rmSync(dir, { recursive: true, force: true }); }); describe("测试用例组", () => { it("保存、读取、列出、删除", () => { const g = saveTestGroup({ name: "基础", cases: [{ name: "c1", charName: "林", charDescription: "冷静", worldBook: "古镇", userInput: "进门" }], }); expect(g.cases[0]!.id).toBeTruthy(); expect(getTestGroup(g.id)?.cases[0]!.charName).toBe("林"); expect(listTestGroups()).toHaveLength(1); deleteTestGroup(g.id); expect(listTestGroups()).toHaveLength(0); }); }); describe("用例(工作台的展示单位)", () => { it("新用例落成只装它的一份组文件,平铺列表能读回", () => { const saved = saveTestCase({ case: { name: "开门", userInput: "我推开门" }, }); expect(saved.groupId).toBeTruthy(); expect(saved.case.id).toBeTruthy(); expect(listTestCases().map((item) => item.case.name)).toEqual(["开门"]); expect(listTestGroups()).toHaveLength(1); }); it("用例全局共享:换预设也能读到同一套", () => { saveTestCase({ case: { name: "全局用例", userInput: "甲" } }); // 没有任何 presetId 参与,读取时也不需要 expect(listTestCases().map((item) => item.case.name)).toEqual(["全局用例"]); expect(listTestGroups()[0]!.presetId).toBe("global"); }); it("旧版按预设分目录的用例会被并进全局目录", () => { const legacyDir = path.join(dir, "presets", "tests", "pp"); mkdirSync(legacyDir, { recursive: true }); writeFileSync( path.join(legacyDir, "old12345.json"), JSON.stringify({ id: "old12345", presetId: "pp", name: "旧组", updatedAt: "2024-01-01T00:00:00.000Z", cases: [{ id: "c1", name: "老用例", charName: "", charDescription: "", worldBook: "", userInput: "x", history: [] }], }), "utf8", ); expect(listTestCases().map((item) => item.case.name)).toEqual(["老用例"]); expect(getTestGroup("old12345")!.presetId).toBe("global"); }); it("存回旧组里的用例时不动同组其他用例", () => { const group = saveTestGroup({ name: "旧组", cases: [{ name: "A", userInput: "甲" }, { name: "B", userInput: "乙" }], }); const target = group.cases[1]!; saveTestCase({ groupId: group.id, case: { ...target, userInput: "乙改" }, }); const after = getTestGroup(group.id)!.cases; expect(after.map((item) => item.userInput)).toEqual(["甲", "乙改"]); }); it("删除最后一份用例时把空组文件一起收掉", () => { const saved = saveTestCase({ case: { name: "单条" } }); deleteTestCase(saved.groupId, saved.case.id); expect(listTestCases()).toEqual([]); expect(listTestGroups()).toEqual([]); }); }); describe("预设的试跑状态", () => { it("按预设记住打开的是哪条用例,重读还在", () => { expect(loadPresetLabState("pp").selectedCaseId).toBe(null); savePresetLabState("pp", { selectedCaseId: "c1" }); expect(loadPresetLabState("pp").selectedCaseId).toBe("c1"); // 另一个预设不受影响 expect(loadPresetLabState("qq").selectedCaseId).toBe(null); // 只传 previewCaseId 时不抹掉 selectedCaseId savePresetLabState("pp", { previewCaseId: "c2" }); const state = loadPresetLabState("pp"); expect(state.selectedCaseId).toBe("c1"); expect(state.previewCaseId).toBe("c2"); }); }); describe("用例消息组装", () => { it("char 描述、世界书、历史、用户输入按顺序进入 messages,且 {{char}} 被替换", () => { const msgs = buildCaseMessages(preset(), { id: "c", name: "c", charName: "林", charDescription: "冷静的侦探", worldBook: "古镇夜雨", userInput: "我推门", history: [{ role: "assistant", content: "门开了。" }], }); const text = msgs.map((m) => `${m.role}:${m.content}`); expect(text[0]).toContain("你是林的叙事者"); expect(text.join("\n")).toContain("古镇夜雨"); expect(text.join("\n")).toContain("冷静的侦探"); expect(text[text.length - 1]).toBe("user:我推门"); expect(text).toContain("assistant:门开了。"); }); it("关掉 currentTurn 位置后,用户输入不再进入 messages", () => { const p = preset(); const turn = p.promptOrder.find((item) => item.promptId === "currentTurn"); if (turn) turn.enabled = false; const msgs = buildCaseMessages(p, { id: "c", name: "c", charName: "林", charDescription: "", worldBook: "", userInput: "这句不该出现", history: [], }); expect(msgs.map((m) => m.content).join("\n")).not.toContain("这句不该出现"); }); it("只替换变动项:同一预设、不同用例的 system 规则保持一致", () => { const a = buildCaseMessages(preset(), { id: "a", name: "a", charName: "甲", charDescription: "", worldBook: "", userInput: "x", history: [] }); const b = buildCaseMessages(preset(), { id: "b", name: "b", charName: "甲", charDescription: "", worldBook: "", userInput: "y", history: [] }); // 默认整形为 strict:system 会并入相邻 user,故只比较规则文本是否都在首条里\n expect(a[0]!.content.startsWith("你是甲的叙事者")).toBe(true);\n expect(b[0]!.content.startsWith("你是甲的叙事者")).toBe(true);\n expect(a[0]!.content.replace("x", "")).toBe(b[0]!.content.replace("y", "")); }); }); describe("运行记录与手动标记", () => { it("保存后可以修改标记并落盘", () => { saveTestRun({ id: "r1", presetId: "pp", presetVersionId: "v1", groupId: "g", createdAt: new Date().toISOString(), requests: [], issues: [], results: [{ profileId: "p", profileName: "P", model: "m", caseId: "c", status: "done", verdict: null, startedAt: "" }], }); updateVerdict("pp", "r1", "p", "c", "success"); expect(getTestRun("pp", "r1")!.results[0]!.verdict).toBe("success"); }); });