import { describe, expect, it } from "vitest"; import { importSillyTavernPreset } from "../src/preset/importer.js"; import { PRESET_HISTORY_MARK } from "../src/preset/assembler.js"; import { assemblePlayWorkerMessages, shapeFramedMessages, } from "../src/preset/play-frame.js"; import { normalizePseudoHistory } from "../src/preset/pseudo-history.js"; import { buildCaseMessages } from "../src/preset/test-run.js"; import type { PresetChatTurn } from "../src/types/preset.js"; const rawPreset = { prompts: [ { identifier: "main", name: "main", role: "system", content: "MAIN" }, { identifier: "dialogueExamples", name: "examples", role: "system", marker: true, content: "", }, { identifier: "chatHistory", name: "history", role: "system", marker: true, content: "", }, ], prompt_order: [ { character_id: 100001, order: [ { identifier: "main", enabled: true }, { identifier: "dialogueExamples", enabled: true }, { identifier: "chatHistory", enabled: true }, ], }, ], }; function assemble(input: { pseudoHistory?: PresetChatTurn[]; history?: Array<{ role: "user" | "assistant"; content: string }>; exampleDialogue?: string; }) { const report = importSillyTavernPreset(rawPreset, { name: "t" }); const preset = { ...report.preset, ...(input.pseudoHistory ? { pseudoHistory: input.pseudoHistory } : {}), }; return assemblePlayWorkerMessages({ systemPrompt: "", preset, pack: { worldBookBefore: [], history: input.history ?? [], worldBookAfter: [], turn: "", postTurn: [], ...(input.exampleDialogue ? { exampleDialogue: input.exampleDialogue } : {}), }, persona: null, charName: "林昭", }); } type Marked = { role: string; content: string }; /** 只取历史区(打了 PRESET_HISTORY_MARK 的那一段)。 */ function historyTurns(messages: Array>): Marked[] { return messages .filter((m) => Boolean((m as Record)[PRESET_HISTORY_MARK])) .map((m) => ({ role: String(m.role), content: String(m.content) })); } describe("预设的假历史:真多轮,不与示例对话混用", () => { it("每条假历史各占一条 user / assistant 消息", () => { const messages = assemble({ pseudoHistory: [ { role: "user", content: "我推开门" }, { role: "assistant", content: "屋里没有人。" }, ], }); expect(historyTurns(messages as never)).toEqual([ { role: "user", content: "我推开门" }, { role: "assistant", content: "屋里没有人。" }, ]); // 不能伪装成示例对话(system + example_* 的 few-shot) expect(messages.some((m) => m.name)).toBe(false); }); it("假历史排在真实历史之前,真实轮次原样保留", () => { const messages = assemble({ pseudoHistory: [ { role: "user", content: "前情-问" }, { role: "assistant", content: "前情-答" }, ], history: [ { role: "user", content: "真-问" }, { role: "assistant", content: "真-答" }, ], }); expect(historyTurns(messages as never).map((m) => m.content)).toEqual([ "前情-问", "前情-答", "真-问", "真-答", ]); }); it("正文里的换行是真换行,不会被当成新的一条", () => { const messages = assemble({ pseudoHistory: [ { role: "assistant", content: "第一段\n\n第二段\nassistant: 这不是新的一条" }, ], }); const turns = historyTurns(messages as never); expect(turns).toHaveLength(1); expect(turns[0]!.content).toContain("\n\n"); expect(turns[0]!.content).toContain("assistant: 这不是新的一条"); }); it("合体后的历史区不被 squash 揉成一段", () => { const shaped = shapeFramedMessages( assemble({ pseudoHistory: [ { role: "user", content: "前情-问" }, { role: "assistant", content: "前情-答" }, ], history: [{ role: "user", content: "真-问" }], }), "strict", ); const contents = historyTurns(shaped as never).map((m) => m.content); expect(contents).toEqual(["前情-问", "前情-答", "真-问"]); }); it("示例对话仍是酒馆的 system + example_* 形状,与假历史并存", () => { const messages = assemble({ pseudoHistory: [{ role: "user", content: "前情" }], exampleDialogue: "\nUser: 你好\n林昭: 你也好", }); const examples = messages.filter((m) => m.name?.startsWith("example_")); expect(examples.map((m) => m.name)).toEqual([ "example_user", "example_assistant", ]); expect(examples.every((m) => m.role === "system")).toBe(true); // 假历史照旧是真多轮 expect(historyTurns(messages as never)).toEqual([ { role: "user", content: "前情" }, ]); }); it("工作台用例预览走同一条路:预设假历史 + 用例历史 + 本轮输入", () => { const report = importSillyTavernPreset(rawPreset, { name: "t" }); const preset = { ...report.preset, pseudoHistory: [{ role: "user" as const, content: "预设前情" }] }; const messages = buildCaseMessages(preset, { id: "c1", name: "c1", charName: "林昭", charDescription: "", worldBook: "", userInput: "本轮输入", history: [{ role: "assistant", content: "用例前情" }], }); const contents = messages.map((m) => String(m.content)); const at = (text: string) => contents.findIndex((c) => c.includes(text)); expect(at("预设前情")).toBeGreaterThanOrEqual(0); expect(at("预设前情")).toBeLessThan(at("用例前情")); expect(at("用例前情")).toBeLessThan(at("本轮输入")); }); it("非法条目被丢掉,不会变成空消息", () => { expect( normalizePseudoHistory([ { role: "user", content: "留着" }, { role: "system", content: "丢掉" }, { role: "assistant", content: " " }, { role: "assistant" }, "垃圾", null, ]), ).toEqual([{ role: "user", content: "留着" }]); expect(normalizePseudoHistory("user: 我")).toEqual([]); }); });