189 lines
6.2 KiB
TypeScript
189 lines
6.2 KiB
TypeScript
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([]);
|
||
});
|
||
});
|