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); }); });