import { describe, expect, it } from "vitest"; import { expandAtDirectives, expandPresetDirectives } from "../src/directives/expand.js"; import { collectMacroNames, createMacroVars, expandMacros } from "../src/directives/macros.js"; import { assemblePresetMessages } from "../src/preset/assembler.js"; import type { PresetPackage } from "../src/types/preset.js"; describe("expandMacros", () => { it("strips {{// }} comments, including multi-line ones", () => { const r = expandMacros("前{{// \n声明:禁止外传\n\n}}后"); expect(r.text).toBe("前后"); expect(r.hits.map((h) => h.kind)).toEqual(["comment"]); }); it("does not expand @ tokens inside a comment", () => { const r = expandAtDirectives("{{// 不要动 @玩家 }}", { persona: { name: "阿泽" }, }); expect(r.text).toBe(""); expect(r.hits.map((h) => h.kind)).toEqual(["comment"]); }); it("{{trim}} expands to empty and asks caller to trim", () => { const r = expandMacros(" {{// 备注 }}{{trim}}\n正文 "); expect(r.text).toBe(" \n正文 "); expect(r.trim).toBe(true); expect(expandAtDirectives(" {{trim}}\n正文 ").text).toBe("正文"); }); it("{{user}} and {{char}}", () => { const r = expandMacros("{{user}} 与 {{char}}", { user: "林晚", char: "长夜行", }); expect(r.text).toBe("林晚 与 长夜行"); }); it("{{char}} falls back to 作品", () => { expect(expandMacros("{{char}}").text).toBe("作品"); }); it("{{persona}} expands to the persona description", () => { expect(expandMacros("{{persona}}", { persona: "二十岁,缄默" }).text).toBe( "二十岁,缄默", ); // 没设人设 → 空串(与酒馆一致) expect(expandMacros("[{{persona}}]").text).toBe("[]"); }); it("treats {{persona}} as a known macro in the import report", () => { expect(collectMacroNames("{{persona}} {{charPrompt}}")).toContain( "{{persona}}", ); }); it("setvar then getvar in the same scope", () => { const vars = createMacroVars(); expect(expandMacros("{{setvar::a::你好}}", { vars }).text).toBe(""); expect(expandMacros("{{getvar::a}}!", { vars }).text).toBe("你好!"); }); it("getvar on unset variable is empty", () => { expect(expandMacros("[{{getvar::nope}}]").text).toBe("[]"); }); it("keeps whitespace inside setvar values", () => { const vars = createMacroVars(); expandMacros("{{setvar::set1:: }}", { vars }); expect(vars.get("set1")).toBe(" "); }); it("addvar appends", () => { const vars = createMacroVars(); expandMacros("{{setvar::k::a}}{{addvar::k::b}}", { vars }); expect(vars.get("k")).toBe("ab"); }); it("{{roll N}} yields an integer in range", () => { for (let i = 0; i < 20; i++) { const value = Number(expandMacros("{{roll 60}}").text); expect(Number.isInteger(value)).toBe(true); expect(value).toBeGreaterThanOrEqual(1); expect(value).toBeLessThanOrEqual(60); } }); it("{{roll:1d6}} and {{roll::d9}} roll dice", () => { for (let i = 0; i < 20; i++) { const six = Number(expandMacros("{{roll:1d6}}").text); expect(six).toBeGreaterThanOrEqual(1); expect(six).toBeLessThanOrEqual(6); const nine = Number(expandMacros("{{roll::d9}}").text); expect(nine).toBeGreaterThanOrEqual(1); expect(nine).toBeLessThanOrEqual(9); } }); it("does not roll when allowDice is false", () => { expect(expandMacros("{{roll 60}}", { allowDice: false }).text).toBe( "{{roll 60}}", ); }); it("leaves unknown macros untouched", () => { const r = expandMacros("{{turnPlus1}} {{MEMORY_TABLE}} {{ }}"); expect(r.text).toBe("{{turnPlus1}} {{MEMORY_TABLE}} {{ }}"); expect(r.hits).toEqual([]); }); it("{{{setvar::x::}} 里第一个花括号是字面量(酒馆语义)", () => { const vars = createMacroVars(); const r = expandMacros("{{{setvar::SEMOD1::}}\n[RESET]", { vars }); expect(r.text).toBe("{\n[RESET]"); expect(vars.get("SEMOD1")).toBe(""); }); it("{{mesExamples}} 有上下文就展开,没有就原样保留", () => { expect(expandMacros("[{{mesExamples}}]", { mesExamples: "\nUser: 甲" }).text).toBe( "[\nUser: 甲]", ); expect(expandMacros("[{{mesExamples}}]").text).toBe("[{{mesExamples}}]"); }); it("三花括号里的宏也进导入报告(按 setvar 计)", () => { expect(collectMacroNames("{{{setvar::a::}}")).toContain("{{setvar}}"); }); it("collectMacroNames reports unknown macros by raw text", () => { const names = collectMacroNames( "{{// c}}{{user}}{{setvar::a::b}}{{turnPlus1}}{{roll 9}}", ); expect(names).toContain("{{comment}}"); expect(names).toContain("{{user}}"); expect(names).toContain("{{setvar}}"); expect(names).toContain("{{roll}}"); expect(names.some((n) => n.includes("turnPlus1"))).toBe(true); }); }); function presetWith(prompts: Array<{ id: string; content: string }>): PresetPackage { return { id: "p", name: "p", source: "sillytavern", prompts: prompts.map((p) => ({ id: p.id, name: p.id, enabled: true, role: "system" as const, content: p.content, marker: false, sourceIdentifier: p.id, })), promptOrder: prompts.map((p, i) => ({ promptId: p.id, enabled: true, orderIndex: i, })), generation: {}, unsupported: [], importedAt: "2026-01-01T00:00:00.000Z", }; } describe("preset assembly macro scope", () => { it("setvar in an earlier entry is visible to getvar in a later entry", () => { const preset = presetWith([ { id: "a", content: "{{setvar::set1:: }}{{// 预设说明 }}" }, { id: "b", content: "规则{{getvar::set1}}结束" }, ]); const vars = createMacroVars(); const messages = assemblePresetMessages( preset, () => null, (content) => expandPresetDirectives(content, { vars, user: "阿泽" }).text, ); expect(messages.map((m) => m.content)).toEqual(["规则 结束"]); }); it("expands {{char}} from assembly context", () => { const preset = presetWith([{ id: "a", content: "<{{char}}>正文" }]); const messages = assemblePresetMessages( preset, () => null, (content) => expandPresetDirectives(content, { charName: "长夜行" }).text, ); expect(messages[0]?.content).toBe("<长夜行>正文"); }); }); describe("{{// 注释 }} 配平", () => { it("注释里嵌套宏不会漏出尾巴", () => { const text = expandMacros("{{// 注释:把 {{user}} 换成别的\n}}|用户:{{user}}|", { user: "User", }).text; expect(text).toBe("|用户:User|"); }); it("普通注释照旧吃掉", () => { expect(expandMacros("A{{// 说明 }}B", {}).text).toBe("AB"); }); });