Files
ST-Bionic-Memory-Ecology/tests/p0-regressions.mjs
2026-04-07 22:24:04 +08:00

6109 lines
178 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 { pruneProcessedMessageHashesFromFloor } from "../chat-history.js";
import {
onBeforeCombinePromptsController,
onCharacterMessageRenderedController,
onChatChangedController,
onGenerationAfterCommandsController,
onGenerationStartedController,
onMessageSentController,
onMessageReceivedController,
onMessageSwipedController,
onUserMessageRenderedController,
registerCoreEventHooksController,
} from "../event-binding.js";
import {
onRerollController,
resolveAutoExtractionPlanController,
runExtractionController,
} from "../extraction-controller.js";
import {
GRAPH_LOAD_STATES,
GRAPH_METADATA_KEY,
GRAPH_PERSISTENCE_META_KEY,
MODULE_NAME,
} from "../graph-persistence.js";
import {
buildPersistedRecallRecord,
bumpPersistedRecallGenerationCount,
markPersistedRecallManualEdit,
readPersistedRecallFromUserMessage,
removePersistedRecallFromUserMessage,
resolveFinalRecallInjectionSource,
resolveGenerationTargetUserMessageIndex,
writePersistedRecallToUserMessage,
} from "../recall-persistence.js";
import {
BATCH_STAGE_ORDER,
BATCH_STAGE_SEVERITY,
clampInt,
createBatchStageStatus,
createBatchStatusSkeleton,
createGraphPersistenceState,
createRecallInputRecord,
createRecallRunResult,
createUiStatus,
finalizeBatchStatus,
getGenerationRecallHookStateFromResult,
getRecallHookLabel,
getStageNoticeDuration,
getStageNoticeTitle,
hashRecallInput,
isFreshRecallInputRecord,
isTerminalGenerationRecallHookState,
normalizeRecallInputText,
normalizeStageNoticeLevel,
pushBatchStageArtifact,
setBatchStageOutcome,
shouldRunRecallForTransaction,
} from "../ui-status.js";
import {
onManualCompressController,
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 = [
"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' };",
"}",
"export function substituteParamsExtended(text = '') {",
" return String(text ?? '');",
"}",
].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,
removeNode,
} = await import("../graph.js");
const { compressType } = await import("../compressor.js");
const { syncGraphVectorIndex } = await import("../vector-index.js");
const {
extractMemories,
generateReflection,
generateSynopsis,
} = 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,
},
{
id: "synopsis",
label: "概要",
columns: [{ name: "summary" }, { name: "scope" }],
},
];
function buildAutoExtractionPlan({
chat = [],
settings = {},
lastProcessedAssistantFloor = -1,
lockedEndFloor = null,
smartTriggerDecision = null,
} = {}) {
return resolveAutoExtractionPlanController(
{
getAssistantTurns(sourceChat = []) {
return sourceChat.flatMap((message, index) =>
!message?.is_user && !message?.is_system ? [index] : [],
);
},
getLastProcessedAssistantFloor: () => lastProcessedAssistantFloor,
getSettings: () => settings,
getSmartTriggerDecision: () =>
smartTriggerDecision || {
triggered: false,
score: 0,
reasons: [],
},
},
{
chat,
settings,
lastProcessedAssistantFloor,
lockedEndFloor,
},
);
}
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);
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,
result: null,
extractionCount: 0,
currentGraph: null,
extractionStatuses: [],
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 }),
analyzeAutoConsolidationGate: async () => ({
triggered: false,
reason: "本批新增少且无明显重复风险,跳过自动整合",
matchedScore: null,
matchedNodeId: "",
}),
inspectAutoCompressionCandidates: () => ({
hasCandidates: false,
reason: "已到自动压缩周期,但当前没有达到内部压缩阈值的候选组",
}),
updateLastExtractedItems: () => {},
ensureCurrentGraphRuntimeState: () => {},
throwIfAborted: () => {},
isAbortError: () => false,
createAbortError: (message) => new Error(message),
BATCH_STAGE_ORDER,
BATCH_STAGE_SEVERITY,
createBatchStageStatus,
createBatchStatusSkeleton,
setBatchStageOutcome,
pushBatchStageArtifact,
finalizeBatchStatus,
createUiStatus,
setLastExtractionStatus(...args) {
context.extractionStatuses.push(args);
},
};
vm.createContext(context);
vm.runInContext(
`${snippet}\nresult = { createBatchStatusSkeleton, finalizeBatchStatus, handleExtractionSuccess, setBatchStageOutcome, shouldAdvanceProcessedHistory };`,
context,
{ filename: indexPath },
);
return context;
});
}
function createHistoryRecoveryHarness() {
return fs.readFile(indexPath, "utf8").then((source) => {
const start = source.indexOf("async function recoverHistoryIfNeeded(");
const endFallback = source.indexOf("async function runExtraction()");
const end = source.indexOf("/**\n * 提取管线:处理未提取的对话楼层");
const resolvedEnd = end >= 0 ? end : endFallback;
if (start < 0 || resolvedEnd < 0 || resolvedEnd <= start) {
throw new Error("无法从 index.js 提取 history recovery 定义");
}
const snippet = source
.slice(start, resolvedEnd)
.replace(/^export\s+/gm, "");
const context = {
console,
Date,
result: null,
currentGraph: null,
extractionCount: 0,
isRecoveringHistory: false,
chat: [],
clearedHistoryDirty: null,
prepareVectorStateCalls: [],
saveGraphToChatCalls: 0,
refreshPanelCalls: 0,
notices: [],
embeddingConfig: { mode: "backend" },
ensureCurrentGraphRuntimeState() {
return context.currentGraph;
},
beginStageAbortController() {
return {
signal: { aborted: false },
abort() {},
};
},
finishStageAbortController() {},
updateStageNotice(...args) {
context.notices.push(args);
},
inspectHistoryMutation() {
return context.inspectHistoryMutationImpl();
},
inspectHistoryMutationImpl() {
return {
dirty: true,
earliestAffectedFloor: 0,
source: "manual-test",
reason: "edited",
};
},
getContext() {
return {
chat: context.chat,
chatId: "chat-main",
};
},
getCurrentChatId() {
return "chat-main";
},
clampRecoveryStartFloor(chat, floor) {
return Math.max(0, Number(floor) || 0);
},
throwIfAborted(signal, message = "aborted") {
if (signal?.aborted) {
const error = new Error(message);
error.name = "AbortError";
throw error;
}
},
createAbortError(message = "aborted") {
const error = new Error(message);
error.name = "AbortError";
return error;
},
isAbortError(error) {
return error?.name === "AbortError";
},
findJournalRecoveryPoint(graph, floor) {
return context.findJournalRecoveryPointImpl(graph, floor);
},
findJournalRecoveryPointImpl() {
return null;
},
buildReverseJournalRecoveryPlan(...args) {
return context.buildReverseJournalRecoveryPlanImpl(...args);
},
buildReverseJournalRecoveryPlanImpl() {
return {
valid: true,
backendDeleteHashes: [],
replayRequiredNodeIds: [],
pendingRepairFromFloor: 0,
legacyGapFallback: false,
dirtyReason: "history-recovery-replay",
};
},
rollbackAffectedJournals() {},
normalizeGraphRuntimeState(graph) {
return graph;
},
createEmptyGraph() {
return {
historyState: {
extractionCount: 0,
lastMutationSource: "",
lastMutationReason: "",
},
vectorIndexState: {
collectionId: "col-1",
dirty: false,
dirtyReason: "",
pendingRepairFromFloor: null,
replayRequiredNodeIds: [],
lastWarning: "",
lastIntegrityIssue: null,
},
batchJournal: [],
lastProcessedSeq: -1,
};
},
getEmbeddingConfig() {
return context.embeddingConfig;
},
getSettings() {
return {};
},
isBackendVectorConfig(config) {
return config?.mode === "backend";
},
async deleteBackendVectorHashesForRecovery(...args) {
context.deletedHashesCalls ||= [];
context.deletedHashesCalls.push(args);
},
async prepareVectorStateForReplay(...args) {
context.prepareVectorStateCalls.push(args);
if (typeof context.prepareVectorStateForReplayImpl === "function") {
return await context.prepareVectorStateForReplayImpl(...args);
}
},
applyRecoveryPlanToVectorState() {},
async replayExtractionFromHistory(...args) {
if (typeof context.replayExtractionFromHistoryImpl === "function") {
return await context.replayExtractionFromHistoryImpl(...args);
}
return 0;
},
updateProcessedHistorySnapshot(chat, lastProcessedAssistantFloor) {
context.updatedProcessedHistorySnapshot = {
chatLength: Array.isArray(chat) ? chat.length : 0,
lastProcessedAssistantFloor,
};
context.currentGraph.historyState ||= {};
context.currentGraph.historyState.lastProcessedAssistantFloor =
lastProcessedAssistantFloor;
context.currentGraph.historyState.processedMessageHashes =
lastProcessedAssistantFloor >= 0
? { [lastProcessedAssistantFloor]: `hash-${lastProcessedAssistantFloor}` }
: {};
},
clearHistoryDirty(graph, result) {
context.clearedHistoryDirty = result;
graph.historyState ||= {};
graph.historyState.historyDirtyFrom = null;
graph.historyState.processedMessageHashes = {};
graph.historyState.lastRecoveryResult = result;
},
buildRecoveryResult(status, extra = {}) {
return {
status,
...extra,
};
},
saveGraphToChat() {
context.saveGraphToChatCalls += 1;
},
clearInjectionState() {},
assertRecoveryChatStillActive() {},
refreshPanelLiveState() {
context.refreshPanelCalls += 1;
},
toastr: {
success() {},
warning() {},
error() {},
},
};
vm.createContext(context);
vm.runInContext(
`${snippet}\nresult = { recoverFromHistoryMutation: recoverHistoryIfNeeded };`,
context,
{ filename: indexPath },
);
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);
},
updateProcessedHistorySnapshot(chat, lastProcessedAssistantFloor) {
context.updatedProcessedHistorySnapshot = {
chatLength: Array.isArray(chat) ? chat.length : 0,
lastProcessedAssistantFloor,
};
context.currentGraph.historyState ||= {};
context.currentGraph.historyState.lastProcessedAssistantFloor =
lastProcessedAssistantFloor;
context.currentGraph.historyState.processedMessageHashes =
lastProcessedAssistantFloor >= 0
? { [lastProcessedAssistantFloor]: `hash-${lastProcessedAssistantFloor}` }
: {};
context.currentGraph.lastProcessedSeq = lastProcessedAssistantFloor;
},
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.processedMessageHashes = {};
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;
}
this.ownerDocument?._notifyMutation({
type: "attributes",
target: this,
attributeName: key,
});
}
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;
this.options = {};
}
observe(_target = null, options = {}) {
this.active = true;
this.options = { ...options };
this.documentRef._registerObserver(this);
}
disconnect() {
this.active = false;
this.documentRef._unregisterObserver(this);
}
_notify(record) {
if (!this.active) return;
if (record?.type === "attributes" && !this.options?.attributes) return;
if (record?.type === "childList" && !this.options?.childList) return;
if (
record?.type === "attributes" &&
Array.isArray(this.options?.attributeFilter) &&
this.options.attributeFilter.length > 0 &&
!this.options.attributeFilter.includes(String(record.attributeName || ""))
) {
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 testRecallCardDelayedStableMessageIndexEventuallyRenders() {
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 {
harness.api.schedulePersistedRecallMessageUiRefresh();
await waitForTick();
messageElement.setAttribute("mesid", "0");
await waitForTick();
await waitForTick();
await new Promise((resolve) => setTimeout(resolve, 35));
await waitForTick();
assert.equal(
harness.chatRoot.querySelectorAll(".bme-recall-card").length,
1,
);
} finally {
harness.restoreGlobals();
}
}
async function testRecallCardSurvivesLateMessageDomReplacement() {
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 });
harness.context.PERSISTED_RECALL_UI_REFRESH_RETRY_DELAYS_MS = [
0,
20,
40,
120,
260,
];
const originalElement = createMessageElement(harness.document, 0, {
stableId: true,
withMesBlock: true,
isUser: true,
});
harness.chatRoot.appendChild(originalElement);
try {
harness.api.schedulePersistedRecallMessageUiRefresh();
await waitForTick();
await waitForTick();
assert.equal(
harness.chatRoot.querySelectorAll(".bme-recall-card").length,
1,
);
originalElement.remove();
await new Promise((resolve) => setTimeout(resolve, 180));
const replacementElement = createMessageElement(harness.document, 0, {
stableId: true,
withMesBlock: true,
isUser: true,
});
harness.chatRoot.appendChild(replacementElement);
await waitForTick();
await new Promise((resolve) => setTimeout(resolve, 120));
await waitForTick();
assert.equal(
harness.chatRoot.querySelectorAll(".bme-recall-card").length,
1,
"延迟重渲染后的当前 user 楼层应自动补挂 Recall Card",
);
assert.equal(
replacementElement.querySelectorAll(".bme-recall-card").length,
1,
"卡片应重新挂到替换后的消息 DOM 上",
);
} finally {
harness.restoreGlobals();
}
}
async function testRecallCardKeepsRetryingWhenOlderCardsAlreadyRendered() {
const chat = [
{
is_user: true,
mes: "user-0",
extra: {
bme_recall: buildPersistedRecallRecord({
injectionText: "recall-0",
selectedNodeIds: ["n1"],
nowIso: "2026-01-01T00:00:00.000Z",
}),
},
},
{
is_user: true,
mes: "user-1",
extra: {
bme_recall: buildPersistedRecallRecord({
injectionText: "recall-1",
selectedNodeIds: ["n2"],
nowIso: "2026-01-01T00:00:00.000Z",
}),
},
},
];
const harness = await createRecallUiHarness({ chat });
const firstMessageElement = createMessageElement(harness.document, 0, {
stableId: true,
withMesBlock: true,
isUser: true,
});
const secondMessageElement = createMessageElement(harness.document, 1, {
stableId: false,
withMesBlock: true,
isUser: true,
});
harness.chatRoot.appendChild(firstMessageElement);
harness.chatRoot.appendChild(secondMessageElement);
try {
harness.api.schedulePersistedRecallMessageUiRefresh();
await waitForTick();
assert.equal(
harness.chatRoot.querySelectorAll(".bme-recall-card").length,
1,
);
secondMessageElement.setAttribute("mesid", "1");
await waitForTick();
await waitForTick();
await new Promise((resolve) => setTimeout(resolve, 35));
await waitForTick();
assert.equal(
harness.chatRoot.querySelectorAll(".bme-recall-card").length,
2,
);
} finally {
harness.restoreGlobals();
}
}
async function testRecallCardPrefersBetterDuplicateMessageAnchor() {
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 staleElement = createMessageElement(harness.document, 0, {
stableId: true,
withMesBlock: false,
isUser: true,
});
const liveElement = createMessageElement(harness.document, 0, {
stableId: true,
withMesBlock: true,
isUser: true,
});
liveElement.classList.add("last_mes");
harness.chatRoot.appendChild(staleElement);
harness.chatRoot.appendChild(liveElement);
try {
const summary = harness.api.refreshPersistedRecallMessageUi();
assert.equal(summary.status, "rendered");
assert.equal(
staleElement.querySelectorAll(".bme-recall-card").length,
0,
"低质量的重复 DOM 不应抢走当前楼层卡片",
);
assert.equal(
liveElement.querySelectorAll(".bme-recall-card").length,
1,
"应优先挂到结构更完整的那条消息 DOM 上",
);
assert.equal(
harness.chatRoot.querySelectorAll(".mes_block .bme-recall-card").length,
1,
);
} 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();
}
}
async function testRecallCardExpandedContentRerendersAfterRecordUpdate() {
const chat = [
{
is_user: true,
mes: "user-0",
extra: {
bme_recall: buildPersistedRecallRecord({
injectionText: "recall-0",
selectedNodeIds: ["n1"],
recallSource: "before",
tokenEstimate: 8,
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 {
let summary = harness.api.refreshPersistedRecallMessageUi();
assert.equal(summary.status, "rendered");
let card = harness.chatRoot.querySelector(".bme-recall-card");
card.querySelector(".bme-recall-bar")?.click();
assert.equal(card.classList.contains("expanded"), true);
const signatureBefore = card.dataset.expandedRenderSignature || "";
assert.equal(card.querySelector(".bme-recall-meta-tag"), null);
chat[0].extra.bme_recall = buildPersistedRecallRecord(
{
injectionText: "recall-1",
selectedNodeIds: ["n1", "n2"],
recallSource: "after",
tokenEstimate: 13,
manuallyEdited: true,
nowIso: "2026-01-01T00:01:00.000Z",
},
chat[0].extra.bme_recall,
);
summary = harness.api.refreshPersistedRecallMessageUi();
assert.equal(summary.status, "rendered");
card = harness.chatRoot.querySelector(".bme-recall-card");
assert.equal(card.dataset.updatedAt, "2026-01-01T00:01:00.000Z");
assert.equal(
card.querySelector(".bme-recall-count-badge")?.textContent,
"记忆 2",
);
assert.equal(
card.querySelector(".bme-recall-token-hint")?.textContent,
"~13 tokens",
);
const metaElements = card.querySelectorAll(".bme-recall-meta");
const latestMeta = metaElements[metaElements.length - 1] || null;
const latestTag =
card.querySelectorAll(".bme-recall-meta-tag").pop() || null;
assert.ok(latestMeta?.textContent.includes("来源: after"));
assert.equal(latestTag?.textContent, "✍ 手动编辑");
assert.notEqual(card.dataset.expandedRenderSignature, signatureBefore);
} finally {
harness.restoreGlobals();
}
}
async function testRecallCardUserTextRefreshesWithoutCardRecreate() {
const chat = [
{
is_user: true,
mes: "before-user",
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 {
harness.api.refreshPersistedRecallMessageUi();
const firstCard = harness.chatRoot.querySelector(".bme-recall-card");
assert.equal(
firstCard.querySelector(".bme-recall-user-text")?.textContent,
"before-user",
);
chat[0].mes = "after-user";
harness.api.refreshPersistedRecallMessageUi();
const secondCard = harness.chatRoot.querySelector(".bme-recall-card");
assert.equal(secondCard, firstCard);
assert.equal(
secondCard.querySelector(".bme-recall-user-text")?.textContent,
"after-user",
);
} finally {
harness.restoreGlobals();
}
}
async function testRecallCardDisplayModeToggleRestoresOriginalUserText() {
const chat = [
{
is_user: true,
mes: "line-1\nline-2",
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,
});
const userTextElement = messageElement.querySelector(".mes_text");
userTextElement.textContent = chat[0].mes;
harness.chatRoot.appendChild(messageElement);
try {
harness.context.getSettings = () => ({
panelTheme: "crimson",
recallCardUserInputDisplayMode: "beautify_only",
});
harness.api.refreshPersistedRecallMessageUi();
let card = harness.chatRoot.querySelector(".bme-recall-card");
assert.equal(card?.dataset.userInputDisplayMode, "beautify_only");
assert.equal(
userTextElement.classList.contains("bme-hide-original-user-text"),
true,
);
assert.equal(
card?.querySelector(".bme-recall-user-text")?.textContent,
"line-1\nline-2",
);
harness.context.getSettings = () => ({
panelTheme: "crimson",
recallCardUserInputDisplayMode: "mirror",
});
harness.api.refreshPersistedRecallMessageUi();
card = harness.chatRoot.querySelector(".bme-recall-card");
assert.equal(card?.dataset.userInputDisplayMode, "mirror");
assert.equal(
userTextElement.classList.contains("bme-hide-original-user-text"),
false,
);
delete chat[0].extra.bme_recall;
harness.api.refreshPersistedRecallMessageUi();
assert.equal(
userTextElement.classList.contains("bme-hide-original-user-text"),
false,
);
assert.equal(
harness.chatRoot.querySelectorAll(".bme-recall-card").length,
0,
);
} 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 testCompressTypeAcceptsTopLevelFieldsResult() {
const graph = createEmptyGraph();
const typeDef = {
id: "event",
label: "事件",
columns: [
{ name: "title" },
{ name: "summary" },
{ name: "participants" },
{ name: "status" },
],
compression: {
mode: "hierarchical",
fanIn: 2,
threshold: 2,
keepRecentLeaves: 0,
},
};
const first = makeEvent(1, "事件甲");
const second = makeEvent(2, "事件乙");
addNode(graph, first);
addNode(graph, second);
const restoreOverrides = pushTestOverrides({
llm: {
async callLLMForJSON() {
return {
title: "压缩事件",
summary: "顶层返回的合并摘要",
participants: "Alice",
status: "done",
};
},
},
});
try {
const result = await compressType({
graph,
typeDef,
embeddingConfig: null,
force: true,
settings: {},
});
assert.equal(result.created, 1);
const compressed = graph.nodes.find(
(node) => node.level === 1 && !node.archived,
);
assert.equal(compressed?.fields?.summary, "顶层返回的合并摘要");
assert.equal(compressed?.fields?.title, "压缩事件");
} 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 testExtractorNormalizesFlatCreateOperation() {
const graph = createEmptyGraph();
const restoreOverrides = pushTestOverrides({
llm: {
async callLLMForJSON() {
return {
operations: [
{
type: "event",
id: "evt1",
title: "午夜越界",
summary: "两人在午夜越界相见,留下了新的冲突线索。",
participants: "悟悟, 晗",
},
],
};
},
},
});
try {
const result = await extractMemories({
graph,
messages: [{ seq: 6, role: "assistant", content: "测试扁平 create" }],
startSeq: 6,
endSeq: 6,
schema,
embeddingConfig: null,
settings: {},
});
assert.equal(result.success, true);
assert.equal(result.newNodes, 1);
assert.equal(graph.lastProcessedSeq, 6);
const created = graph.nodes.find((node) => !node.archived && node.type === "event");
assert.ok(created);
assert.equal(created.fields.title, "午夜越界");
assert.equal(
created.fields.summary,
"两人在午夜越界相见,留下了新的冲突线索。",
);
assert.equal(created.fields.participants, "悟悟, 晗");
} finally {
restoreOverrides();
}
}
async function testExtractorNormalizesArrayPayloadAndPreservesScopeField() {
const graph = createEmptyGraph();
const restoreOverrides = pushTestOverrides({
llm: {
async callLLMForJSON() {
return [
{
type: "synopsis",
id: "syn1",
summary: "最近的整体剧情进入高压对峙阶段。",
scope: "20-2-2",
},
];
},
},
});
try {
const result = await extractMemories({
graph,
messages: [{ seq: 8, role: "assistant", content: "测试数组 payload" }],
startSeq: 8,
endSeq: 8,
schema,
embeddingConfig: null,
settings: {},
});
assert.equal(result.success, true);
assert.equal(result.newNodes, 1);
const created = graph.nodes.find(
(node) => !node.archived && node.type === "synopsis",
);
assert.ok(created);
assert.equal(created.fields.summary, "最近的整体剧情进入高压对峙阶段。");
assert.equal(created.fields.scope, "20-2-2");
assert.equal(created.scope?.layer, "objective");
} 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.equal(recoveryPlan.valid, true);
assert.equal(recoveryPlan.invalidReason, "");
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.equal(recoveryPlan.valid, true);
assert.equal(recoveryPlan.invalidReason, "");
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.equal(recoveryPlan.valid, true);
assert.equal(recoveryPlan.invalidReason, "");
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.inspectAutoCompressionCandidates = () => ({
hasCandidates: true,
reason: "",
});
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,
compressionEveryN: 1,
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 testExtractionPostProcessStatusesExposeMaintenancePhases() {
const harness = await createBatchStageHarness();
const { createBatchStatusSkeleton, handleExtractionSuccess } = harness.result;
harness.currentGraph = {
historyState: { extractionCount: 0 },
vectorIndexState: {},
};
harness.ensureCurrentGraphRuntimeState = () => {
harness.currentGraph.historyState ||= {};
harness.currentGraph.vectorIndexState ||= {};
};
harness.consolidateMemories = async () => ({
merged: 1,
skipped: 0,
kept: 0,
evolved: 1,
connections: 0,
updates: 0,
});
harness.generateSynopsis = async () => ({ ok: true });
harness.generateReflection = async () => ({ ok: true });
harness.sleepCycle = () => ({ forgotten: 0 });
harness.inspectAutoCompressionCandidates = () => ({
hasCandidates: true,
reason: "",
});
harness.compressAll = async () => ({ created: 1, archived: 2 });
harness.syncVectorState = async () => ({
insertedHashes: ["hash-stage"],
stats: { pending: 0, indexed: 3 },
});
const batchStatus = createBatchStatusSkeleton({
processedRange: [8, 8],
extractionCountBefore: 0,
});
await handleExtractionSuccess(
{
newNodeIds: ["node-stage"],
},
8,
{
enableConsolidation: true,
consolidationAutoMinNewNodes: 1,
enableSynopsis: true,
synopsisEveryN: 1,
enableReflection: true,
reflectEveryN: 1,
enableSleepCycle: true,
sleepEveryN: 1,
enableAutoCompression: true,
compressionEveryN: 1,
},
undefined,
batchStatus,
);
const statusTexts = harness.extractionStatuses.map((entry) => entry[0]);
assert.ok(statusTexts.includes("提取收尾中"));
assert.ok(statusTexts.includes("整合/进化中"));
assert.ok(statusTexts.includes("概要更新中"));
assert.ok(statusTexts.includes("反思生成中"));
assert.ok(statusTexts.includes("主动遗忘中"));
assert.ok(statusTexts.includes("自动压缩中"));
assert.ok(statusTexts.includes("向量同步中"));
}
async function testAutoConsolidationRunsOnHighDuplicateRiskSingleNode() {
const harness = await createBatchStageHarness();
const { createBatchStatusSkeleton, handleExtractionSuccess } = harness.result;
harness.currentGraph = {
historyState: { extractionCount: 0 },
vectorIndexState: {},
};
harness.ensureCurrentGraphRuntimeState = () => {
harness.currentGraph.historyState ||= {};
harness.currentGraph.vectorIndexState ||= {};
};
let gateCalls = 0;
let consolidateCalls = 0;
harness.analyzeAutoConsolidationGate = async () => {
gateCalls += 1;
return {
triggered: true,
reason:
"本批仅新增 1 个节点但与旧记忆高度相似0.930 >= 0.85),已触发自动整合",
matchedScore: 0.93,
matchedNodeId: "old-1",
};
};
harness.consolidateMemories = async () => {
consolidateCalls += 1;
return {
merged: 1,
skipped: 0,
kept: 0,
evolved: 0,
connections: 0,
updates: 0,
};
};
harness.syncVectorState = async () => ({
insertedHashes: [],
stats: { pending: 0 },
});
const batchStatus = createBatchStatusSkeleton({
processedRange: [6, 6],
extractionCountBefore: 0,
});
const effects = await handleExtractionSuccess(
{ newNodeIds: ["node-dup"] },
6,
{
enableConsolidation: true,
consolidationAutoMinNewNodes: 2,
consolidationThreshold: 0.85,
enableAutoCompression: false,
compressionEveryN: 10,
enableSynopsis: false,
enableReflection: false,
enableSleepCycle: false,
synopsisEveryN: 1,
reflectEveryN: 1,
sleepEveryN: 1,
},
undefined,
batchStatus,
);
assert.equal(gateCalls, 1);
assert.equal(consolidateCalls, 1);
assert.equal(effects.batchStatus.consolidationGateTriggered, true);
assert.equal(
effects.batchStatus.consolidationGateMatchedNodeId,
"old-1",
);
assert.equal(effects.batchStatus.consolidationGateSimilarity, 0.93);
assert.match(
effects.batchStatus.consolidationGateReason,
/高度相似/,
);
assert.equal(effects.batchStatus.autoCompressionScheduled, false);
assert.match(
effects.batchStatus.autoCompressionSkippedReason,
/自动压缩.*已关闭/,
);
}
async function testAutoConsolidationSkipsLowRiskSingleNode() {
const harness = await createBatchStageHarness();
const { createBatchStatusSkeleton, handleExtractionSuccess } = harness.result;
harness.currentGraph = {
historyState: { extractionCount: 0 },
vectorIndexState: {},
};
harness.ensureCurrentGraphRuntimeState = () => {
harness.currentGraph.historyState ||= {};
harness.currentGraph.vectorIndexState ||= {};
};
let consolidateCalls = 0;
harness.analyzeAutoConsolidationGate = async () => ({
triggered: false,
reason:
"本批新增少且最高相似度 0.420 未达到阈值 0.85,跳过自动整合",
matchedScore: 0.42,
matchedNodeId: "old-2",
});
harness.consolidateMemories = async () => {
consolidateCalls += 1;
return {
merged: 0,
skipped: 0,
kept: 1,
evolved: 0,
connections: 0,
updates: 0,
};
};
harness.syncVectorState = async () => ({
insertedHashes: [],
stats: { pending: 0 },
});
const batchStatus = createBatchStatusSkeleton({
processedRange: [7, 7],
extractionCountBefore: 0,
});
const effects = await handleExtractionSuccess(
{ newNodeIds: ["node-low-risk"] },
7,
{
enableConsolidation: true,
consolidationAutoMinNewNodes: 2,
consolidationThreshold: 0.85,
enableAutoCompression: false,
compressionEveryN: 10,
enableSynopsis: false,
enableReflection: false,
enableSleepCycle: false,
synopsisEveryN: 1,
reflectEveryN: 1,
sleepEveryN: 1,
},
undefined,
batchStatus,
);
assert.equal(consolidateCalls, 0);
assert.equal(effects.batchStatus.consolidationGateTriggered, false);
assert.equal(
effects.batchStatus.consolidationGateMatchedNodeId,
"old-2",
);
assert.equal(effects.batchStatus.consolidationGateSimilarity, 0.42);
assert.match(
effects.batchStatus.consolidationGateReason,
/跳过自动整合/,
);
assert.equal(
effects.batchStatus.stages.structural.artifacts.includes(
"consolidation-skipped",
),
true,
);
}
async function testAutoCompressionRunsOnlyOnConfiguredInterval() {
const harness = await createBatchStageHarness();
const { createBatchStatusSkeleton, handleExtractionSuccess } = harness.result;
harness.currentGraph = {
historyState: { extractionCount: 9 },
vectorIndexState: {},
};
harness.ensureCurrentGraphRuntimeState = () => {
harness.currentGraph.historyState ||= {};
harness.currentGraph.vectorIndexState ||= {};
};
harness.extractionCount = 9;
let compressionCalls = 0;
harness.inspectAutoCompressionCandidates = () => ({
hasCandidates: true,
reason: "",
});
harness.compressAll = async () => {
compressionCalls += 1;
return { created: 1, archived: 2 };
};
harness.syncVectorState = async () => ({
insertedHashes: [],
stats: { pending: 0 },
});
const batchStatus = createBatchStatusSkeleton({
processedRange: [8, 8],
extractionCountBefore: 9,
});
const effects = await handleExtractionSuccess(
{ newNodeIds: ["node-for-compress"] },
8,
{
enableConsolidation: false,
enableAutoCompression: true,
compressionEveryN: 10,
enableSynopsis: false,
enableReflection: false,
enableSleepCycle: false,
synopsisEveryN: 1,
reflectEveryN: 1,
sleepEveryN: 1,
},
undefined,
batchStatus,
);
assert.equal(compressionCalls, 1);
assert.equal(effects.batchStatus.autoCompressionScheduled, true);
assert.equal(effects.batchStatus.nextCompressionAtExtractionCount, 20);
assert.equal(effects.batchStatus.autoCompressionSkippedReason, "");
}
async function testAutoCompressionSkipsWhenNotScheduledOrNoCandidates() {
const offCycleHarness = await createBatchStageHarness();
const {
createBatchStatusSkeleton: createOffCycleBatchStatus,
handleExtractionSuccess: handleOffCycleExtractionSuccess,
} = offCycleHarness.result;
offCycleHarness.currentGraph = {
historyState: { extractionCount: 0 },
vectorIndexState: {},
};
offCycleHarness.ensureCurrentGraphRuntimeState = () => {
offCycleHarness.currentGraph.historyState ||= {};
offCycleHarness.currentGraph.vectorIndexState ||= {};
};
let offCycleCompressionCalls = 0;
offCycleHarness.compressAll = async () => {
offCycleCompressionCalls += 1;
return { created: 1, archived: 1 };
};
offCycleHarness.syncVectorState = async () => ({
insertedHashes: [],
stats: { pending: 0 },
});
const offCycleStatus = createOffCycleBatchStatus({
processedRange: [9, 9],
extractionCountBefore: 0,
});
const offCycleEffects = await handleOffCycleExtractionSuccess(
{ newNodeIds: ["node-off-cycle"] },
9,
{
enableConsolidation: false,
enableAutoCompression: true,
compressionEveryN: 10,
enableSynopsis: false,
enableReflection: false,
enableSleepCycle: false,
synopsisEveryN: 1,
reflectEveryN: 1,
sleepEveryN: 1,
},
undefined,
offCycleStatus,
);
assert.equal(offCycleCompressionCalls, 0);
assert.equal(offCycleEffects.batchStatus.autoCompressionScheduled, false);
assert.match(
offCycleEffects.batchStatus.autoCompressionSkippedReason,
/未到每 10 次自动压缩周期/,
);
assert.equal(offCycleEffects.batchStatus.nextCompressionAtExtractionCount, 10);
const scheduledHarness = await createBatchStageHarness();
const {
createBatchStatusSkeleton: createScheduledBatchStatus,
handleExtractionSuccess: handleScheduledExtractionSuccess,
} = scheduledHarness.result;
scheduledHarness.currentGraph = {
historyState: { extractionCount: 9 },
vectorIndexState: {},
};
scheduledHarness.ensureCurrentGraphRuntimeState = () => {
scheduledHarness.currentGraph.historyState ||= {};
scheduledHarness.currentGraph.vectorIndexState ||= {};
};
scheduledHarness.extractionCount = 9;
let scheduledCompressionCalls = 0;
scheduledHarness.inspectAutoCompressionCandidates = () => ({
hasCandidates: false,
reason: "已到自动压缩周期,但当前没有达到内部压缩阈值的候选组",
});
scheduledHarness.compressAll = async () => {
scheduledCompressionCalls += 1;
return { created: 1, archived: 1 };
};
scheduledHarness.syncVectorState = async () => ({
insertedHashes: [],
stats: { pending: 0 },
});
const scheduledStatus = createScheduledBatchStatus({
processedRange: [10, 10],
extractionCountBefore: 9,
});
const scheduledEffects = await handleScheduledExtractionSuccess(
{ newNodeIds: ["node-scheduled"] },
10,
{
enableConsolidation: false,
enableAutoCompression: true,
compressionEveryN: 10,
enableSynopsis: false,
enableReflection: false,
enableSleepCycle: false,
synopsisEveryN: 1,
reflectEveryN: 1,
sleepEveryN: 1,
},
undefined,
scheduledStatus,
);
assert.equal(scheduledCompressionCalls, 0);
assert.equal(scheduledEffects.batchStatus.autoCompressionScheduled, true);
assert.match(
scheduledEffects.batchStatus.autoCompressionSkippedReason,
/没有达到内部压缩阈值的候选组/,
);
assert.equal(
scheduledEffects.batchStatus.stages.structural.artifacts.includes(
"compression-skipped",
),
true,
);
}
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 testProcessedHistoryAdvanceTracksCoreExtractionSuccess() {
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), true);
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), true);
const finalizeFailed = createBatchStatusSkeleton({
processedRange: [6, 7],
extractionCountBefore: 0,
});
setBatchStageOutcome(finalizeFailed, "core", "success");
setBatchStageOutcome(
finalizeFailed,
"finalize",
"failed",
"vector finalize down",
);
finalizeBatchStatus(finalizeFailed);
assert.equal(finalizeFailed.completed, false);
assert.equal(finalizeFailed.outcome, "failed");
assert.equal(shouldAdvanceProcessedHistory(finalizeFailed), true);
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 testGenerationRecallTransactionDedupesReverseHookOrder() {
const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: true, mes: "逆序同轮输入" }];
await harness.result.onBeforeCombinePrompts();
await harness.result.onGenerationAfterCommands("normal", {}, false);
assert.equal(harness.runRecallCalls.length, 1);
assert.equal(
harness.runRecallCalls[0].hookName,
"GENERATE_BEFORE_COMBINE_PROMPTS",
);
}
async function testGenerationRecallHistoryModesUseSameBindingAcrossHooks() {
for (const generationType of ["continue", "regenerate", "swipe"]) {
const harness = await createGenerationRecallHarness();
const userMessage = `历史输入-${generationType}`;
harness.chat = [
{ is_user: true, mes: userMessage },
{ is_user: false, mes: "assistant-tail" },
];
await harness.result.onGenerationAfterCommands(generationType, {}, false);
await harness.result.onBeforeCombinePrompts();
assert.equal(
harness.runRecallCalls.length,
1,
`${generationType} 应只执行一次召回`,
);
assert.equal(
harness.runRecallCalls[0].hookName,
"GENERATION_AFTER_COMMANDS",
);
assert.equal(harness.runRecallCalls[0].targetUserMessageIndex, 0);
assert.equal(harness.runRecallCalls[0].overrideUserMessage, userMessage);
}
}
async function testGenerationRecallFrozenBindingSurvivesCrossHookInputDrift() {
const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: true, mes: "稳定输入-A" }];
await harness.result.onGenerationAfterCommands("normal", {}, false);
harness.chat = [{ is_user: true, mes: "稳定输入-B" }];
await harness.result.onBeforeCombinePrompts();
assert.equal(harness.runRecallCalls.length, 1);
assert.equal(harness.runRecallCalls[0].overrideUserMessage, "稳定输入-A");
}
async function testGenerationRecallSkipsUntilTargetUserFloorAvailable() {
const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: false, mes: "assistant-only" }];
await harness.result.onGenerationAfterCommands("normal", {}, false);
assert.equal(harness.runRecallCalls.length, 0);
harness.chat = [{ is_user: true, mes: "补齐 user 楼层" }];
await harness.result.onBeforeCombinePrompts();
assert.equal(harness.runRecallCalls.length, 1);
assert.equal(
harness.runRecallCalls[0].hookName,
"GENERATE_BEFORE_COMBINE_PROMPTS",
);
}
async function testGenerationRecallBeforeCombineCanUseProvisionalSendIntentBinding() {
const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: false, mes: "assistant-tail" }];
harness.__sendTextareaValue = "发送前输入";
harness.pendingRecallSendIntent = {
text: "发送前输入",
hash: "hash-send-intent",
at: Date.now(),
};
harness.result.pendingRecallSendIntent = harness.pendingRecallSendIntent;
await harness.result.onBeforeCombinePrompts();
assert.equal(harness.runRecallCalls.length, 1);
assert.equal(
harness.runRecallCalls[0].hookName,
"GENERATE_BEFORE_COMBINE_PROMPTS",
);
assert.equal(harness.runRecallCalls[0].overrideUserMessage, "发送前输入");
assert.equal(harness.runRecallCalls[0].overrideSource, "send-intent");
assert.equal(harness.runRecallCalls[0].overrideSourceLabel, "发送意图");
assert.equal(
harness.runRecallCalls[0].overrideReason,
"send-intent-captured",
);
assert.equal(harness.runRecallCalls[0].targetUserMessageIndex, null);
}
async function testGenerationRecallHostLifecycleSnapshotSurvivesTextareaClearWithoutDomIntent() {
const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: false, mes: "assistant-tail" }];
harness.__sendTextareaValue = "宿主冻结输入";
const frozenSnapshot = harness.result.freezeHostGenerationInputSnapshot(
harness.__sendTextareaValue,
);
harness.__sendTextareaValue = "";
await harness.result.onGenerationAfterCommands(
"normal",
{ frozenInputSnapshot: frozenSnapshot },
false,
);
await harness.result.onBeforeCombinePrompts();
assert.equal(harness.runRecallCalls.length, 1);
assert.equal(
harness.runRecallCalls[0].hookName,
"GENERATE_BEFORE_COMBINE_PROMPTS",
);
assert.equal(harness.runRecallCalls[0].overrideUserMessage, "宿主冻结输入");
assert.equal(
harness.runRecallCalls[0].overrideSource,
"host-generation-lifecycle",
);
assert.equal(harness.runRecallCalls[0].overrideSourceLabel, "宿主发送快照");
assert.equal(
harness.runRecallCalls[0].overrideReason,
"host-snapshot-captured",
);
assert.equal(harness.runRecallCalls[0].targetUserMessageIndex, null);
assert.deepEqual(harness.result.getPendingHostGenerationInputSnapshot(), {
text: "",
hash: "",
at: 0,
source: "",
messageId: null,
});
}
async function testGenerationRecallAfterCommandsStillSkipsWithoutStableUserFloor() {
const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: false, mes: "assistant-tail" }];
harness.__sendTextareaValue = "发送前输入";
harness.pendingRecallSendIntent = {
text: "发送前输入",
hash: "hash-send-intent",
at: Date.now(),
};
await harness.result.onGenerationAfterCommands("normal", {}, false);
assert.equal(
harness.runRecallCalls.length,
0,
"after-commands 在缺失稳定 user floor 时应继续跳过,避免错误楼层绑定",
);
}
async function testGenerationRecallSameKeyCanRunAgainImmediatelyAsNewGeneration() {
const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: true, mes: "同 key 连续生成" }];
await harness.result.onGenerationAfterCommands("normal", {}, false);
await harness.result.onGenerationAfterCommands("normal", {}, false);
assert.equal(harness.runRecallCalls.length, 2);
assert.equal(
harness.runRecallCalls[0].recallKey,
harness.runRecallCalls[1].recallKey,
);
}
async function testGenerationRecallSameKeyCanRunAgainAfterBridgeWindow() {
const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: true, mes: "同 key 重复生成" }];
await harness.result.onGenerationAfterCommands("normal", {}, false);
const transaction = [
...harness.result.generationRecallTransactions.values(),
][0];
transaction.updatedAt = Date.now() - 5000;
harness.result.generationRecallTransactions.set(transaction.id, transaction);
await harness.result.onGenerationAfterCommands("normal", {}, false);
assert.equal(harness.runRecallCalls.length, 2);
}
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 testGenerationRecallDryRunPreviewDoesNotTriggerBeforeCombineRecall() {
const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: true, mes: "Prompt Viewer 预览" }];
harness.result.onGenerationStarted("normal", {}, true);
await harness.result.onBeforeCombinePrompts();
assert.equal(harness.runRecallCalls.length, 0);
}
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 testGenerationRecallSentMessageClearsStaleTransactionForSameKey() {
const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: true, mes: "同 key 发送后重开" }];
await harness.result.onGenerationAfterCommands("normal", {}, false);
assert.equal(harness.runRecallCalls.length, 1);
assert.equal(harness.result.generationRecallTransactions.size, 1);
harness.recordRecallSentUserMessage(0, "同 key 发送后重开");
await harness.result.onGenerationAfterCommands("normal", {}, false);
assert.equal(harness.runRecallCalls.length, 2);
}
async function testRegisterCoreEventHooksIsIdempotent() {
const eventRegistrations = [];
const makeFirstRegistrations = [];
const bindingState = { registered: false, cleanups: [], registeredAt: 0 };
const eventSource = {
on(eventName, listener) {
eventRegistrations.push({ eventName, listener });
},
off() {},
};
const runtime = {
console: { warn() {} },
eventSource,
eventTypes: {
CHAT_CHANGED: "chat-changed",
CHAT_LOADED: "chat-loaded",
MESSAGE_SENT: "message-sent",
GENERATION_STARTED: "generation-started",
GENERATION_ENDED: "generation-ended",
MESSAGE_RECEIVED: "message-received",
MESSAGE_DELETED: "message-deleted",
MESSAGE_EDITED: "message-edited",
MESSAGE_SWIPED: "message-swiped",
MESSAGE_UPDATED: "message-updated",
USER_MESSAGE_RENDERED: "user-message-rendered",
CHARACTER_MESSAGE_RENDERED: "character-message-rendered",
},
handlers: {
onChatChanged() {},
onChatLoaded() {},
onMessageSent() {},
onGenerationStarted() {},
onGenerationEnded() {},
onGenerationAfterCommands() {},
onBeforeCombinePrompts() {},
onMessageReceived() {},
onMessageDeleted() {},
onMessageEdited() {},
onMessageSwiped() {},
onUserMessageRendered() {},
onCharacterMessageRendered() {},
},
registerGenerationAfterCommands(listener) {
makeFirstRegistrations.push({ hook: "after", listener });
return () => {};
},
registerBeforeCombinePrompts(listener) {
makeFirstRegistrations.push({ hook: "before", listener });
return () => {};
},
getCoreEventBindingState: () => bindingState,
setCoreEventBindingState(nextState) {
bindingState.registered = Boolean(nextState?.registered);
bindingState.cleanups = Array.isArray(nextState?.cleanups)
? nextState.cleanups
: [];
bindingState.registeredAt = Number(nextState?.registeredAt) || 0;
return bindingState;
},
};
registerCoreEventHooksController(runtime);
registerCoreEventHooksController(runtime);
assert.equal(eventRegistrations.length, 12);
assert.equal(makeFirstRegistrations.length, 2);
assert.equal(bindingState.registered, true);
}
async function testChatChangedDoesNotClearCoreEventBindings() {
let clearCoreBindingsCalls = 0;
let clearPendingAutoExtractionCalls = 0;
onChatChangedController({
clearCoreEventBindingState() {
clearCoreBindingsCalls += 1;
},
clearPendingHistoryMutationChecks() {},
clearTimeout() {},
getPendingHistoryRecoveryTimer: () => null,
setPendingHistoryRecoveryTimer() {},
setPendingHistoryRecoveryTrigger() {},
clearPendingAutoExtraction() {
clearPendingAutoExtractionCalls += 1;
},
clearPendingGraphLoadRetry() {},
setSkipBeforeCombineRecallUntil() {},
setLastPreGenerationRecallKey() {},
setLastPreGenerationRecallAt() {},
clearGenerationRecallTransactionsForChat() {},
abortAllRunningStages() {},
dismissAllStageNotices() {},
syncGraphLoadFromLiveContext() {},
clearInjectionState() {},
clearRecallInputTracking() {},
installSendIntentHooks() {},
refreshPersistedRecallMessageUi() {},
});
assert.equal(
clearCoreBindingsCalls,
0,
"聊天切换不应清空核心事件监听,否则后续自动链会失联",
);
assert.equal(clearPendingAutoExtractionCalls, 1);
}
async function testSwipeRoutesToRerollWithoutHistoryRecoveryFallback() {
const invalidationReasons = [];
const rerollCalls = [];
let historyRecheckCalls = 0;
let refreshCalls = 0;
const result = await onMessageSwipedController(
{
invalidateRecallAfterHistoryMutation(reason) {
invalidationReasons.push(reason);
},
async onReroll(payload) {
rerollCalls.push(payload);
return {
success: true,
rollbackPerformed: true,
extractionTriggered: true,
requestedFloor: payload.fromFloor,
effectiveFromFloor: payload.fromFloor,
recoveryPath: "reverse-journal",
affectedBatchCount: 1,
error: "",
};
},
scheduleHistoryMutationRecheck() {
historyRecheckCalls += 1;
},
refreshPersistedRecallMessageUi() {
refreshCalls += 1;
},
console: {
warn() {},
error() {},
},
},
16,
{ reason: "host-swipe" },
);
assert.equal(invalidationReasons.length, 1);
assert.deepEqual(rerollCalls, [{ fromFloor: 16, meta: { reason: "host-swipe" } }]);
assert.equal(historyRecheckCalls, 0);
assert.equal(refreshCalls, 1);
assert.equal(result.success, true);
assert.equal(result.recoveryPath, "reverse-journal");
}
async function testMessageSentFallsBackToLatestUserWhenHostMessageIdInvalid() {
const recorded = [];
let refreshCalls = 0;
onMessageSentController(
{
getContext: () => ({
chat: [
{ is_user: true, mes: "较早用户楼层" },
{ is_user: false, mes: "assistant-tail" },
{ is_user: true, mes: "最新用户楼层" },
],
}),
recordRecallSentUserMessage(messageId, text, source = "message-sent") {
recorded.push({ messageId, text, source });
},
refreshPersistedRecallMessageUi() {
refreshCalls += 1;
},
},
null,
);
assert.deepEqual(recorded, [
{
messageId: 2,
text: "最新用户楼层",
source: "message-sent",
},
]);
assert.equal(refreshCalls, 1);
}
async function testUserMessageRenderedRefreshesRecallUiAfterRealDomRender() {
const refreshCalls = [];
const result = onUserMessageRenderedController(
{
refreshPersistedRecallMessageUi(delayMs = 0) {
refreshCalls.push(delayMs);
},
},
7,
);
assert.deepEqual(refreshCalls, [40]);
assert.equal(result.messageId, 7);
assert.equal(result.source, "user-message-rendered");
}
async function testCharacterMessageRenderedRefreshesRecallUiAfterAssistantRender() {
const refreshCalls = [];
const result = onCharacterMessageRenderedController(
{
refreshPersistedRecallMessageUi(delayMs = 0) {
refreshCalls.push(delayMs);
},
},
8,
"normal",
);
assert.deepEqual(refreshCalls, [80]);
assert.equal(result.messageId, 8);
assert.equal(result.type, "normal");
assert.equal(result.source, "character-message-rendered");
}
async function testMessageReceivedQueuesExtractionWithoutRuntimeQueueMicrotask() {
let runExtractionCalls = 0;
let refreshCalls = 0;
const chat = [
{ is_user: true, mes: "u1" },
{ is_user: false, mes: "a1" },
];
const settings = {
extractEvery: 1,
extractAutoDelayLatestAssistant: false,
enableSmartTrigger: false,
};
onMessageReceivedController(
{
getGraphPersistenceState: () => ({ loadState: "loaded", dbReady: true }),
getCurrentGraph: () => null,
getPendingRecallSendIntent: () => ({ text: "", at: 0 }),
isFreshRecallInputRecord: () => true,
createRecallInputRecord: () => ({ text: "", at: 0 }),
setPendingRecallSendIntent() {},
getContext: () => ({
chat,
}),
getSettings: () => settings,
getLastProcessedAssistantFloor: () => -1,
isAssistantChatMessage(message) {
return Boolean(message) && !message.is_user && !message.is_system;
},
resolveAutoExtractionPlan: (options = {}) =>
buildAutoExtractionPlan({
chat,
settings,
lastProcessedAssistantFloor: -1,
...(options || {}),
}),
runExtraction: async () => {
runExtractionCalls += 1;
},
console: {
error() {},
},
notifyExtractionIssue() {},
refreshPersistedRecallMessageUi() {
refreshCalls += 1;
},
},
1,
"assistant",
);
await waitForTick();
assert.equal(runExtractionCalls, 1);
assert.equal(refreshCalls, 1);
}
async function testMessageReceivedDefersExtractionDuringHostGeneration() {
let runExtractionCalls = 0;
const deferred = [];
const chat = [
{ is_user: true, mes: "u1" },
{ is_user: false, mes: "a1" },
];
const settings = {
extractEvery: 1,
extractAutoDelayLatestAssistant: false,
enableSmartTrigger: false,
};
onMessageReceivedController(
{
getGraphPersistenceState: () => ({ loadState: "loaded", dbReady: true }),
getCurrentGraph: () => null,
getPendingRecallSendIntent: () => ({ text: "", at: 0 }),
getIsHostGenerationRunning: () => true,
isFreshRecallInputRecord: () => true,
createRecallInputRecord: () => ({ text: "", at: 0 }),
deferAutoExtraction(reason, meta = {}) {
deferred.push({
reason,
messageId: Number.isFinite(Number(meta?.messageId))
? Number(meta.messageId)
: null,
targetEndFloor: Number.isFinite(Number(meta?.targetEndFloor))
? Number(meta.targetEndFloor)
: null,
});
},
setPendingRecallSendIntent() {},
getContext: () => ({
chat,
}),
getSettings: () => settings,
getLastProcessedAssistantFloor: () => -1,
isAssistantChatMessage(message) {
return Boolean(message) && !message.is_user && !message.is_system;
},
resolveAutoExtractionPlan: (options = {}) =>
buildAutoExtractionPlan({
chat,
settings,
lastProcessedAssistantFloor: -1,
...(options || {}),
}),
runExtraction: async () => {
runExtractionCalls += 1;
},
console: {
error() {},
},
notifyExtractionIssue() {},
refreshPersistedRecallMessageUi() {},
},
1,
"assistant",
);
await waitForTick();
assert.equal(runExtractionCalls, 0);
assert.deepEqual(deferred, [
{
reason: "generation-running",
messageId: 1,
targetEndFloor: 1,
},
]);
}
async function testMessageReceivedLagModeWaitsSilentlyForNextAssistant() {
let runExtractionCalls = 0;
const deferred = [];
let refreshCalls = 0;
const chat = [
{ is_user: true, mes: "u1" },
{ is_user: false, mes: "a1" },
];
const settings = {
extractEvery: 1,
extractAutoDelayLatestAssistant: true,
enableSmartTrigger: false,
};
onMessageReceivedController(
{
getGraphPersistenceState: () => ({ loadState: "loaded", dbReady: true }),
getCurrentGraph: () => null,
getPendingRecallSendIntent: () => ({ text: "", at: 0 }),
isFreshRecallInputRecord: () => true,
createRecallInputRecord: () => ({ text: "", at: 0 }),
setPendingRecallSendIntent() {},
getContext: () => ({ chat }),
getSettings: () => settings,
getLastProcessedAssistantFloor: () => -1,
isAssistantChatMessage(message) {
return Boolean(message) && !message.is_user && !message.is_system;
},
resolveAutoExtractionPlan: (options = {}) =>
buildAutoExtractionPlan({
chat,
settings,
lastProcessedAssistantFloor: -1,
...(options || {}),
}),
runExtraction: async () => {
runExtractionCalls += 1;
},
deferAutoExtraction(reason) {
deferred.push(reason);
},
console: {
error() {},
},
notifyExtractionIssue() {},
refreshPersistedRecallMessageUi() {
refreshCalls += 1;
},
},
1,
"assistant",
);
await waitForTick();
assert.equal(runExtractionCalls, 0);
assert.deepEqual(deferred, []);
assert.equal(refreshCalls, 1);
}
async function testMessageReceivedLagModeQueuesPreviousAssistantOnly() {
const runExtractionCalls = [];
const chat = [
{ is_user: true, mes: "u1" },
{ is_user: false, mes: "a1" },
{ is_user: true, mes: "u2" },
{ is_user: false, mes: "a2" },
];
const settings = {
extractEvery: 1,
extractAutoDelayLatestAssistant: true,
enableSmartTrigger: false,
};
onMessageReceivedController(
{
getGraphPersistenceState: () => ({ loadState: "loaded", dbReady: true }),
getCurrentGraph: () => null,
getPendingRecallSendIntent: () => ({ text: "", at: 0 }),
isFreshRecallInputRecord: () => true,
createRecallInputRecord: () => ({ text: "", at: 0 }),
setPendingRecallSendIntent() {},
getContext: () => ({ chat }),
getSettings: () => settings,
getLastProcessedAssistantFloor: () => -1,
isAssistantChatMessage(message) {
return Boolean(message) && !message.is_user && !message.is_system;
},
resolveAutoExtractionPlan: (options = {}) =>
buildAutoExtractionPlan({
chat,
settings,
lastProcessedAssistantFloor: -1,
...(options || {}),
}),
runExtraction: async (options = {}) => {
runExtractionCalls.push({ ...options });
},
console: {
error() {},
},
notifyExtractionIssue() {},
refreshPersistedRecallMessageUi() {},
},
3,
"assistant",
);
await waitForTick();
assert.equal(runExtractionCalls.length, 1);
assert.equal(runExtractionCalls[0]?.lockedEndFloor, 1);
assert.equal(runExtractionCalls[0]?.triggerSource, "message-received");
}
async function testLagModeSmartTriggerOnlyScoresEligibleWindow() {
const endFloors = [];
const chat = [
{ is_user: true, mes: "u1" },
{ is_user: false, mes: "a1" },
{ is_user: true, mes: "u2" },
{ is_user: false, mes: "a2" },
];
const plan = resolveAutoExtractionPlanController(
{
getAssistantTurns(sourceChat = []) {
return sourceChat.flatMap((message, index) =>
!message?.is_user && !message?.is_system ? [index] : [],
);
},
getLastProcessedAssistantFloor: () => -1,
getSettings: () => ({
extractEvery: 10,
extractAutoDelayLatestAssistant: true,
enableSmartTrigger: true,
}),
getSmartTriggerDecision(_chat, _lastProcessed, _settings, endFloor) {
endFloors.push(endFloor);
return {
triggered: true,
score: 3,
reasons: ["test"],
};
},
},
{
chat,
settings: {
extractEvery: 10,
extractAutoDelayLatestAssistant: true,
enableSmartTrigger: true,
},
lastProcessedAssistantFloor: -1,
},
);
assert.equal(plan.canRun, true);
assert.deepEqual(endFloors, [1]);
assert.deepEqual(plan.batchAssistantTurns, [1]);
}
async function testLagModeRespectsExtractEveryAgainstEligibleWindow() {
const 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" },
];
const plan = buildAutoExtractionPlan({
chat,
settings: {
extractEvery: 2,
extractAutoDelayLatestAssistant: true,
enableSmartTrigger: false,
},
lastProcessedAssistantFloor: -1,
});
assert.equal(plan.canRun, true);
assert.deepEqual(plan.eligibleAssistantTurns, [1, 3]);
assert.deepEqual(plan.batchAssistantTurns, [1, 3]);
assert.equal(plan.plannedBatchEndFloor, 3);
}
async function testGenerationEndedResumesPendingAutoExtractionAfterSettle() {
const harness = await createGenerationRecallHarness();
harness.chat = [
{ is_user: true, mes: "u1" },
{ is_user: false, mes: "streaming response" },
];
harness.result.setGraphPersistenceState({
loadState: "loaded",
dbReady: true,
chatId: "chat-main",
});
harness.result.onGenerationStarted("normal", {}, false);
harness.invokeOnMessageReceived(1, "assistant");
await waitForTick();
assert.equal(harness.runExtractionCalls.length, 0);
assert.equal(
harness.result.getPendingAutoExtraction().reason,
"generation-running",
);
harness.result.onGenerationEnded();
await new Promise((resolve) => setTimeout(resolve, 180));
assert.equal(harness.runExtractionCalls.length, 1);
harness.result.clearPendingAutoExtraction();
}
async function testLagModePendingResumeKeepsLockedPreviousAssistantAfterLatestDisappears() {
const harness = await createGenerationRecallHarness();
harness.settings = {
extractEvery: 1,
extractAutoDelayLatestAssistant: true,
enableSmartTrigger: false,
};
harness.chat = [
{ is_user: true, mes: "u1" },
{ is_user: false, mes: "a1" },
{ is_user: true, mes: "u2" },
{ is_user: false, mes: "a2" },
];
harness.result.setGraphPersistenceState({
loadState: "loaded",
dbReady: true,
chatId: "chat-main",
});
harness.result.onGenerationStarted("normal", {}, false);
harness.invokeOnMessageReceived(3, "assistant");
await waitForTick();
assert.equal(harness.runExtractionCalls.length, 0);
assert.equal(
harness.result.getPendingAutoExtraction().targetEndFloor,
1,
);
harness.chat = [
{ is_user: true, mes: "u1" },
{ is_user: false, mes: "a1" },
{ is_user: true, mes: "u2" },
];
harness.result.onGenerationEnded();
await new Promise((resolve) => setTimeout(resolve, 180));
assert.equal(harness.runExtractionCalls.length, 1);
assert.equal(harness.runExtractionCalls[0]?.[0]?.lockedEndFloor, 1);
harness.result.clearPendingAutoExtraction();
}
async function testAutoExtractionDefersWhenGraphNotReady() {
const deferredReasons = [];
const statuses = [];
await runExtractionController({
getIsExtracting: () => false,
getCurrentGraph: () => null,
getSettings: () => ({ enabled: true }),
ensureGraphMutationReady: () => false,
deferAutoExtraction(reason) {
deferredReasons.push(reason);
},
setLastExtractionStatus(...args) {
statuses.push(args);
},
getGraphMutationBlockReason: () =>
"自动提取已暂停:正在加载 IndexedDB 图谱。",
});
assert.deepEqual(deferredReasons, ["graph-not-ready"]);
assert.equal(statuses[0]?.[0], "等待图谱加载");
}
async function testAutoExtractionDefersWhenAlreadyExtracting() {
const deferredReasons = [];
await runExtractionController({
getIsExtracting: () => true,
deferAutoExtraction(reason) {
deferredReasons.push(reason);
},
});
assert.deepEqual(deferredReasons, ["extracting"]);
}
async function testAutoExtractionDefersWhenHistoryRecoveryBusy() {
const deferredReasons = [];
await runExtractionController({
getIsExtracting: () => false,
getCurrentGraph: () => ({}),
getSettings: () => ({ enabled: true }),
ensureGraphMutationReady: () => true,
ensureCurrentGraphRuntimeState() {},
recoverHistoryIfNeeded: async () => false,
getIsRecoveringHistory: () => true,
deferAutoExtraction(reason) {
deferredReasons.push(reason);
},
});
assert.deepEqual(deferredReasons, ["history-recovering"]);
}
async function testRemoveNodeHandlesCyclicChildGraph() {
const graph = createEmptyGraph();
const nodeA = addNode(
graph,
createNode({ type: "event", fields: { title: "A" }, seq: 0 }),
);
const nodeB = addNode(
graph,
createNode({ type: "event", fields: { title: "B" }, seq: 1 }),
);
nodeA.childIds = [nodeB.id];
nodeB.parentId = nodeA.id;
nodeB.childIds = [nodeA.id];
nodeA.parentId = nodeB.id;
addEdge(
graph,
createEdge({ fromId: nodeA.id, toId: nodeB.id, relation: "cycle" }),
);
const removed = removeNode(graph, nodeA.id);
assert.equal(removed, true);
assert.equal(graph.nodes.length, 0);
assert.equal(graph.edges.length, 0);
}
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 testGenerationRecallDeferredRewriteMutatesFinalMesSendPayload() {
const harness = await createGenerationRecallHarness({ realApplyFinal: true });
harness.chat = [{ is_user: false, mes: "assistant-tail" }];
harness.__sendTextareaValue = "发送前真实输入";
await harness.result.onGenerationStarted("normal", {}, false);
harness.__sendTextareaValue = "";
await harness.result.onGenerationAfterCommands("normal", {}, false);
const promptData = {
finalMesSend: [
{
injected: false,
message: "发送前真实输入",
extensionPrompts: [],
},
],
};
const resolution = await harness.result.onBeforeCombinePrompts(promptData);
assert.equal(harness.runRecallCalls.length, 1);
assert.equal(harness.applyFinalCalls.length, 1);
assert.equal(resolution.applicationMode, "rewrite");
assert.equal(resolution.deliveryMode, "deferred");
assert.equal(resolution.rewrite.applied, true);
assert.equal(resolution.rewrite.path, "finalMesSend");
assert.match(
promptData.finalMesSend[0].extensionPrompts.join("\n"),
/注入:发送前真实输入/,
);
assert.equal(
harness.moduleInjectionCalls.every((text) => text === ""),
true,
);
assert.equal(
harness.recordedInjectionSnapshots.at(-1)?.applicationMode,
"rewrite",
);
}
async function testGenerationRecallSendIntentBeatsChatTailAndStaysObservable() {
const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: true, mes: "旧的 chat tail" }];
harness.pendingRecallSendIntent = {
text: "刚触发发送的新输入",
hash: "hash-send-intent-priority",
at: Date.now(),
source: "dom-intent",
};
await harness.result.onGenerationAfterCommands("normal", {}, false);
assert.equal(harness.runRecallCalls.length, 1);
assert.equal(harness.runRecallCalls[0].overrideUserMessage, "旧的 chat tail");
assert.equal(harness.runRecallCalls[0].overrideSource, "send-intent");
assert.equal(harness.runRecallCalls[0].overrideSourceLabel, "发送意图");
assert.equal(
harness.runRecallCalls[0].overrideReason,
"send-intent-overrides-chat-tail",
);
assert.equal(
JSON.stringify(
harness.runRecallCalls[0].sourceCandidates.map(
(candidate) => candidate.source,
),
),
JSON.stringify(["send-intent", "chat-tail-user"]),
);
const transaction = [
...harness.result.generationRecallTransactions.values(),
][0];
assert.equal(
transaction.frozenRecallOptions.overrideUserMessage,
"旧的 chat tail",
);
assert.equal(transaction.frozenRecallOptions.lockedSource, "send-intent");
assert.equal(transaction.frozenRecallOptions.lockedSourceLabel, "发送意图");
assert.equal(
transaction.frozenRecallOptions.lockedReason,
"send-intent-overrides-chat-tail",
);
assert.equal(
transaction.frozenRecallOptions.sourceCandidates[0]?.text,
"刚触发发送的新输入",
);
}
async function testGenerationRecallSendIntentWinsOverHostSnapshotStably() {
const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: false, mes: "assistant-tail" }];
harness.pendingRecallSendIntent = {
text: "发送意图优先输入",
hash: "hash-send-intent-vs-host",
at: Date.now(),
source: "dom-intent",
};
const frozenSnapshot =
harness.result.freezeHostGenerationInputSnapshot("宿主快照输入");
await harness.result.onGenerationAfterCommands(
"normal",
{ frozenInputSnapshot: frozenSnapshot },
false,
);
await harness.result.onBeforeCombinePrompts();
assert.equal(harness.runRecallCalls.length, 1);
assert.equal(
harness.runRecallCalls[0].overrideUserMessage,
"发送意图优先输入",
);
assert.equal(harness.runRecallCalls[0].overrideSource, "send-intent");
assert.equal(
JSON.stringify(
harness.runRecallCalls[0].sourceCandidates.map(
(candidate) => candidate.source,
),
),
JSON.stringify(["send-intent", "host-generation-lifecycle"]),
);
assert.equal(harness.applyFinalCalls.length, 1);
}
async function testGenerationRecallLockedSourceDoesNotDriftWithinTransaction() {
const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: false, mes: "assistant-tail" }];
harness.pendingRecallSendIntent = {
text: "事务锁定输入-A",
hash: "hash-locked-source",
at: Date.now(),
source: "dom-intent",
};
await harness.result.onGenerationAfterCommands("normal", {}, false);
harness.pendingRecallSendIntent = {
text: "事务漂移输入-B",
hash: "hash-drift-source",
at: Date.now(),
source: "dom-intent",
};
await harness.result.onBeforeCombinePrompts();
assert.equal(harness.runRecallCalls.length, 1);
assert.equal(harness.runRecallCalls[0].overrideUserMessage, "事务漂移输入-B");
const transaction = [
...harness.result.generationRecallTransactions.values(),
][0];
assert.equal(
transaction.frozenRecallOptions.overrideUserMessage,
"事务漂移输入-B",
);
assert.equal(transaction.frozenRecallOptions.lockedSource, "send-intent");
assert.equal(transaction.frozenRecallOptions.lockedSourceLabel, "发送意图");
assert.equal(
transaction.frozenRecallOptions.lockedReason,
"send-intent-captured",
);
}
async function testBeforeCombineRecallNotSkippedWhenGraphLoadingButRuntimeGraphReadable() {
const { runRecallController } = await import("../recall-controller.js");
const statuses = [];
const graph = normalizeGraphRuntimeState(createEmptyGraph(), "chat-main");
graph.nodes.push(
createNode("event", {
title: "旧事件",
summary: "来自 runtime graph",
}),
);
const runtime = {
getIsRecalling: () => false,
abortRecallStageWithReason() {},
waitForActiveRecallToSettle: async () => ({ settled: true }),
getCurrentGraph: () => graph,
getSettings: () => ({
enabled: true,
recallEnabled: true,
recallLlmContextMessages: 4,
}),
isGraphReadable: () => false,
isGraphReadableForRecall: () => true,
getGraphMutationBlockReason: () => "召回已暂停:正在加载 IndexedDB 图谱。",
setLastRecallStatus: (...args) => {
statuses.push(args);
},
isGraphMetadataWriteAllowed: () => false,
recoverHistoryIfNeeded: async () => {
throw new Error("loading 期间不应触发历史恢复");
},
getContext: () => ({
chat: [{ is_user: true, mes: "发送前输入" }],
}),
nextRecallRunSequence: () => 1,
setIsRecalling() {},
beginStageAbortController: () => ({
signal: { aborted: false, addEventListener() {} },
abort() {},
}),
createAbortError: (message) => new Error(message),
ensureVectorReadyIfNeeded: async () => {},
clampInt,
resolveRecallInput: () => ({
userMessage: "发送前输入",
recentMessages: ["[user]: 发送前输入"],
source: "send-intent",
sourceLabel: "发送意图",
generationType: "normal",
targetUserMessageIndex: null,
}),
console,
getRecallHookLabel: () => "发送前拦截",
retrieve: async ({ graph: passedGraph, userMessage }) => {
assert.equal(passedGraph, graph);
assert.equal(userMessage, "发送前输入");
return {
stats: { recallCount: 1, coreCount: 1 },
selectedNodeIds: [graph.nodes[0].id],
meta: {
retrieval: {
vectorHits: 1,
diffusionHits: 0,
llm: { status: "disabled", candidatePool: 0 },
},
},
};
},
getEmbeddingConfig: () => null,
getSchema: () => schema,
buildRecallRetrieveOptions: () => ({}),
applyRecallInjection: (_settings, recallInput) => ({
injectionText: `注入:${recallInput.userMessage}`,
}),
createRecallInputRecord,
createRecallRunResult,
isAbortError: () => false,
toastr: {
warning() {},
error() {},
},
finishStageAbortController() {},
getActiveRecallPromise: () => null,
setActiveRecallPromise() {},
setPendingRecallSendIntent() {},
refreshPanelLiveState() {},
};
const result = await runRecallController(runtime, {
hookName: "GENERATE_BEFORE_COMBINE_PROMPTS",
});
assert.equal(result.status, "completed");
assert.equal(result.didRecall, true);
assert.equal(result.injectionText, "注入:发送前输入");
assert.equal(
statuses.some(([title]) => title === "等待图谱加载"),
false,
"runtime graph 可读时不应再被 loading 门禁误判为等待图谱加载",
);
}
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 testGenerationRecallFinalInjectionRebindsLatestMatchingUserFloor() {
{
const harness = await createGenerationRecallHarness({ realApplyFinal: true });
harness.chat = [
{ is_user: true, mes: "当前输入" },
{ is_user: false, mes: "assistant-tail" },
];
harness.result.recordRecallSentUserMessage(0, "当前输入", "message-sent");
const resolution =
harness.result.applyFinalRecallInjectionForGeneration({
generationType: "normal",
hookName: "GENERATION_AFTER_COMMANDS",
freshRecallResult: {
status: "completed",
didRecall: true,
injectionText: "fresh-memory",
},
transaction: {
frozenRecallOptions: {
generationType: "normal",
targetUserMessageIndex: null,
overrideUserMessage: "当前输入",
},
},
});
assert.equal(resolution.targetUserMessageIndex, 0);
}
{
const harness = await createGenerationRecallHarness({ realApplyFinal: true });
harness.chat = [
{ is_user: true, mes: "尾部 user 仍可匹配" },
{ is_user: false, mes: "assistant-tail" },
];
const resolution =
harness.result.applyFinalRecallInjectionForGeneration({
generationType: "normal",
hookName: "GENERATION_AFTER_COMMANDS",
freshRecallResult: {
status: "completed",
didRecall: true,
injectionText: "fresh-memory",
sourceCandidates: [
{
text: "尾部 user 仍可匹配",
},
],
},
transaction: {
frozenRecallOptions: {
generationType: "normal",
targetUserMessageIndex: null,
overrideUserMessage: "尾部 user 仍可匹配",
},
},
});
assert.equal(resolution.targetUserMessageIndex, 0);
}
{
const harness = await createGenerationRecallHarness({ realApplyFinal: true });
harness.chat = [
{ is_user: true, mes: "酒馆最终写入的用户楼层文本" },
{ is_user: false, mes: "assistant-tail" },
];
harness.result.recordRecallSentUserMessage(0, "发送前捕获的原始文本", "message-sent");
const resolution =
harness.result.applyFinalRecallInjectionForGeneration({
generationType: "normal",
hookName: "GENERATION_AFTER_COMMANDS",
freshRecallResult: {
status: "completed",
didRecall: true,
injectionText: "fresh-memory",
sourceCandidates: [
{
text: "发送前捕获的原始文本",
},
],
},
transaction: {
frozenRecallOptions: {
generationType: "normal",
targetUserMessageIndex: null,
overrideUserMessage: "发送前捕获的原始文本",
},
},
});
assert.equal(
resolution.targetUserMessageIndex,
0,
"normal 生成时即便用户文本被宿主改写,也应回绑到最新 user 楼层",
);
}
}
async function testGenerationRecallFinalInjectionBackfillsPersistedRecord() {
const harness = await createGenerationRecallHarness({ realApplyFinal: true });
harness.chat = [
{ is_user: true, mes: "最终阶段补写目标" },
{ is_user: false, mes: "assistant-tail" },
];
harness.result.recordRecallSentUserMessage(0, "最终阶段补写目标", "message-sent");
const resolution =
harness.result.applyFinalRecallInjectionForGeneration({
generationType: "normal",
hookName: "GENERATION_AFTER_COMMANDS",
freshRecallResult: {
status: "completed",
didRecall: true,
injectionText: "fresh-memory",
selectedNodeIds: ["node-a", "node-b"],
},
transaction: {
frozenRecallOptions: {
generationType: "normal",
targetUserMessageIndex: null,
overrideUserMessage: "最终阶段补写目标",
lockedSource: "send-intent",
hookName: "GENERATION_AFTER_COMMANDS",
},
},
});
assert.equal(resolution.source, "fresh");
assert.equal(resolution.targetUserMessageIndex, 0);
assert.equal(
harness.chat[0]?.extra?.bme_recall?.injectionText,
"fresh-memory",
);
assert.deepEqual(
harness.chat[0]?.extra?.bme_recall?.selectedNodeIds,
["node-a", "node-b"],
);
assert.equal(harness.metadataSaveCalls > 0, true);
}
async function testGenerationRecallImmediateAfterCommandsBackfillsPersistedRecord() {
const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: true, mes: "即时模式补写目标" }];
harness.result.recordRecallSentUserMessage(0, "即时模式补写目标", "message-sent");
const result = await harness.result.onGenerationAfterCommands(
"normal",
{},
false,
);
assert.equal(result?.status, "completed");
assert.equal(
harness.chat[0]?.extra?.bme_recall?.injectionText,
"注入:即时模式补写目标",
);
assert.deepEqual(
harness.chat[0]?.extra?.bme_recall?.selectedNodeIds,
["node-test-1"],
);
assert.equal(harness.metadataSaveCalls > 0, true);
}
async function testGenerationEndedBackfillsRecentRecallAndSchedulesHideRefresh() {
const harness = await createGenerationRecallHarness({ realApplyFinal: true });
harness.chat = [{ is_user: true, mes: "生成结束后补写目标" }];
const transaction = harness.result.beginGenerationRecallTransaction({
chatId: "chat-main",
generationType: "normal",
recallKey: "chat-main:normal:test-generation-ended",
forceNew: true,
});
transaction.frozenRecallOptions = {
generationType: "normal",
targetUserMessageIndex: null,
overrideUserMessage: "生成结束后补写目标",
lockedSource: "send-intent",
hookName: "GENERATION_AFTER_COMMANDS",
};
harness.result.generationRecallTransactions.set(transaction.id, transaction);
harness.result.markGenerationRecallTransactionHookState(
transaction,
"GENERATION_AFTER_COMMANDS",
"completed",
);
harness.result.getGenerationRecallTransactionResult(transaction);
transaction.lastRecallResult = {
status: "completed",
didRecall: true,
injectionText: "generation-ended-memory",
selectedNodeIds: ["node-z"],
sourceCandidates: [{ text: "生成结束后补写目标" }],
hookName: "GENERATION_AFTER_COMMANDS",
};
transaction.updatedAt = Date.now();
harness.result.generationRecallTransactions.set(transaction.id, transaction);
harness.result.onGenerationEnded();
assert.equal(
harness.chat[0]?.extra?.bme_recall?.injectionText,
"generation-ended-memory",
);
assert.equal(harness.hideScheduleCalls.length, 1);
assert.equal(harness.hideScheduleCalls[0]?.[2], 180);
}
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(result.resultCode, "reroll.rollback.applied");
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.currentGraph.vectorIndexState.lastIntegrityIssue, null);
assert.equal(
harness.currentGraph.historyState.lastRecoveryResult.resultCode,
"reroll.rollback.applied",
);
assert.equal(harness.lastExtractedItems.length, 0);
}
async function testRerollRejectsInvalidReverseJournalPlanFailClosed() {
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",
},
lastRecoveryResult: null,
},
vectorIndexState: {
collectionId: "col-1",
},
batchJournal: [{ id: "journal-1" }],
lastProcessedSeq: 3,
};
harness.findJournalRecoveryPointImpl = () => ({
path: "reverse-journal",
affectedBatchCount: 1,
affectedJournals: [{ id: "journal-1" }],
});
harness.buildReverseJournalRecoveryPlanImpl = () => ({
valid: false,
invalidReason: "pending-repair-floor-missing",
backendDeleteHashes: [],
replayRequiredNodeIds: [],
});
const result = await harness.result.onReroll({ fromFloor: 3 });
assert.equal(result.success, false);
assert.equal(result.recoveryPath, "reverse-journal-rejected");
assert.equal(result.resultCode, "reroll.rollback.plan-invalid");
assert.equal(harness.rollbackAffectedJournalsCalls.length, 0);
assert.equal(harness.prepareVectorStateCalls.length, 0);
assert.equal(harness.deletedHashesCalls.length, 0);
assert.equal(harness.saveGraphToChatCalls, 1);
assert.equal(harness.refreshPanelCalls, 1);
assert.equal(
harness.currentGraph.historyState.lastRecoveryResult.status,
"reroll-rollback-rejected",
);
assert.equal(
harness.currentGraph.historyState.lastRecoveryResult.resultCode,
"reroll.rollback.plan-invalid",
);
assert.equal(
harness.currentGraph.historyState.lastRecoveryResult.debugReason,
"reroll-rollback-plan-invalid:pending-repair-floor-missing",
);
}
async function testHistoryRecoveryAbortClearsVectorRepairState() {
const harness = await createHistoryRecoveryHarness();
harness.chat = [
{ is_user: true, mes: "u1" },
{ is_user: false, mes: "a1" },
];
harness.currentGraph = {
historyState: {
lastProcessedAssistantFloor: 1,
processedMessageHashes: { 1: "hash-1" },
historyDirtyFrom: 1,
lastMutationSource: "message-edited",
},
vectorIndexState: {
collectionId: "col-1",
dirty: true,
dirtyReason: "history-recovery-replay",
pendingRepairFromFloor: 1,
replayRequiredNodeIds: ["node-1"],
lastWarning: "repair pending",
lastIntegrityIssue: { code: "dangling-vector" },
},
batchJournal: [],
lastProcessedSeq: 1,
};
harness.findJournalRecoveryPointImpl = () => ({
path: "full-rebuild",
affectedBatchCount: 0,
});
harness.prepareVectorStateForReplayImpl = async () => {
throw harness.createAbortError("manual abort");
};
const result = await harness.result.recoverFromHistoryMutation({
trigger: "message-edited",
dirtyFrom: 1,
detection: { source: "manual-test", reason: "edited" },
});
assert.equal(result, false);
assert.equal(
harness.currentGraph.historyState.lastRecoveryResult.resultCode,
"history.recovery.aborted",
);
assert.equal(
harness.currentGraph.historyState.lastRecoveryResult.debugReason,
"history-recovery-aborted:full-rebuild",
);
assert.equal(harness.currentGraph.vectorIndexState.lastIntegrityIssue, null);
assert.equal(harness.currentGraph.vectorIndexState.lastWarning, "");
assert.equal(
harness.currentGraph.vectorIndexState.pendingRepairFromFloor,
null,
);
assert.equal(
harness.currentGraph.vectorIndexState.replayRequiredNodeIds.length,
0,
);
assert.equal(harness.currentGraph.vectorIndexState.dirty, false);
assert.equal(harness.currentGraph.vectorIndexState.dirtyReason, "");
}
async function testHistoryRecoveryFallbackFullRebuildCarriesResultCode() {
const harness = await createHistoryRecoveryHarness();
harness.chat = [
{ is_user: true, mes: "u1" },
{ is_user: false, mes: "a1" },
];
harness.currentGraph = {
historyState: {
lastProcessedAssistantFloor: 1,
processedMessageHashes: { 1: "hash-1" },
historyDirtyFrom: 1,
lastMutationSource: "message-edited",
},
vectorIndexState: {
collectionId: "col-1",
dirty: true,
dirtyReason: "history-recovery-replay",
pendingRepairFromFloor: 1,
replayRequiredNodeIds: ["node-1"],
lastWarning: "repair pending",
lastIntegrityIssue: { code: "dangling-vector" },
},
batchJournal: [],
lastProcessedSeq: 1,
};
harness.findJournalRecoveryPointImpl = () => ({
path: "legacy-snapshot",
affectedBatchCount: 2,
snapshotBefore: {
historyState: { extractionCount: 0 },
vectorIndexState: { collectionId: "col-1" },
batchJournal: [],
lastProcessedSeq: -1,
},
});
let replayCallCount = 0;
harness.replayExtractionFromHistoryImpl = async () => {
replayCallCount += 1;
if (replayCallCount === 1) {
throw new Error("replay failed");
}
return 1;
};
const result = await harness.result.recoverFromHistoryMutation({
trigger: "message-edited",
dirtyFrom: 1,
detection: { source: "manual-test", reason: "edited" },
});
assert.equal(result, true);
assert.equal(
harness.clearedHistoryDirty.resultCode,
"history.recovery.fallback-full-rebuild",
);
assert.equal(
harness.clearedHistoryDirty.debugReason,
"history-recovery-fallback-full-rebuild:legacy-snapshot",
);
}
async function testHistoryRecoverySuccessRestoresProcessedHashesAfterReplay() {
const harness = await createHistoryRecoveryHarness();
harness.chat = [
{ is_user: true, mes: "u1" },
{ is_user: false, mes: "a1" },
];
harness.currentGraph = {
historyState: {
lastProcessedAssistantFloor: 1,
processedMessageHashes: { 1: "old-hash-1" },
historyDirtyFrom: 1,
lastMutationSource: "message-edited",
},
vectorIndexState: {
collectionId: "col-1",
dirty: false,
dirtyReason: "",
pendingRepairFromFloor: null,
replayRequiredNodeIds: [],
lastWarning: "",
lastIntegrityIssue: null,
},
batchJournal: [],
lastProcessedSeq: 1,
};
harness.findJournalRecoveryPointImpl = () => ({
path: "full-rebuild",
affectedBatchCount: 0,
});
harness.replayExtractionFromHistoryImpl = async () => {
harness.currentGraph.historyState.lastProcessedAssistantFloor = 1;
harness.currentGraph.lastProcessedSeq = 1;
return 1;
};
const result = await harness.result.recoverFromHistoryMutation({
trigger: "message-edited",
dirtyFrom: 1,
detection: { source: "manual-test", reason: "edited" },
});
assert.equal(result, true);
assert.deepEqual(harness.updatedProcessedHistorySnapshot, {
chatLength: 2,
lastProcessedAssistantFloor: 1,
});
assert.deepEqual(harness.currentGraph.historyState.processedMessageHashes, {
1: "hash-1",
});
}
async function testHistoryRecoveryFailureCarriesResultCode() {
const harness = await createHistoryRecoveryHarness();
harness.chat = [
{ is_user: true, mes: "u1" },
{ is_user: false, mes: "a1" },
];
harness.currentGraph = {
historyState: {
lastProcessedAssistantFloor: 1,
processedMessageHashes: { 1: "hash-1" },
historyDirtyFrom: 1,
lastMutationSource: "message-edited",
},
vectorIndexState: {
collectionId: "col-1",
dirty: true,
dirtyReason: "history-recovery-replay",
pendingRepairFromFloor: 1,
replayRequiredNodeIds: ["node-1"],
lastWarning: "repair pending",
lastIntegrityIssue: { code: "dangling-vector" },
},
batchJournal: [],
lastProcessedSeq: 1,
};
harness.findJournalRecoveryPointImpl = () => ({
path: "legacy-snapshot",
affectedBatchCount: 1,
snapshotBefore: {
historyState: { extractionCount: 0 },
vectorIndexState: { collectionId: "col-1" },
batchJournal: [],
lastProcessedSeq: -1,
},
});
harness.replayExtractionFromHistoryImpl = async () => {
throw new Error("replay failed twice");
};
const result = await harness.result.recoverFromHistoryMutation({
trigger: "message-edited",
dirtyFrom: 1,
detection: { source: "manual-test", reason: "edited" },
});
assert.equal(result, false);
assert.equal(
harness.currentGraph.historyState.lastRecoveryResult.resultCode,
"history.recovery.failed",
);
assert.equal(
harness.currentGraph.historyState.lastRecoveryResult.debugReason,
"history-recovery-failed:legacy-snapshot",
);
assert.equal(harness.currentGraph.vectorIndexState.lastIntegrityIssue, null);
}
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(result.resultCode, "reroll.rollback.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(result.resultCode, undefined);
assert.equal(harness.onManualExtractCalls, 1);
assert.equal(harness.saveGraphToChatCalls, 0);
}
async function testRerollPreservesPrefixHashesWhenReextractDoesNotAdvance() {
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: "old-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: [],
replayRequiredNodeIds: [],
pendingRepairFromFloor: 2,
legacyGapFallback: false,
dirtyReason: "history-recovery-replay",
});
harness.manualExtractLevel = "error";
const result = await harness.result.onReroll({ fromFloor: 3 });
assert.equal(result.success, true);
assert.equal(result.extractionStatus, "error");
assert.deepEqual(harness.updatedProcessedHistorySnapshot, {
chatLength: 6,
lastProcessedAssistantFloor: 1,
});
assert.deepEqual(harness.currentGraph.historyState.processedMessageHashes, {
1: "hash-1",
});
}
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;
}
}
async function testSynopsisUsesPromptMessagesWithoutFallbackSystemPrompt() {
const graph = createEmptyGraph();
addNode(
graph,
createNode({
type: "event",
seq: 1,
fields: {
title: "起点",
summary: "剧情开始,角色进入新的冲突环境。",
participants: "Alice",
status: "active",
},
}),
);
addNode(
graph,
createNode({
type: "event",
seq: 2,
fields: {
title: "升级",
summary: "角色发现关键线索,冲突升级。",
participants: "Alice, Bob",
status: "active",
},
}),
);
addNode(
graph,
createNode({
type: "event",
seq: 3,
fields: {
title: "转折",
summary: "双方对峙,局势进入新的阶段。",
participants: "Alice, Bob",
status: "active",
},
}),
);
const captured = [];
const restoreOverrides = pushTestOverrides({
llm: {
async callLLMForJSON(params = {}) {
captured.push(params);
return {
summary: "这是新的概要",
};
},
},
});
try {
await generateSynopsis({
graph,
currentSeq: 3,
settings: {
taskProfilesVersion: 3,
taskProfiles: createDefaultTaskProfiles(),
},
});
assert.equal(captured.length, 1);
assert.equal(captured[0].taskType, "synopsis");
assert.equal(Array.isArray(captured[0].promptMessages), true);
assert.ok(captured[0].promptMessages.length > 0);
assert.equal(captured[0].systemPrompt, "");
assert.equal(
graph.nodes.some(
(node) =>
node.type === "synopsis" &&
!node.archived &&
node.fields.summary === "这是新的概要",
),
true,
);
} finally {
restoreOverrides();
}
}
async function testReflectionUsesPromptMessagesWithoutFallbackSystemPrompt() {
const graph = createEmptyGraph();
addNode(
graph,
createNode({
type: "event",
seq: 4,
fields: {
title: "事件一",
summary: "角色开始怀疑盟友的动机。",
participants: "Alice, Bob",
status: "active",
},
}),
);
addNode(
graph,
createNode({
type: "event",
seq: 5,
fields: {
title: "事件二",
summary: "隐藏矛盾被进一步放大。",
participants: "Alice, Bob",
status: "active",
},
}),
);
addNode(
graph,
createNode({
type: "character",
seq: 5,
fields: {
name: "Alice",
state: "戒备",
},
}),
);
addNode(
graph,
createNode({
type: "thread",
seq: 5,
fields: {
title: "信任危机",
status: "active",
},
}),
);
const captured = [];
const restoreOverrides = pushTestOverrides({
llm: {
async callLLMForJSON(params = {}) {
captured.push(params);
return {
insight: "最近的关系裂痕正在固定化。",
trigger: "连续两次试探与怀疑",
suggestion: "后续检索时优先关注信任破裂相关节点",
importance: 7,
};
},
},
});
try {
const result = await generateReflection({
graph,
currentSeq: 5,
settings: {
taskProfilesVersion: 3,
taskProfiles: createDefaultTaskProfiles(),
},
});
assert.equal(captured.length, 1);
assert.equal(captured[0].taskType, "reflection");
assert.equal(Array.isArray(captured[0].promptMessages), true);
assert.ok(captured[0].promptMessages.length > 0);
assert.equal(captured[0].systemPrompt, "");
const reflectionNode = graph.nodes.find((node) => node.id === result);
assert.equal(
reflectionNode?.fields?.insight,
"最近的关系裂痕正在固定化。",
);
} finally {
restoreOverrides();
}
}
async function testManualCompressSkipsWithoutCandidatesAndDoesNotPretendItRan() {
const calls = {
compressAll: 0,
recordGraphMutation: 0,
recordMaintenanceAction: 0,
};
const toastMessages = [];
const graph = { nodes: [], historyState: {} };
const result = await onManualCompressController({
getCurrentGraph: () => graph,
ensureGraphMutationReady: () => true,
getSchema: () => [],
inspectCompressionCandidates: () => ({
hasCandidates: false,
reason: "当前没有可压缩候选组,本次未发起 LLM 压缩",
}),
cloneGraphSnapshot: (value) => JSON.parse(JSON.stringify(value ?? null)),
compressAll: async () => {
calls.compressAll += 1;
return { created: 1, archived: 1 };
},
getEmbeddingConfig: () => ({}),
getSettings: () => ({}),
recordMaintenanceAction() {
calls.recordMaintenanceAction += 1;
},
recordGraphMutation: async () => {
calls.recordGraphMutation += 1;
},
toastr: {
info(message) {
toastMessages.push(["info", message]);
},
success(message) {
toastMessages.push(["success", message]);
},
},
});
assert.equal(calls.compressAll, 0);
assert.equal(calls.recordMaintenanceAction, 0);
assert.equal(calls.recordGraphMutation, 0);
assert.equal(result?.handledToast, true);
assert.equal(result?.requestDispatched, false);
assert.match(String(toastMessages[0]?.[1] || ""), /未发起 LLM 压缩/);
}
async function testManualCompressUsesForcedCompressionAndPersistsRealMutation() {
const calls = {
forceFlag: null,
recordGraphMutation: 0,
recordMaintenanceAction: 0,
};
const graph = { nodes: [], historyState: {} };
const result = await onManualCompressController({
getCurrentGraph: () => graph,
ensureGraphMutationReady: () => true,
getSchema: () => [{ id: "event", compression: { mode: "hierarchical" } }],
inspectCompressionCandidates: () => ({
hasCandidates: true,
reason: "",
}),
cloneGraphSnapshot: (value) => JSON.parse(JSON.stringify(value ?? null)),
compressAll: async (_graph, _schema, _embeddingConfig, force) => {
calls.forceFlag = force;
return { created: 1, archived: 2 };
},
getEmbeddingConfig: () => ({}),
getSettings: () => ({}),
recordMaintenanceAction() {
calls.recordMaintenanceAction += 1;
},
recordGraphMutation: async () => {
calls.recordGraphMutation += 1;
},
buildMaintenanceSummary: () => "手动压缩",
toastr: {
info() {},
success() {},
},
});
assert.equal(calls.forceFlag, true);
assert.equal(calls.recordMaintenanceAction, 1);
assert.equal(calls.recordGraphMutation, 1);
assert.equal(result?.handledToast, true);
assert.equal(result?.requestDispatched, true);
assert.equal(result?.mutated, true);
}
async function testManualCompressUpdatesRuntimeStatusForPanelUi() {
const statusUpdates = [];
const graph = { nodes: [], historyState: {} };
const result = await onManualCompressController({
getCurrentGraph: () => graph,
ensureGraphMutationReady: () => true,
getSchema: () => [{ id: "event", compression: { mode: "hierarchical" } }],
inspectCompressionCandidates: () => ({
hasCandidates: true,
reason: "",
}),
cloneGraphSnapshot: (value) => JSON.parse(JSON.stringify(value ?? null)),
compressAll: async () => ({ created: 1, archived: 2 }),
getEmbeddingConfig: () => ({}),
getSettings: () => ({}),
recordMaintenanceAction() {},
recordGraphMutation: async () => {},
buildMaintenanceSummary: () => "手动压缩",
setRuntimeStatus(text, meta = "", level = "idle") {
statusUpdates.push({ text, meta, level });
},
refreshPanelLiveState() {},
toastr: {
info() {},
success() {},
},
});
assert.equal(result?.handledToast, true);
assert.equal(result?.mutated, true);
assert.equal(statusUpdates[0]?.text, "手动压缩中");
assert.equal(statusUpdates[0]?.level, "running");
assert.equal(statusUpdates.at(-1)?.text, "手动压缩完成");
assert.equal(statusUpdates.at(-1)?.level, "success");
}
async function testManualEvolveFallsBackToLatestExtractionBatchAfterRefresh() {
const graph = {
nodes: [
{
id: "evt-1",
type: "event",
archived: false,
level: 0,
},
],
historyState: {
extractionCount: 3,
},
batchJournal: [
{
stateBefore: {
extractionCount: 3,
},
createdNodeIds: ["compression-1"],
},
{
stateBefore: {
extractionCount: 2,
},
createdNodeIds: ["evt-1"],
},
],
};
let receivedCandidateIds = null;
let recordGraphMutationCalls = 0;
const toastMessages = [];
const result = await onManualEvolveController({
getCurrentGraph: () => graph,
ensureGraphMutationReady: () => true,
getEmbeddingConfig: () => ({ mode: "direct" }),
validateVectorConfig: () => ({ valid: true }),
getLastExtractedItems: () => [],
cloneGraphSnapshot: (value) => JSON.parse(JSON.stringify(value ?? null)),
getSettings: () => ({
consolidationNeighborCount: 5,
consolidationThreshold: 0.85,
}),
consolidateMemories: async ({ newNodeIds }) => {
receivedCandidateIds = [...newNodeIds];
return {
merged: 0,
skipped: 0,
kept: 1,
evolved: 0,
connections: 0,
updates: 0,
};
},
recordMaintenanceAction() {
throw new Error("keep-only 结果不应写入维护账本");
},
recordGraphMutation: async () => {
recordGraphMutationCalls += 1;
},
toastr: {
info(message) {
toastMessages.push(["info", message]);
},
success(message) {
toastMessages.push(["success", message]);
},
warning(message) {
toastMessages.push(["warning", message]);
},
},
});
assert.deepEqual(receivedCandidateIds, ["evt-1"]);
assert.equal(recordGraphMutationCalls, 0);
assert.equal(result?.handledToast, true);
assert.equal(result?.requestDispatched, true);
assert.equal(result?.mutated, false);
assert.match(String(toastMessages[0]?.[1] || ""), /最近一批提取落盘/);
}
async function testManualEvolveWarnsOnInvalidVectorConfigInsteadOfPretendingComplete() {
let consolidateCalls = 0;
const toastMessages = [];
const result = await onManualEvolveController({
getCurrentGraph: () => ({
nodes: [{ id: "evt-2", type: "event", archived: false, level: 0 }],
historyState: { extractionCount: 1 },
batchJournal: [],
}),
ensureGraphMutationReady: () => true,
getEmbeddingConfig: () => ({ mode: "direct" }),
validateVectorConfig: () => ({
valid: false,
error: "Embedding 配置无效",
}),
getLastExtractedItems: () => [{ id: "evt-2" }],
consolidateMemories: async () => {
consolidateCalls += 1;
return {
merged: 1,
skipped: 0,
kept: 0,
evolved: 0,
connections: 0,
updates: 0,
};
},
toastr: {
warning(message) {
toastMessages.push(["warning", message]);
},
info(message) {
toastMessages.push(["info", message]);
},
},
});
assert.equal(consolidateCalls, 0);
assert.equal(result?.handledToast, true);
assert.equal(result?.requestDispatched, false);
assert.match(String(toastMessages[0]?.[1] || ""), /配置无效/);
}
async function testManualSleepExplainsThatItIsLocalOnlyWhenNothingChanges() {
let recordGraphMutationCalls = 0;
const toastMessages = [];
const result = await onManualSleepController({
getCurrentGraph: () => ({ nodes: [] }),
ensureGraphMutationReady: () => true,
cloneGraphSnapshot: (value) => JSON.parse(JSON.stringify(value ?? null)),
sleepCycle: () => ({ forgotten: 0 }),
getSettings: () => ({ forgetThreshold: 0.5 }),
recordMaintenanceAction() {
throw new Error("无归档时不应写入维护账本");
},
recordGraphMutation: async () => {
recordGraphMutationCalls += 1;
},
toastr: {
info(message) {
toastMessages.push(["info", message]);
},
success(message) {
toastMessages.push(["success", message]);
},
},
});
assert.equal(recordGraphMutationCalls, 0);
assert.equal(result?.handledToast, true);
assert.equal(result?.requestDispatched, false);
assert.match(String(toastMessages[0]?.[1] || ""), /不会发送 LLM 请求/);
}
await testCompressorMigratesEdgesToCompressedNode();
await testVectorIndexKeepsDirtyOnDirectPartialEmbeddingFailure();
await testCompressTypeAcceptsTopLevelFieldsResult();
await testExtractorFailsOnUnknownOperation();
await testExtractorNormalizesFlatCreateOperation();
await testExtractorNormalizesArrayPayloadAndPreservesScopeField();
await testConsolidatorMergeUpdatesSeqRange();
await testConsolidatorMergeFallbackKeepsNodeWhenTargetMissing();
await testBatchJournalVectorDeltaCapturesRecoveryFields();
await testReverseJournalRecoveryPlanLegacyFallback();
await testReverseJournalRecoveryPlanAggregatesDeletesAndReplay();
await testReverseJournalRollbackStateFormsReplayClosure();
await testReverseJournalRecoveryPlanMixedLegacyAndCurrentRetainsRepairSet();
await testBatchStatusStructuralPartialRemainsRecoverable();
await testBatchStatusSemanticFailureDoesNotHideCoreSuccess();
await testExtractionPostProcessStatusesExposeMaintenancePhases();
await testAutoConsolidationRunsOnHighDuplicateRiskSingleNode();
await testAutoConsolidationSkipsLowRiskSingleNode();
await testAutoCompressionRunsOnlyOnConfiguredInterval();
await testAutoCompressionSkipsWhenNotScheduledOrNoCandidates();
await testBatchStatusFinalizeFailureIsNotCompleteSuccess();
await testProcessedHistoryAdvanceTracksCoreExtractionSuccess();
await testGenerationRecallTransactionDedupesDoubleHookBySameKey();
await testGenerationRecallTransactionDedupesReverseHookOrder();
await testGenerationRecallHistoryModesUseSameBindingAcrossHooks();
await testGenerationRecallFrozenBindingSurvivesCrossHookInputDrift();
await testGenerationRecallSkipsUntilTargetUserFloorAvailable();
await testGenerationRecallBeforeCombineCanUseProvisionalSendIntentBinding();
await testGenerationRecallHostLifecycleSnapshotSurvivesTextareaClearWithoutDomIntent();
await testGenerationRecallAfterCommandsStillSkipsWithoutStableUserFloor();
await testGenerationRecallSendIntentBeatsChatTailAndStaysObservable();
await testGenerationRecallSendIntentWinsOverHostSnapshotStably();
await testGenerationRecallLockedSourceDoesNotDriftWithinTransaction();
await testGenerationRecallSameKeyCanRunAgainImmediatelyAsNewGeneration();
await testGenerationRecallSameKeyCanRunAgainAfterBridgeWindow();
await testBeforeCombineRecallNotSkippedWhenGraphLoadingButRuntimeGraphReadable();
await testGenerationRecallBeforeCombineRunsStandalone();
await testGenerationRecallDryRunPreviewDoesNotTriggerBeforeCombineRecall();
await testGenerationRecallDifferentKeyCanRunAgain();
await testGenerationRecallSkippedStateDoesNotLoopToBeforeCombine();
await testGenerationRecallSentMessageClearsStaleTransactionForSameKey();
await testRegisterCoreEventHooksIsIdempotent();
await testChatChangedDoesNotClearCoreEventBindings();
await testSwipeRoutesToRerollWithoutHistoryRecoveryFallback();
await testMessageSentFallsBackToLatestUserWhenHostMessageIdInvalid();
await testUserMessageRenderedRefreshesRecallUiAfterRealDomRender();
await testCharacterMessageRenderedRefreshesRecallUiAfterAssistantRender();
await testMessageReceivedQueuesExtractionWithoutRuntimeQueueMicrotask();
await testMessageReceivedDefersExtractionDuringHostGeneration();
await testMessageReceivedLagModeWaitsSilentlyForNextAssistant();
await testMessageReceivedLagModeQueuesPreviousAssistantOnly();
await testLagModeSmartTriggerOnlyScoresEligibleWindow();
await testLagModeRespectsExtractEveryAgainstEligibleWindow();
await testGenerationEndedResumesPendingAutoExtractionAfterSettle();
await testLagModePendingResumeKeepsLockedPreviousAssistantAfterLatestDisappears();
await testAutoExtractionDefersWhenGraphNotReady();
await testAutoExtractionDefersWhenAlreadyExtracting();
await testAutoExtractionDefersWhenHistoryRecoveryBusy();
await testRemoveNodeHandlesCyclicChildGraph();
await testGenerationRecallAppliesFinalInjectionOncePerTransaction();
await testGenerationRecallDeferredRewriteMutatesFinalMesSendPayload();
await testPersistentRecallDataLayerLifecycleAndCompatibility();
await testPersistentRecallSourceResolutionAndTargetRouting();
await testGenerationRecallFinalInjectionRebindsLatestMatchingUserFloor();
await testGenerationRecallFinalInjectionBackfillsPersistedRecord();
await testGenerationRecallImmediateAfterCommandsBackfillsPersistedRecord();
await testGenerationEndedBackfillsRecentRecallAndSchedulesHideRefresh();
await testRecallCardMountsOnStandardUserMessageDom();
await testRecallCardSkipsMountWithoutStableMessageIndex();
await testRecallCardDelayedDomInsertionEventuallyRenders();
await testRecallCardDelayedStableMessageIndexEventuallyRenders();
await testRecallCardSurvivesLateMessageDomReplacement();
await testRecallCardKeepsRetryingWhenOlderCardsAlreadyRendered();
await testRecallCardPrefersBetterDuplicateMessageAnchor();
await testRecallCardDoesNotMountOnNonUserFloor();
await testRecallCardRefreshCleansLegacyBadgeAndAvoidsDuplicates();
await testRecallCardExpandedContentRerendersAfterRecordUpdate();
await testRecallCardUserTextRefreshesWithoutCardRecreate();
await testRecallCardDisplayModeToggleRestoresOriginalUserText();
await testRecallSubGraphAndDataLayerEntryPoints();
await testRerollUsesBatchBoundaryRollbackAndPersistsState();
await testHistoryRecoveryAbortClearsVectorRepairState();
await testHistoryRecoveryFallbackFullRebuildCarriesResultCode();
await testHistoryRecoverySuccessRestoresProcessedHashesAfterReplay();
await testHistoryRecoveryFailureCarriesResultCode();
await testRerollRejectsMissingRecoveryPoint();
await testRerollFallsBackToDirectExtractForUnprocessedFloor();
await testRerollPreservesPrefixHashesWhenReextractDoesNotAdvance();
await testLlmDebugSnapshotRedactsSecretsBeforeStorage();
await testEmbeddingUsesConfigTimeoutInsteadOfDefault();
await testLlmOutputRegexCleansResponseBeforeJsonParse();
await testSynopsisUsesPromptMessagesWithoutFallbackSystemPrompt();
await testReflectionUsesPromptMessagesWithoutFallbackSystemPrompt();
await testManualCompressSkipsWithoutCandidatesAndDoesNotPretendItRan();
await testManualCompressUsesForcedCompressionAndPersistsRealMutation();
await testManualCompressUpdatesRuntimeStatusForPanelUi();
await testManualEvolveFallsBackToLatestExtractionBatchAfterRefresh();
await testManualEvolveWarnsOnInvalidVectorConfigInsteadOfPretendingComplete();
await testManualSleepExplainsThatItIsLocalOnlyWhenNothingChanges();
console.log("p0-regressions tests passed");