- 装配只认 prompt_order.enabled;squash_system_messages 只合并相邻 system(新增 system 档,连接符按酒馆用单换行)
- 包装层:new_chat_prompt、示例对话(new_example_chat_prompt + <START> 切段 + name)、角色卡三块与 personality_format/scenario_format、{{user}}/{{char}} 取值、任务契约并进预设条目
- 工具协议:按 ToolBindings / 伪工具协议数据声明真 tools(presetToolPolicy=preset),工具描述用到的条目不再重复作为消息发出
- 正则:promptOnly 作用于对话历史(含 depth 过滤)、写入时脚本改写回复;{{//}} 注释按配平扫描
- tools/st-request-diff:真假上游抓包对比工具、三份预设的机制结论与实测报告
296 lines
8.6 KiB
TypeScript
296 lines
8.6 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
buildPresetDiagnostics,
|
|
diagnosticsWarnings,
|
|
} from "../src/preset/diagnostics.js";
|
|
import { importSillyTavernPreset } from "../src/preset/importer.js";
|
|
|
|
function entry(
|
|
id: string,
|
|
content: string,
|
|
extra: Partial<Record<string, unknown>> = {},
|
|
) {
|
|
return {
|
|
identifier: id,
|
|
name: id,
|
|
role: "system",
|
|
content,
|
|
enabled: true,
|
|
...extra,
|
|
};
|
|
}
|
|
|
|
function rawPreset(prompts: unknown[], extras: Record<string, unknown> = {}) {
|
|
return {
|
|
prompts,
|
|
prompt_order: [
|
|
{
|
|
character_id: 100001,
|
|
order: (prompts as Array<{ identifier: string }>).map((p) => ({
|
|
identifier: p.identifier,
|
|
enabled: true,
|
|
})),
|
|
},
|
|
],
|
|
...extras,
|
|
};
|
|
}
|
|
|
|
describe("导入诊断:跨条目标签", () => {
|
|
it("detects tags opened in one entry and closed in another", () => {
|
|
const report = importSillyTavernPreset(
|
|
rawPreset([
|
|
entry("a", "<system>开局"),
|
|
entry("b", "中间"),
|
|
entry("c", "</system>收尾"),
|
|
]),
|
|
{ name: "t" },
|
|
);
|
|
const cross = report.diagnostics.tags.crossEntry;
|
|
expect(cross).toEqual([{ tag: "system", openIn: "a", closeIn: "c" }]);
|
|
expect(
|
|
report.warnings.some((w) => w.includes("跨条目标签配对")),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("supports CJK tag names", () => {
|
|
const report = importSillyTavernPreset(
|
|
rawPreset([entry("a", "以下是<对话历史>"), entry("b", "</对话历史>结束")]),
|
|
{ name: "t" },
|
|
);
|
|
expect(report.diagnostics.tags.crossEntry).toEqual([
|
|
{ tag: "对话历史", openIn: "a", closeIn: "b" },
|
|
]);
|
|
});
|
|
|
|
it("reports tags never closed", () => {
|
|
const report = importSillyTavernPreset(
|
|
rawPreset([entry("a", "<game>正文")]),
|
|
{ name: "t" },
|
|
);
|
|
expect(report.diagnostics.tags.unclosed).toEqual([
|
|
{ tag: "game", openIn: "a" },
|
|
]);
|
|
});
|
|
|
|
it("ignores self-closing tags", () => {
|
|
const report = importSillyTavernPreset(
|
|
rawPreset([entry("a", "换行<br/>结束")]),
|
|
{ name: "t" },
|
|
);
|
|
expect(report.diagnostics.tags.unclosed).toEqual([]);
|
|
expect(report.diagnostics.tags.orphanClose).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("导入诊断:伪工具协议", () => {
|
|
it("finds placeholder-style protocols", () => {
|
|
const report = importSillyTavernPreset(
|
|
rawPreset([
|
|
entry(
|
|
"sys",
|
|
"[Interface]\nactive_interface = %TOOL_NAME%\noutput_channel = %TOOL_NAME%",
|
|
),
|
|
entry("tool", '[contract]\nstream_target = "display_stream"'),
|
|
]),
|
|
{ name: "t" },
|
|
);
|
|
const proto = report.diagnostics.toolProtocol;
|
|
expect(proto.entries).toEqual(["sys", "tool"]);
|
|
expect(proto.placeholders).toContain("%TOOL_NAME%");
|
|
expect(report.warnings.some((w) => w.includes("伪工具协议"))).toBe(true);
|
|
});
|
|
|
|
it("reads SPreset.ToolBindings form into tool name and params", () => {
|
|
const report = importSillyTavernPreset(
|
|
rawPreset([entry("bound", "所有输出必须调用 game_content 工具")], {
|
|
extensions: {
|
|
SPreset: {
|
|
ToolBindings: {
|
|
bound: {
|
|
enabled: true,
|
|
form: {
|
|
name: "game_content",
|
|
description: "全部输出在此工具内",
|
|
parameters: [{ name: "content", type: "string", required: true }],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
{ name: "t" },
|
|
);
|
|
expect(report.diagnostics.toolProtocol.bindings).toEqual([
|
|
{
|
|
entry: "bound",
|
|
toolName: "game_content",
|
|
params: ["content"],
|
|
description: "全部输出在此工具内",
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("导入诊断:宏与随机宏", () => {
|
|
it("separates known from unknown macros", () => {
|
|
const report = importSillyTavernPreset(
|
|
rawPreset([
|
|
entry("a", "{{user}} {{setvar::k::v}}{{getvar::k}}{{turnPlus1}}"),
|
|
]),
|
|
{ name: "t" },
|
|
);
|
|
const macros = report.diagnostics.macros;
|
|
expect(macros.find((m) => m.name === "{{user}}")?.known).toBe(true);
|
|
expect(macros.find((m) => m.name === "{{turnPlus1}}")?.known).toBe(false);
|
|
expect(
|
|
report.warnings.some((w) => w.includes("未识别宏")),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("marks random macros before chatHistory as cache-breaking", () => {
|
|
const report = importSillyTavernPreset(
|
|
rawPreset([
|
|
entry("noise", "{{roll 999}}"),
|
|
entry("chatHistory", "", { marker: true }),
|
|
]),
|
|
{ name: "t" },
|
|
);
|
|
const noise = report.diagnostics.randomMacros.find(
|
|
(r) => r.entry === "noise",
|
|
);
|
|
expect(noise?.afterHistory).toBe(false);
|
|
expect(
|
|
report.warnings.some((w) => w.includes("提示缓存每轮失效")),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("marks random macros after chatHistory as harmless", () => {
|
|
const report = importSillyTavernPreset(
|
|
rawPreset([
|
|
entry("chatHistory", "", { marker: true }),
|
|
entry("noise", "{{roll 999}}"),
|
|
]),
|
|
{ name: "t" },
|
|
);
|
|
const noise = report.diagnostics.randomMacros.find(
|
|
(r) => r.entry === "noise",
|
|
);
|
|
expect(noise?.afterHistory).toBe(true);
|
|
expect(
|
|
report.warnings.some((w) => w.includes("提示缓存每轮失效")),
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("导入诊断:squash_system_messages 映射", () => {
|
|
it.each([
|
|
[true, "system"],
|
|
[false, "none"],
|
|
["semi", "semi"],
|
|
["STRICT", "strict"],
|
|
[2, "semi"],
|
|
[undefined, undefined],
|
|
] as const)("%s → %s", (input, expected) => {
|
|
const report = importSillyTavernPreset(
|
|
rawPreset([entry("a", "x")], { squash_system_messages: input }),
|
|
{ name: "t" },
|
|
);
|
|
expect(report.preset.messagePolicy?.squash).toBe(expected);
|
|
});
|
|
});
|
|
|
|
describe("导入诊断:静默失效告警", () => {
|
|
it("reports injection_position=1 entries as depth insertions", () => {
|
|
const report = importSillyTavernPreset(
|
|
rawPreset([
|
|
entry("main", "主线"),
|
|
entry("deepjail", "深度注入的破限", { injection_position: 1 }),
|
|
]),
|
|
{ name: "t" },
|
|
);
|
|
expect(report.diagnostics.silentGaps.deepInjection).toEqual(["deepjail"]);
|
|
expect(
|
|
report.warnings.some((w) => w.includes("injection_depth")),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("ignores depth-injection entries that prompt_order leaves off", () => {
|
|
const report = importSillyTavernPreset(
|
|
rawPreset(
|
|
[entry("main", "主线"), entry("off", "关掉的", { injection_position: 1 })],
|
|
{
|
|
prompt_order: [
|
|
{
|
|
character_id: 100001,
|
|
order: [
|
|
{ identifier: "main", enabled: true },
|
|
{ identifier: "off", enabled: false },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
),
|
|
{ name: "t" },
|
|
);
|
|
expect(report.diagnostics.silentGaps.deepInjection).toEqual([]);
|
|
});
|
|
|
|
it("flags script capabilities that have no native replacement", () => {
|
|
const report = importSillyTavernPreset(
|
|
rawPreset([entry("main", "主线")], {
|
|
extensions: {
|
|
tavern_helper: {
|
|
scripts: [
|
|
{
|
|
type: "script",
|
|
name: "防截断",
|
|
content:
|
|
"const r = await fetch(url); const reader = res.body.getReader(); // text/event-stream",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
}),
|
|
{ name: "t" },
|
|
);
|
|
const script = report.diagnostics.scripts[0];
|
|
expect(script?.features).toContain("改写请求 / 拦截 fetch");
|
|
expect(script?.features).toContain("流式解包 / 防截断");
|
|
expect(report.diagnostics.silentGaps.unportedScriptFeatures[0]?.script).toBe(
|
|
"防截断",
|
|
);
|
|
expect(
|
|
report.warnings.some((w) => w.includes("本版本没有替代")),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("mentions display-layer regexes leaking their tags", () => {
|
|
const report = importSillyTavernPreset(
|
|
rawPreset([entry("main", "主线")], {
|
|
regex_scripts: [
|
|
{ id: "r1", scriptName: "隐藏选项", markdownOnly: true, placement: [2] },
|
|
],
|
|
}),
|
|
{ name: "t" },
|
|
);
|
|
expect(
|
|
report.warnings.some((w) => w.includes("显示层规则")),
|
|
).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("diagnosticsWarnings", () => {
|
|
it("keeps quiet for a clean preset", () => {
|
|
const report = importSillyTavernPreset(
|
|
rawPreset([
|
|
entry("main", "主线"),
|
|
entry("chatHistory", "", { marker: true }),
|
|
entry("jailbreak", "收尾"),
|
|
]),
|
|
{ name: "t" },
|
|
);
|
|
expect(diagnosticsWarnings(report.diagnostics)).toEqual([]);
|
|
});
|
|
});
|