Fix Tavern regex reuse inspection and matching

This commit is contained in:
Youzini-afk
2026-04-05 02:00:37 +08:00
parent 5cc33fabda
commit e9758feed3
5 changed files with 914 additions and 355 deletions

View File

@@ -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) {