新增预设工作台:编辑、版本树、结构检查、用例试跑放在同一页。
改预设要能一边写一边看装出来的 messages,还要多模型对照,所以把编辑与试跑拆成两个视图塞进一个页面;版本存完整快照、可分叉,用例全局共用一份、运行记录按预设存。顺带让读预设文件先剥 BOM,静态资源改成不缓存,免得改了脚本还要强刷。
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user