Files
ST-Bionic-Memory-Ecology/tests/task-worldinfo.mjs
2026-04-10 15:14:19 +08:00

1064 lines
31 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import assert from "node:assert/strict";
import {
installResolveHooks,
} from "./helpers/register-hooks-compat.mjs";
const extensionsShimSource = [
"export const extension_settings = {};",
"export function getContext(...args) {",
" return globalThis.SillyTavern?.getContext?.(...args) || null;",
"}",
].join("\n");
const scriptShimSource = [
"export function substituteParamsExtended(text) {",
" return String(text ?? '');",
"}",
].join("\n");
const extensionsShimUrl = `data:text/javascript,${encodeURIComponent(
extensionsShimSource,
)}`;
const scriptShimUrl = `data:text/javascript,${encodeURIComponent(
scriptShimSource,
)}`;
installResolveHooks([
{
specifiers: [
"../../../extensions.js",
"../../../../extensions.js",
"../../../../../extensions.js",
],
url: extensionsShimUrl,
},
{
specifiers: [
"../../../../script.js",
"../../../../../script.js",
],
url: scriptShimUrl,
},
]);
const originalSillyTavern = globalThis.SillyTavern;
const originalEjsTemplate = globalThis.EjsTemplate;
const originalMvu = globalThis.Mvu;
const originalGetCharWorldbookNames = globalThis.getCharWorldbookNames;
const originalGetWorldbook = globalThis.getWorldbook;
const originalGetLorebookEntries = globalThis.getLorebookEntries;
function createWorldbookEntry({
uid,
name,
comment = name,
content,
enabled = true,
positionType = "before_character_definition",
role = "system",
depth = 0,
order = 10,
strategyType = "constant",
keys = [],
keysSecondary = [],
}) {
return {
uid,
name,
comment,
content,
enabled,
position: {
type: positionType,
role,
depth,
order,
},
strategy: {
type: strategyType,
keys,
keys_secondary: { logic: "and_any", keys: keysSecondary },
},
probability: 100,
extra: {},
};
}
function createConstantWorldbookEntry(uid, name, content, comment = name) {
return createWorldbookEntry({
uid,
name,
comment,
content,
});
}
const constantEntry = createWorldbookEntry({
uid: 1,
name: "常驻设定",
comment: "常驻设定",
content: "这里是常驻世界设定。",
order: 10,
});
const dynEntry = createWorldbookEntry({
uid: 2,
name: "EW/Dyn/线索",
comment: "线索条目",
content: "隐藏线索:<%= charName %> 正在调查。",
enabled: false,
strategyType: "selective",
keys: ["调查"],
order: 15,
});
const inlineSummaryEntry = createWorldbookEntry({
uid: 3,
name: "普通 EJS 汇总",
comment: "EJS 汇总",
content: '控制摘要:<%= await getwi("EW/Dyn/线索") %>',
order: 20,
});
const inlineDataSummaryEntry = createWorldbookEntry({
uid: 12,
name: "数据 EJS 汇总",
comment: "数据 EJS 汇总",
content:
'数据摘要:<%= await getwi("数据模板", { clue: "蓝钥匙", mood: "紧张" }) %>',
order: 21,
});
const inlineDataTemplateEntry = createWorldbookEntry({
uid: 13,
name: "数据模板",
comment: "数据模板",
content:
"线索=<%= clue %>;情绪=<%= mood %>;角色=<%= char %>;用户=<%= user %>;上下文=<%= recentMessages %>",
enabled: false,
order: 22,
});
const commentKeywordProbeEntry = createWorldbookEntry({
uid: 14,
name: "备注命中测试",
comment: "常驻备注",
content: "这条只用于验证 comment 不参与自定义过滤。",
strategyType: "selective",
keys: ["绝不会匹配到这里"],
order: 23,
});
const extensionLiteralEntry = createWorldbookEntry({
uid: 4,
name: "扩展语义正文",
comment: "扩展语义正文",
content: "@@generate\n[GENERATE:Test]\n扩展语义只是普通文本。",
order: 25,
});
const externalInlineEntry = createWorldbookEntry({
uid: 5,
name: "外部书汇总",
comment: "外部书汇总",
content: '外部补充:<%= await getwi("bonus-book", "Bonus 条目") %>',
order: 26,
});
const forceControlEntry = createWorldbookEntry({
uid: 6,
name: "普通 EJS 控制",
comment: "EJS 控制",
content: '<% await activewi("强制 after") %>',
order: 30,
});
const forcedAfterEntry = createWorldbookEntry({
uid: 7,
name: "强制 after",
comment: "强制后置",
content: "这是被 EJS 强制激活的后置条目。",
enabled: false,
positionType: "after_character_definition",
strategyType: "selective",
keys: ["永远不会命中"],
order: 40,
});
const atDepthEntry = createWorldbookEntry({
uid: 8,
name: "深度注入",
comment: "深度注入",
content: "这是一条 atDepth 消息。",
positionType: "at_depth_as_system",
depth: 2,
order: 5,
});
const mvuTaggedEntry = createWorldbookEntry({
uid: 9,
name: "[mvu_update] 状态同步",
comment: "MVU tagged",
content: "这一条不应该进入结果。",
order: 28,
});
const mvuHeuristicEntry = createWorldbookEntry({
uid: 10,
name: "MVU 启发式条目",
comment: "MVU heuristic",
content: "<status_current_variable>secret=true</status_current_variable>",
order: 29,
});
const mvuLazyProbeEntry = createWorldbookEntry({
uid: 11,
name: "MVU 懒加载探测",
comment: "MVU 懒加载探测",
content: 'MVU lazy: <%= await getwi("bonus-book", "Bonus MVU") %>',
order: 27,
});
const statDataControllerEntry = createWorldbookEntry({
uid: 15,
name: "StatData Controller",
comment: "StatData Controller",
content:
'<% if (typeof stat_data !== "undefined" && stat_data?.user?.["\u610f\u8bc6\u72b6\u6001"] === "\u6c89\u7720") { %>stat_data controller payload<% } %>',
order: 24,
});
const statDataTargetEntry = createWorldbookEntry({
uid: 16,
name: "StatData Target",
comment: "StatData Target",
content: "stat_data controller payload",
enabled: false,
order: 24.1,
});
const messageVarMacroEntry = createWorldbookEntry({
uid: 17,
name: "MessageVar Macro",
comment: "MessageVar Macro",
content: "latest state={{get_message_variable::stat_data.user.\u610f\u8bc6\u72b6\u6001}}",
order: 24.2,
});
const customContextProbeEntry = createWorldbookEntry({
uid: 18,
name: "Custom Context Probe",
comment: "Custom Context Probe",
content: "上下文探针user=<%= user_input %>;char=<%= charName %>",
strategyType: "selective",
keys: ["probe custom mode"],
order: 24.3,
});
const bonusEntry = createWorldbookEntry({
uid: 101,
name: "Bonus 条目",
comment: "Bonus 条目",
content: "来自 bonus-book 的补充内容。",
order: 10,
});
const bonusMvuEntry = createWorldbookEntry({
uid: 102,
name: "Bonus MVU",
comment: "Bonus MVU",
content: "变量更新规则:\ntype: sync\n当前时间: 12:00",
order: 20,
});
const worldbooksByName = {
"main-book": [
constantEntry,
dynEntry,
inlineSummaryEntry,
inlineDataSummaryEntry,
inlineDataTemplateEntry,
commentKeywordProbeEntry,
extensionLiteralEntry,
externalInlineEntry,
mvuLazyProbeEntry,
statDataControllerEntry,
statDataTargetEntry,
messageVarMacroEntry,
customContextProbeEntry,
forceControlEntry,
forcedAfterEntry,
atDepthEntry,
mvuTaggedEntry,
mvuHeuristicEntry,
],
"bonus-book": [bonusEntry, bonusMvuEntry],
};
try {
globalThis.SillyTavern = {
getContext() {
return {
name1: "User",
name2: "Alice",
chat: [{ is_user: true, mes: "我们继续调查那条线索" }],
chatMetadata: {},
extensionSettings: {},
};
},
};
globalThis.getCharWorldbookNames = () => ({
primary: "main-book",
additional: [],
});
globalThis.getWorldbook = async (worldbookName) =>
worldbooksByName[worldbookName] || [];
globalThis.getLorebookEntries = async (worldbookName) =>
(worldbooksByName[worldbookName] || []).map((entry) => ({
uid: entry.uid,
comment: entry.comment,
}));
const { resolveTaskWorldInfo } = await import("../prompting/task-worldinfo.js");
const { buildTaskPrompt, buildTaskLlmPayload } = await import(
"../prompting/prompt-builder.js"
);
const emptyTriggerWorldInfo = await resolveTaskWorldInfo({
chatMessages: [],
userMessage: "",
templateContext: {},
});
assert.equal(
emptyTriggerWorldInfo.beforeEntries.some((entry) => entry.name === "常驻设定"),
true,
"constant world info should still resolve without trigger text",
);
assert.equal(
emptyTriggerWorldInfo.beforeEntries.some((entry) => entry.name === "数据 EJS 汇总"),
true,
"constant EJS entry should still render with empty template context defaults",
);
assert.match(emptyTriggerWorldInfo.beforeText, /数据摘要:线索=蓝钥匙;情绪=紧张;角色=Alice用户=User上下文=/);
assert.equal(
emptyTriggerWorldInfo.debug.warnings.some((warning) => warning.includes("渲染失败")),
false,
);
const worldInfo = await resolveTaskWorldInfo({
templateContext: {
recentMessages: "我们继续调查那条线索",
charName: "Alice",
},
userMessage: "继续调查",
});
assert.equal(worldInfo.beforeEntries.length, 6);
assert.equal(worldInfo.afterEntries.length, 1);
assert.equal(worldInfo.additionalMessages.length, 1);
assert.match(worldInfo.additionalMessages[0].content, /atDepth/);
assert.match(worldInfo.beforeText, /Alice/);
assert.match(worldInfo.beforeText, /bonus-book/);
assert.match(worldInfo.beforeText, /MVU lazy:/);
assert.match(worldInfo.beforeText, /@@generate/);
assert.match(worldInfo.beforeText, /\[GENERATE:Test\]/);
assert.doesNotMatch(worldInfo.beforeText, /getwi|<%=?/);
assert.doesNotMatch(worldInfo.beforeText, /status_current_variable|updatevariable/i);
assert.equal(worldInfo.debug.ejsInlinePullCount, 3);
assert.equal(worldInfo.debug.ejsForcedActivationCount, 1);
assert.equal(worldInfo.debug.resolvePassCount >= 2, true);
assert.equal(worldInfo.debug.forcedActivatedEntries.length, 1);
assert.equal(worldInfo.debug.inlinePulledEntries.length, 3);
assert.deepEqual(worldInfo.debug.lazyLoadedWorldbooks, ["bonus-book"]);
assert.equal(worldInfo.debug.mvu.filteredEntryCount, 3);
assert.equal(worldInfo.debug.mvu.lazyFilteredEntryCount, 1);
assert.equal(worldInfo.debug.mvu.blockedContentsCount, 4);
const defaultFilteredSourceNames = worldInfo.debug.mvu.filteredEntries
.map((entry) => entry.sourceName)
.sort();
assert.equal(defaultFilteredSourceNames.includes("Bonus MVU"), true);
assert.equal(defaultFilteredSourceNames.some((name) => String(name || "").includes("MVU")), true);
assert.equal(defaultFilteredSourceNames.some((name) => String(name || "").startsWith("[mvu_update]")), true);
assert.equal(
worldInfo.debug.warnings.some((warning) => warning.includes("EW/")),
true,
);
assert.equal(
worldInfo.debug.recursionWarnings.some((warning) =>
warning.includes("mvu filtered world info blocked"),
),
true,
);
assert.equal(worldInfo.debug.customFilter.mode, "default");
assert.equal(worldInfo.debug.customFilter.filteredEntryCount, 0);
globalThis.Mvu = {
getMvuData({ type, message_id: messageId } = {}) {
if (type === "message" && messageId === "latest") {
return {
stat_data: {
user: {
"意识状态": "沉眠",
},
"恼恼": {
"发情值": 71,
},
},
};
}
return {};
},
};
const customWorldInfo = await resolveTaskWorldInfo({
settings: {
worldInfoFilterMode: "custom",
worldInfoFilterCustomKeywords: "",
},
templateContext: {
recentMessages: "custom-mode regression probe",
charName: "Alice",
},
userMessage: "probe custom mode",
});
assert.equal(
customWorldInfo.beforeEntries.some((entry) =>
String(entry.sourceName || "").startsWith("[mvu_update]"),
),
true,
);
assert.equal(
customWorldInfo.beforeEntries.some((entry) =>
String(entry.sourceName || "").includes("MVU"),
),
true,
);
assert.match(
customWorldInfo.beforeText,
/<status_current_variable>secret=true<\/status_current_variable>/,
);
assert.match(
customWorldInfo.beforeText,
/控制摘要隐藏线索Alice 正在调查。/,
);
assert.match(
customWorldInfo.beforeText,
/上下文探针user=probe custom mode;char=Alice/,
);
assert.equal(
customWorldInfo.allEntries.some((entry) => String(entry.name || "").startsWith("EW/Dyn/")),
true,
);
assert.equal(
customWorldInfo.afterEntries.some((entry) => entry.sourceName === "强制 after"),
true,
);
assert.equal(customWorldInfo.debug.mvu.filteredEntryCount, 0);
assert.equal(customWorldInfo.debug.customFilter.mode, "custom");
assert.equal(customWorldInfo.debug.customFilter.filteredEntryCount, 0);
assert.equal(
customWorldInfo.debug.customRender.bridgedStatDataFromLatestMessage,
true,
);
assert.equal(customWorldInfo.debug.customRender.taskEjsStatDataRoots.cache, true);
assert.equal(
customWorldInfo.debug.customRender.taskEjsStatDataRoots.message,
true,
);
assert.equal(customWorldInfo.debug.customRender.fallbackEntryCount > 0, true);
assert.match(customWorldInfo.beforeText, /stat_data controller payload/);
assert.match(customWorldInfo.beforeText, /latest state=.+/);
globalThis.EjsTemplate = {
async prepareContext() {
return {
user_input: "OLD_FROM_NATIVE",
charName: "OLD_CHAR",
};
},
async evalTemplate(text, env) {
return String(text)
.replace(/<%=\s*user_input\s*%>/g, String(env.user_input ?? ""))
.replace(/<%=\s*charName\s*%>/g, String(env.charName ?? ""));
},
};
const customWorldInfoWithNativeRuntime = await resolveTaskWorldInfo({
settings: {
worldInfoFilterMode: "custom",
worldInfoFilterCustomKeywords: "",
},
templateContext: {
recentMessages: "custom-mode regression probe",
charName: "Alice",
},
userMessage: "probe custom mode",
});
assert.match(
customWorldInfoWithNativeRuntime.beforeText,
/上下文探针user=probe custom mode;char=Alice/,
);
assert.doesNotMatch(
customWorldInfoWithNativeRuntime.beforeText,
/OLD_FROM_NATIVE|OLD_CHAR/,
);
delete globalThis.EjsTemplate;
const keywordWorldInfo = await resolveTaskWorldInfo({
settings: {
worldInfoFilterMode: "custom",
worldInfoFilterCustomKeywords: "常驻",
},
templateContext: {
recentMessages: "我们继续调查那条线索",
charName: "Alice",
},
userMessage: "继续调查",
});
assert.equal(
keywordWorldInfo.beforeEntries.some(
(entry) => entry.sourceName === "常驻设定",
),
false,
);
assert.equal(
keywordWorldInfo.allEntries.some((entry) => entry.name === "备注命中测试"),
true,
);
assert.equal(keywordWorldInfo.debug.customFilter.filteredEntryCount, 1);
assert.equal(
keywordWorldInfo.debug.customFilter.filteredEntries[0].name,
"常驻设定",
);
assert.equal(
keywordWorldInfo.debug.customFilter.filteredEntries[0].matchedKeyword,
"常驻",
);
const keywordCachePrime = await resolveTaskWorldInfo({
settings: {
worldInfoFilterMode: "custom",
worldInfoFilterCustomKeywords: "常驻,缓存探针",
},
templateContext: {
recentMessages: "我们继续调查那条线索",
charName: "Alice",
},
userMessage: "继续调查",
});
assert.equal(keywordCachePrime.debug.cache.hit, false);
assert.equal(keywordCachePrime.debug.customFilter.filteredEntryCount, 1);
const keywordCacheHit = await resolveTaskWorldInfo({
settings: {
worldInfoFilterMode: "custom",
worldInfoFilterCustomKeywords: "常驻,缓存探针",
},
templateContext: {
recentMessages: "我们继续调查那条线索",
charName: "Alice",
},
userMessage: "继续调查",
});
assert.equal(keywordCacheHit.debug.cache.hit, true);
assert.equal(keywordCacheHit.debug.customFilter.filteredEntryCount, 1);
assert.equal(
keywordCacheHit.debug.customFilter.filteredEntries[0].name,
"常驻设定",
);
delete globalThis.Mvu;
const defaultModeWithKeywords = await resolveTaskWorldInfo({
settings: {
worldInfoFilterMode: "default",
worldInfoFilterCustomKeywords: "常驻",
},
templateContext: {
recentMessages: "我们继续调查那条线索",
charName: "Alice",
},
userMessage: "继续调查",
});
assert.equal(
defaultModeWithKeywords.beforeEntries.some(
(entry) => entry.sourceName === "常驻设定",
),
true,
);
assert.equal(defaultModeWithKeywords.debug.mvu.filteredEntryCount > 0, true);
assert.equal(defaultModeWithKeywords.debug.customFilter.filteredEntryCount, 0);
const settings = {
taskProfiles: {
recall: {
activeProfileId: "custom",
profiles: [
{
id: "custom",
name: "测试预设",
taskType: "recall",
builtin: false,
blocks: [
{
id: "b1",
type: "builtin",
sourceKey: "worldInfoBefore",
role: "system",
enabled: true,
order: 0,
injectionMode: "append",
},
{
id: "b2",
type: "builtin",
sourceKey: "worldInfoAfter",
role: "system",
enabled: true,
order: 1,
injectionMode: "append",
},
{
id: "b3",
type: "custom",
content: "角色: {{charName}}",
role: "user",
enabled: true,
order: 2,
injectionMode: "append",
},
],
},
],
},
},
};
const promptBuild = await buildTaskPrompt(settings, "recall", {
taskName: "recall",
userMessage: "继续调查",
recentMessages: "我们继续调查那条线索",
charName: "Alice",
});
assert.match(promptBuild.systemPrompt, /这里是常驻世界设定/);
assert.match(promptBuild.systemPrompt, /控制摘要隐藏线索Alice 正在调查/);
assert.match(
promptBuild.systemPrompt,
/数据摘要:线索=蓝钥匙;情绪=紧张;角色=Alice用户=User上下文=我们继续调查那条线索/,
);
assert.match(promptBuild.systemPrompt, /扩展语义只是普通文本/);
assert.match(promptBuild.systemPrompt, /来自 bonus-book 的补充内容/);
assert.match(promptBuild.systemPrompt, /MVU lazy:/);
assert.doesNotMatch(promptBuild.systemPrompt, /getwi|<%=?/);
assert.doesNotMatch(promptBuild.systemPrompt, /status_current_variable|变量更新规则|updatevariable/i);
assert.equal(
promptBuild.privateTaskMessages.length,
2,
"custom user block + atDepth world info should both enter private task messages",
);
assert.deepEqual(
promptBuild.privateTaskMessages.map((message) => message.role),
["system", "user"],
);
assert.equal(
promptBuild.privateTaskMessages[0].content,
"这是一条 atDepth 消息。",
);
assert.deepEqual(
promptBuild.hostInjections.before.map((entry) => entry.name),
[
"常驻设定",
"EJS 汇总",
"数据 EJS 汇总",
"扩展语义正文",
"外部书汇总",
"MVU 懒加载探测",
],
);
assert.deepEqual(
promptBuild.hostInjections.after.map((entry) => entry.name),
["强制后置"],
);
assert.equal(promptBuild.hostInjections.atDepth.length, 1);
assert.equal(promptBuild.hostInjections.atDepth[0].depth, 2);
assert.equal(promptBuild.hostInjectionPlan.before.length, 1);
assert.equal(promptBuild.hostInjectionPlan.before[0].blockId, "b1");
assert.equal(promptBuild.hostInjectionPlan.before[0].sourceKey, "worldInfoBefore");
assert.deepEqual(promptBuild.hostInjectionPlan.before[0].entryNames, [
"常驻设定",
"EJS 汇总",
"数据 EJS 汇总",
"扩展语义正文",
"外部书汇总",
"MVU 懒加载探测",
]);
assert.equal(promptBuild.hostInjectionPlan.after.length, 1);
assert.equal(promptBuild.hostInjectionPlan.after[0].blockId, "b2");
assert.equal(promptBuild.hostInjectionPlan.after[0].sourceKey, "worldInfoAfter");
assert.deepEqual(promptBuild.hostInjectionPlan.after[0].entryNames, ["强制后置"]);
assert.equal(promptBuild.hostInjectionPlan.atDepth.length, 1);
assert.equal(promptBuild.hostInjectionPlan.atDepth[0].entryName, "深度注入");
assert.equal(typeof promptBuild.debug.worldInfoCacheHit, "boolean");
assert.equal(promptBuild.executionMessages.length, 4);
assert.deepEqual(
promptBuild.executionMessages.map((message) => message.role),
["system", "system", "system", "user"],
);
assert.equal(
promptBuild.executionMessages[0].content,
"这是一条 atDepth 消息。",
);
assert.deepEqual(
promptBuild.renderedBlocks.map((block) => block.delivery),
["private.system", "private.system", "private.message"],
);
assert.equal(promptBuild.additionalMessages.length, 1);
assert.equal(promptBuild.additionalMessages[0].content, "这是一条 atDepth 消息。");
assert.equal(promptBuild.debug.mvu.sanitizedFieldCount >= 0, true);
const customPromptBuild = await buildTaskPrompt(
{
...settings,
worldInfoFilterMode: "custom",
worldInfoFilterCustomKeywords: "",
},
"recall",
{
taskName: "recall",
userMessage: "继续调查",
recentMessages: "我们继续调查那条线索",
charName: "Alice",
},
);
assert.match(
customPromptBuild.systemPrompt,
/<status_current_variable>secret=true<\/status_current_variable>/,
);
assert.match(customPromptBuild.systemPrompt, /这一条不应该进入结果/);
assert.match(customPromptBuild.systemPrompt, /控制摘要隐藏线索Alice 正在调查/);
const customPayload = buildTaskLlmPayload(customPromptBuild, "unused fallback");
assert.equal(
customPayload.promptMessages.some((message) =>
/<status_current_variable>secret=true<\/status_current_variable>/.test(
message.content,
),
),
true,
);
const interpolatedSettings = {
taskProfiles: {
recall: {
activeProfileId: "interpolated",
profiles: [
{
id: "interpolated",
name: "插值预设",
taskType: "recall",
builtin: false,
blocks: [
{
id: "interp-system",
type: "custom",
content: "世界书插值:\\n{{worldInfoBefore}}",
role: "system",
enabled: true,
order: 0,
injectionMode: "append",
},
],
},
],
},
},
worldInfoFilterMode: "custom",
worldInfoFilterCustomKeywords: "",
};
const customInterpolatedPromptBuild = await buildTaskPrompt(
interpolatedSettings,
"recall",
{
taskName: "recall",
userMessage: "继续调查",
recentMessages: "我们继续调查那条线索",
charName: "Alice",
},
);
assert.match(
customInterpolatedPromptBuild.systemPrompt,
/<status_current_variable>secret=true<\/status_current_variable>/,
);
const customInterpolatedPayload = buildTaskLlmPayload(
customInterpolatedPromptBuild,
"unused fallback",
);
assert.equal(
customInterpolatedPayload.promptMessages.some((message) =>
/<status_current_variable>secret=true<\/status_current_variable>/.test(
message.content,
),
),
true,
);
const noWorldInfoBlockSettings = {
taskProfiles: {
recall: {
activeProfileId: "custom",
profiles: [
{
id: "custom",
name: "无世界书显式块",
taskType: "recall",
builtin: false,
blocks: [
{
id: "u1",
type: "custom",
content: "角色: {{charName}}",
role: "user",
enabled: true,
order: 0,
injectionMode: "append",
},
],
},
],
},
},
};
const atDepthOnlyPromptBuild = await buildTaskPrompt(
noWorldInfoBlockSettings,
"recall",
{
taskName: "recall",
userMessage: "继续调查",
recentMessages: "我们继续调查那条线索",
charName: "Alice",
},
);
assert.equal(atDepthOnlyPromptBuild.debug.worldInfoRequested, true);
assert.equal(atDepthOnlyPromptBuild.debug.worldInfoAtDepthCount, 1);
assert.equal(atDepthOnlyPromptBuild.additionalMessages.length, 1);
assert.equal(
atDepthOnlyPromptBuild.additionalMessages[0].content,
"这是一条 atDepth 消息。",
);
assert.deepEqual(
atDepthOnlyPromptBuild.executionMessages.map((message) => message.role),
["system", "user"],
);
assert.equal(
atDepthOnlyPromptBuild.executionMessages[0].content,
"这是一条 atDepth 消息。",
);
const depthD4Entry = createWorldbookEntry({
uid: 201,
name: "深度注入 D4",
comment: "深度注入 D4",
content: "这是 d4 atDepth 消息。",
positionType: "at_depth_as_system",
depth: 4,
order: 8,
});
const depthD1Entry = createWorldbookEntry({
uid: 202,
name: "深度注入 D1",
comment: "深度注入 D1",
content: "这是 d1 atDepth 消息。",
positionType: "at_depth_as_system",
depth: 1,
order: 3,
});
worldbooksByName["main-book"].push(depthD4Entry, depthD1Entry);
const previousGetContext = globalThis.SillyTavern.getContext;
globalThis.SillyTavern.getContext = () => ({
...previousGetContext(),
chatId: "depth-aware-chat",
});
const depthAwareSettings = {
taskProfiles: {
recall: {
activeProfileId: "depth-aware",
profiles: [
{
id: "depth-aware",
name: "深度顺序预设",
taskType: "recall",
builtin: false,
blocks: [
{
id: "depth-recent",
type: "builtin",
sourceKey: "recentMessages",
role: "system",
enabled: true,
order: 0,
injectionMode: "append",
},
{
id: "depth-user",
type: "custom",
content: "用户问题:{{userMessage}}",
role: "user",
enabled: true,
order: 1,
injectionMode: "append",
},
],
},
],
},
},
};
const depthAwarePromptBuild = await buildTaskPrompt(depthAwareSettings, "recall", {
taskName: "recall",
userMessage: "继续调查 depth 排序",
recentMessages: "这里会被 chatMessages 替换",
chatMessages: [
{ seq: 11, role: "user", content: "第一句" },
{ seq: 12, role: "assistant", content: "第二句" },
],
charName: "Alice",
});
assert.deepEqual(
depthAwarePromptBuild.executionMessages.map((message) => message.content),
[
"#1 [assistant]: 这是 d4 atDepth 消息。\n\n#2 [assistant]: 这是一条 atDepth 消息。\n\n#11 [user]: 第一句\n\n#4 [assistant]: 这是 d1 atDepth 消息。\n\n#12 [assistant]: 第二句",
"用户问题:继续调查 depth 排序",
],
);
assert.deepEqual(
depthAwarePromptBuild.hostInjections.atDepth.map((entry) => entry.name),
["深度注入 D4", "深度注入", "深度注入 D1"],
);
assert.deepEqual(
depthAwarePromptBuild.hostInjectionPlan.atDepth.map((entry) => entry.entryName),
["深度注入 D4", "深度注入", "深度注入 D1"],
);
assert.equal(
depthAwarePromptBuild.executionMessages.at(-1)?.content.includes("atDepth"),
false,
);
worldbooksByName["main-book"].splice(-2, 2);
globalThis.SillyTavern.getContext = previousGetContext;
const { initializeHostAdapter } = await import("../host/adapter/index.js");
const partialBridgeCalls = [];
const partialBridgeEntriesByWorldbook = {
"main-book": [createConstantWorldbookEntry(11, "主书原名", "主书内容。", "主书注释")],
"side-book": [createConstantWorldbookEntry(12, "支线原名", "支线内容。", "支线注释")],
"persona-book": [createConstantWorldbookEntry(13, "人格原名", "人格内容。", "人格注释")],
"chat-book": [createConstantWorldbookEntry(14, "聊天原名", "聊天内容。", "聊天注释")],
};
globalThis.SillyTavern = {
getContext() {
return {
name1: "User",
name2: "Alice",
chat: [{ is_user: true, mes: "我们继续调查那条线索" }],
chatMetadata: {
world: "chat-book",
},
extensionSettings: {
persona_description_lorebook: "persona-book",
},
};
},
};
globalThis.getCharWorldbookNames = () => ({
primary: "main-book",
additional: ["side-book"],
});
globalThis.getWorldbook = async () => {
throw new Error(
"legacy getWorldbook should not be used when bridge getWorldbook is available",
);
};
globalThis.getLorebookEntries = async (worldbookName) =>
(partialBridgeEntriesByWorldbook[worldbookName] || []).map((entry) => ({
uid: entry.uid,
comment: entry.comment,
}));
initializeHostAdapter({
worldbookProvider: {
async getWorldbook(worldbookName) {
partialBridgeCalls.push(worldbookName);
return partialBridgeEntriesByWorldbook[worldbookName] || [];
},
},
});
const partialBridgeWorldInfo = await resolveTaskWorldInfo({
templateContext: {
recentMessages: "我们继续调查那条线索",
charName: "Alice",
},
userMessage: "继续调查",
});
assert.deepEqual(partialBridgeCalls, [
"main-book",
"side-book",
"persona-book",
"chat-book",
]);
assert.deepEqual(
partialBridgeWorldInfo.beforeEntries.map((entry) => entry.name).sort(),
["主书注释", "支线注释", "人格注释", "聊天注释"].sort(),
);
console.log("task-worldinfo tests passed");
} finally {
if (originalSillyTavern === undefined) {
delete globalThis.SillyTavern;
} else {
globalThis.SillyTavern = originalSillyTavern;
}
if (originalEjsTemplate === undefined) {
delete globalThis.EjsTemplate;
} else {
globalThis.EjsTemplate = originalEjsTemplate;
}
if (originalMvu === undefined) {
delete globalThis.Mvu;
} else {
globalThis.Mvu = originalMvu;
}
if (originalGetCharWorldbookNames === undefined) {
delete globalThis.getCharWorldbookNames;
} else {
globalThis.getCharWorldbookNames = originalGetCharWorldbookNames;
}
if (originalGetWorldbook === undefined) {
delete globalThis.getWorldbook;
} else {
globalThis.getWorldbook = originalGetWorldbook;
}
if (originalGetLorebookEntries === undefined) {
delete globalThis.getLorebookEntries;
} else {
globalThis.getLorebookEntries = originalGetLorebookEntries;
}
try {
const { initializeHostAdapter } = await import("../host/adapter/index.js");
initializeHostAdapter({});
} catch {
// ignore reset failures in test cleanup
}
}