Files
writing-agent/tests/preset-macros.test.ts
chenmoranzhi 7e7b9c5083 宏扫描认酒馆的连带花括号写法,并补上示例对话宏。
{{{setvar::x::}} 的首个花括号在酒馆里是字面量,以前整段漏进正文;顺手把 mesExamples 接进来,导入报告也用酒馆写法显示宏名。
2026-10-26 01:09:23 +08:00

203 lines
6.7 KiB
TypeScript

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: "<START>\nUser: 甲" }).text).toBe(
"[<START>\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}}>正文</{{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");
});
});