test(planner): characterize plot fallback and handoff order

This commit is contained in:
youzini
2026-06-09 15:48:36 +00:00
parent 6ae507b47e
commit 43936b962b
3 changed files with 159 additions and 48 deletions

View File

@@ -0,0 +1,64 @@
export function extractLastNPlots(chat, n) {
if (!Array.isArray(chat) || chat.length === 0) return [];
const want = Math.max(0, Number(n) || 0);
if (!want) return [];
const plots = [];
const plotRe = /<plot\b[^>]*>[\s\S]*?<\/plot>/gi;
for (let i = chat.length - 1; i >= 0; i--) {
const text = chat[i]?.mes ?? '';
if (!text) continue;
const matches = [...text.matchAll(plotRe)];
for (let j = matches.length - 1; j >= 0; j--) {
plots.push(matches[j][0]);
if (plots.length >= want) return plots;
}
}
return plots;
}
export function formatPlotsBlock(plotList) {
if (!Array.isArray(plotList) || plotList.length === 0) return '';
const chrono = [...plotList].reverse();
const lines = [];
chrono.forEach((p, idx) => {
lines.push(`【plot -${chrono.length - idx}\n${p}`);
});
return `<previous_plots>\n${lines.join('\n\n')}\n</previous_plots>`;
}
export function applyPlannerResultAndSend({
textarea,
button,
rawUserInput = '',
filtered = '',
plannerRecall = null,
runtime = null,
plannerState = null,
} = {}) {
if (!textarea || !button) return { applied: false, reason: 'missing-target' };
const raw = String(rawUserInput ?? '').trim();
const merged = `${raw}\n\n${String(filtered ?? '')}`.trim();
textarea.value = merged;
if (plannerState && typeof plannerState === 'object') {
plannerState.lastInjectedText = merged;
}
let handoffPrepared = false;
if (runtime?.preparePlannerRecallHandoff && plannerRecall?.result) {
runtime.preparePlannerRecallHandoff({
rawUserInput: raw,
plannerAugmentedMessage: merged,
plannerRecall,
});
handoffPrepared = true;
}
if (plannerState && typeof plannerState === 'object') {
plannerState.bypassNextSend = true;
}
button.click();
return { applied: true, merged, handoffPrepared };
}

View File

@@ -1,6 +1,11 @@
import { extension_settings } from '../../../../extensions.js';
import { getRequestHeaders, saveSettingsDebounced, substituteParamsExtended } from '../../../../../script.js';
import { EnaPlannerStorage, migrateFromLWBIfNeeded } from './ena-planner-storage.js';
import {
applyPlannerResultAndSend,
extractLastNPlots,
formatPlotsBlock,
} from './ena-planner-runtime-utils.js';
import { DEFAULT_PROMPT_BLOCKS, BUILTIN_TEMPLATES } from './ena-planner-presets.js';
import {
createBuiltinPromptBlock,
@@ -794,38 +799,6 @@ function collectRecentChatSnippet(chat, maxMessages) {
* Plot extraction
* --------------------------
*/
function extractLastNPlots(chat, n) {
if (!Array.isArray(chat) || chat.length === 0) return [];
const want = Math.max(0, Number(n) || 0);
if (!want) return [];
const plots = [];
const plotRe = /<plot\b[^>]*>[\s\S]*?<\/plot>/gi;
for (let i = chat.length - 1; i >= 0; i--) {
const text = chat[i]?.mes ?? '';
if (!text) continue;
const matches = [...text.matchAll(plotRe)];
for (let j = matches.length - 1; j >= 0; j--) {
plots.push(matches[j][0]);
if (plots.length >= want) return plots;
}
}
return plots;
}
function formatPlotsBlock(plotList) {
if (!Array.isArray(plotList) || plotList.length === 0) return '';
// plotList is [newest, ..., oldest] from extractLastNPlots
// Reverse to chronological: oldest first, newest last
const chrono = [...plotList].reverse();
const lines = [];
chrono.forEach((p, idx) => {
lines.push(`【plot -${chrono.length - idx}\n${p}`);
});
return `<previous_plots>\n${lines.join('\n\n')}\n</previous_plots>`;
}
/**
* -------------------------
* Worldbook — read via ST API (like idle-watcher)
@@ -1941,22 +1914,18 @@ async function doInterceptAndPlanThenSend() {
ta.value = `${raw}\n\n${preview}`.trim();
}
});
const merged = `${raw}\n\n${filtered}`.trim();
ta.value = merged;
state.lastInjectedText = merged;
// Ordering requirement: register the one-shot planner recall handoff
// synchronously before btn.click(), with no await/timer hop in between.
if (_bmeRuntime?.preparePlannerRecallHandoff && plannerRecall?.result) {
_bmeRuntime.preparePlannerRecallHandoff({
rawUserInput: raw,
plannerAugmentedMessage: merged,
plannerRecall,
});
}
state.bypassNextSend = true;
btn.click();
// Ordering requirement: write the merged textarea, register the
// one-shot planner recall handoff synchronously, then click send with
// no await/timer hop in between.
applyPlannerResultAndSend({
textarea: ta,
button: btn,
rawUserInput: raw,
filtered,
plannerRecall,
runtime: _bmeRuntime,
plannerState: state,
});
} catch (err) {
ta.value = raw;
state.lastInjectedText = '';

View File

@@ -0,0 +1,78 @@
import assert from 'node:assert/strict';
import {
applyPlannerResultAndSend,
extractLastNPlots,
formatPlotsBlock,
} from '../ena-planner/ena-planner-runtime-utils.js';
{
const chat = [
{ mes: 'no plot here' },
{ mes: '<plot>old one</plot>\n<plot>old two</plot>' },
{ mes: 'assistant says <plot>new one</plot>' },
];
assert.deepEqual(extractLastNPlots(chat, 2), [
'<plot>new one</plot>',
'<plot>old two</plot>',
]);
assert.deepEqual(extractLastNPlots(chat, 0), []);
assert.deepEqual(extractLastNPlots(null, 3), []);
}
{
const block = formatPlotsBlock([
'<plot>newest</plot>',
'<plot>older</plot>',
]);
assert.equal(
block,
'<previous_plots>\n【plot -2】\n<plot>older</plot>\n\n【plot -1】\n<plot>newest</plot>\n</previous_plots>',
);
assert.equal(formatPlotsBlock([]), '');
}
{
const order = [];
const textarea = { value: 'raw' };
const button = { click: () => order.push('click') };
const plannerState = { bypassNextSend: false, lastInjectedText: '' };
const plannerRecall = { result: { selected: ['memory-a'] } };
const runtime = {
preparePlannerRecallHandoff(payload) {
order.push('handoff');
assert.equal(payload.rawUserInput, 'raw input');
assert.equal(payload.plannerAugmentedMessage, 'raw input\n\n<plot>next</plot>');
assert.equal(payload.plannerRecall, plannerRecall);
},
};
const result = applyPlannerResultAndSend({
textarea,
button,
rawUserInput: 'raw input',
filtered: '<plot>next</plot>',
plannerRecall,
runtime,
plannerState,
});
assert.deepEqual(order, ['handoff', 'click']);
assert.equal(result.applied, true);
assert.equal(result.handoffPrepared, true);
assert.equal(textarea.value, 'raw input\n\n<plot>next</plot>');
assert.equal(plannerState.lastInjectedText, textarea.value);
assert.equal(plannerState.bypassNextSend, true);
}
{
const order = [];
const result = applyPlannerResultAndSend({
textarea: null,
button: { click: () => order.push('click') },
});
assert.deepEqual(result, { applied: false, reason: 'missing-target' });
assert.deepEqual(order, []);
}
console.log('ena-planner-plots tests passed');