mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
Skip trivial user input recall flows
This commit is contained in:
363
tests/helpers/generation-recall-harness.mjs
Normal file
363
tests/helpers/generation-recall-harness.mjs
Normal file
@@ -0,0 +1,363 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import vm from "node:vm";
|
||||
import {
|
||||
onBeforeCombinePromptsController,
|
||||
onGenerationAfterCommandsController,
|
||||
onGenerationStartedController,
|
||||
onMessageReceivedController,
|
||||
onMessageSentController,
|
||||
} from "../../event-binding.js";
|
||||
import {
|
||||
GRAPH_LOAD_STATES,
|
||||
GRAPH_METADATA_KEY,
|
||||
GRAPH_PERSISTENCE_META_KEY,
|
||||
MODULE_NAME,
|
||||
} from "../../graph-persistence.js";
|
||||
import {
|
||||
buildPersistedRecallRecord,
|
||||
bumpPersistedRecallGenerationCount,
|
||||
readPersistedRecallFromUserMessage,
|
||||
resolveFinalRecallInjectionSource,
|
||||
writePersistedRecallToUserMessage,
|
||||
} from "../../recall-persistence.js";
|
||||
import {
|
||||
createGraphPersistenceState,
|
||||
createRecallInputRecord,
|
||||
createRecallRunResult,
|
||||
createUiStatus,
|
||||
getGenerationRecallHookStateFromResult,
|
||||
getRecallHookLabel,
|
||||
getStageNoticeDuration,
|
||||
getStageNoticeTitle,
|
||||
hashRecallInput,
|
||||
isFreshRecallInputRecord,
|
||||
isTerminalGenerationRecallHookState,
|
||||
isTrivialUserInput,
|
||||
normalizeRecallInputText,
|
||||
normalizeStageNoticeLevel,
|
||||
shouldRunRecallForTransaction,
|
||||
} from "../../ui-status.js";
|
||||
|
||||
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const indexPath = path.resolve(moduleDir, "../../index.js");
|
||||
|
||||
export function createGenerationRecallHarness(options = {}) {
|
||||
const { realApplyFinal = false } = options;
|
||||
return fs.readFile(indexPath, "utf8").then((source) => {
|
||||
const start = source.indexOf("const RECALL_INPUT_RECORD_TTL_MS = 60000;");
|
||||
const end = source.indexOf(
|
||||
'function onMessageReceived(messageId = null, type = "") {',
|
||||
);
|
||||
const endFallback = source.indexOf("async function runExtraction()");
|
||||
const resolvedEnd = end >= 0 ? end : endFallback;
|
||||
if (start < 0 || resolvedEnd < 0 || resolvedEnd <= start) {
|
||||
throw new Error("无法从 index.js 提取生成召回事务定义");
|
||||
}
|
||||
const snippet = source
|
||||
.slice(start, resolvedEnd)
|
||||
.replace(/^export\s+/gm, "");
|
||||
const context = {
|
||||
console,
|
||||
Date,
|
||||
Map,
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
__sendTextareaValue: "",
|
||||
document: {
|
||||
getElementById(id) {
|
||||
if (
|
||||
id === "send_textarea" &&
|
||||
typeof context.__sendTextareaValue === "string" &&
|
||||
context.__sendTextareaValue
|
||||
) {
|
||||
return { value: context.__sendTextareaValue };
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
result: null,
|
||||
currentGraph: {},
|
||||
_panelModule: null,
|
||||
defaultSettings: {},
|
||||
settings: {},
|
||||
graphPersistenceState: createGraphPersistenceState(),
|
||||
extension_settings: { [MODULE_NAME]: {} },
|
||||
extension_prompt_types: {
|
||||
NONE: 0,
|
||||
BEFORE_PROMPT: 1,
|
||||
IN_PROMPT: 2,
|
||||
IN_CHAT: 3,
|
||||
},
|
||||
extension_prompt_roles: {
|
||||
SYSTEM: 0,
|
||||
USER: 1,
|
||||
ASSISTANT: 2,
|
||||
},
|
||||
clampInt: (value, fallback = 0, min = 0, max = 9999) => {
|
||||
const numeric = Number(value);
|
||||
if (!Number.isFinite(numeric)) return fallback;
|
||||
return Math.min(max, Math.max(min, Math.trunc(numeric)));
|
||||
},
|
||||
getHostAdapter: () => null,
|
||||
migrateLegacyTaskProfiles: (settings = {}) => ({
|
||||
taskProfilesVersion: settings?.taskProfilesVersion || 0,
|
||||
taskProfiles: settings?.taskProfiles || {},
|
||||
}),
|
||||
refreshPanelLiveStateController: () => {
|
||||
context.refreshPanelCalls += 1;
|
||||
},
|
||||
isRecalling: false,
|
||||
getCurrentChatId: () => "chat-main",
|
||||
normalizeRecallInputText: (text = "") => String(text || "").trim(),
|
||||
isTrivialUserInput,
|
||||
getLatestUserChatMessage: (chat = []) =>
|
||||
[...chat].reverse().find((message) => message?.is_user) || null,
|
||||
getLastNonSystemChatMessage: (chat = []) =>
|
||||
[...chat].reverse().find((message) => !message?.is_system) || null,
|
||||
getSendTextareaValue: () => context.__sendTextareaValue,
|
||||
getRecallUserMessageSourceLabel: (source = "") => source,
|
||||
getRecallUserMessageSourceLabelController: (source = "") => source,
|
||||
buildRecallRecentMessages: (
|
||||
chat = [],
|
||||
_limit,
|
||||
syntheticUserMessage = "",
|
||||
) =>
|
||||
syntheticUserMessage
|
||||
? [...chat, { is_user: true, mes: syntheticUserMessage }]
|
||||
: [...chat],
|
||||
getContext: () => ({
|
||||
chatId: "chat-main",
|
||||
chat: context.chat,
|
||||
}),
|
||||
chat: [],
|
||||
runRecallCalls: [],
|
||||
runExtractionCalls: [],
|
||||
extractionIssues: [],
|
||||
applyFinalCalls: [],
|
||||
moduleInjectionCalls: [],
|
||||
recordedInjectionSnapshots: [],
|
||||
refreshPanelCalls: 0,
|
||||
hideScheduleCalls: [],
|
||||
createRecallInputRecord,
|
||||
createRecallRunResult,
|
||||
hashRecallInput,
|
||||
isFreshRecallInputRecord,
|
||||
isTerminalGenerationRecallHookState,
|
||||
shouldRunRecallForTransaction,
|
||||
getGenerationRecallHookStateFromResult,
|
||||
createUiStatus,
|
||||
createGraphPersistenceState,
|
||||
getRecallHookLabel,
|
||||
getStageNoticeTitle,
|
||||
getStageNoticeDuration,
|
||||
normalizeStageNoticeLevel,
|
||||
MODULE_NAME,
|
||||
GRAPH_LOAD_STATES,
|
||||
GRAPH_METADATA_KEY,
|
||||
GRAPH_PERSISTENCE_META_KEY,
|
||||
onBeforeCombinePromptsController,
|
||||
onGenerationAfterCommandsController,
|
||||
onGenerationStartedController,
|
||||
readPersistedRecallFromUserMessage,
|
||||
writePersistedRecallToUserMessage,
|
||||
buildPersistedRecallRecord,
|
||||
resolveFinalRecallInjectionSource,
|
||||
bumpPersistedRecallGenerationCount,
|
||||
applyModuleInjectionPrompt: (text = "") => {
|
||||
const normalizedText = String(text || "");
|
||||
context.moduleInjectionCalls.push(normalizedText);
|
||||
return {
|
||||
applied: Boolean(normalizedText.trim()),
|
||||
source: normalizedText.trim() ? "module-injection" : "rewrite-clear",
|
||||
mode: normalizedText.trim() ? "module-injection" : "rewrite-clear",
|
||||
};
|
||||
},
|
||||
getSettings: () => context.settings,
|
||||
$: () => ({}),
|
||||
triggerChatMetadataSave: () => {
|
||||
context.metadataSaveCalls += 1;
|
||||
return "debounced";
|
||||
},
|
||||
refreshPanelLiveState: () => {
|
||||
context.refreshPanelCalls += 1;
|
||||
},
|
||||
recordInjectionSnapshot: (_kind, snapshot = {}) => {
|
||||
context.recordedInjectionSnapshots.push({ ...snapshot });
|
||||
},
|
||||
schedulePersistedRecallMessageUiRefresh: () => {
|
||||
context.recallUiRefreshCalls += 1;
|
||||
},
|
||||
getMessageHideSettings: () => ({}),
|
||||
getHideRuntimeAdapters: () => ({}),
|
||||
scheduleHideSettingsApply: (...args) => {
|
||||
context.hideScheduleCalls.push(args);
|
||||
},
|
||||
estimateTokens: (text = "") =>
|
||||
normalizeRecallInputText(text)
|
||||
.split(/\s+/)
|
||||
.filter(Boolean).length || (normalizeRecallInputText(text) ? 1 : 0),
|
||||
resolveGenerationTargetUserMessageIndex: (
|
||||
chat = [],
|
||||
{ generationType } = {},
|
||||
) => {
|
||||
const normalized = String(generationType || "normal");
|
||||
if (!Array.isArray(chat) || chat.length === 0) return null;
|
||||
if (normalized === "normal")
|
||||
return chat[chat.length - 1]?.is_user ? chat.length - 1 : null;
|
||||
for (let index = chat.length - 1; index >= 0; index--)
|
||||
if (chat[index]?.is_user) return index;
|
||||
return null;
|
||||
},
|
||||
metadataSaveCalls: 0,
|
||||
recallUiRefreshCalls: 0,
|
||||
};
|
||||
vm.createContext(context);
|
||||
vm.runInContext(
|
||||
`${snippet}\nresult = { hashRecallInput, buildPreGenerationRecallKey, buildGenerationAfterCommandsRecallInput, buildNormalGenerationRecallInput, cleanupGenerationRecallTransactions, buildGenerationRecallTransactionId, beginGenerationRecallTransaction, markGenerationRecallTransactionHookState, shouldRunRecallForTransaction, createGenerationRecallContext, onGenerationStarted, onGenerationEnded, onGenerationAfterCommands, onBeforeCombinePrompts, applyFinalRecallInjectionForGeneration, ensurePersistedRecallRecordForGeneration, findRecentGenerationRecallTransactionForChat, getGenerationRecallTransactionResult, generationRecallTransactions, freezeHostGenerationInputSnapshot, consumeHostGenerationInputSnapshot, getPendingHostGenerationInputSnapshot, clearPendingHostGenerationInputSnapshot, recordRecallSendIntent, clearPendingRecallSendIntent, recordRecallSentUserMessage, getPendingRecallSendIntent: () => pendingRecallSendIntent, getLastRecallSentUserMessage: () => lastRecallSentUserMessage, getCurrentGenerationTrivialSkip, markCurrentGenerationTrivialSkip, clearCurrentGenerationTrivialSkip, consumeCurrentGenerationTrivialSkip, runPlannerRecallForEna, getGraphPersistenceState: () => graphPersistenceState, setGraphPersistenceState: (value = {}) => { graphPersistenceState = { ...graphPersistenceState, ...(value || {}) }; return graphPersistenceState; } };`,
|
||||
context,
|
||||
{ filename: indexPath },
|
||||
);
|
||||
Object.defineProperties(context, {
|
||||
pendingRecallSendIntent: {
|
||||
get() {
|
||||
return context.result.getPendingRecallSendIntent();
|
||||
},
|
||||
set(value) {
|
||||
if (value?.text) {
|
||||
context.result.recordRecallSendIntent(
|
||||
value?.text || "",
|
||||
value?.source,
|
||||
);
|
||||
return;
|
||||
}
|
||||
context.result.clearPendingRecallSendIntent();
|
||||
},
|
||||
configurable: true,
|
||||
},
|
||||
lastRecallSentUserMessage: {
|
||||
get() {
|
||||
return context.result.getLastRecallSentUserMessage();
|
||||
},
|
||||
set(value) {
|
||||
context.result.recordRecallSentUserMessage(
|
||||
value?.messageId,
|
||||
value?.text || "",
|
||||
value?.source,
|
||||
);
|
||||
},
|
||||
configurable: true,
|
||||
},
|
||||
});
|
||||
const originalApplyFinalRecallInjectionForGeneration =
|
||||
context.result.applyFinalRecallInjectionForGeneration;
|
||||
context.applyFinalRecallInjectionForGeneration = (payload = {}) => {
|
||||
context.applyFinalCalls.push({ ...payload });
|
||||
if (realApplyFinal) {
|
||||
return originalApplyFinalRecallInjectionForGeneration(payload);
|
||||
}
|
||||
return {
|
||||
source: "fresh",
|
||||
targetUserMessageIndex: null,
|
||||
};
|
||||
};
|
||||
context.runRecall = async (options = {}) => {
|
||||
context.runRecallCalls.push({ ...options });
|
||||
const overrideUserMessage = String(
|
||||
options.overrideUserMessage || options.userMessage || "",
|
||||
);
|
||||
return {
|
||||
status: "completed",
|
||||
didRecall: true,
|
||||
ok: true,
|
||||
injectionText: `注入:${overrideUserMessage}`,
|
||||
deliveryMode: String(options.deliveryMode || "immediate"),
|
||||
source: options.overrideSource,
|
||||
sourceLabel: options.overrideSourceLabel,
|
||||
reason: options.overrideReason,
|
||||
sourceCandidates: Array.isArray(options.sourceCandidates)
|
||||
? options.sourceCandidates.map((candidate) => ({ ...candidate }))
|
||||
: [],
|
||||
selectedNodeIds: ["node-test-1"],
|
||||
retrievalMeta: {
|
||||
vectorHits: 1,
|
||||
vectorMergedHits: 0,
|
||||
diffusionHits: 0,
|
||||
candidatePoolAfterDpp: 1,
|
||||
},
|
||||
llmMeta: {
|
||||
status: "disabled",
|
||||
reason: "test-disabled",
|
||||
candidatePool: 0,
|
||||
},
|
||||
stats: {
|
||||
coreCount: 1,
|
||||
recallCount: 1,
|
||||
},
|
||||
};
|
||||
};
|
||||
context.runExtraction = async (...args) => {
|
||||
context.runExtractionCalls.push(args);
|
||||
return {
|
||||
ok: true,
|
||||
};
|
||||
};
|
||||
context.invokeOnMessageSent = (messageId = null) =>
|
||||
onMessageSentController(
|
||||
{
|
||||
getContext: context.getContext,
|
||||
isTrivialUserInput,
|
||||
recordRecallSentUserMessage: context.result.recordRecallSentUserMessage,
|
||||
refreshPersistedRecallMessageUi: () => {
|
||||
context.recallUiRefreshCalls += 1;
|
||||
},
|
||||
},
|
||||
messageId,
|
||||
);
|
||||
context.invokeOnMessageReceived = (messageId = null, type = "") =>
|
||||
onMessageReceivedController(
|
||||
{
|
||||
console,
|
||||
consumeCurrentGenerationTrivialSkip:
|
||||
context.result.consumeCurrentGenerationTrivialSkip,
|
||||
createRecallInputRecord,
|
||||
getContext: context.getContext,
|
||||
getCurrentGraph: () => context.currentGraph,
|
||||
getGraphPersistenceState: () => context.result.getGraphPersistenceState(),
|
||||
getPendingHostGenerationInputSnapshot:
|
||||
context.result.getPendingHostGenerationInputSnapshot,
|
||||
getPendingRecallSendIntent: () => context.result.getPendingRecallSendIntent(),
|
||||
isAssistantChatMessage: (message) =>
|
||||
Boolean(message) && !message.is_user && !message.is_system,
|
||||
isFreshRecallInputRecord,
|
||||
isGraphMetadataWriteAllowed: () => true,
|
||||
syncGraphLoadFromLiveContext: () => {},
|
||||
maybeCaptureGraphShadowSnapshot: () => {},
|
||||
maybeFlushQueuedGraphPersist: () => {},
|
||||
notifyExtractionIssue: (message) => {
|
||||
context.extractionIssues.push(String(message || ""));
|
||||
},
|
||||
queueMicrotask: (task) => task(),
|
||||
runExtraction: context.runExtraction,
|
||||
refreshPersistedRecallMessageUi: () => {
|
||||
context.recallUiRefreshCalls += 1;
|
||||
},
|
||||
setPendingHostGenerationInputSnapshot: () => {},
|
||||
setPendingRecallSendIntent: (record) => {
|
||||
if (record?.text) {
|
||||
context.result.recordRecallSendIntent(
|
||||
record.text || "",
|
||||
record.source,
|
||||
);
|
||||
return;
|
||||
}
|
||||
context.result.clearPendingRecallSendIntent();
|
||||
},
|
||||
},
|
||||
messageId,
|
||||
type,
|
||||
);
|
||||
return context;
|
||||
});
|
||||
}
|
||||
@@ -66,6 +66,7 @@ import {
|
||||
onManualEvolveController,
|
||||
onManualSleepController,
|
||||
} from "../ui-actions-controller.js";
|
||||
import { createGenerationRecallHarness } from "./helpers/generation-recall-harness.mjs";
|
||||
|
||||
const waitForTick = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||
const extensionsShimSource = [
|
||||
@@ -277,255 +278,6 @@ function createBatchStageHarness() {
|
||||
});
|
||||
}
|
||||
|
||||
function createGenerationRecallHarness(options = {}) {
|
||||
const { realApplyFinal = false } = options;
|
||||
return fs.readFile(indexPath, "utf8").then((source) => {
|
||||
const start = source.indexOf("const RECALL_INPUT_RECORD_TTL_MS = 60000;");
|
||||
const end = source.indexOf(
|
||||
'function onMessageReceived(messageId = null, type = "") {',
|
||||
);
|
||||
const resolvedEnd = end >= 0 ? end : endFallback;
|
||||
if (start < 0 || resolvedEnd < 0 || resolvedEnd <= start) {
|
||||
throw new Error("无法从 index.js 提取生成召回事务定义");
|
||||
}
|
||||
const snippet = source
|
||||
.slice(start, resolvedEnd)
|
||||
.replace(/^export\s+/gm, "");
|
||||
const context = {
|
||||
console,
|
||||
Date,
|
||||
Map,
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
__sendTextareaValue: "",
|
||||
document: {
|
||||
getElementById(id) {
|
||||
if (
|
||||
id === "send_textarea" &&
|
||||
typeof context.__sendTextareaValue === "string" &&
|
||||
context.__sendTextareaValue
|
||||
) {
|
||||
return { value: context.__sendTextareaValue };
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
result: null,
|
||||
currentGraph: {},
|
||||
_panelModule: null,
|
||||
defaultSettings: {},
|
||||
extension_settings: { [MODULE_NAME]: {} },
|
||||
extension_prompt_types: {
|
||||
NONE: 0,
|
||||
BEFORE_PROMPT: 1,
|
||||
IN_PROMPT: 2,
|
||||
IN_CHAT: 3,
|
||||
},
|
||||
extension_prompt_roles: {
|
||||
SYSTEM: 0,
|
||||
USER: 1,
|
||||
ASSISTANT: 2,
|
||||
},
|
||||
clampInt: (value, fallback = 0, min = 0, max = 9999) => {
|
||||
const numeric = Number(value);
|
||||
if (!Number.isFinite(numeric)) return fallback;
|
||||
return Math.min(max, Math.max(min, Math.trunc(numeric)));
|
||||
},
|
||||
getHostAdapter: () => null,
|
||||
migrateLegacyTaskProfiles: (settings = {}) => ({
|
||||
taskProfilesVersion: settings?.taskProfilesVersion || 0,
|
||||
taskProfiles: settings?.taskProfiles || {},
|
||||
}),
|
||||
refreshPanelLiveStateController: () => {
|
||||
context.refreshPanelCalls += 1;
|
||||
},
|
||||
isRecalling: false,
|
||||
getCurrentChatId: () => "chat-main",
|
||||
normalizeRecallInputText: (text = "") => String(text || "").trim(),
|
||||
getLatestUserChatMessage: (chat = []) =>
|
||||
[...chat].reverse().find((message) => message?.is_user) || null,
|
||||
getLastNonSystemChatMessage: (chat = []) =>
|
||||
[...chat].reverse().find((message) => !message?.is_system) || null,
|
||||
getSendTextareaValue: () => context.__sendTextareaValue,
|
||||
getRecallUserMessageSourceLabel: (source = "") => source,
|
||||
getRecallUserMessageSourceLabelController: (source = "") => source,
|
||||
buildRecallRecentMessages: (
|
||||
chat = [],
|
||||
_limit,
|
||||
syntheticUserMessage = "",
|
||||
) =>
|
||||
syntheticUserMessage
|
||||
? [...chat, { is_user: true, mes: syntheticUserMessage }]
|
||||
: [...chat],
|
||||
getContext: () => ({
|
||||
chatId: "chat-main",
|
||||
chat: context.chat,
|
||||
}),
|
||||
chat: [],
|
||||
runRecallCalls: [],
|
||||
applyFinalCalls: [],
|
||||
moduleInjectionCalls: [],
|
||||
recordedInjectionSnapshots: [],
|
||||
refreshPanelCalls: 0,
|
||||
hideScheduleCalls: [],
|
||||
createRecallInputRecord,
|
||||
createRecallRunResult,
|
||||
hashRecallInput,
|
||||
normalizeRecallInputText,
|
||||
isFreshRecallInputRecord,
|
||||
isTerminalGenerationRecallHookState,
|
||||
shouldRunRecallForTransaction,
|
||||
getGenerationRecallHookStateFromResult,
|
||||
createUiStatus,
|
||||
createGraphPersistenceState,
|
||||
getRecallHookLabel,
|
||||
getStageNoticeTitle,
|
||||
getStageNoticeDuration,
|
||||
normalizeStageNoticeLevel,
|
||||
MODULE_NAME,
|
||||
GRAPH_LOAD_STATES,
|
||||
GRAPH_METADATA_KEY,
|
||||
GRAPH_PERSISTENCE_META_KEY,
|
||||
onBeforeCombinePromptsController,
|
||||
onGenerationAfterCommandsController,
|
||||
onGenerationStartedController,
|
||||
readPersistedRecallFromUserMessage,
|
||||
writePersistedRecallToUserMessage,
|
||||
buildPersistedRecallRecord,
|
||||
resolveFinalRecallInjectionSource,
|
||||
bumpPersistedRecallGenerationCount,
|
||||
applyModuleInjectionPrompt: (text = "") => {
|
||||
const normalizedText = String(text || "");
|
||||
context.moduleInjectionCalls.push(normalizedText);
|
||||
return {
|
||||
applied: Boolean(normalizedText.trim()),
|
||||
source: normalizedText.trim() ? "module-injection" : "rewrite-clear",
|
||||
mode: normalizedText.trim() ? "module-injection" : "rewrite-clear",
|
||||
};
|
||||
},
|
||||
getSettings: () => ({}),
|
||||
$: () => ({}),
|
||||
triggerChatMetadataSave: () => {
|
||||
context.metadataSaveCalls += 1;
|
||||
return "debounced";
|
||||
},
|
||||
refreshPanelLiveState: () => {
|
||||
context.refreshPanelCalls += 1;
|
||||
},
|
||||
recordInjectionSnapshot: (_kind, snapshot = {}) => {
|
||||
context.recordedInjectionSnapshots.push({ ...snapshot });
|
||||
},
|
||||
schedulePersistedRecallMessageUiRefresh: () => {
|
||||
context.recallUiRefreshCalls += 1;
|
||||
},
|
||||
getMessageHideSettings: () => ({}),
|
||||
getHideRuntimeAdapters: () => ({}),
|
||||
scheduleHideSettingsApply: (...args) => {
|
||||
context.hideScheduleCalls.push(args);
|
||||
},
|
||||
estimateTokens: (text = "") =>
|
||||
normalizeRecallInputText(text)
|
||||
.split(/\s+/)
|
||||
.filter(Boolean).length || (normalizeRecallInputText(text) ? 1 : 0),
|
||||
resolveGenerationTargetUserMessageIndex: (
|
||||
chat = [],
|
||||
{ generationType } = {},
|
||||
) => {
|
||||
const normalized = String(generationType || "normal");
|
||||
if (!Array.isArray(chat) || chat.length === 0) return null;
|
||||
if (normalized === "normal")
|
||||
return chat[chat.length - 1]?.is_user ? chat.length - 1 : null;
|
||||
for (let index = chat.length - 1; index >= 0; index--)
|
||||
if (chat[index]?.is_user) return index;
|
||||
return null;
|
||||
},
|
||||
metadataSaveCalls: 0,
|
||||
recallUiRefreshCalls: 0,
|
||||
};
|
||||
vm.createContext(context);
|
||||
vm.runInContext(
|
||||
`${snippet}\nresult = { hashRecallInput, buildPreGenerationRecallKey, buildGenerationAfterCommandsRecallInput, cleanupGenerationRecallTransactions, buildGenerationRecallTransactionId, beginGenerationRecallTransaction, markGenerationRecallTransactionHookState, shouldRunRecallForTransaction, createGenerationRecallContext, onGenerationStarted, onGenerationEnded, onGenerationAfterCommands, onBeforeCombinePrompts, applyFinalRecallInjectionForGeneration, ensurePersistedRecallRecordForGeneration, findRecentGenerationRecallTransactionForChat, getGenerationRecallTransactionResult, generationRecallTransactions, freezeHostGenerationInputSnapshot, consumeHostGenerationInputSnapshot, getPendingHostGenerationInputSnapshot, recordRecallSendIntent, recordRecallSentUserMessage, getPendingRecallSendIntent: () => pendingRecallSendIntent, getLastRecallSentUserMessage: () => lastRecallSentUserMessage };`,
|
||||
context,
|
||||
{ filename: indexPath },
|
||||
);
|
||||
Object.defineProperties(context, {
|
||||
pendingRecallSendIntent: {
|
||||
get() {
|
||||
return context.result.getPendingRecallSendIntent();
|
||||
},
|
||||
set(value) {
|
||||
context.result.recordRecallSendIntent(
|
||||
value?.text || "",
|
||||
value?.source,
|
||||
);
|
||||
},
|
||||
configurable: true,
|
||||
},
|
||||
lastRecallSentUserMessage: {
|
||||
get() {
|
||||
return context.result.getLastRecallSentUserMessage();
|
||||
},
|
||||
set(value) {
|
||||
context.result.recordRecallSentUserMessage(
|
||||
value?.messageId,
|
||||
value?.text || "",
|
||||
value?.source,
|
||||
);
|
||||
},
|
||||
configurable: true,
|
||||
},
|
||||
});
|
||||
const originalApplyFinalRecallInjectionForGeneration =
|
||||
context.result.applyFinalRecallInjectionForGeneration;
|
||||
context.applyFinalRecallInjectionForGeneration = (payload = {}) => {
|
||||
context.applyFinalCalls.push({ ...payload });
|
||||
if (realApplyFinal) {
|
||||
return originalApplyFinalRecallInjectionForGeneration(payload);
|
||||
}
|
||||
return {
|
||||
source: "fresh",
|
||||
targetUserMessageIndex: null,
|
||||
};
|
||||
};
|
||||
context.runRecall = async (options = {}) => {
|
||||
context.runRecallCalls.push({ ...options });
|
||||
const overrideUserMessage = String(
|
||||
options.overrideUserMessage || options.userMessage || "",
|
||||
);
|
||||
return {
|
||||
status: "completed",
|
||||
didRecall: true,
|
||||
ok: true,
|
||||
injectionText: `注入:${overrideUserMessage}`,
|
||||
deliveryMode: String(options.deliveryMode || "immediate"),
|
||||
source: options.overrideSource,
|
||||
sourceLabel: options.overrideSourceLabel,
|
||||
reason: options.overrideReason,
|
||||
sourceCandidates: Array.isArray(options.sourceCandidates)
|
||||
? options.sourceCandidates.map((candidate) => ({ ...candidate }))
|
||||
: [],
|
||||
selectedNodeIds: ["node-test-1"],
|
||||
retrievalMeta: {
|
||||
vectorHits: 1,
|
||||
vectorMergedHits: 0,
|
||||
diffusionHits: 0,
|
||||
candidatePoolAfterDpp: 1,
|
||||
},
|
||||
llmMeta: {
|
||||
status: "disabled",
|
||||
reason: "test-disabled",
|
||||
candidatePool: 0,
|
||||
},
|
||||
stats: {
|
||||
coreCount: 1,
|
||||
recallCount: 1,
|
||||
},
|
||||
};
|
||||
};
|
||||
return context;
|
||||
});
|
||||
}
|
||||
|
||||
function createHistoryRecoveryHarness() {
|
||||
return fs.readFile(indexPath, "utf8").then((source) => {
|
||||
const start = source.indexOf("async function recoverHistoryIfNeeded(");
|
||||
|
||||
@@ -22,12 +22,14 @@ const chat = [
|
||||
const hashes = snapshotProcessedMessageHashes(chat, 3);
|
||||
const cleanDetection = detectHistoryMutation(chat, {
|
||||
lastProcessedAssistantFloor: 3,
|
||||
processedMessageHashVersion: PROCESSED_MESSAGE_HASH_VERSION,
|
||||
processedMessageHashes: hashes,
|
||||
});
|
||||
assert.equal(cleanDetection.dirty, false);
|
||||
|
||||
const missingHashesDetection = detectHistoryMutation(chat, {
|
||||
lastProcessedAssistantFloor: 3,
|
||||
processedMessageHashVersion: PROCESSED_MESSAGE_HASH_VERSION,
|
||||
processedMessageHashes: {},
|
||||
});
|
||||
assert.equal(missingHashesDetection.dirty, true);
|
||||
@@ -35,6 +37,7 @@ assert.equal(missingHashesDetection.earliestAffectedFloor, 0);
|
||||
|
||||
const sparseHashesDetection = detectHistoryMutation(chat, {
|
||||
lastProcessedAssistantFloor: 3,
|
||||
processedMessageHashVersion: PROCESSED_MESSAGE_HASH_VERSION,
|
||||
processedMessageHashes: {
|
||||
0: hashes[0],
|
||||
2: hashes[2],
|
||||
@@ -48,6 +51,7 @@ const editedChat = structuredClone(chat);
|
||||
editedChat[1].mes = "我改过内容了。";
|
||||
const editedDetection = detectHistoryMutation(editedChat, {
|
||||
lastProcessedAssistantFloor: 3,
|
||||
processedMessageHashVersion: PROCESSED_MESSAGE_HASH_VERSION,
|
||||
processedMessageHashes: hashes,
|
||||
});
|
||||
assert.equal(editedDetection.dirty, true);
|
||||
@@ -58,6 +62,7 @@ bmeHiddenChat[1].is_system = true;
|
||||
bmeHiddenChat[1].extra = { __st_bme_hide_managed: true };
|
||||
const bmeHiddenDetection = detectHistoryMutation(bmeHiddenChat, {
|
||||
lastProcessedAssistantFloor: 3,
|
||||
processedMessageHashVersion: PROCESSED_MESSAGE_HASH_VERSION,
|
||||
processedMessageHashes: hashes,
|
||||
});
|
||||
assert.equal(bmeHiddenDetection.dirty, false);
|
||||
@@ -66,6 +71,7 @@ const realSystemFlipChat = structuredClone(chat);
|
||||
realSystemFlipChat[1].is_system = true;
|
||||
const realSystemFlipDetection = detectHistoryMutation(realSystemFlipChat, {
|
||||
lastProcessedAssistantFloor: 3,
|
||||
processedMessageHashVersion: PROCESSED_MESSAGE_HASH_VERSION,
|
||||
processedMessageHashes: hashes,
|
||||
});
|
||||
assert.equal(realSystemFlipDetection.dirty, false);
|
||||
@@ -91,6 +97,7 @@ assert.equal(migratedDetection.dirty, false);
|
||||
const truncatedChat = chat.slice(0, 2);
|
||||
const truncatedDetection = detectHistoryMutation(truncatedChat, {
|
||||
lastProcessedAssistantFloor: 3,
|
||||
processedMessageHashVersion: PROCESSED_MESSAGE_HASH_VERSION,
|
||||
processedMessageHashes: hashes,
|
||||
});
|
||||
assert.equal(truncatedDetection.dirty, true);
|
||||
|
||||
276
tests/trivial-user-input.mjs
Normal file
276
tests/trivial-user-input.mjs
Normal file
@@ -0,0 +1,276 @@
|
||||
// wired into npm run test:all
|
||||
import assert from "node:assert/strict";
|
||||
import { MODULE_NAME } from "../graph-persistence.js";
|
||||
import { isTrivialUserInput } from "../ui-status.js";
|
||||
import { createGenerationRecallHarness } from "./helpers/generation-recall-harness.mjs";
|
||||
|
||||
function assertEmptyRecallInputRecord(record) {
|
||||
assert.deepEqual(record, {
|
||||
text: "",
|
||||
hash: "",
|
||||
messageId: null,
|
||||
source: "",
|
||||
at: 0,
|
||||
});
|
||||
}
|
||||
|
||||
function testIsTrivialUserInputTable() {
|
||||
const cases = [
|
||||
["", true, "empty"],
|
||||
[" \n\t ", true, "empty"],
|
||||
["/echo hello", true, "slash-command"],
|
||||
["/", true, "slash-command"],
|
||||
[" /echo", true, "slash-command"],
|
||||
["a", true, "under-min-tokens"],
|
||||
["好", true, "under-min-tokens"],
|
||||
["ok", true, "under-min-tokens"],
|
||||
["ok a", false, ""],
|
||||
["好的", false, ""],
|
||||
["好的呀", false, ""],
|
||||
["hello world", false, ""],
|
||||
["你好", false, ""],
|
||||
];
|
||||
|
||||
for (const [input, trivial, reason] of cases) {
|
||||
const result = isTrivialUserInput(input);
|
||||
assert.equal(result.trivial, trivial, `trivial mismatch for ${JSON.stringify(input)}`);
|
||||
assert.equal(result.reason, reason, `reason mismatch for ${JSON.stringify(input)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function testSlashCommandSkipsRecallAndExtraction() {
|
||||
const harness = await createGenerationRecallHarness();
|
||||
harness.chat = [];
|
||||
harness.__sendTextareaValue = "/echo test";
|
||||
|
||||
const startResult = harness.result.onGenerationStarted("normal", {}, false);
|
||||
assert.equal(startResult, null);
|
||||
assertEmptyRecallInputRecord(harness.result.getPendingHostGenerationInputSnapshot());
|
||||
assertEmptyRecallInputRecord(harness.pendingRecallSendIntent);
|
||||
assert.equal(
|
||||
harness.result.getCurrentGenerationTrivialSkip()?.generationStartMinChatIndex,
|
||||
0,
|
||||
);
|
||||
|
||||
await harness.result.onGenerationAfterCommands("normal", {}, false);
|
||||
assert.equal(harness.runRecallCalls.length, 0);
|
||||
|
||||
const beforeCombine = await harness.result.onBeforeCombinePrompts();
|
||||
assert.deepEqual(beforeCombine, {
|
||||
skipped: true,
|
||||
reason: "trivial:slash-command",
|
||||
});
|
||||
assert.equal(harness.runRecallCalls.length, 0);
|
||||
|
||||
harness.chat.push({ is_user: false, mes: "assistant reply" });
|
||||
harness.invokeOnMessageReceived(0, "");
|
||||
assert.equal(harness.runExtractionCalls.length, 0);
|
||||
assert.equal(harness.result.getCurrentGenerationTrivialSkip(), null);
|
||||
}
|
||||
|
||||
async function testUnderMinTokensSkipsRecallAndExtraction() {
|
||||
const harness = await createGenerationRecallHarness();
|
||||
harness.chat = [];
|
||||
harness.__sendTextareaValue = "a";
|
||||
|
||||
const startResult = harness.result.onGenerationStarted("normal", {}, false);
|
||||
assert.equal(startResult, null);
|
||||
assert.equal(
|
||||
harness.result.getCurrentGenerationTrivialSkip()?.reason,
|
||||
"under-min-tokens",
|
||||
);
|
||||
|
||||
await harness.result.onGenerationAfterCommands("normal", {}, false);
|
||||
assert.equal(harness.runRecallCalls.length, 0);
|
||||
|
||||
harness.chat.push({ is_user: false, mes: "assistant reply" });
|
||||
harness.invokeOnMessageReceived(0, "");
|
||||
assert.equal(harness.runExtractionCalls.length, 0);
|
||||
assert.equal(harness.result.getCurrentGenerationTrivialSkip(), null);
|
||||
}
|
||||
|
||||
async function testNormalInputStillRecalls() {
|
||||
const harness = await createGenerationRecallHarness();
|
||||
harness.chat = [];
|
||||
harness.__sendTextareaValue = "好的呀";
|
||||
|
||||
const snapshot = harness.result.onGenerationStarted("normal", {}, false);
|
||||
assert.equal(snapshot?.text, "好的呀");
|
||||
assert.equal(harness.result.getCurrentGenerationTrivialSkip(), null);
|
||||
|
||||
const beforeCombine = await harness.result.onBeforeCombinePrompts();
|
||||
assert.equal(beforeCombine?.source, "fresh");
|
||||
assert.equal(harness.runRecallCalls.length, 1);
|
||||
assert.equal(harness.runRecallCalls[0].overrideUserMessage, "好的呀");
|
||||
}
|
||||
|
||||
async function testSentinelBlocksHistoryFallback() {
|
||||
const harness = await createGenerationRecallHarness();
|
||||
harness.chat = [{ is_user: true, mes: "真实旧消息" }];
|
||||
harness.pendingRecallSendIntent = {
|
||||
text: "/echo hidden",
|
||||
source: "send-button",
|
||||
at: Date.now(),
|
||||
};
|
||||
|
||||
const beforeCombine = await harness.result.onBeforeCombinePrompts();
|
||||
assert.deepEqual(beforeCombine, {
|
||||
skipped: true,
|
||||
reason: "trivial:slash-command",
|
||||
});
|
||||
assert.equal(harness.runRecallCalls.length, 0);
|
||||
}
|
||||
|
||||
async function testAfterCommandsTrivialSentinelMarksExtractionBypass() {
|
||||
const harness = await createGenerationRecallHarness();
|
||||
harness.chat = [{ is_user: true, mes: "/echo from chat tail" }];
|
||||
|
||||
await harness.result.onGenerationAfterCommands("normal", {}, false);
|
||||
assert.equal(harness.runRecallCalls.length, 0);
|
||||
assert.equal(
|
||||
harness.result.getCurrentGenerationTrivialSkip()?.generationStartMinChatIndex,
|
||||
1,
|
||||
);
|
||||
|
||||
harness.chat.push({ is_user: false, mes: "assistant after bypass flag" });
|
||||
harness.invokeOnMessageReceived(1, "");
|
||||
assert.equal(harness.runExtractionCalls.length, 0);
|
||||
assert.equal(harness.result.getCurrentGenerationTrivialSkip(), null);
|
||||
}
|
||||
|
||||
async function testPlannerRecallTrivialAndNonTrivialPaths() {
|
||||
const harness = await createGenerationRecallHarness();
|
||||
|
||||
let recall = await harness.result.runPlannerRecallForEna({
|
||||
rawUserInput: "",
|
||||
});
|
||||
assert.equal(recall.reason, "trivial-user-input:empty");
|
||||
|
||||
recall = await harness.result.runPlannerRecallForEna({
|
||||
rawUserInput: "/echo",
|
||||
});
|
||||
assert.equal(recall.reason, "trivial-user-input:slash-command");
|
||||
|
||||
harness.extension_settings[MODULE_NAME] = {
|
||||
enabled: true,
|
||||
recallEnabled: true,
|
||||
};
|
||||
harness.result.setGraphPersistenceState({
|
||||
loadState: "loaded",
|
||||
dbReady: true,
|
||||
});
|
||||
harness.currentGraph = {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
historyState: {},
|
||||
};
|
||||
recall = await harness.result.runPlannerRecallForEna({
|
||||
rawUserInput: "好的呀",
|
||||
});
|
||||
assert.equal(recall.reason, "graph-empty");
|
||||
}
|
||||
|
||||
async function testOnMessageSentSkipsTrivialText() {
|
||||
const harness = await createGenerationRecallHarness();
|
||||
harness.chat = [{ is_user: true, mes: "/echo" }];
|
||||
|
||||
harness.invokeOnMessageSent(0);
|
||||
|
||||
assert.equal(harness.lastRecallSentUserMessage.text, "");
|
||||
}
|
||||
|
||||
async function testNonTrivialGenerationClearsResidualTrivialSkip() {
|
||||
const harness = await createGenerationRecallHarness();
|
||||
harness.chat = [];
|
||||
harness.__sendTextareaValue = "/echo";
|
||||
harness.result.onGenerationStarted("normal", {}, false);
|
||||
assert.ok(harness.result.getCurrentGenerationTrivialSkip());
|
||||
|
||||
harness.__sendTextareaValue = "hello world";
|
||||
const snapshot = harness.result.onGenerationStarted("normal", {}, false);
|
||||
assert.equal(snapshot?.text, "hello world");
|
||||
assert.equal(harness.result.getCurrentGenerationTrivialSkip(), null);
|
||||
|
||||
harness.chat.push({ is_user: false, mes: "assistant after non-trivial" });
|
||||
harness.invokeOnMessageReceived(0, "");
|
||||
await Promise.resolve();
|
||||
assert.equal(harness.runExtractionCalls.length, 1);
|
||||
}
|
||||
|
||||
async function testNonTargetMessageIdDoesNotConsumeFlag() {
|
||||
const harness = await createGenerationRecallHarness();
|
||||
harness.chat = [
|
||||
{ is_user: true, mes: "u0" },
|
||||
{ is_user: false, mes: "a1" },
|
||||
{ is_user: true, mes: "u2" },
|
||||
{ is_user: false, mes: "old assistant" },
|
||||
{ is_user: true, mes: "u4" },
|
||||
];
|
||||
harness.__sendTextareaValue = "/echo";
|
||||
harness.result.onGenerationStarted("normal", {}, false);
|
||||
assert.equal(
|
||||
harness.result.getCurrentGenerationTrivialSkip()?.generationStartMinChatIndex,
|
||||
5,
|
||||
);
|
||||
|
||||
harness.invokeOnMessageReceived(3, "");
|
||||
await Promise.resolve();
|
||||
assert.equal(harness.runExtractionCalls.length, 1);
|
||||
assert.ok(harness.result.getCurrentGenerationTrivialSkip());
|
||||
|
||||
harness.chat.push({ is_user: false, mes: "target assistant" });
|
||||
harness.invokeOnMessageReceived(5, "");
|
||||
assert.equal(harness.runExtractionCalls.length, 1);
|
||||
assert.equal(harness.result.getCurrentGenerationTrivialSkip(), null);
|
||||
}
|
||||
|
||||
async function testNullMessageIdFallsBackToLastAssistantIndex() {
|
||||
const harness = await createGenerationRecallHarness();
|
||||
harness.chat = [
|
||||
{ is_user: true, mes: "u0" },
|
||||
{ is_user: false, mes: "a1" },
|
||||
{ is_user: true, mes: "u2" },
|
||||
{ is_user: false, mes: "a3" },
|
||||
{ is_user: true, mes: "u4" },
|
||||
];
|
||||
harness.__sendTextareaValue = "/echo";
|
||||
harness.result.onGenerationStarted("normal", {}, false);
|
||||
|
||||
harness.chat.push({ is_user: false, mes: "latest assistant" });
|
||||
harness.invokeOnMessageReceived(null, "");
|
||||
assert.equal(harness.runExtractionCalls.length, 0);
|
||||
assert.equal(harness.result.getCurrentGenerationTrivialSkip(), null);
|
||||
}
|
||||
|
||||
async function testSkipFlagTtlExpires() {
|
||||
const harness = await createGenerationRecallHarness();
|
||||
harness.result.markCurrentGenerationTrivialSkip({
|
||||
reason: "slash-command",
|
||||
chatId: "chat-main",
|
||||
chatLength: 2,
|
||||
});
|
||||
const originalNow = Date.now;
|
||||
Date.now = () => originalNow() + 60001;
|
||||
try {
|
||||
assert.equal(harness.result.consumeCurrentGenerationTrivialSkip(2), false);
|
||||
assert.equal(harness.result.getCurrentGenerationTrivialSkip(), null);
|
||||
} finally {
|
||||
Date.now = originalNow;
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.resolve();
|
||||
testIsTrivialUserInputTable();
|
||||
await testSlashCommandSkipsRecallAndExtraction();
|
||||
await testUnderMinTokensSkipsRecallAndExtraction();
|
||||
await testNormalInputStillRecalls();
|
||||
await testSentinelBlocksHistoryFallback();
|
||||
await testAfterCommandsTrivialSentinelMarksExtractionBypass();
|
||||
await testPlannerRecallTrivialAndNonTrivialPaths();
|
||||
await testOnMessageSentSkipsTrivialText();
|
||||
await testNonTrivialGenerationClearsResidualTrivialSkip();
|
||||
await testNonTargetMessageIdDoesNotConsumeFlag();
|
||||
await testNullMessageIdFallsBackToLastAssistantIndex();
|
||||
await testSkipFlagTtlExpires();
|
||||
|
||||
console.log("trivial-user-input tests passed");
|
||||
Reference in New Issue
Block a user