改预设要能一边写一边看装出来的 messages,还要多模型对照,所以把编辑与试跑拆成两个视图塞进一个页面;版本存完整快照、可分叉,用例全局共用一份、运行记录按预设存。顺带让读预设文件先剥 BOM,静态资源改成不缓存,免得改了脚本还要强刷。
106 lines
4.1 KiB
TypeScript
106 lines
4.1 KiB
TypeScript
import { mkdtempSync, rmSync } 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 { checkAuthoringPreset } from "../src/preset/authoring-check.js";
|
|
import {
|
|
checkoutVersion,
|
|
commitVersion,
|
|
loadVersionTree,
|
|
versionLineage,
|
|
} from "../src/preset/versions.js";
|
|
|
|
function preset(id: string, content = "正文"): PresetPackage {
|
|
return {
|
|
id,
|
|
name: id,
|
|
source: "native",
|
|
prompts: [
|
|
{ id: "sys", name: "规则", enabled: true, role: "system", content, 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(),
|
|
};
|
|
}
|
|
|
|
let dir = "";
|
|
beforeEach(() => {
|
|
dir = mkdtempSync(path.join(os.tmpdir(), "preset-versions-"));
|
|
process.env.WRITING_AGENT_DATA_DIR = dir;
|
|
});
|
|
afterEach(() => {
|
|
delete process.env.WRITING_AGENT_DATA_DIR;
|
|
rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
describe("版本树", () => {
|
|
it("线性保存并可追溯", () => {
|
|
commitVersion({ presetId: "p1", preset: preset("p1", "v1") });
|
|
const tree = commitVersion({ presetId: "p1", preset: preset("p1", "v2") });
|
|
const lineage = versionLineage(tree, tree.currentId!);
|
|
expect(lineage.map((n) => n.preset.prompts[0]!.content)).toEqual(["v1", "v2"]);
|
|
});
|
|
|
|
it("从历史节点分叉,原分支保持不变", () => {
|
|
const first = commitVersion({ presetId: "p2", preset: preset("p2", "a") });
|
|
const rootId = first.currentId!;
|
|
commitVersion({ presetId: "p2", preset: preset("p2", "b") });
|
|
const forked = commitVersion({ presetId: "p2", preset: preset("p2", "c"), parentId: rootId });
|
|
expect(forked.nodes).toHaveLength(3);
|
|
const branch = forked.nodes.filter((n) => n.parentId === rootId);
|
|
expect(branch).toHaveLength(2);
|
|
expect(versionLineage(forked, forked.currentId!)[0]!.id).toBe(rootId);
|
|
});
|
|
|
|
it("不允许再建第二个根,也不允许挂到不存在的父节点", () => {
|
|
commitVersion({ presetId: "p3", preset: preset("p3") });
|
|
expect(() => commitVersion({ presetId: "p3", preset: preset("p3"), parentId: null })).toThrow();
|
|
expect(() => commitVersion({ presetId: "p3", preset: preset("p3"), parentId: "missing" })).toThrow();
|
|
});
|
|
|
|
it("checkout 只移动当前位置,不新增节点", () => {
|
|
const tree = commitVersion({ presetId: "p4", preset: preset("p4") });
|
|
const rootId = tree.currentId!;
|
|
commitVersion({ presetId: "p4", preset: preset("p4", "x") });
|
|
const moved = checkoutVersion("p4", rootId);
|
|
expect(moved.currentId).toBe(rootId);
|
|
expect(loadVersionTree("p4").nodes).toHaveLength(2);
|
|
});
|
|
|
|
it("拒绝非法预设 id(防路径穿越)", () => {
|
|
expect(() => loadVersionTree("../evil")).toThrow();
|
|
});
|
|
});
|
|
|
|
describe("结构检查", () => {
|
|
it("正常预设没有 error", () => {
|
|
const issues = checkAuthoringPreset(preset("ok"));
|
|
expect(issues.filter((i) => i.level === "error")).toHaveLength(0);
|
|
});
|
|
|
|
it("全角 {{ 给 warn 而不是 error", () => {
|
|
const issues = checkAuthoringPreset(preset("fw", "你好 {{user{{"));
|
|
expect(issues.some((i) => i.level === "warn" && i.message.includes("全角"))).toBe(true);
|
|
expect(issues.some((i) => i.level === "error")).toBe(false);
|
|
});
|
|
|
|
it("未闭合的 {{ 为 error", () => {
|
|
const issues = checkAuthoringPreset(preset("open", "{{setvar::a::1"));
|
|
expect(issues.some((i) => i.level === "error" && i.message.includes("未闭合"))).toBe(true);
|
|
});
|
|
|
|
it("未启用对话历史位置给 warn", () => {
|
|
const p = preset("nohist");
|
|
p.promptOrder = p.promptOrder.filter((o) => o.promptId !== "chatHistory");
|
|
const issues = checkAuthoringPreset(p);
|
|
expect(issues.some((i) => i.message.includes("对话历史"))).toBe(true);
|
|
});
|
|
});
|