Files
writing-agent/tests/preset-test-cases.test.ts
T
chenmoranzhi f3303038fe 新增预设工作台:编辑、版本树、结构检查、用例试跑放在同一页。
改预设要能一边写一边看装出来的 messages,还要多模型对照,所以把编辑与试跑拆成两个视图塞进一个页面;版本存完整快照、可分叉,用例全局共用一份、运行记录按预设存。顺带让读预设文件先剥 BOM,静态资源改成不缓存,免得改了脚本还要强刷。
2026-10-26 01:09:40 +08:00

178 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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");
});
});