Files
writing-agent/tests/preset-pseudo-history.test.ts
chenmoranzhi 860eff59b2 预设可以带一段手写的假历史,按真多轮发出去。
它进对话历史区、占真实轮次,跟酒馆示例对话(只做示范、不占轮次)不是一回事,所以要分开存:有它时排在真实历史之前,宏照样展开。
2026-10-26 01:09:29 +08:00

189 lines
6.2 KiB
TypeScript
Raw Permalink 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 { 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<Record<string, unknown>>): Marked[] {
return messages
.filter((m) => Boolean((m as Record<symbol, unknown>)[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: "<START>\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([]);
});
});