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> = {}, ) { return { identifier: id, name: id, role: "system", content, enabled: true, ...extra, }; } function rawPreset(prompts: unknown[], extras: Record = {}) { 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", "开局"), entry("b", "中间"), entry("c", "收尾"), ]), { 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", "正文")]), { name: "t" }, ); expect(report.diagnostics.tags.unclosed).toEqual([ { tag: "game", openIn: "a" }, ]); }); it("ignores self-closing tags", () => { const report = importSillyTavernPreset( rawPreset([entry("a", "换行
结束")]), { 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([]); }); });