Files
ST-Bionic-Memory-Ecology/tests/p0-regressions.mjs

2494 lines
76 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 fs from "node:fs/promises";
import { createRequire, registerHooks } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
import vm from "node:vm";
import {
BATCH_STAGE_ORDER,
BATCH_STAGE_SEVERITY,
clampFloat,
clampInt,
createBatchStageStatus,
createBatchStatusSkeleton,
createGraphPersistenceState,
createRecallInputRecord,
createRecallRunResult,
createUiStatus,
finalizeBatchStatus,
formatRecallContextLine,
getGenerationRecallHookStateFromResult,
getRecallHookLabel,
getStageNoticeDuration,
getStageNoticeTitle,
hashRecallInput,
isFreshRecallInputRecord,
isTerminalGenerationRecallHookState,
normalizeRecallInputText,
normalizeStageNoticeLevel,
pushBatchStageArtifact,
setBatchStageOutcome,
shouldRunRecallForTransaction,
} from "../ui-status.js";
import {
cloneRuntimeDebugValue,
GRAPH_LOAD_STATES,
GRAPH_METADATA_KEY,
GRAPH_PERSISTENCE_META_KEY,
GRAPH_PERSISTENCE_SESSION_ID,
MODULE_NAME,
readGraphShadowSnapshot,
stampGraphPersistenceMeta,
writeChatMetadataPatch,
writeGraphShadowSnapshot,
} from "../graph-persistence.js";
import {
buildExtractionMessages,
clampRecoveryStartFloor,
getAssistantTurns,
getChatIndexForAssistantSeq,
getChatIndexForPlayableSeq,
getMinExtractableAssistantFloor,
isAssistantChatMessage,
pruneProcessedMessageHashesFromFloor,
rollbackAffectedJournals,
} from "../chat-history.js";
import {
onBeforeCombinePromptsController,
onGenerationAfterCommandsController,
} from "../event-binding.js";
import { onRerollController } from "../extraction-controller.js";
import {
buildPersistedRecallRecord,
readPersistedRecallFromUserMessage,
removePersistedRecallFromUserMessage,
resolveFinalRecallInjectionSource,
resolveGenerationTargetUserMessageIndex,
writePersistedRecallToUserMessage,
bumpPersistedRecallGenerationCount,
markPersistedRecallManualEdit,
} from "../recall-persistence.js";
const waitForTick = () => new Promise((resolve) => setTimeout(resolve, 0));
const extensionsShimSource = [
"export const extension_settings = globalThis.__p0ExtensionSettings || {};",
"export function getContext(...args) {",
" return globalThis.SillyTavern?.getContext?.(...args) || null;",
"}",
].join("\n");
const scriptShimSource = [
"export function getRequestHeaders() {",
" return { 'Content-Type': 'application/json' };",
"}",
].join("\n");
const openAiShimSource = [
"export const chat_completion_sources = { CUSTOM: 'custom', OPENAI: 'openai' };",
"export async function sendOpenAIRequest(...args) {",
" if (typeof globalThis.__p0SendOpenAIRequest === 'function') {",
" return await globalThis.__p0SendOpenAIRequest(...args);",
" }",
" return { choices: [{ message: { content: '{}' } }] };",
"}",
].join("\n");
const extensionsShimUrl = `data:text/javascript,${encodeURIComponent(
extensionsShimSource,
)}`;
const scriptShimUrl = `data:text/javascript,${encodeURIComponent(
scriptShimSource,
)}`;
const openAiShimUrl = `data:text/javascript,${encodeURIComponent(
openAiShimSource,
)}`;
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
const indexPath = path.resolve(moduleDir, "../index.js");
registerHooks({
resolve(specifier, context, nextResolve) {
if (
specifier === "../../../extensions.js" ||
specifier === "../../../../extensions.js"
) {
return {
shortCircuit: true,
url: extensionsShimUrl,
};
}
if (specifier === "../../../../script.js") {
return {
shortCircuit: true,
url: scriptShimUrl,
};
}
if (specifier === "../../../openai.js") {
return {
shortCircuit: true,
url: openAiShimUrl,
};
}
return nextResolve(specifier, context);
},
});
const require = createRequire(import.meta.url);
const originalRequire = globalThis.require;
const originalP0ExtensionSettings = globalThis.__p0ExtensionSettings;
const originalP0SendOpenAIRequest = globalThis.__p0SendOpenAIRequest;
const originalStBmeTestOverrides = globalThis.__stBmeTestOverrides;
globalThis.__p0ExtensionSettings = {
st_bme: {},
};
globalThis.__stBmeTestOverrides = {};
globalThis.require = require;
const { createEmptyGraph, createNode, addNode, createEdge, addEdge } =
await import("../graph.js");
const { compressType } = await import("../compressor.js");
const { syncGraphVectorIndex } = await import("../vector-index.js");
const { extractMemories } = await import("../extractor.js");
const { consolidateMemories } = await import("../consolidator.js");
const {
createBatchJournalEntry,
buildReverseJournalRecoveryPlan,
normalizeGraphRuntimeState,
rollbackBatch,
} = await import("../runtime-state.js");
const { createDefaultTaskProfiles } = await import("../prompt-profiles.js");
const extensionsApi = await import("../../../../extensions.js");
const llm = await import("../llm.js");
const embedding = await import("../embedding.js");
if (originalRequire === undefined) {
delete globalThis.require;
} else {
globalThis.require = originalRequire;
}
if (originalP0ExtensionSettings === undefined) {
delete globalThis.__p0ExtensionSettings;
} else {
globalThis.__p0ExtensionSettings = originalP0ExtensionSettings;
}
if (originalP0SendOpenAIRequest === undefined) {
delete globalThis.__p0SendOpenAIRequest;
} else {
globalThis.__p0SendOpenAIRequest = originalP0SendOpenAIRequest;
}
if (originalStBmeTestOverrides === undefined) {
delete globalThis.__stBmeTestOverrides;
} else {
globalThis.__stBmeTestOverrides = originalStBmeTestOverrides;
}
const schema = [
{
id: "event",
label: "事件",
columns: [
{ name: "title" },
{ name: "summary" },
{ name: "participants" },
{ name: "status" },
],
compression: {
mode: "hierarchical",
threshold: 2,
},
},
{
id: "character",
label: "角色",
columns: [{ name: "name" }, { name: "state" }],
latestOnly: true,
},
];
function createBatchStageHarness() {
return fs.readFile(indexPath, "utf8").then((source) => {
const marker = "function notifyHistoryDirty(dirtyFrom, reason) {";
const start = source.indexOf("function shouldAdvanceProcessedHistory(");
const end = source.indexOf(marker);
if (start < 0 || end < 0 || end <= start) {
throw new Error("无法从 index.js 提取批次状态机定义");
}
const snippet = source.slice(start, end).replace(/^export\s+/gm, "");
const context = {
console,
result: null,
extractionCount: 0,
currentGraph: null,
consolidateMemories: async () => {},
generateSynopsis: async () => {},
generateReflection: async () => {},
sleepCycle: () => {},
compressAll: async () => ({ created: 0, archived: 0 }),
syncVectorState: async () => ({
insertedHashes: [],
stats: { pending: 0 },
}),
getSchema: () => schema,
getEmbeddingConfig: () => null,
getVectorIndexStats: () => ({ pending: 0 }),
updateLastExtractedItems: () => {},
ensureCurrentGraphRuntimeState: () => {},
throwIfAborted: () => {},
isAbortError: () => false,
createAbortError: (message) => new Error(message),
BATCH_STAGE_ORDER,
BATCH_STAGE_SEVERITY,
createBatchStageStatus,
createBatchStatusSkeleton,
setBatchStageOutcome,
pushBatchStageArtifact,
finalizeBatchStatus,
createUiStatus,
};
vm.createContext(context);
vm.runInContext(
`${snippet}\nresult = { createBatchStatusSkeleton, finalizeBatchStatus, handleExtractionSuccess, setBatchStageOutcome, shouldAdvanceProcessedHistory };`,
context,
{ filename: indexPath },
);
return context;
});
}
function createGenerationRecallHarness() {
return fs.readFile(indexPath, "utf8").then((source) => {
const start = source.indexOf("const RECALL_INPUT_RECORD_TTL_MS = 60000;");
const end = source.indexOf("function onMessageReceived() {");
if (start < 0 || end < 0 || end <= start) {
throw new Error("无法从 index.js 提取生成召回事务定义");
}
const snippet = source.slice(start, end).replace(/^export\s+/gm, "");
const context = {
console,
Date,
Map,
setTimeout,
clearTimeout,
document: {
getElementById() {
return null;
},
},
result: null,
currentGraph: {},
isRecalling: false,
getCurrentChatId: () => "chat-main",
normalizeRecallInputText: (text = "") => String(text || "").trim(),
pendingRecallSendIntent: { text: "", hash: "", at: 0 },
lastRecallSentUserMessage: { text: "", hash: "", at: 0 },
getLatestUserChatMessage: (chat = []) =>
[...chat].reverse().find((message) => message?.is_user) || null,
getLastNonSystemChatMessage: (chat = []) =>
[...chat].reverse().find((message) => !message?.is_system) || null,
getSendTextareaValue: () => "",
getRecallUserMessageSourceLabel: (source = "") => source,
buildRecallRecentMessages: (
chat = [],
_limit,
syntheticUserMessage = "",
) =>
syntheticUserMessage
? [...chat, { is_user: true, mes: syntheticUserMessage }]
: [...chat],
getContext: () => ({
chatId: "chat-main",
chat: context.chat,
}),
chat: [],
runRecallCalls: [],
applyFinalCalls: [],
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,
readPersistedRecallFromUserMessage: () => null,
resolveFinalRecallInjectionSource: ({ freshRecallResult = null } = {}) => ({
source: freshRecallResult?.didRecall ? "fresh" : "none",
injectionText: String(freshRecallResult?.injectionText || ""),
record: null,
}),
bumpPersistedRecallGenerationCount: () => null,
applyModuleInjectionPrompt: () => ({}),
getSettings: () => ({}),
triggerChatMetadataSave: () => "debounced",
refreshPanelLiveState: () => {},
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;
},
};
vm.createContext(context);
vm.runInContext(
`${snippet}\nresult = { hashRecallInput, buildPreGenerationRecallKey, buildGenerationAfterCommandsRecallInput, cleanupGenerationRecallTransactions, buildGenerationRecallTransactionId, beginGenerationRecallTransaction, markGenerationRecallTransactionHookState, shouldRunRecallForTransaction, createGenerationRecallContext, onGenerationAfterCommands, onBeforeCombinePrompts, generationRecallTransactions };`,
context,
{ filename: indexPath },
);
context.applyFinalRecallInjectionForGeneration = (payload = {}) => {
context.applyFinalCalls.push({ ...payload });
return {
source: "fresh",
targetUserMessageIndex: null,
};
};
context.runRecall = async (options = {}) => {
context.runRecallCalls.push({ ...options });
return { status: "completed", didRecall: true, ok: true };
};
return context;
});
}
function createRerollHarness() {
return fs.readFile(indexPath, "utf8").then((source) => {
const rollbackStart = source.indexOf("async function rollbackGraphForReroll(");
const rollbackEnd = source.indexOf("async function recoverHistoryIfNeeded(");
const rerollStart = source.indexOf("async function onReroll(");
const rerollEnd = source.indexOf("async function onManualSleep()");
if (
rollbackStart < 0 ||
rollbackEnd < 0 ||
rerollStart < 0 ||
rerollEnd < 0 ||
rollbackEnd <= rollbackStart ||
rerollEnd <= rerollStart
) {
throw new Error("无法从 index.js 提取 reroll 定义");
}
const snippet = [source.slice(rollbackStart, rollbackEnd), source.slice(rerollStart, rerollEnd)]
.join("\n")
.replace(/^export\s+/gm, "");
const context = {
console,
Date,
result: null,
currentGraph: null,
isExtracting: false,
extractionCount: 0,
lastExtractedItems: ["stale-node"],
lastExtractionStatus: { level: "idle" },
chat: [],
embeddingConfig: { mode: "backend" },
rollbackAffectedJournalsCalls: [],
deletedHashesCalls: [],
prepareVectorStateCalls: [],
recoveryPlans: [],
saveGraphToChatCalls: 0,
refreshPanelCalls: 0,
clearInjectionCalls: 0,
onManualExtractCalls: 0,
clearedHistoryDirty: null,
postRollbackGraph: null,
manualExtractLevel: "success",
ensureCurrentGraphRuntimeState() {
return context.currentGraph;
},
getContext() {
return {
chat: context.chat,
chatId: "chat-main",
};
},
getCurrentChatId() {
return "chat-main";
},
getAssistantTurns(chat = []) {
return chat.flatMap((message, index) =>
!message?.is_user && !message?.is_system ? [index] : [],
);
},
getLastProcessedAssistantFloor() {
return Number(
context.currentGraph?.historyState?.lastProcessedAssistantFloor ?? -1,
);
},
findJournalRecoveryPoint(graph, floor) {
return context.findJournalRecoveryPointImpl(graph, floor);
},
findJournalRecoveryPointImpl() {
return null;
},
buildReverseJournalRecoveryPlan(...args) {
return context.buildReverseJournalRecoveryPlanImpl(...args);
},
buildReverseJournalRecoveryPlanImpl() {
return {
backendDeleteHashes: [],
replayRequiredNodeIds: [],
pendingRepairFromFloor: null,
legacyGapFallback: false,
dirtyReason: "history-recovery-replay",
};
},
rollbackAffectedJournals(graph, journals) {
context.rollbackAffectedJournalsCalls.push({ graph, journals });
if (context.postRollbackGraph) {
context.currentGraph = context.postRollbackGraph;
}
},
normalizeGraphRuntimeState(graph) {
return graph;
},
getEmbeddingConfig() {
return context.embeddingConfig;
},
applyRecoveryPlanToVectorState(plan, floor) {
context.recoveryPlans.push({ plan, floor });
},
isBackendVectorConfig(config) {
return config?.mode === "backend";
},
async deleteBackendVectorHashesForRecovery(...args) {
context.deletedHashesCalls.push(args);
},
pruneProcessedMessageHashesFromFloor(graph, fromFloor) {
return pruneProcessedMessageHashesFromFloor(graph, fromFloor);
},
async prepareVectorStateForReplay(...args) {
context.prepareVectorStateCalls.push(args);
},
clearHistoryDirty(graph, result) {
context.clearedHistoryDirty = result;
graph.historyState ||= {};
graph.historyState.historyDirtyFrom = null;
graph.historyState.lastRecoveryResult = result;
},
buildRecoveryResult(status, extra = {}) {
return {
status,
...extra,
};
},
saveGraphToChat() {
context.saveGraphToChatCalls += 1;
return true;
},
refreshPanelLiveState() {
context.refreshPanelCalls += 1;
},
setRuntimeStatus(text, meta = "", level = "info") {
context.runtimeStatus = { text, meta, level };
},
clearInjectionState() {
context.clearInjectionCalls += 1;
},
async onManualExtract() {
context.onManualExtractCalls += 1;
context.lastExtractionStatus = { level: context.manualExtractLevel };
},
ensureGraphMutationReady() {
return true;
},
getGraphMutationBlockReason() {
return "graph-not-ready";
},
graphPersistenceState: {
loadState: "loaded",
},
createUiStatus,
onRerollController,
isAbortError: (e) => e?.name === "AbortError",
assertRecoveryChatStillActive() {
// no-op in test
},
toastr: {
info() {},
error() {},
success() {},
},
};
vm.createContext(context);
vm.runInContext(
`${snippet}\nresult = { rollbackGraphForReroll, onReroll };`,
context,
{ filename: indexPath },
);
return context;
});
}
function pushTestOverrides(patch = {}) {
const previous = globalThis.__stBmeTestOverrides || {};
globalThis.__stBmeTestOverrides = {
...previous,
...patch,
llm: {
...(previous.llm || {}),
...(patch.llm || {}),
},
embedding: {
...(previous.embedding || {}),
...(patch.embedding || {}),
},
};
return () => {
globalThis.__stBmeTestOverrides = previous;
};
}
class FakeClassList {
constructor(owner) {
this.owner = owner;
this.tokens = new Set();
}
setFromString(value = "") {
this.tokens = new Set(String(value || "").split(/\s+/).filter(Boolean));
}
add(...tokens) {
for (const token of tokens) {
if (token) this.tokens.add(token);
}
this.owner._syncClassName();
}
remove(...tokens) {
for (const token of tokens) this.tokens.delete(token);
this.owner._syncClassName();
}
contains(token) {
return this.tokens.has(token);
}
toggle(token, force) {
if (force === true) {
this.tokens.add(token);
this.owner._syncClassName();
return true;
}
if (force === false) {
this.tokens.delete(token);
this.owner._syncClassName();
return false;
}
if (this.tokens.has(token)) {
this.tokens.delete(token);
this.owner._syncClassName();
return false;
}
this.tokens.add(token);
this.owner._syncClassName();
return true;
}
toString() {
return [...this.tokens].join(" ");
}
}
class FakeElement {
constructor(tagName, ownerDocument) {
this.tagName = String(tagName || "div").toUpperCase();
this.ownerDocument = ownerDocument;
this.children = [];
this.parentElement = null;
this.dataset = {};
this.attributes = new Map();
this.eventListeners = new Map();
this.classList = new FakeClassList(this);
this._className = "";
this.id = "";
this.textContent = "";
this.innerHTML = "";
this.disabled = false;
}
_syncClassName() {
this._className = this.classList.toString();
}
get className() {
return this._className;
}
set className(value) {
this._className = String(value || "");
this.classList.setFromString(this._className);
this._className = this.classList.toString();
}
get parentNode() {
return this.parentElement;
}
setAttribute(name, value) {
const key = String(name || "");
const normalized = String(value ?? "");
this.attributes.set(key, normalized);
if (key === "id") {
this.id = normalized;
} else if (key === "class") {
this.classList.setFromString(normalized);
this.className = this.classList.toString();
} else if (key.startsWith("data-")) {
const datasetKey = key
.slice(5)
.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
this.dataset[datasetKey] = normalized;
}
}
getAttribute(name) {
const key = String(name || "");
if (this.attributes.has(key)) return this.attributes.get(key);
if (key === "id") return this.id || null;
if (key === "class") return this.className || null;
if (key.startsWith("data-")) {
const datasetKey = key
.slice(5)
.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
return this.dataset[datasetKey] ?? null;
}
return null;
}
appendChild(child) {
if (!child) return child;
if (child.parentElement) {
child.parentElement.removeChild(child);
}
child.parentElement = this;
child.ownerDocument = this.ownerDocument;
this.children.push(child);
this.ownerDocument?._notifyMutation({ type: "childList", target: this, addedNodes: [child] });
return child;
}
removeChild(child) {
const index = this.children.indexOf(child);
if (index >= 0) {
this.children.splice(index, 1);
child.parentElement = null;
this.ownerDocument?._notifyMutation({ type: "childList", target: this, removedNodes: [child] });
}
return child;
}
remove() {
this.parentElement?.removeChild(this);
}
addEventListener(type, handler) {
const key = String(type || "");
const handlers = this.eventListeners.get(key) || [];
handlers.push(handler);
this.eventListeners.set(key, handlers);
}
dispatchEvent(event = {}) {
const key = String(event.type || "");
const handlers = this.eventListeners.get(key) || [];
for (const handler of handlers) {
handler({
stopPropagation() {},
preventDefault() {},
...event,
target: this,
currentTarget: this,
});
}
}
click() {
this.dispatchEvent({ type: "click" });
}
get isConnected() {
return Boolean(this.parentElement) || this === this.ownerDocument?.body;
}
querySelector(selector) {
return this.querySelectorAll(selector)[0] || null;
}
querySelectorAll(selector) {
return this.ownerDocument?._querySelectorAll(selector, this) || [];
}
}
class FakeDocument {
constructor() {
this.body = new FakeElement("body", this);
this._observers = new Set();
}
createElement(tagName) {
return new FakeElement(tagName, this);
}
contains(node) {
return Boolean(this._flatten(this.body).includes(node));
}
getElementById(id) {
return this._flatten(this.body).find((node) => node.id === id) || null;
}
querySelector(selector) {
return this.body.querySelector(selector);
}
querySelectorAll(selector) {
return this.body.querySelectorAll(selector);
}
_flatten(root) {
const nodes = [];
const visit = (node) => {
nodes.push(node);
for (const child of node.children) visit(child);
};
visit(root);
return nodes;
}
_matchesSimple(node, selector) {
if (!selector) return false;
if (selector.startsWith("#")) {
return node.id === selector.slice(1);
}
const attrMatches = [...selector.matchAll(/\[([^=\]]+)="([^\]]*)"\]/g)];
const attrless = selector.replace(/\[[^\]]+\]/g, "");
const classMatches = [...attrless.matchAll(/\.([A-Za-z0-9_-]+)/g)].map((m) => m[1]);
const tagMatch = attrless.match(/^[A-Za-z][A-Za-z0-9_-]*/);
if (tagMatch && node.tagName.toLowerCase() !== tagMatch[0].toLowerCase()) return false;
for (const className of classMatches) {
if (!node.classList.contains(className)) return false;
}
for (const [, rawName, expected] of attrMatches) {
const actual = node.getAttribute(rawName);
if (String(actual ?? "") !== expected) return false;
}
return true;
}
_matchesSelectorChain(node, segments) {
if (!segments.length) return false;
if (!this._matchesSimple(node, segments[segments.length - 1])) return false;
let current = node.parentElement;
for (let index = segments.length - 2; index >= 0; index--) {
while (current && !this._matchesSimple(current, segments[index])) {
current = current.parentElement;
}
if (!current) return false;
current = current.parentElement;
}
return true;
}
_querySelectorAll(selector, scopeRoot) {
const segments = String(selector || "").trim().split(/\s+/).filter(Boolean);
const nodes = this._flatten(scopeRoot);
return nodes.filter((node) => node !== scopeRoot && this._matchesSelectorChain(node, segments));
}
_registerObserver(observer) {
this._observers.add(observer);
}
_unregisterObserver(observer) {
this._observers.delete(observer);
}
_notifyMutation(record) {
for (const observer of this._observers) {
observer._notify(record);
}
}
}
class FakeMutationObserver {
constructor(callback, documentRef) {
this.callback = callback;
this.documentRef = documentRef;
this.active = false;
}
observe() {
this.active = true;
this.documentRef._registerObserver(this);
}
disconnect() {
this.active = false;
this.documentRef._unregisterObserver(this);
}
_notify(record) {
if (!this.active) return;
queueMicrotask(() => {
if (this.active) this.callback([record]);
});
}
}
function createDomHarness(chat) {
const document = new FakeDocument();
const chatRoot = document.createElement("div");
chatRoot.setAttribute("id", "chat");
document.body.appendChild(chatRoot);
const observerClass = class extends FakeMutationObserver {
constructor(callback) {
super(callback, document);
}
};
return { document, chatRoot, MutationObserver: observerClass, chat };
}
function createMessageElement(document, messageIndex, { stableId = true, withMesBlock = true, isUser = true } = {}) {
const mes = document.createElement("div");
mes.classList.add("mes");
if (stableId) mes.setAttribute("mesid", String(messageIndex));
if (isUser) mes.classList.add("user_mes");
const block = document.createElement("div");
block.classList.add("mes_block");
const textWrap = document.createElement("div");
textWrap.classList.add("mes_text");
if (withMesBlock) {
block.appendChild(textWrap);
mes.appendChild(block);
} else {
mes.appendChild(textWrap);
}
return mes;
}
function appendLegacyBadge(document, messageElement) {
const badge = document.createElement("div");
badge.classList.add("st-bme-recall-badge");
messageElement.appendChild(badge);
return badge;
}
async function createRecallUiHarness({ chat, graph = { nodes: [], edges: [] } } = {}) {
const harness = createDomHarness(chat);
const previousDocument = globalThis.document;
globalThis.document = harness.document;
const source = await fs.readFile(indexPath, "utf8");
const start = source.indexOf("function debugWithThrottle(");
const end = source.indexOf("async function rerunRecallForMessage(");
if (start < 0 || end < 0 || end <= start) {
throw new Error("无法从 index.js 提取 Recall UI 逻辑");
}
const snippet = source.slice(start, end).replace(/^export\s+/gm, "");
const context = {
console,
Date,
JSON,
Math,
Map,
Set,
Array,
Number,
String,
Object,
RegExp,
parseInt: Number.parseInt,
setTimeout,
clearTimeout,
queueMicrotask,
document: harness.document,
currentGraph: graph,
persistedRecallUiRefreshTimer: null,
persistedRecallUiRefreshObserver: null,
persistedRecallUiRefreshSession: 0,
PERSISTED_RECALL_UI_REFRESH_RETRY_DELAYS_MS: [0, 10, 20],
PERSISTED_RECALL_UI_DIAGNOSTIC_THROTTLE_MS: 0,
persistedRecallUiDiagnosticTimestamps: new Map(),
persistedRecallPersistDiagnosticTimestamps: new Map(),
getContext: () => ({ chat }),
getSettings: () => ({ panelTheme: "crimson" }),
triggerChatMetadataSave: () => "debounced",
estimateTokens: (text = "") => String(text || "").trim().split(/\s+/).filter(Boolean).length || 1,
toastr: {
success() {},
warning() {},
info() {},
},
openRecallSidebar() {},
readPersistedRecallFromUserMessage,
removePersistedRecallFromUserMessage,
writePersistedRecallToUserMessage,
buildPersistedRecallRecord,
markPersistedRecallManualEdit,
createRecallCardElement: null,
updateRecallCardData: null,
globalThis: null,
result: null,
};
context.globalThis = context;
const recallUiModule = await import("../recall-message-ui.js");
context.createRecallCardElement = recallUiModule.createRecallCardElement;
context.updateRecallCardData = recallUiModule.updateRecallCardData;
context.MutationObserver = harness.MutationObserver;
vm.createContext(context);
vm.runInContext(
`${snippet}\nresult = { refreshPersistedRecallMessageUi, schedulePersistedRecallMessageUiRefresh, cleanupPersistedRecallMessageUi, resolveMessageIndexFromElement, resolveRecallCardAnchor };`,
context,
{ filename: indexPath },
);
return {
...harness,
context,
api: context.result,
restoreGlobals() {
globalThis.document = previousDocument;
},
};
}
async function testRecallCardMountsOnStandardUserMessageDom() {
const chat = [
{ is_user: true, mes: "user-0", extra: { bme_recall: buildPersistedRecallRecord({ injectionText: "recall-0", selectedNodeIds: ["n1"], nowIso: "2026-01-01T00:00:00.000Z" }) } },
];
const harness = await createRecallUiHarness({ chat });
const messageElement = createMessageElement(harness.document, 0, { stableId: true, withMesBlock: true, isUser: true });
harness.chatRoot.appendChild(messageElement);
try {
const summary = harness.api.refreshPersistedRecallMessageUi();
assert.equal(summary.status, "rendered");
assert.equal(summary.renderedCount, 1);
assert.equal(harness.chatRoot.querySelectorAll(".bme-recall-card").length, 1);
assert.equal(harness.chatRoot.querySelectorAll(".mes_block .bme-recall-card").length, 1);
} finally {
harness.restoreGlobals();
}
}
async function testRecallCardSkipsMountWithoutStableMessageIndex() {
const chat = [
{ is_user: true, mes: "user-0", extra: { bme_recall: buildPersistedRecallRecord({ injectionText: "recall-0", selectedNodeIds: ["n1"], nowIso: "2026-01-01T00:00:00.000Z" }) } },
];
const harness = await createRecallUiHarness({ chat });
const messageElement = createMessageElement(harness.document, 0, { stableId: false, withMesBlock: true, isUser: true });
harness.chatRoot.appendChild(messageElement);
try {
const summary = harness.api.refreshPersistedRecallMessageUi();
assert.equal(summary.status, "waiting_dom");
assert.deepEqual(Array.from(summary.waitingMessageIndices), [0]);
assert.equal(harness.chatRoot.querySelectorAll(".bme-recall-card").length, 0);
} finally {
harness.restoreGlobals();
}
}
async function testRecallCardDelayedDomInsertionEventuallyRenders() {
const chat = [
{ is_user: true, mes: "user-0", extra: { bme_recall: buildPersistedRecallRecord({ injectionText: "recall-0", selectedNodeIds: ["n1"], nowIso: "2026-01-01T00:00:00.000Z" }) } },
];
const harness = await createRecallUiHarness({ chat });
try {
let updateCalls = 0;
const originalUpdateRecallCardData = harness.context.updateRecallCardData;
harness.context.updateRecallCardData = (...args) => {
updateCalls += 1;
return originalUpdateRecallCardData(...args);
};
harness.api.schedulePersistedRecallMessageUiRefresh();
await waitForTick();
const messageElement = createMessageElement(harness.document, 0, { stableId: true, withMesBlock: true, isUser: true });
harness.chatRoot.appendChild(messageElement);
await waitForTick();
await waitForTick();
await new Promise((resolve) => setTimeout(resolve, 35));
await waitForTick();
assert.equal(harness.chatRoot.querySelectorAll(".bme-recall-card").length, 1);
assert.equal(updateCalls, 0, "observer 先触发后不应再被旧 timeout 重复刷新");
} finally {
harness.restoreGlobals();
}
}
async function testRecallCardDoesNotMountOnNonUserFloor() {
const chat = [
{ is_user: false, mes: "assistant-0", extra: { bme_recall: buildPersistedRecallRecord({ injectionText: "recall-0", selectedNodeIds: ["n1"], nowIso: "2026-01-01T00:00:00.000Z" }) } },
];
const harness = await createRecallUiHarness({ chat });
const messageElement = createMessageElement(harness.document, 0, { stableId: true, withMesBlock: true, isUser: false });
harness.chatRoot.appendChild(messageElement);
try {
const summary = harness.api.refreshPersistedRecallMessageUi();
assert.equal(summary.status, "skipped_non_user");
assert.deepEqual(Array.from(summary.skippedNonUserIndices), [0]);
assert.equal(harness.chatRoot.querySelectorAll(".bme-recall-card").length, 0);
} finally {
harness.restoreGlobals();
}
}
async function testRecallCardRefreshCleansLegacyBadgeAndAvoidsDuplicates() {
const chat = [
{ is_user: true, mes: "user-0", extra: { bme_recall: buildPersistedRecallRecord({ injectionText: "recall-0", selectedNodeIds: ["n1", "n2"], nowIso: "2026-01-01T00:00:00.000Z" }) } },
];
const harness = await createRecallUiHarness({ chat });
const messageElement = createMessageElement(harness.document, 0, { stableId: true, withMesBlock: true, isUser: true });
const staleCard = harness.document.createElement("div");
staleCard.classList.add("bme-recall-card");
staleCard.dataset.messageIndex = "999";
staleCard._bmeDestroyRenderer = () => {
staleCard.dataset.destroyed = "1";
};
appendLegacyBadge(harness.document, messageElement);
messageElement.appendChild(staleCard);
harness.chatRoot.appendChild(messageElement);
try {
harness.api.refreshPersistedRecallMessageUi();
harness.api.refreshPersistedRecallMessageUi();
assert.equal(harness.chatRoot.querySelectorAll(".st-bme-recall-badge").length, 0);
assert.equal(harness.chatRoot.querySelectorAll(".bme-recall-card").length, 1);
assert.equal(staleCard.dataset.destroyed, "1");
} finally {
harness.restoreGlobals();
}
}
function makeEvent(seq, title) {
return createNode({
type: "event",
seq,
fields: {
title,
summary: `${title} 摘要`,
participants: "Alice",
status: "active",
},
});
}
async function testCompressorMigratesEdgesToCompressedNode() {
const graph = createEmptyGraph();
const external = createNode({
type: "character",
seq: 0,
fields: { name: "Alice", state: "awake" },
});
const first = makeEvent(1, "事件1");
const second = makeEvent(2, "事件2");
addNode(graph, external);
addNode(graph, first);
addNode(graph, second);
addEdge(
graph,
createEdge({
fromId: first.id,
toId: external.id,
relation: "mentions",
strength: 0.7,
}),
);
const restoreOverrides = pushTestOverrides({
llm: {
async callLLMForJSON() {
return {
fields: {
title: "压缩事件",
summary: "合并摘要",
participants: "Alice",
status: "done",
},
};
},
},
});
try {
const result = await compressType({
graph,
typeDef: schema[0],
embeddingConfig: null,
force: true,
settings: {},
});
assert.equal(result.created, 1);
const compressed = graph.nodes.find(
(node) => node.level === 1 && !node.archived,
);
assert.ok(compressed);
const migrated = graph.edges.find(
(edge) =>
edge.fromId === compressed.id &&
edge.toId === external.id &&
edge.relation === "mentions" &&
!edge.invalidAt &&
!edge.expiredAt,
);
assert.ok(migrated);
} finally {
restoreOverrides();
}
}
async function testVectorIndexKeepsDirtyOnDirectPartialEmbeddingFailure() {
const graph = createEmptyGraph();
const first = makeEvent(1, "向量事件1");
const second = makeEvent(2, "向量事件2");
addNode(graph, first);
addNode(graph, second);
graph.vectorIndexState.dirty = true;
graph.vectorIndexState.lastWarning = "旧 warning";
const restoreOverrides = pushTestOverrides({
embedding: {
async embedBatch() {
return [[0.1, 0.2], null];
},
},
});
try {
const result = await syncGraphVectorIndex(
graph,
{
mode: "direct",
source: "direct",
apiUrl: "https://example.com/v1",
model: "text-embedding-3-small",
},
{},
);
assert.equal(result.insertedHashes.length, 1);
assert.equal(graph.vectorIndexState.dirty, true);
assert.equal(typeof result.stats.pending, "number");
assert.equal(graph.vectorIndexState.lastStats, result.stats);
assert.match(
graph.vectorIndexState.lastWarning,
/部分节点 embedding 生成失败/,
);
assert.equal(
graph.vectorIndexState.lastWarning,
"部分节点 embedding 生成失败,向量索引仍待修复",
);
assert.equal(second.embedding, null);
} finally {
restoreOverrides();
}
}
async function testConsolidatorMergeFallbackKeepsNodeWhenTargetMissing() {
const graph = createEmptyGraph();
const target = createNode({
type: "event",
seq: 3,
fields: {
title: "旧记忆",
summary: "旧摘要",
participants: "Alice",
status: "active",
},
});
const incoming = createNode({
type: "event",
seq: 8,
fields: {
title: "新记忆",
summary: "新摘要",
participants: "Alice",
status: "updated",
},
});
target.embedding = [0.9, 0.1];
addNode(graph, target);
addNode(graph, incoming);
const restoreOverrides = pushTestOverrides({
embedding: {
async embedBatch() {
return [[0.2, 0.3]];
},
searchSimilar() {
return [{ nodeId: target.id, score: 0.99 }];
},
},
llm: {
async callLLMForJSON() {
return {
results: [
{
node_id: incoming.id,
action: "merge",
merge_target_id: "missing-node-id",
reason: "故意触发无效 merge target 回退",
},
],
};
},
},
});
try {
const stats = await consolidateMemories({
graph,
newNodeIds: [incoming.id],
embeddingConfig: {
mode: "direct",
source: "direct",
apiUrl: "https://example.com/v1",
model: "text-embedding-3-small",
},
settings: {},
});
assert.equal(stats.merged, 0);
assert.equal(stats.kept, 1);
assert.equal(incoming.archived, false);
assert.deepEqual(target.embedding, [0.9, 0.1]);
} finally {
restoreOverrides();
}
}
async function testExtractorFailsOnUnknownOperation() {
const graph = createEmptyGraph();
const restoreOverrides = pushTestOverrides({
llm: {
async callLLMForJSON() {
return {
operations: [{ action: "nonsense", foo: 1 }],
};
},
},
});
try {
const result = await extractMemories({
graph,
messages: [{ seq: 4, role: "assistant", content: "测试非法操作" }],
startSeq: 4,
endSeq: 4,
schema,
embeddingConfig: null,
settings: {},
});
assert.equal(result.success, false);
assert.match(result.error, /未知操作类型/);
assert.equal(graph.lastProcessedSeq, -1);
} finally {
restoreOverrides();
}
}
async function testConsolidatorMergeUpdatesSeqRange() {
const graph = createEmptyGraph();
const target = createNode({
type: "event",
seq: 3,
seqRange: [3, 4],
fields: {
title: "旧记忆",
summary: "旧摘要",
participants: "Alice",
status: "active",
},
});
target.embedding = [0.8, 0.2];
const incoming = createNode({
type: "event",
seq: 8,
seqRange: [8, 9],
fields: {
title: "新记忆",
summary: "新摘要",
participants: "Alice",
status: "updated",
},
});
addNode(graph, target);
addNode(graph, incoming);
const restoreOverrides = pushTestOverrides({
embedding: {
async embedBatch() {
return [[0.4, 0.5]];
},
searchSimilar() {
return [{ nodeId: target.id, score: 0.99 }];
},
},
llm: {
async callLLMForJSON() {
return {
results: [
{
node_id: incoming.id,
action: "merge",
merge_target_id: target.id,
merged_fields: { summary: "合并后摘要" },
},
],
};
},
},
});
try {
const stats = await consolidateMemories({
graph,
newNodeIds: [incoming.id],
embeddingConfig: {
mode: "direct",
source: "direct",
apiUrl: "https://example.com/v1",
model: "text-embedding-3-small",
},
settings: {},
});
assert.equal(stats.merged, 1);
assert.deepEqual(target.seqRange, [3, 9]);
assert.equal(target.seq, 8);
assert.equal(target.fields.summary, "合并后摘要");
assert.equal(target.embedding, null);
assert.equal(incoming.archived, true);
} finally {
restoreOverrides();
}
}
async function testBatchJournalVectorDeltaCapturesRecoveryFields() {
const before = normalizeGraphRuntimeState(createEmptyGraph(), "chat-a");
const after = normalizeGraphRuntimeState(createEmptyGraph(), "chat-a");
const beforeNode = createNode({
type: "event",
seq: 1,
fields: { title: "旧", summary: "旧", participants: "A", status: "old" },
});
beforeNode.id = "node-before";
const afterNode = createNode({
type: "event",
seq: 1,
fields: { title: "新", summary: "新", participants: "A", status: "new" },
});
afterNode.id = "node-before";
addNode(before, beforeNode);
addNode(after, afterNode);
before.vectorIndexState.hashToNodeId = { hash_old: "node-before" };
before.vectorIndexState.nodeToHash = { "node-before": "hash_old" };
after.vectorIndexState.hashToNodeId = {
hash_new: "node-before",
hash_inserted: "node-extra",
};
after.vectorIndexState.nodeToHash = {
"node-before": "hash_new",
"node-extra": "hash_inserted",
};
after.vectorIndexState.replayRequiredNodeIds = ["node-before", "node-extra"];
const journal = createBatchJournalEntry(before, after, {
processedRange: [4, 6],
vectorHashesInserted: ["hash_inserted"],
});
assert.deepEqual(journal.vectorDelta.insertedHashes.sort(), [
"hash_inserted",
"hash_new",
]);
assert.deepEqual(journal.vectorDelta.removedHashes, ["hash_old"]);
assert.deepEqual(journal.vectorDelta.touchedNodeIds.sort(), [
"node-before",
"node-extra",
]);
assert.deepEqual(journal.vectorDelta.replayRequiredNodeIds.sort(), [
"node-before",
"node-extra",
]);
assert.deepEqual(journal.vectorDelta.backendDeleteHashes, ["hash_old"]);
assert.deepEqual(journal.vectorDelta.replacedMappings, [
{ nodeId: "node-before", previousHash: "hash_old", nextHash: "hash_new" },
{ nodeId: "node-extra", previousHash: "", nextHash: "hash_inserted" },
]);
}
async function testReverseJournalRecoveryPlanLegacyFallback() {
const recoveryPlan = buildReverseJournalRecoveryPlan(
[
{
processedRange: [5, 7],
vectorDelta: {
insertedHashes: ["hash_1"],
},
},
],
5,
);
assert.equal(recoveryPlan.legacyGapFallback, true);
assert.equal(recoveryPlan.dirtyReason, "legacy-gap");
assert.equal(recoveryPlan.pendingRepairFromFloor, 5);
assert.deepEqual(recoveryPlan.backendDeleteHashes, ["hash_1"]);
assert.deepEqual(recoveryPlan.replayRequiredNodeIds, []);
}
async function testReverseJournalRecoveryPlanAggregatesDeletesAndReplay() {
const recoveryPlan = buildReverseJournalRecoveryPlan(
[
{
processedRange: [8, 9],
vectorDelta: {
insertedHashes: ["hash_new"],
removedHashes: ["hash_removed"],
replacedMappings: [
{
nodeId: "node-1",
previousHash: "hash_old",
nextHash: "hash_new",
},
],
touchedNodeIds: ["node-1"],
replayRequiredNodeIds: ["node-2"],
backendDeleteHashes: ["hash_backend"],
},
},
{
processedRange: [4, 6],
vectorDelta: {
insertedHashes: ["hash_other"],
removedHashes: [],
replacedMappings: [],
touchedNodeIds: ["node-3"],
replayRequiredNodeIds: ["node-3"],
backendDeleteHashes: [],
},
},
],
6,
);
assert.equal(recoveryPlan.legacyGapFallback, false);
assert.equal(recoveryPlan.dirtyReason, "history-recovery-replay");
assert.equal(recoveryPlan.pendingRepairFromFloor, 4);
assert.deepEqual(recoveryPlan.backendDeleteHashes.sort(), [
"hash_backend",
"hash_new",
"hash_old",
"hash_other",
"hash_removed",
]);
assert.deepEqual(recoveryPlan.replayRequiredNodeIds.sort(), [
"node-1",
"node-2",
"node-3",
]);
assert.deepEqual(recoveryPlan.touchedNodeIds.sort(), ["node-1", "node-3"]);
}
async function testReverseJournalRollbackStateFormsReplayClosure() {
const before = normalizeGraphRuntimeState(createEmptyGraph(), "chat-replay");
const after = normalizeGraphRuntimeState(createEmptyGraph(), "chat-replay");
const stableNode = createNode({
type: "event",
seq: 1,
fields: {
title: "稳定节点",
summary: "稳定摘要",
participants: "Alice",
status: "stable",
},
});
stableNode.id = "node-stable";
const touchedBefore = createNode({
type: "event",
seq: 2,
fields: {
title: "回滚前节点",
summary: "旧摘要",
participants: "Bob",
status: "old",
},
});
touchedBefore.id = "node-touched";
const touchedAfter = createNode({
type: "event",
seq: 5,
fields: {
title: "回滚后节点",
summary: "新摘要",
participants: "Bob",
status: "updated",
},
});
touchedAfter.id = "node-touched";
const appendedNode = createNode({
type: "event",
seq: 6,
fields: {
title: "新增节点",
summary: "新增摘要",
participants: "Cara",
status: "new",
},
});
appendedNode.id = "node-appended";
addNode(before, stableNode);
addNode(before, touchedBefore);
addNode(after, stableNode);
addNode(after, touchedAfter);
addNode(after, appendedNode);
before.historyState.lastProcessedAssistantFloor = 3;
before.historyState.processedMessageHashes = {
0: "h0",
1: "h1",
2: "h2",
3: "h3",
};
before.historyState.extractionCount = 1;
before.vectorIndexState.hashToNodeId = {
hash_stable: stableNode.id,
hash_old: touchedBefore.id,
};
before.vectorIndexState.nodeToHash = {
[stableNode.id]: "hash_stable",
[touchedBefore.id]: "hash_old",
};
after.historyState.lastProcessedAssistantFloor = 6;
after.historyState.processedMessageHashes = {
0: "h0",
1: "h1",
2: "h2",
3: "h3",
4: "h4",
5: "h5",
6: "h6",
};
after.historyState.extractionCount = 2;
after.vectorIndexState.hashToNodeId = {
hash_stable: stableNode.id,
hash_new: touchedAfter.id,
hash_added: appendedNode.id,
};
after.vectorIndexState.nodeToHash = {
[stableNode.id]: "hash_stable",
[touchedAfter.id]: "hash_new",
[appendedNode.id]: "hash_added",
};
after.vectorIndexState.replayRequiredNodeIds = [appendedNode.id];
const journal = createBatchJournalEntry(before, after, {
processedRange: [4, 6],
extractionCountBefore: before.historyState.extractionCount,
});
const runtimeGraph = normalizeGraphRuntimeState(
JSON.parse(JSON.stringify(after)),
"chat-replay",
);
rollbackBatch(runtimeGraph, journal);
assert.deepEqual(runtimeGraph.nodes.map((node) => node.id).sort(), [
stableNode.id,
touchedBefore.id,
]);
assert.deepEqual(runtimeGraph.vectorIndexState.hashToNodeId, {
hash_stable: stableNode.id,
hash_old: touchedBefore.id,
});
assert.deepEqual(runtimeGraph.vectorIndexState.nodeToHash, {
[stableNode.id]: "hash_stable",
[touchedBefore.id]: "hash_old",
});
assert.equal(runtimeGraph.historyState.lastProcessedAssistantFloor, 3);
const recoveryPlan = buildReverseJournalRecoveryPlan([journal], 4);
runtimeGraph.vectorIndexState.replayRequiredNodeIds = [stableNode.id];
runtimeGraph.vectorIndexState.dirty = false;
runtimeGraph.vectorIndexState.dirtyReason = "";
runtimeGraph.vectorIndexState.pendingRepairFromFloor = null;
const replayRequiredNodeIds = new Set(
runtimeGraph.vectorIndexState.replayRequiredNodeIds,
);
for (const nodeId of recoveryPlan.replayRequiredNodeIds) {
replayRequiredNodeIds.add(nodeId);
}
runtimeGraph.vectorIndexState.replayRequiredNodeIds = [
...replayRequiredNodeIds,
];
runtimeGraph.vectorIndexState.dirty = true;
runtimeGraph.vectorIndexState.dirtyReason =
recoveryPlan.dirtyReason ||
runtimeGraph.vectorIndexState.dirtyReason ||
"history-recovery-replay";
runtimeGraph.vectorIndexState.pendingRepairFromFloor =
recoveryPlan.pendingRepairFromFloor;
runtimeGraph.vectorIndexState.lastWarning = recoveryPlan.legacyGapFallback
? "历史恢复检测到 legacy-gap向量索引需按受影响后缀修复"
: "历史恢复后需要修复受影响后缀的向量索引";
assert.deepEqual(
runtimeGraph.vectorIndexState.replayRequiredNodeIds.sort(),
[appendedNode.id, stableNode.id, touchedBefore.id].sort(),
);
assert.equal(runtimeGraph.vectorIndexState.pendingRepairFromFloor, 4);
assert.equal(
runtimeGraph.vectorIndexState.dirtyReason,
"history-recovery-replay",
);
assert.equal(
runtimeGraph.vectorIndexState.lastWarning,
"历史恢复后需要修复受影响后缀的向量索引",
);
assert.deepEqual(runtimeGraph.vectorIndexState.hashToNodeId, {
hash_stable: stableNode.id,
hash_old: touchedBefore.id,
});
assert.deepEqual(runtimeGraph.vectorIndexState.nodeToHash, {
[stableNode.id]: "hash_stable",
[touchedBefore.id]: "hash_old",
});
}
async function testReverseJournalRecoveryPlanMixedLegacyAndCurrentRetainsRepairSet() {
const recoveryPlan = buildReverseJournalRecoveryPlan(
[
{
processedRange: [10, 12],
vectorDelta: {
insertedHashes: ["hash-current"],
removedHashes: ["hash-removed"],
replacedMappings: [
{
nodeId: "node-current",
previousHash: "hash-prev",
nextHash: "hash-current",
},
],
touchedNodeIds: ["node-current"],
replayRequiredNodeIds: ["node-extra"],
backendDeleteHashes: ["hash-backend"],
},
},
{
processedRange: [7, 9],
vectorDelta: {
insertedHashes: ["hash-legacy"],
},
},
],
9,
);
assert.equal(recoveryPlan.legacyGapFallback, true);
assert.equal(recoveryPlan.dirtyReason, "legacy-gap");
assert.equal(recoveryPlan.pendingRepairFromFloor, 7);
assert.deepEqual(recoveryPlan.replayRequiredNodeIds.sort(), [
"node-current",
"node-extra",
]);
assert.deepEqual(recoveryPlan.touchedNodeIds, ["node-current"]);
assert.deepEqual(recoveryPlan.backendDeleteHashes.sort(), [
"hash-backend",
"hash-current",
"hash-legacy",
"hash-prev",
"hash-removed",
]);
}
async function testBatchStatusStructuralPartialRemainsRecoverable() {
const harness = await createBatchStageHarness();
const { createBatchStatusSkeleton, handleExtractionSuccess } = harness.result;
harness.currentGraph = {
historyState: { extractionCount: 0 },
vectorIndexState: {},
};
harness.ensureCurrentGraphRuntimeState = () => {
harness.currentGraph.historyState ||= {};
harness.currentGraph.vectorIndexState ||= {};
};
harness.compressAll = async () => {
throw new Error("compression down");
};
harness.syncVectorState = async () => ({
insertedHashes: ["hash-ok"],
stats: { pending: 0 },
});
const batchStatus = createBatchStatusSkeleton({
processedRange: [2, 4],
extractionCountBefore: 0,
});
const effects = await handleExtractionSuccess(
{ newNodeIds: ["node-1"] },
4,
{
enableConsolidation: false,
enableSynopsis: false,
enableReflection: false,
enableSleepCycle: false,
synopsisEveryN: 1,
reflectEveryN: 1,
sleepEveryN: 1,
},
undefined,
batchStatus,
);
assert.equal(effects.batchStatus.stages.core.outcome, "success");
assert.equal(effects.batchStatus.stages.structural.outcome, "partial");
assert.equal(effects.batchStatus.stages.finalize.outcome, "success");
assert.equal(effects.batchStatus.outcome, "partial");
assert.equal(effects.batchStatus.completed, true);
assert.equal(effects.batchStatus.consistency, "weak");
assert.match(effects.batchStatus.warnings[0], /压缩阶段失败/);
}
async function testBatchStatusSemanticFailureDoesNotHideCoreSuccess() {
const harness = await createBatchStageHarness();
const { createBatchStatusSkeleton, handleExtractionSuccess } = harness.result;
harness.currentGraph = {
historyState: { extractionCount: 0 },
vectorIndexState: {},
};
harness.ensureCurrentGraphRuntimeState = () => {
harness.currentGraph.historyState ||= {};
harness.currentGraph.vectorIndexState ||= {};
};
harness.generateSynopsis = async () => {
throw new Error("semantic down");
};
harness.syncVectorState = async () => ({
insertedHashes: [],
stats: { pending: 0 },
});
const batchStatus = createBatchStatusSkeleton({
processedRange: [5, 5],
extractionCountBefore: 0,
});
const effects = await handleExtractionSuccess(
{ newNodeIds: ["node-2"] },
5,
{
enableConsolidation: false,
enableSynopsis: true,
enableReflection: false,
enableSleepCycle: false,
synopsisEveryN: 1,
reflectEveryN: 1,
sleepEveryN: 1,
},
undefined,
batchStatus,
);
assert.equal(effects.batchStatus.stages.core.outcome, "success");
assert.equal(effects.batchStatus.stages.semantic.outcome, "failed");
assert.equal(effects.batchStatus.stages.finalize.outcome, "success");
assert.equal(effects.batchStatus.outcome, "failed");
assert.equal(effects.batchStatus.completed, true);
assert.match(effects.batchStatus.errors[0], /概要生成失败/);
}
async function testBatchStatusFinalizeFailureIsNotCompleteSuccess() {
const harness = await createBatchStageHarness();
const { createBatchStatusSkeleton, handleExtractionSuccess } = harness.result;
harness.currentGraph = {
historyState: { extractionCount: 0 },
vectorIndexState: {},
};
harness.ensureCurrentGraphRuntimeState = () => {
harness.currentGraph.historyState ||= {};
harness.currentGraph.vectorIndexState ||= {};
};
harness.syncVectorState = async () => ({
insertedHashes: [],
stats: { pending: 1 },
error: "vector finalize down",
});
const batchStatus = createBatchStatusSkeleton({
processedRange: [6, 7],
extractionCountBefore: 0,
});
const effects = await handleExtractionSuccess(
{ newNodeIds: ["node-3"] },
7,
{
enableConsolidation: false,
enableSynopsis: false,
enableReflection: false,
enableSleepCycle: false,
synopsisEveryN: 1,
reflectEveryN: 1,
sleepEveryN: 1,
},
undefined,
batchStatus,
);
assert.equal(effects.batchStatus.stages.core.outcome, "success");
assert.equal(effects.batchStatus.stages.finalize.outcome, "failed");
assert.equal(effects.batchStatus.outcome, "failed");
assert.equal(effects.batchStatus.completed, false);
assert.equal(effects.batchStatus.consistency, "weak");
assert.equal(effects.vectorError, "vector finalize down");
}
async function testProcessedHistoryAdvanceRequiresCompleteStrongSuccess() {
const harness = await createBatchStageHarness();
const {
createBatchStatusSkeleton,
finalizeBatchStatus,
setBatchStageOutcome,
shouldAdvanceProcessedHistory,
} = harness.result;
const structuralPartial = createBatchStatusSkeleton({
processedRange: [2, 4],
extractionCountBefore: 0,
});
setBatchStageOutcome(structuralPartial, "core", "success");
setBatchStageOutcome(
structuralPartial,
"structural",
"partial",
"compression down",
);
setBatchStageOutcome(structuralPartial, "finalize", "success");
finalizeBatchStatus(structuralPartial);
assert.equal(structuralPartial.completed, true);
assert.equal(structuralPartial.outcome, "partial");
assert.equal(structuralPartial.consistency, "weak");
assert.equal(shouldAdvanceProcessedHistory(structuralPartial), false);
const semanticFailed = createBatchStatusSkeleton({
processedRange: [5, 5],
extractionCountBefore: 0,
});
setBatchStageOutcome(semanticFailed, "core", "success");
setBatchStageOutcome(semanticFailed, "semantic", "failed", "semantic down");
setBatchStageOutcome(semanticFailed, "finalize", "success");
finalizeBatchStatus(semanticFailed);
assert.equal(semanticFailed.completed, true);
assert.equal(semanticFailed.outcome, "failed");
assert.equal(semanticFailed.consistency, "strong");
assert.equal(shouldAdvanceProcessedHistory(semanticFailed), false);
const fullSuccess = createBatchStatusSkeleton({
processedRange: [8, 9],
extractionCountBefore: 0,
});
setBatchStageOutcome(fullSuccess, "core", "success");
setBatchStageOutcome(fullSuccess, "structural", "success");
setBatchStageOutcome(fullSuccess, "semantic", "success");
setBatchStageOutcome(fullSuccess, "finalize", "success");
finalizeBatchStatus(fullSuccess);
assert.equal(fullSuccess.completed, true);
assert.equal(fullSuccess.outcome, "success");
assert.equal(fullSuccess.consistency, "strong");
assert.equal(shouldAdvanceProcessedHistory(fullSuccess), true);
}
async function testGenerationRecallTransactionDedupesDoubleHookBySameKey() {
const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: true, mes: "同一轮输入" }];
await harness.result.onGenerationAfterCommands("normal", {}, false);
await harness.result.onBeforeCombinePrompts();
assert.equal(harness.runRecallCalls.length, 1);
assert.equal(harness.runRecallCalls[0].hookName, "GENERATION_AFTER_COMMANDS");
}
async function testGenerationRecallBeforeCombineRunsStandalone() {
const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: true, mes: "仅 before combine" }];
await harness.result.onBeforeCombinePrompts();
assert.equal(harness.runRecallCalls.length, 1);
assert.equal(
harness.runRecallCalls[0].hookName,
"GENERATE_BEFORE_COMBINE_PROMPTS",
);
}
async function testGenerationRecallDifferentKeyCanRunAgain() {
const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: true, mes: "第一条" }];
await harness.result.onGenerationAfterCommands("normal", {}, false);
harness.chat = [{ is_user: true, mes: "第二条" }];
await harness.result.onGenerationAfterCommands("normal", {}, false);
assert.equal(harness.runRecallCalls.length, 2);
assert.notEqual(
harness.runRecallCalls[0].recallKey,
harness.runRecallCalls[1].recallKey,
);
}
async function testGenerationRecallSkippedStateDoesNotLoopToBeforeCombine() {
const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: true, mes: "同一条但本次跳过" }];
harness.runRecall = async (options = {}) => {
harness.runRecallCalls.push({ ...options });
return {
status: "skipped",
didRecall: false,
ok: false,
reason: "测试跳过",
};
};
await harness.result.onGenerationAfterCommands("normal", {}, false);
await harness.result.onBeforeCombinePrompts();
assert.equal(harness.runRecallCalls.length, 1);
assert.equal(
harness.result.generationRecallTransactions.size,
1,
);
const transaction = [...harness.result.generationRecallTransactions.values()][0];
assert.equal(transaction.hookStates.GENERATION_AFTER_COMMANDS, "skipped");
}
async function testGenerationRecallAppliesFinalInjectionOncePerTransaction() {
const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: true, mes: "同一轮仅一次最终注入" }];
await harness.result.onGenerationAfterCommands("normal", {}, false);
await harness.result.onBeforeCombinePrompts();
assert.equal(harness.applyFinalCalls.length, 1);
assert.equal(harness.applyFinalCalls[0].generationType, "normal");
}
async function testPersistentRecallDataLayerLifecycleAndCompatibility() {
const chat = [
{ is_user: true, mes: "u0" },
{ is_user: false, mes: "a1" },
{ is_user: true, mes: "u2" },
];
const record = buildPersistedRecallRecord({
injectionText: "fresh-memory",
selectedNodeIds: ["n1", "n2"],
recallInput: "u2",
recallSource: "chat-last-user",
hookName: "GENERATION_AFTER_COMMANDS",
tokenEstimate: 24,
manuallyEdited: false,
nowIso: "2026-01-01T00:00:00.000Z",
});
assert.equal(writePersistedRecallToUserMessage(chat, 2, record), true);
const loaded = readPersistedRecallFromUserMessage(chat, 2);
assert.ok(loaded);
assert.equal(loaded.injectionText, "fresh-memory");
assert.equal(loaded.generationCount, 0);
assert.equal(loaded.manuallyEdited, false);
chat[2].mes = "u2 edited";
assert.equal(readPersistedRecallFromUserMessage(chat, 2)?.injectionText, "fresh-memory");
const bumped = bumpPersistedRecallGenerationCount(chat, 2);
assert.equal(bumped?.generationCount, 1);
const edited = markPersistedRecallManualEdit(
chat,
2,
true,
"2026-01-01T00:00:01.000Z",
);
assert.equal(edited?.manuallyEdited, true);
assert.equal(edited?.updatedAt, "2026-01-01T00:00:01.000Z");
const overwrite = buildPersistedRecallRecord(
{
injectionText: "system-rerecall",
selectedNodeIds: ["n3"],
recallInput: "u2 edited",
recallSource: "message-floor-rerecall",
hookName: "MESSAGE_RECALL_BADGE_RERUN",
tokenEstimate: 30,
manuallyEdited: false,
nowIso: "2026-01-01T00:00:02.000Z",
},
readPersistedRecallFromUserMessage(chat, 2),
);
assert.equal(writePersistedRecallToUserMessage(chat, 2, overwrite), true);
const overwritten = readPersistedRecallFromUserMessage(chat, 2);
assert.equal(overwritten?.manuallyEdited, false);
assert.equal(overwritten?.injectionText, "system-rerecall");
assert.equal(removePersistedRecallFromUserMessage(chat, 2), true);
assert.equal(readPersistedRecallFromUserMessage(chat, 2), null);
assert.equal(readPersistedRecallFromUserMessage([{ is_user: true, mes: "legacy" }], 0), null);
}
async function testPersistentRecallSourceResolutionAndTargetRouting() {
const chat = [
{ is_user: true, mes: "u0" },
{ is_user: false, mes: "a1" },
{ is_user: true, mes: "u2" },
{ is_user: false, mes: "a3" },
];
assert.equal(resolveGenerationTargetUserMessageIndex(chat, { generationType: "normal" }), null);
assert.equal(resolveGenerationTargetUserMessageIndex(chat, { generationType: "continue" }), 2);
const withTailUser = [...chat, { is_user: true, mes: "u4" }];
assert.equal(resolveGenerationTargetUserMessageIndex(withTailUser, { generationType: "normal" }), 4);
const freshWins = resolveFinalRecallInjectionSource({
freshRecallResult: { status: "completed", didRecall: true, injectionText: "fresh" },
persistedRecord: { injectionText: "persisted" },
});
assert.equal(freshWins.source, "fresh");
assert.equal(freshWins.injectionText, "fresh");
const fallback = resolveFinalRecallInjectionSource({
freshRecallResult: { status: "skipped", didRecall: false, injectionText: "" },
persistedRecord: { injectionText: "persisted" },
});
assert.equal(fallback.source, "persisted");
assert.equal(fallback.injectionText, "persisted");
}
async function testRecallSubGraphAndDataLayerEntryPoints() {
// Sub-graph build test (pure function, no DOM needed)
const { buildRecallSubGraph } = await import("../recall-message-ui.js");
const graph = {
nodes: [
{ id: "n1", type: "character", name: "赵管家", importance: 7 },
{ id: "n2", type: "event", name: "喂食", importance: 5 },
{ id: "n3", type: "location", name: "厨房", importance: 3, archived: true },
{ id: "n4", type: "thread", name: "主线", importance: 8 },
],
edges: [
{ fromId: "n1", toId: "n2", strength: 0.8, relation: "related" },
{ fromId: "n2", toId: "n3", strength: 0.5, relation: "located" },
{ fromId: "n1", toId: "n4", strength: 0.6, relation: "participates" },
],
};
const sub1 = buildRecallSubGraph(graph, ["n1", "n2"]);
assert.equal(sub1.nodes.length, 2);
assert.equal(sub1.edges.length, 1);
assert.equal(sub1.edges[0].fromId, "n1");
// archived node should be excluded
const sub2 = buildRecallSubGraph(graph, ["n1", "n3"]);
assert.equal(sub2.nodes.length, 1);
assert.equal(sub2.edges.length, 0);
// empty/null safety
assert.equal(buildRecallSubGraph(null, ["n1"]).nodes.length, 0);
assert.equal(buildRecallSubGraph(graph, null).nodes.length, 0);
assert.equal(buildRecallSubGraph(graph, []).nodes.length, 0);
// Data layer: edit and delete still work
const chat = [{ is_user: true, mes: "u0", extra: { bme_recall: { version: 1, injectionText: "test", selectedNodeIds: ["n1"], generationCount: 0, manuallyEdited: false, createdAt: "2026-01-01T00:00:00Z", updatedAt: "2026-01-01T00:00:00Z", recallInput: "u0", recallSource: "test", hookName: "TEST", tokenEstimate: 4 } } }];
assert.ok(readPersistedRecallFromUserMessage(chat, 0));
assert.equal(removePersistedRecallFromUserMessage(chat, 0), true);
assert.equal(readPersistedRecallFromUserMessage(chat, 0), null);
}
async function testRerollUsesBatchBoundaryRollbackAndPersistsState() {
const harness = await createRerollHarness();
harness.chat = [
{ is_user: true, mes: "u1" },
{ is_user: false, mes: "a1" },
{ is_user: true, mes: "u2" },
{ is_user: false, mes: "a2" },
{ is_user: true, mes: "u3" },
{ is_user: false, mes: "a3" },
];
harness.currentGraph = {
historyState: {
lastProcessedAssistantFloor: 5,
processedMessageHashes: {
1: "hash-1",
3: "hash-3",
5: "hash-5",
},
},
vectorIndexState: {
collectionId: "col-1",
},
batchJournal: [{ id: "journal-1" }],
lastProcessedSeq: 5,
};
harness.postRollbackGraph = {
historyState: {
lastProcessedAssistantFloor: 1,
processedMessageHashes: {
1: "hash-1",
3: "stale-hash",
},
},
vectorIndexState: {
collectionId: "col-1",
},
batchJournal: [],
lastProcessedSeq: 1,
};
harness.findJournalRecoveryPointImpl = () => ({
path: "reverse-journal",
affectedBatchCount: 1,
affectedJournals: [{ id: "journal-1" }],
});
harness.buildReverseJournalRecoveryPlanImpl = () => ({
backendDeleteHashes: ["hash-old"],
replayRequiredNodeIds: ["node-1"],
pendingRepairFromFloor: 2,
legacyGapFallback: false,
dirtyReason: "history-recovery-replay",
});
const result = await harness.result.onReroll({ fromFloor: 3 });
assert.equal(result.success, true);
assert.equal(result.rollbackPerformed, true);
assert.equal(result.recoveryPath, "reverse-journal");
assert.equal(result.effectiveFromFloor, 2);
assert.equal(harness.rollbackAffectedJournalsCalls.length, 1);
assert.equal(harness.deletedHashesCalls.length, 1);
assert.equal(harness.prepareVectorStateCalls.length, 1);
assert.equal(harness.prepareVectorStateCalls[0][2].skipBackendPurge, true);
assert.equal(harness.saveGraphToChatCalls, 1);
assert.equal(harness.refreshPanelCalls, 2);
assert.equal(harness.clearInjectionCalls, 1);
assert.equal(harness.onManualExtractCalls, 1);
assert.equal(harness.currentGraph.historyState.processedMessageHashes[3], undefined);
assert.equal(harness.lastExtractedItems.length, 0);
}
async function testRerollRejectsMissingRecoveryPoint() {
const harness = await createRerollHarness();
harness.chat = [
{ is_user: true, mes: "u1" },
{ is_user: false, mes: "a1" },
{ is_user: true, mes: "u2" },
{ is_user: false, mes: "a2" },
];
harness.currentGraph = {
historyState: {
lastProcessedAssistantFloor: 3,
processedMessageHashes: {
1: "hash-1",
3: "hash-3",
},
},
vectorIndexState: {
collectionId: "col-1",
},
batchJournal: [],
lastProcessedSeq: 3,
};
const result = await harness.result.onReroll({ fromFloor: 3 });
assert.equal(result.success, false);
assert.equal(result.recoveryPath, "unavailable");
assert.equal(harness.onManualExtractCalls, 0);
assert.equal(harness.saveGraphToChatCalls, 0);
}
async function testRerollFallsBackToDirectExtractForUnprocessedFloor() {
const harness = await createRerollHarness();
harness.chat = [
{ is_user: true, mes: "u1" },
{ is_user: false, mes: "a1" },
{ is_user: true, mes: "u2" },
{ is_user: false, mes: "a2" },
];
harness.currentGraph = {
historyState: {
lastProcessedAssistantFloor: 1,
processedMessageHashes: {
1: "hash-1",
},
},
vectorIndexState: {
collectionId: "col-1",
},
batchJournal: [],
lastProcessedSeq: 1,
};
const result = await harness.result.onReroll({ fromFloor: 3 });
assert.equal(result.success, true);
assert.equal(result.rollbackPerformed, false);
assert.equal(result.recoveryPath, "direct-extract");
assert.equal(result.effectiveFromFloor, 2);
assert.equal(harness.onManualExtractCalls, 1);
assert.equal(harness.saveGraphToChatCalls, 0);
}
async function testLlmDebugSnapshotRedactsSecretsBeforeStorage() {
const originalFetch = globalThis.fetch;
const previousSettings = JSON.parse(
JSON.stringify(extensionsApi.extension_settings.st_bme || {}),
);
delete globalThis.__stBmeRuntimeDebugState;
extensionsApi.extension_settings.st_bme = {
...previousSettings,
llmApiUrl: "https://example.com/v1",
llmApiKey: "sk-secret-redaction",
llmModel: "gpt-test",
timeoutMs: 1234,
};
globalThis.fetch = async () =>
new Response(
JSON.stringify({
choices: [
{
message: {
content: '{"ok":true}',
},
finish_reason: "stop",
},
],
}),
{
status: 200,
headers: {
"Content-Type": "application/json",
},
},
);
try {
const result = await llm.callLLMForJSON({
systemPrompt: "system",
userPrompt: "user",
maxRetries: 0,
requestSource: "test:redaction",
});
assert.deepEqual(result, { ok: true });
const snapshot =
globalThis.__stBmeRuntimeDebugState?.taskLlmRequests?.["test:redaction"];
assert.ok(snapshot);
assert.equal(snapshot.redacted, true);
const serialized = JSON.stringify(snapshot);
assert.doesNotMatch(serialized, /sk-secret-redaction/);
assert.match(serialized, /\[REDACTED\]/);
} finally {
globalThis.fetch = originalFetch;
extensionsApi.extension_settings.st_bme = previousSettings;
}
}
async function testEmbeddingUsesConfigTimeoutInsteadOfDefault() {
const originalFetch = globalThis.fetch;
const originalSetTimeout = globalThis.setTimeout;
const originalClearTimeout = globalThis.clearTimeout;
let capturedDelay = null;
globalThis.setTimeout = (fn, delay, ...args) => {
capturedDelay = delay;
return originalSetTimeout(fn, 0, ...args);
};
globalThis.clearTimeout = originalClearTimeout;
globalThis.fetch = async (_url, options = {}) =>
await new Promise((resolve, reject) => {
options.signal?.addEventListener(
"abort",
() => reject(options.signal.reason),
{ once: true },
);
});
try {
await assert.rejects(
embedding.embedText("timeout test", {
apiUrl: "https://example.com/v1",
model: "text-embedding-test",
timeoutMs: 7,
}),
/Embedding 请求超时/,
);
assert.equal(capturedDelay, 7);
} finally {
globalThis.fetch = originalFetch;
globalThis.setTimeout = originalSetTimeout;
globalThis.clearTimeout = originalClearTimeout;
}
}
async function testLlmOutputRegexCleansResponseBeforeJsonParse() {
const originalFetch = globalThis.fetch;
const previousSettings = JSON.parse(
JSON.stringify(extensionsApi.extension_settings.st_bme || {}),
);
delete globalThis.__stBmeRuntimeDebugState;
const taskProfiles = createDefaultTaskProfiles();
taskProfiles.extract.profiles[0].regex = {
...taskProfiles.extract.profiles[0].regex,
enabled: true,
inheritStRegex: false,
stages: {
...taskProfiles.extract.profiles[0].regex.stages,
"output.rawResponse": true,
"output.beforeParse": true,
},
localRules: [
{
id: "strip-prefix",
script_name: "strip-prefix",
enabled: true,
find_regex: "/^NOTE:\\s*/g",
replace_string: "",
trim_strings: [],
source: {
ai_output: true,
},
destination: {
prompt: true,
display: false,
},
},
{
id: "strip-suffix",
script_name: "strip-suffix",
enabled: true,
find_regex: "/\\s*END$/g",
replace_string: "",
trim_strings: [],
source: {
ai_output: true,
},
destination: {
prompt: true,
display: false,
},
},
],
};
extensionsApi.extension_settings.st_bme = {
...previousSettings,
llmApiUrl: "https://example.com/v1",
llmApiKey: "sk-secret-redaction",
llmModel: "gpt-test",
taskProfilesVersion: 1,
taskProfiles,
};
globalThis.fetch = async () =>
new Response(
JSON.stringify({
choices: [
{
message: {
content: 'NOTE: {"ok":true} END',
},
finish_reason: "stop",
},
],
}),
{
status: 200,
headers: {
"Content-Type": "application/json",
},
},
);
try {
const result = await llm.callLLMForJSON({
systemPrompt: "system",
userPrompt: "user",
maxRetries: 0,
taskType: "extract",
requestSource: "test:output-regex",
});
assert.deepEqual(result, { ok: true });
const snapshot =
globalThis.__stBmeRuntimeDebugState?.taskLlmRequests?.extract;
assert.ok(snapshot);
assert.equal(snapshot.responseCleaning?.applied, true);
assert.equal(snapshot.responseCleaning?.changed, true);
assert.deepEqual(
snapshot.responseCleaning?.stages?.map((entry) => entry.stage),
["output.rawResponse", "output.beforeParse"],
);
} finally {
globalThis.fetch = originalFetch;
extensionsApi.extension_settings.st_bme = previousSettings;
}
}
await testCompressorMigratesEdgesToCompressedNode();
await testVectorIndexKeepsDirtyOnDirectPartialEmbeddingFailure();
await testExtractorFailsOnUnknownOperation();
await testConsolidatorMergeUpdatesSeqRange();
await testConsolidatorMergeFallbackKeepsNodeWhenTargetMissing();
await testBatchJournalVectorDeltaCapturesRecoveryFields();
await testReverseJournalRecoveryPlanLegacyFallback();
await testReverseJournalRecoveryPlanAggregatesDeletesAndReplay();
await testReverseJournalRollbackStateFormsReplayClosure();
await testReverseJournalRecoveryPlanMixedLegacyAndCurrentRetainsRepairSet();
await testBatchStatusStructuralPartialRemainsRecoverable();
await testBatchStatusSemanticFailureDoesNotHideCoreSuccess();
await testBatchStatusFinalizeFailureIsNotCompleteSuccess();
await testProcessedHistoryAdvanceRequiresCompleteStrongSuccess();
await testGenerationRecallTransactionDedupesDoubleHookBySameKey();
await testGenerationRecallBeforeCombineRunsStandalone();
await testGenerationRecallDifferentKeyCanRunAgain();
await testGenerationRecallSkippedStateDoesNotLoopToBeforeCombine();
await testGenerationRecallAppliesFinalInjectionOncePerTransaction();
await testPersistentRecallDataLayerLifecycleAndCompatibility();
await testPersistentRecallSourceResolutionAndTargetRouting();
await testRecallCardMountsOnStandardUserMessageDom();
await testRecallCardSkipsMountWithoutStableMessageIndex();
await testRecallCardDelayedDomInsertionEventuallyRenders();
await testRecallCardDoesNotMountOnNonUserFloor();
await testRecallCardRefreshCleansLegacyBadgeAndAvoidsDuplicates();
await testRecallSubGraphAndDataLayerEntryPoints();
await testRerollUsesBatchBoundaryRollbackAndPersistsState();
await testRerollRejectsMissingRecoveryPoint();
await testRerollFallsBackToDirectExtractForUnprocessedFloor();
await testLlmDebugSnapshotRedactsSecretsBeforeStorage();
await testEmbeddingUsesConfigTimeoutInsteadOfDefault();
await testLlmOutputRegexCleansResponseBeforeJsonParse();
console.log("p0-regressions tests passed");