mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-13 18:31:16 +08:00
Fix Tavern regex reuse inspection and matching
This commit is contained in:
@@ -32,7 +32,14 @@ const originalIsCharacterTavernRegexesEnabled =
|
||||
globalThis.isCharacterTavernRegexesEnabled;
|
||||
const originalExtensionSettings = globalThis.__taskRegexTestExtensionSettings;
|
||||
|
||||
function createRule(id, find, replace, overrides = {}) {
|
||||
const PLACEMENT = Object.freeze({
|
||||
USER_INPUT: 1,
|
||||
AI_OUTPUT: 2,
|
||||
WORLD_INFO: 5,
|
||||
REASONING: 6,
|
||||
});
|
||||
|
||||
function createLocalRule(id, find, replace, overrides = {}) {
|
||||
return {
|
||||
id,
|
||||
script_name: id,
|
||||
@@ -53,56 +60,32 @@ function createRule(id, find, replace, overrides = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
globalThis.__taskRegexTestExtensionSettings = {
|
||||
regex: {
|
||||
regex_scripts: [createRule("legacy-global", "/Gamma/g", "G")],
|
||||
},
|
||||
function createTavernRule(id, findRegex, replaceString, overrides = {}) {
|
||||
return {
|
||||
id,
|
||||
scriptName: id,
|
||||
enabled: true,
|
||||
findRegex,
|
||||
replaceString,
|
||||
trimStrings: [],
|
||||
placement: [PLACEMENT.WORLD_INFO],
|
||||
promptOnly: false,
|
||||
markdownOnly: false,
|
||||
minDepth: null,
|
||||
maxDepth: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
globalThis.SillyTavern = {
|
||||
getContext() {
|
||||
return {
|
||||
extensionSettings: globalThis.__taskRegexTestExtensionSettings,
|
||||
chatCompletionSettings: {
|
||||
regex_scripts: [createRule("legacy-preset", "/Delta/g", "D")],
|
||||
},
|
||||
characterId: 0,
|
||||
characters: [
|
||||
{
|
||||
extensions: {
|
||||
regex_scripts: [
|
||||
createRule("legacy-character", "/Epsilon/g", "E"),
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
globalThis.getTavernRegexes = () => {
|
||||
throw new Error(
|
||||
"legacy global getter should not be used when bridge exists",
|
||||
);
|
||||
};
|
||||
globalThis.isCharacterTavernRegexesEnabled = () => {
|
||||
throw new Error(
|
||||
"legacy character toggle should not be used when bridge full capability exists",
|
||||
);
|
||||
};
|
||||
|
||||
const { initializeHostAdapter } = await import("../host-adapter/index.js");
|
||||
const { applyTaskRegex } = await import("../task-regex.js");
|
||||
|
||||
const settings = {
|
||||
function buildSettings(regex = {}) {
|
||||
return {
|
||||
taskProfiles: {
|
||||
extract: {
|
||||
activeProfileId: "bridge-profile",
|
||||
activeProfileId: "default",
|
||||
profiles: [
|
||||
{
|
||||
id: "bridge-profile",
|
||||
name: "Regex Bridge Test",
|
||||
id: "default",
|
||||
name: "Regex Test",
|
||||
taskType: "extract",
|
||||
builtin: false,
|
||||
blocks: [],
|
||||
@@ -117,28 +100,105 @@ try {
|
||||
stages: {
|
||||
input: true,
|
||||
output: true,
|
||||
"input.userMessage": true,
|
||||
"input.recentMessages": true,
|
||||
"input.candidateText": true,
|
||||
"input.finalPrompt": true,
|
||||
"output.rawResponse": true,
|
||||
"output.beforeParse": true,
|
||||
},
|
||||
localRules: [createRule("local-tail", "/Beta/g", "B")],
|
||||
localRules: [],
|
||||
...regex,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function setTestContext({
|
||||
extensionSettings,
|
||||
presetScripts = [],
|
||||
presetName = "Live Preset",
|
||||
apiId = "openai",
|
||||
characterId = 0,
|
||||
characters = [],
|
||||
} = {}) {
|
||||
globalThis.__taskRegexTestExtensionSettings = extensionSettings;
|
||||
globalThis.SillyTavern = {
|
||||
getContext() {
|
||||
return {
|
||||
extensionSettings,
|
||||
characterId,
|
||||
characters,
|
||||
getPresetManager() {
|
||||
return {
|
||||
apiId,
|
||||
getSelectedPresetName() {
|
||||
return presetName;
|
||||
},
|
||||
readPresetExtensionField({ path } = {}) {
|
||||
return path === "regex_scripts" ? presetScripts : [];
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const { initializeHostAdapter } = await import("../host-adapter/index.js");
|
||||
const { applyTaskRegex, inspectTaskRegexReuse } = await import(
|
||||
"../task-regex.js"
|
||||
);
|
||||
|
||||
globalThis.getTavernRegexes = () => {
|
||||
throw new Error("legacy global getter should not be used in regex tests");
|
||||
};
|
||||
globalThis.isCharacterTavernRegexesEnabled = () => {
|
||||
throw new Error(
|
||||
"legacy character toggle should not be used in regex tests",
|
||||
);
|
||||
};
|
||||
|
||||
setTestContext({
|
||||
extensionSettings: {
|
||||
regex: [],
|
||||
preset_allowed_regex: {},
|
||||
character_allowed_regex: [],
|
||||
},
|
||||
});
|
||||
|
||||
const fullBridgeSettings = buildSettings({
|
||||
localRules: [createLocalRule("local-tail", "/Beta/g", "B")],
|
||||
});
|
||||
const bridgeCalls = [];
|
||||
initializeHostAdapter({
|
||||
regexProvider: {
|
||||
getTavernRegexes(request) {
|
||||
bridgeCalls.push(request);
|
||||
if (request?.type === "global") {
|
||||
return [createRule("bridge-global", "/Alpha/g", "A")];
|
||||
return [
|
||||
createTavernRule("bridge-global", "/Alpha/g", "A", {
|
||||
promptOnly: true,
|
||||
}),
|
||||
];
|
||||
}
|
||||
if (request?.type === "preset") {
|
||||
return [createRule("bridge-preset", "/A/g", "P")];
|
||||
return [
|
||||
createTavernRule("bridge-preset", "/A/g", "P", {
|
||||
promptOnly: true,
|
||||
}),
|
||||
];
|
||||
}
|
||||
if (request?.type === "character") {
|
||||
return [createRule("bridge-character", "/P/g", "C")];
|
||||
return [
|
||||
createTavernRule("bridge-character", "/P/g", "C", {
|
||||
promptOnly: true,
|
||||
}),
|
||||
];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
@@ -150,7 +210,7 @@ try {
|
||||
|
||||
const fullBridgeDebug = { entries: [] };
|
||||
const fullBridgeOutput = applyTaskRegex(
|
||||
settings,
|
||||
fullBridgeSettings,
|
||||
"extract",
|
||||
"finalPrompt",
|
||||
"Alpha Beta",
|
||||
@@ -168,140 +228,225 @@ try {
|
||||
fullBridgeDebug.entries[0].appliedRules.map((item) => item.id),
|
||||
["bridge-global", "bridge-preset", "bridge-character", "local-tail"],
|
||||
);
|
||||
assert.deepEqual(fullBridgeDebug.entries[0].sourceCount, {
|
||||
tavern: 3,
|
||||
local: 1,
|
||||
});
|
||||
|
||||
const partialBridgeCalls = [];
|
||||
initializeHostAdapter({
|
||||
regexProvider: {
|
||||
getTavernRegexes(request) {
|
||||
partialBridgeCalls.push(request);
|
||||
if (request?.type === "global") {
|
||||
return [createRule("partial-global", "/Gamma/g", "G1")];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
const fallbackExtensionSettings = {
|
||||
regex: [
|
||||
createTavernRule("global-fallback", "/Gamma/g", "G1", {
|
||||
promptOnly: true,
|
||||
}),
|
||||
],
|
||||
preset_allowed_regex: {
|
||||
openai: ["Live Preset"],
|
||||
},
|
||||
character_allowed_regex: ["hero.png"],
|
||||
};
|
||||
setTestContext({
|
||||
extensionSettings: fallbackExtensionSettings,
|
||||
presetScripts: [
|
||||
createTavernRule("preset-fallback", "/G1/g", "P1", {
|
||||
promptOnly: true,
|
||||
}),
|
||||
],
|
||||
characters: [
|
||||
{
|
||||
avatar: "hero.png",
|
||||
data: {
|
||||
extensions: {
|
||||
regex_scripts: [
|
||||
createTavernRule("character-fallback", "/P1/g", "C1", {
|
||||
promptOnly: true,
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
initializeHostAdapter({});
|
||||
|
||||
const partialBridgeDebug = { entries: [] };
|
||||
const partialBridgeOutput = applyTaskRegex(
|
||||
settings,
|
||||
const fallbackDebug = { entries: [] };
|
||||
const fallbackOutput = applyTaskRegex(
|
||||
buildSettings(),
|
||||
"extract",
|
||||
"finalPrompt",
|
||||
"Gamma Delta Epsilon",
|
||||
partialBridgeDebug,
|
||||
"input.finalPrompt",
|
||||
"Gamma",
|
||||
fallbackDebug,
|
||||
"system",
|
||||
);
|
||||
assert.equal(fallbackOutput, "C1");
|
||||
|
||||
assert.equal(partialBridgeOutput, "G1 Delta E");
|
||||
assert.deepEqual(partialBridgeCalls, [
|
||||
{ type: "global" },
|
||||
{ type: "preset", name: "in_use" },
|
||||
]);
|
||||
const fallbackInspect = inspectTaskRegexReuse(buildSettings(), "extract");
|
||||
assert.equal(fallbackInspect.activeRuleCount, 3);
|
||||
assert.deepEqual(
|
||||
partialBridgeDebug.entries[0].appliedRules.map((item) => item.id),
|
||||
["partial-global", "legacy-character"],
|
||||
);
|
||||
assert.deepEqual(partialBridgeDebug.entries[0].sourceCount, {
|
||||
tavern: 2,
|
||||
local: 1,
|
||||
});
|
||||
|
||||
const emptyBridgeCalls = [];
|
||||
initializeHostAdapter({
|
||||
regexProvider: {
|
||||
getTavernRegexes(request) {
|
||||
emptyBridgeCalls.push(request);
|
||||
if (request?.type === "global") {
|
||||
return [];
|
||||
}
|
||||
if (request?.type === "preset") {
|
||||
return [createRule("bridge-preset-empty-guard", "/Theta/g", "T")];
|
||||
}
|
||||
if (request?.type === "character") {
|
||||
return [createRule("bridge-character-empty-guard", "/T/g", "C2")];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
isCharacterTavernRegexesEnabled() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const emptyBridgeDebug = { entries: [] };
|
||||
const emptyBridgeOutput = applyTaskRegex(
|
||||
settings,
|
||||
"extract",
|
||||
"finalPrompt",
|
||||
"Gamma Theta",
|
||||
emptyBridgeDebug,
|
||||
"system",
|
||||
);
|
||||
|
||||
assert.equal(emptyBridgeOutput, "Gamma C2");
|
||||
assert.deepEqual(emptyBridgeCalls, [
|
||||
{ type: "global" },
|
||||
{ type: "preset", name: "in_use" },
|
||||
{ type: "character", name: "current" },
|
||||
]);
|
||||
assert.deepEqual(
|
||||
emptyBridgeDebug.entries[0].appliedRules.map((item) => item.id),
|
||||
["bridge-preset-empty-guard", "bridge-character-empty-guard"],
|
||||
fallbackInspect.activeRules.map((rule) => rule.id),
|
||||
["global-fallback", "preset-fallback", "character-fallback"],
|
||||
);
|
||||
assert.equal(
|
||||
emptyBridgeDebug.entries[0].appliedRules.some(
|
||||
(item) => item.id === "legacy-global",
|
||||
),
|
||||
fallbackInspect.sources.find((source) => source.type === "preset")
|
||||
?.resolvedVia,
|
||||
"fallback",
|
||||
);
|
||||
assert.equal(
|
||||
fallbackInspect.sources.find((source) => source.type === "character")
|
||||
?.allowed,
|
||||
true,
|
||||
);
|
||||
|
||||
const disallowedExtensionSettings = {
|
||||
regex: [
|
||||
createTavernRule("global-only", "/Gamma/g", "G2", {
|
||||
promptOnly: true,
|
||||
}),
|
||||
],
|
||||
preset_allowed_regex: {},
|
||||
character_allowed_regex: [],
|
||||
};
|
||||
setTestContext({
|
||||
extensionSettings: disallowedExtensionSettings,
|
||||
presetScripts: [
|
||||
createTavernRule("preset-blocked", "/G2/g", "P2", {
|
||||
promptOnly: true,
|
||||
}),
|
||||
],
|
||||
characters: [
|
||||
{
|
||||
avatar: "blocked.png",
|
||||
data: {
|
||||
extensions: {
|
||||
regex_scripts: [
|
||||
createTavernRule("character-blocked", "/P2/g", "C2", {
|
||||
promptOnly: true,
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
initializeHostAdapter({});
|
||||
|
||||
const disallowedOutput = applyTaskRegex(
|
||||
buildSettings(),
|
||||
"extract",
|
||||
"input.finalPrompt",
|
||||
"Gamma",
|
||||
{ entries: [] },
|
||||
"system",
|
||||
);
|
||||
assert.equal(disallowedOutput, "G2");
|
||||
|
||||
const disallowedInspect = inspectTaskRegexReuse(buildSettings(), "extract");
|
||||
assert.equal(disallowedInspect.activeRuleCount, 1);
|
||||
assert.equal(
|
||||
disallowedInspect.sources.find((source) => source.type === "preset")
|
||||
?.allowed,
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
disallowedInspect.sources.find((source) => source.type === "character")
|
||||
?.allowed,
|
||||
false,
|
||||
);
|
||||
assert.deepEqual(emptyBridgeDebug.entries[0].sourceCount, {
|
||||
tavern: 2,
|
||||
local: 1,
|
||||
});
|
||||
|
||||
const outputGuardSettings = {
|
||||
taskProfiles: {
|
||||
extract: {
|
||||
activeProfileId: "output-guard",
|
||||
profiles: [
|
||||
{
|
||||
id: "output-guard",
|
||||
name: "Output Guard",
|
||||
taskType: "extract",
|
||||
builtin: false,
|
||||
blocks: [],
|
||||
regex: {
|
||||
enabled: true,
|
||||
inheritStRegex: false,
|
||||
stages: {
|
||||
input: true,
|
||||
output: true,
|
||||
"output.rawResponse": true,
|
||||
},
|
||||
localRules: [
|
||||
createRule("display-only-output", "/美化/g", "<b>美化</b>", {
|
||||
destination: {
|
||||
prompt: false,
|
||||
display: true,
|
||||
},
|
||||
}),
|
||||
createRule("prompt-output", "/JSON/g", "DONE", {
|
||||
destination: {
|
||||
prompt: true,
|
||||
display: false,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
const tavernSemanticsSettings = buildSettings({
|
||||
sources: {
|
||||
global: true,
|
||||
preset: false,
|
||||
character: false,
|
||||
},
|
||||
};
|
||||
});
|
||||
setTestContext({
|
||||
extensionSettings: {
|
||||
regex: [
|
||||
createTavernRule("user-prompt-only", "/Alpha/g", "A", {
|
||||
placement: [PLACEMENT.USER_INPUT],
|
||||
promptOnly: true,
|
||||
}),
|
||||
createTavernRule("markdown-only", "/Alpha/g", "M", {
|
||||
placement: [PLACEMENT.USER_INPUT],
|
||||
markdownOnly: true,
|
||||
}),
|
||||
createTavernRule("output-only", "/Answer/g", "AI", {
|
||||
placement: [PLACEMENT.AI_OUTPUT],
|
||||
}),
|
||||
createTavernRule("world-info-only", "/Lore/g", "SYS", {
|
||||
placement: [PLACEMENT.WORLD_INFO],
|
||||
}),
|
||||
createTavernRule("recent-user", "/User/g", "U", {
|
||||
placement: [PLACEMENT.USER_INPUT],
|
||||
}),
|
||||
createTavernRule("recent-ai", "/Reply/g", "R", {
|
||||
placement: [PLACEMENT.AI_OUTPUT],
|
||||
}),
|
||||
],
|
||||
preset_allowed_regex: {},
|
||||
character_allowed_regex: [],
|
||||
},
|
||||
});
|
||||
initializeHostAdapter({});
|
||||
|
||||
assert.equal(
|
||||
applyTaskRegex(
|
||||
tavernSemanticsSettings,
|
||||
"extract",
|
||||
"input.userMessage",
|
||||
"Alpha",
|
||||
{ entries: [] },
|
||||
"user",
|
||||
),
|
||||
"Alpha",
|
||||
);
|
||||
assert.equal(
|
||||
applyTaskRegex(
|
||||
tavernSemanticsSettings,
|
||||
"extract",
|
||||
"input.finalPrompt",
|
||||
"Alpha",
|
||||
{ entries: [] },
|
||||
"user",
|
||||
),
|
||||
"A",
|
||||
);
|
||||
assert.equal(
|
||||
applyTaskRegex(
|
||||
tavernSemanticsSettings,
|
||||
"extract",
|
||||
"output.rawResponse",
|
||||
"Answer Lore",
|
||||
{ entries: [] },
|
||||
"assistant",
|
||||
),
|
||||
"AI Lore",
|
||||
);
|
||||
assert.equal(
|
||||
applyTaskRegex(
|
||||
tavernSemanticsSettings,
|
||||
"extract",
|
||||
"input.recentMessages",
|
||||
"User Reply Lore",
|
||||
{ entries: [] },
|
||||
"mixed",
|
||||
),
|
||||
"U R Lore",
|
||||
);
|
||||
|
||||
const outputGuardSettings = buildSettings({
|
||||
inheritStRegex: false,
|
||||
localRules: [
|
||||
createLocalRule("display-only-output", "/美化/g", "<b>美化</b>", {
|
||||
destination: {
|
||||
prompt: false,
|
||||
display: true,
|
||||
},
|
||||
}),
|
||||
createLocalRule("prompt-output", "/JSON/g", "DONE", {
|
||||
destination: {
|
||||
prompt: true,
|
||||
display: false,
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
const outputGuardDebug = { entries: [] };
|
||||
const outputGuardResult = applyTaskRegex(
|
||||
outputGuardSettings,
|
||||
@@ -317,127 +462,6 @@ try {
|
||||
["prompt-output"],
|
||||
);
|
||||
|
||||
const exactStageSettings = {
|
||||
taskProfilesVersion: 1,
|
||||
taskProfiles: {
|
||||
extract: {
|
||||
activeProfileId: "default",
|
||||
profiles: [
|
||||
{
|
||||
id: "default",
|
||||
taskType: "extract",
|
||||
regex: {
|
||||
enabled: true,
|
||||
inheritStRegex: false,
|
||||
sources: {
|
||||
global: false,
|
||||
preset: false,
|
||||
character: false,
|
||||
},
|
||||
stages: {
|
||||
output: true,
|
||||
"output.rawResponse": false,
|
||||
"output.beforeParse": true,
|
||||
},
|
||||
localRules: [
|
||||
createRule("exact-stage", "/JSON/g", "DONE", {
|
||||
destination: {
|
||||
prompt: true,
|
||||
display: false,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const exactStageDebug = { entries: [] };
|
||||
const exactStageResult = applyTaskRegex(
|
||||
exactStageSettings,
|
||||
"extract",
|
||||
"output.rawResponse",
|
||||
"JSON",
|
||||
exactStageDebug,
|
||||
"assistant",
|
||||
);
|
||||
assert.equal(exactStageResult, "JSON");
|
||||
assert.deepEqual(exactStageDebug.entries[0].appliedRules, []);
|
||||
|
||||
const legacyStageCompatibilitySettings = {
|
||||
taskProfilesVersion: 1,
|
||||
taskProfiles: {
|
||||
extract: {
|
||||
activeProfileId: "legacy-stage-compat",
|
||||
profiles: [
|
||||
{
|
||||
id: "legacy-stage-compat",
|
||||
taskType: "extract",
|
||||
regex: {
|
||||
enabled: true,
|
||||
inheritStRegex: false,
|
||||
sources: {
|
||||
global: false,
|
||||
preset: false,
|
||||
character: false,
|
||||
},
|
||||
stages: {
|
||||
input: true,
|
||||
output: true,
|
||||
"input.userMessage": false,
|
||||
"input.recentMessages": false,
|
||||
"input.candidateText": false,
|
||||
"input.finalPrompt": false,
|
||||
"output.rawResponse": false,
|
||||
"output.beforeParse": false,
|
||||
},
|
||||
localRules: [
|
||||
createRule("legacy-input-user", "/Alpha/g", "A1"),
|
||||
createRule("legacy-output-raw", "/Omega/g", "O1", {
|
||||
source: {
|
||||
user_input: false,
|
||||
ai_output: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const legacyStageInputDebug = { entries: [] };
|
||||
const legacyStageInputResult = applyTaskRegex(
|
||||
legacyStageCompatibilitySettings,
|
||||
"extract",
|
||||
"input.userMessage",
|
||||
"Alpha",
|
||||
legacyStageInputDebug,
|
||||
"user",
|
||||
);
|
||||
assert.equal(legacyStageInputResult, "A1");
|
||||
assert.deepEqual(
|
||||
legacyStageInputDebug.entries[0].appliedRules.map((item) => item.id),
|
||||
["legacy-input-user"],
|
||||
);
|
||||
|
||||
const legacyStageOutputDebug = { entries: [] };
|
||||
const legacyStageOutputResult = applyTaskRegex(
|
||||
legacyStageCompatibilitySettings,
|
||||
"extract",
|
||||
"output.rawResponse",
|
||||
"Omega",
|
||||
legacyStageOutputDebug,
|
||||
"assistant",
|
||||
);
|
||||
assert.equal(legacyStageOutputResult, "O1");
|
||||
assert.deepEqual(
|
||||
legacyStageOutputDebug.entries[0].appliedRules.map((item) => item.id),
|
||||
["legacy-output-raw"],
|
||||
);
|
||||
|
||||
console.log("task-regex tests passed");
|
||||
} finally {
|
||||
if (originalSillyTavern === undefined) {
|
||||
|
||||
Reference in New Issue
Block a user