Files
ST-Bionic-Memory-Ecology/tests/graph-persistence.mjs
2026-04-07 17:18:18 +08:00

2474 lines
70 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 path from "node:path";
import { fileURLToPath } from "node:url";
import vm from "node:vm";
import {
buildBmeDbName,
buildGraphFromSnapshot,
buildSnapshotFromGraph,
} from "../bme-db.js";
import { onMessageReceivedController } from "../event-binding.js";
import {
cloneGraphForPersistence,
cloneRuntimeDebugValue,
findGraphShadowSnapshotByIntegrity,
getGraphPersistedRevision,
getGraphIdentityAliasCandidates,
getGraphPersistenceMeta,
getGraphShadowSnapshotStorageKey,
GRAPH_LOAD_PENDING_CHAT_ID,
GRAPH_IDENTITY_ALIAS_STORAGE_KEY,
GRAPH_LOAD_STATES,
GRAPH_METADATA_KEY,
GRAPH_PERSISTENCE_META_KEY,
GRAPH_PERSISTENCE_SESSION_ID,
GRAPH_SHADOW_SNAPSHOT_STORAGE_PREFIX,
GRAPH_STARTUP_RECONCILE_DELAYS_MS,
MODULE_NAME,
readGraphShadowSnapshot,
rememberGraphIdentityAlias,
removeGraphShadowSnapshot,
resolveGraphIdentityAliasByHostChatId,
shouldPreferShadowSnapshotOverOfficial,
stampGraphPersistenceMeta,
writeChatMetadataPatch,
writeGraphShadowSnapshot,
} from "../graph-persistence.js";
import {
createEmptyGraph,
deserializeGraph,
getGraphStats,
getNode,
serializeGraph,
} from "../graph.js";
import {
buildPersistedRecallRecord,
readPersistedRecallFromUserMessage,
} from "../recall-persistence.js";
import { getNodeDisplayName } from "../node-labels.js";
import { normalizeGraphRuntimeState } from "../runtime-state.js";
import {
clampFloat,
clampInt,
createGraphPersistenceState,
createRecallInputRecord,
createRecallRunResult,
createUiStatus,
formatRecallContextLine,
getStageNoticeDuration,
getStageNoticeTitle,
hashRecallInput,
isFreshRecallInputRecord,
normalizeRecallInputText,
normalizeStageNoticeLevel,
} from "../ui-status.js";
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
const indexPath = path.resolve(moduleDir, "../index.js");
const indexSource = await fs.readFile(indexPath, "utf8");
function extractSnippet(startMarker, endMarker) {
const start = indexSource.indexOf(startMarker);
const end = indexSource.indexOf(endMarker);
if (start < 0 || end < 0 || end <= start) {
throw new Error(`无法提取 index.js 片段: ${startMarker} -> ${endMarker}`);
}
return indexSource.slice(start, end).replace(/^export\s+/gm, "");
}
const persistencePrelude = extractSnippet(
'const SERVER_SETTINGS_FILENAME = "st-bme-settings.json";',
"function clearInjectionState(options = {}) {",
);
const persistenceCore = extractSnippet(
"function loadGraphFromChat(options = {}) {",
"function handleGraphShadowSnapshotPageHide() {",
);
const messageSnippet = extractSnippet(
'function onMessageReceived(messageId = null, type = "") {',
"// ==================== UI 操作 ====================",
);
function createSessionStorage(seed = null) {
const store = seed instanceof Map ? seed : new Map();
return {
__store: store,
get length() {
return store.size;
},
key(index) {
return Array.from(store.keys())[Number(index)] ?? null;
},
getItem(key) {
return store.has(key) ? store.get(key) : null;
},
setItem(key, value) {
store.set(String(key), String(value));
},
removeItem(key) {
store.delete(String(key));
},
};
}
function createLocalStorage(seed = null) {
const store = seed instanceof Map ? seed : new Map();
return {
__store: store,
getItem(key) {
return store.has(key) ? store.get(key) : null;
},
setItem(key, value) {
store.set(String(key), String(value));
},
removeItem(key) {
store.delete(String(key));
},
};
}
function createMeaningfulGraph(chatId = "chat-test", suffix = "base") {
const graph = createEmptyGraph();
graph.historyState.chatId = chatId;
graph.historyState.extractionCount = 3;
graph.historyState.lastProcessedAssistantFloor = 6;
graph.lastProcessedSeq = 6;
graph.lastRecallResult = [{ id: `recall-${suffix}` }];
graph.nodes.push({
id: `node-${suffix}`,
type: "event",
fields: {
title: `事件-${suffix}`,
summary: `摘要-${suffix}`,
},
seq: 6,
seqRange: [6, 6],
archived: false,
embedding: null,
importance: 5,
accessCount: 0,
lastAccessTime: Date.now(),
createdTime: Date.now(),
level: 0,
parentId: null,
childIds: [],
prevId: null,
nextId: null,
clusters: [],
});
return normalizeGraphRuntimeState(graph, chatId);
}
function stampPersistedGraph(
graph,
{
revision = 1,
integrity = "",
chatId = graph?.historyState?.chatId || "",
reason = "test",
} = {},
) {
graph.__stBmePersistence = {
revision,
integrity,
chatId,
reason,
updatedAt: new Date().toISOString(),
sessionId: "test-session",
};
return graph;
}
async function createGraphPersistenceHarness({
chatId = "chat-test",
chatMetadata = undefined,
sessionStore = null,
localStore = null,
globalChatId = "",
characterId = "",
groupId = null,
indexedDbSnapshot = null,
indexedDbSnapshots = null,
chat = [],
} = {}) {
const timers = new Map();
let nextTimerId = 1;
const storage = createSessionStorage(sessionStore);
const localStorage = createLocalStorage(localStore);
const indexedDbSnapshotMap =
indexedDbSnapshots instanceof Map
? new Map(indexedDbSnapshots)
: new Map(
Object.entries(
indexedDbSnapshots &&
typeof indexedDbSnapshots === "object" &&
!Array.isArray(indexedDbSnapshots)
? indexedDbSnapshots
: {},
),
);
if (indexedDbSnapshot) {
const primaryChatId = String(chatId || globalChatId || "");
if (primaryChatId) {
indexedDbSnapshotMap.set(primaryChatId, structuredClone(indexedDbSnapshot));
}
}
function buildEmptyIndexedDbSnapshot(targetChatId = "") {
return {
meta: { revision: 0, chatId: String(targetChatId || "") },
nodes: [],
edges: [],
tombstones: [],
state: { lastProcessedFloor: -1, extractionCount: 0 },
};
}
function getIndexedDbSnapshotForChat(targetChatId = "") {
const normalizedChatId = String(targetChatId || "");
if (normalizedChatId && indexedDbSnapshotMap.has(normalizedChatId)) {
return structuredClone(indexedDbSnapshotMap.get(normalizedChatId));
}
if (
normalizedChatId &&
indexedDbSnapshot &&
!indexedDbSnapshotMap.size &&
normalizedChatId === String(chatId || globalChatId || "")
) {
return structuredClone(indexedDbSnapshot);
}
return buildEmptyIndexedDbSnapshot(normalizedChatId);
}
function setIndexedDbSnapshotForChat(targetChatId = "", snapshot = null) {
const normalizedChatId = String(targetChatId || "");
if (!normalizedChatId) return;
if (!snapshot) {
indexedDbSnapshotMap.delete(normalizedChatId);
return;
}
indexedDbSnapshotMap.set(normalizedChatId, structuredClone(snapshot));
}
const runtimeContext = {
console,
Date,
Math,
JSON,
Object,
Array,
String,
Number,
Boolean,
structuredClone,
result: null,
__indexedDbSnapshot: getIndexedDbSnapshotForChat(
String(chatId || globalChatId || ""),
),
__indexedDbSnapshots: indexedDbSnapshotMap,
sessionStorage: storage,
localStorage,
setTimeout(fn, delay) {
const id = nextTimerId++;
timers.set(id, { fn, delay });
return id;
},
clearTimeout(id) {
timers.delete(id);
},
queueMicrotask(fn) {
fn();
},
toastr: {
info() {},
warning() {},
error() {},
success() {},
},
window: {
addEventListener() {},
removeEventListener() {},
},
document: {
visibilityState: "visible",
getElementById() {
return null;
},
},
SillyTavern: {
getCurrentChatId() {
return runtimeContext.__globalChatId;
},
},
__globalChatId: String(globalChatId || ""),
Dexie: {
async exists(dbName = "") {
return Array.from(indexedDbSnapshotMap.keys()).some(
(candidateChatId) => buildBmeDbName(candidateChatId) === String(dbName),
);
},
async getDatabaseNames() {
return Array.from(indexedDbSnapshotMap.keys()).map((candidateChatId) =>
buildBmeDbName(candidateChatId),
);
},
},
async ensureDexieLoaded() {
return runtimeContext.Dexie;
},
refreshPanelLiveState() {
runtimeContext.__panelRefreshCount += 1;
},
__panelRefreshCount: 0,
createEmptyGraph,
normalizeGraphRuntimeState,
serializeGraph,
deserializeGraph,
getGraphStats,
getNode,
getNodeDisplayName,
createUiStatus,
createGraphPersistenceState,
createRecallInputRecord,
createRecallRunResult,
normalizeStageNoticeLevel,
getStageNoticeTitle,
getStageNoticeDuration,
normalizeRecallInputText,
hashRecallInput,
isFreshRecallInputRecord,
clampInt,
clampFloat,
formatRecallContextLine,
readPersistedRecallFromUserMessage,
cloneGraphForPersistence,
cloneRuntimeDebugValue,
onMessageReceivedController,
getGraphPersistenceMeta,
getGraphPersistedRevision,
getGraphIdentityAliasCandidates,
getGraphShadowSnapshotStorageKey,
GRAPH_IDENTITY_ALIAS_STORAGE_KEY,
GRAPH_LOAD_PENDING_CHAT_ID,
GRAPH_LOAD_STATES,
GRAPH_METADATA_KEY,
GRAPH_PERSISTENCE_META_KEY,
GRAPH_PERSISTENCE_SESSION_ID,
GRAPH_SHADOW_SNAPSHOT_STORAGE_PREFIX,
GRAPH_STARTUP_RECONCILE_DELAYS_MS,
MODULE_NAME,
findGraphShadowSnapshotByIntegrity,
readGraphShadowSnapshot,
rememberGraphIdentityAlias,
removeGraphShadowSnapshot,
resolveGraphIdentityAliasByHostChatId,
shouldPreferShadowSnapshotOverOfficial,
stampGraphPersistenceMeta,
writeChatMetadataPatch,
writeGraphShadowSnapshot,
// Shadow snapshot functions need VM-local sessionStorage overrides
// because imported versions use the outer globalThis (no sessionStorage)
rememberGraphIdentityAlias({
integrity = "",
hostChatId = "",
persistenceChatId = "",
} = {}) {
const normalizedIntegrity = String(integrity || "").trim();
if (!normalizedIntegrity) return null;
const normalizedHostChatId = String(hostChatId || "").trim();
const normalizedPersistenceChatId = String(
persistenceChatId || normalizedIntegrity,
).trim();
let registry = { byIntegrity: {} };
try {
const raw = localStorage.getItem(GRAPH_IDENTITY_ALIAS_STORAGE_KEY);
if (raw) {
const parsed = JSON.parse(raw);
if (
parsed?.byIntegrity &&
typeof parsed.byIntegrity === "object" &&
!Array.isArray(parsed.byIntegrity)
) {
registry = { byIntegrity: parsed.byIntegrity };
}
}
} catch {
registry = { byIntegrity: {} };
}
const current = registry.byIntegrity[normalizedIntegrity] || {};
const hostChatIds = Array.from(
new Set(
[
normalizedHostChatId,
...(Array.isArray(current.hostChatIds) ? current.hostChatIds : []),
].filter(Boolean),
),
);
const next = {
integrity: normalizedIntegrity,
persistenceChatId: normalizedPersistenceChatId,
hostChatIds,
updatedAt: new Date().toISOString(),
};
registry.byIntegrity[normalizedIntegrity] = next;
localStorage.setItem(
GRAPH_IDENTITY_ALIAS_STORAGE_KEY,
JSON.stringify(registry),
);
return next;
},
resolveGraphIdentityAliasByHostChatId(hostChatId = "") {
const normalizedHostChatId = String(hostChatId || "").trim();
if (!normalizedHostChatId) return "";
try {
const raw = localStorage.getItem(GRAPH_IDENTITY_ALIAS_STORAGE_KEY);
const parsed = raw ? JSON.parse(raw) : { byIntegrity: {} };
let best = "";
let bestUpdatedAt = "";
for (const value of Object.values(parsed.byIntegrity || {})) {
const hostChatIds = Array.isArray(value?.hostChatIds)
? value.hostChatIds.map((item) => String(item || "").trim())
: [];
if (!hostChatIds.includes(normalizedHostChatId)) continue;
const persistenceChatId = String(
value?.persistenceChatId || value?.integrity || "",
).trim();
if (!persistenceChatId) continue;
const updatedAt = String(value?.updatedAt || "");
if (!best || updatedAt > bestUpdatedAt) {
best = persistenceChatId;
bestUpdatedAt = updatedAt;
}
}
return best;
} catch {
return "";
}
},
getGraphIdentityAliasCandidates({
integrity = "",
hostChatId = "",
persistenceChatId = "",
} = {}) {
const normalizedIntegrity = String(integrity || "").trim();
const normalizedHostChatId = String(hostChatId || "").trim();
const normalizedPersistenceChatId = String(
persistenceChatId || "",
).trim();
const candidates = [];
const seen = new Set();
const addCandidate = (value) => {
const normalized = String(value || "").trim();
if (!normalized || seen.has(normalized)) return;
seen.add(normalized);
candidates.push(normalized);
};
try {
const raw = localStorage.getItem(GRAPH_IDENTITY_ALIAS_STORAGE_KEY);
const parsed = raw ? JSON.parse(raw) : { byIntegrity: {} };
if (normalizedIntegrity) {
const value = parsed.byIntegrity?.[normalizedIntegrity] || {};
addCandidate(value?.persistenceChatId || value?.integrity || "");
for (const candidate of Array.isArray(value?.hostChatIds)
? value.hostChatIds
: []) {
addCandidate(candidate);
}
} else if (normalizedHostChatId) {
addCandidate(
runtimeContext.resolveGraphIdentityAliasByHostChatId(
normalizedHostChatId,
),
);
}
} catch {
// ignore
}
addCandidate(normalizedHostChatId);
addCandidate(normalizedPersistenceChatId);
return candidates;
},
readGraphShadowSnapshot(chatId = "") {
const key = getGraphShadowSnapshotStorageKey(chatId);
if (!key) return null;
try {
const raw = storage.getItem(key);
if (!raw) return null;
const snap = JSON.parse(raw);
if (
!snap ||
String(snap.chatId || "") !== String(chatId || "") ||
typeof snap.serializedGraph !== "string" ||
!snap.serializedGraph
)
return null;
return {
chatId: String(snap.chatId || ""),
revision: Number.isFinite(snap.revision) ? snap.revision : 0,
serializedGraph: snap.serializedGraph,
updatedAt: String(snap.updatedAt || ""),
reason: String(snap.reason || ""),
integrity: String(snap.integrity || ""),
persistedChatId: String(snap.persistedChatId || ""),
debugReason: String(snap.debugReason || snap.reason || ""),
};
} catch {
return null;
}
},
findGraphShadowSnapshotByIntegrity(integrity = "", { excludeChatIds = [] } = {}) {
const normalizedIntegrity = String(integrity || "").trim();
if (!normalizedIntegrity) return null;
const excluded = new Set(
(Array.isArray(excludeChatIds) ? excludeChatIds : [])
.map((value) => String(value || "").trim())
.filter(Boolean),
);
let best = null;
for (const key of storage.__store.keys()) {
if (!String(key || "").startsWith(GRAPH_SHADOW_SNAPSHOT_STORAGE_PREFIX)) {
continue;
}
try {
const snap = JSON.parse(storage.getItem(key));
if (
!snap ||
String(snap.integrity || "") !== normalizedIntegrity ||
typeof snap.serializedGraph !== "string" ||
!snap.serializedGraph
) {
continue;
}
const normalizedChatId = String(snap.chatId || "").trim();
if (!normalizedChatId || excluded.has(normalizedChatId)) {
continue;
}
if (
!best ||
Number(snap.revision || 0) > Number(best.revision || 0) ||
(Number(snap.revision || 0) === Number(best.revision || 0) &&
String(snap.updatedAt || "") > String(best.updatedAt || ""))
) {
best = {
chatId: normalizedChatId,
revision: Number.isFinite(snap.revision) ? snap.revision : 0,
serializedGraph: snap.serializedGraph,
updatedAt: String(snap.updatedAt || ""),
reason: String(snap.reason || ""),
integrity: String(snap.integrity || ""),
persistedChatId: String(snap.persistedChatId || ""),
debugReason: String(snap.debugReason || snap.reason || ""),
};
}
} catch {
// ignore
}
}
return best;
},
writeGraphShadowSnapshot(
chatId = "",
graph = null,
{ revision = 0, reason = "", integrity = "", debugReason = "" } = {},
) {
const key = getGraphShadowSnapshotStorageKey(chatId);
if (!key || !graph) return false;
const persistedMeta = getGraphPersistenceMeta(graph) || {};
try {
storage.setItem(
key,
JSON.stringify({
chatId: String(chatId || ""),
revision: Number.isFinite(revision) ? revision : 0,
serializedGraph: serializeGraph(graph),
updatedAt: new Date().toISOString(),
reason: String(reason || ""),
integrity: String(integrity || persistedMeta.integrity || ""),
persistedChatId: String(persistedMeta.chatId || ""),
debugReason: String(debugReason || reason || ""),
}),
);
return true;
} catch {
return false;
}
},
removeGraphShadowSnapshot(chatId = "") {
const key = getGraphShadowSnapshotStorageKey(chatId);
if (!key) return false;
try {
storage.removeItem(key);
return true;
} catch {
return false;
}
},
createDefaultTaskProfiles() {
return {
extract: { activeProfileId: "default", profiles: [] },
recall: { activeProfileId: "default", profiles: [] },
compress: { activeProfileId: "default", profiles: [] },
synopsis: { activeProfileId: "default", profiles: [] },
reflection: { activeProfileId: "default", profiles: [] },
};
},
getContext() {
return runtimeContext.__chatContext;
},
async saveMetadata() {
runtimeContext.__globalImmediateSaveCalls += 1;
},
saveMetadataDebounced() {
runtimeContext.__globalSaveCalls += 1;
},
__globalSaveCalls: 0,
__globalImmediateSaveCalls: 0,
isAssistantChatMessage() {
return false;
},
isFreshRecallInputRecord() {
return true;
},
notifyExtractionIssue() {},
debugDebug() {},
debugLog() {},
async runExtraction() {},
getRequestHeaders() {
return {};
},
__syncNowCalls: [],
async syncNow(chatId, options = {}) {
runtimeContext.__syncNowCalls.push({
chatId,
options: {
reason: String(options?.reason || ""),
trigger: String(options?.trigger || ""),
},
});
return { synced: true, chatId, reason: String(options?.reason || "") };
},
__chatContext: {
chatId,
chatMetadata,
characterId,
groupId,
chat,
updateChatMetadata(patch) {
const base =
this.chatMetadata &&
typeof this.chatMetadata === "object" &&
!Array.isArray(this.chatMetadata)
? this.chatMetadata
: {};
this.chatMetadata = {
...base,
...(patch || {}),
};
},
saveMetadataDebounced() {
runtimeContext.__contextSaveCalls += 1;
},
async saveMetadata() {
runtimeContext.__contextImmediateSaveCalls += 1;
},
},
__contextSaveCalls: 0,
__contextImmediateSaveCalls: 0,
buildGraphFromSnapshot,
buildSnapshotFromGraph,
buildBmeDbName,
scheduleUpload() {},
BmeDatabase: class {
constructor(dbChatId = "") {
this.chatId = String(dbChatId || "");
}
async open() {}
async close() {}
async exportSnapshot() {
return getIndexedDbSnapshotForChat(this.chatId);
}
async importSnapshot(snapshot) {
setIndexedDbSnapshotForChat(this.chatId, snapshot);
return {
revision: Number(snapshot?.meta?.revision) || 0,
};
}
},
BmeChatManager: class {
constructor() {
this._currentChatId = "";
}
_createDb(dbChatId = "") {
return {
async exportSnapshot() {
return getIndexedDbSnapshotForChat(dbChatId);
},
async importSnapshot(snapshot) {
setIndexedDbSnapshotForChat(dbChatId, snapshot);
runtimeContext.__indexedDbSnapshot =
getIndexedDbSnapshotForChat(dbChatId);
return {
revision:
Number(snapshot?.meta?.revision) ||
Number(runtimeContext.__indexedDbSnapshot?.meta?.revision) ||
0,
};
},
async getMeta(key, fallbackValue = 0) {
const snapshot = getIndexedDbSnapshotForChat(dbChatId) || {};
if (!snapshot?.meta || !(key in snapshot.meta)) {
return fallbackValue;
}
return snapshot.meta[key];
},
async getRevision() {
const snapshot = getIndexedDbSnapshotForChat(dbChatId) || {};
return Number(snapshot?.meta?.revision) || 0;
},
async isEmpty() {
const snapshot = getIndexedDbSnapshotForChat(dbChatId) || {};
const nodes = Array.isArray(snapshot?.nodes)
? snapshot.nodes.length
: 0;
const edges = Array.isArray(snapshot?.edges)
? snapshot.edges.length
: 0;
const tombstones = Array.isArray(snapshot?.tombstones)
? snapshot.tombstones.length
: 0;
return {
empty: nodes === 0 && edges === 0,
nodes,
edges,
tombstones,
};
},
async importLegacyGraph(graph, options = {}) {
const revision = Number(options?.revision) || 1;
const migratedSnapshot = buildSnapshotFromGraph(graph, {
chatId: dbChatId || runtimeContext.__chatContext?.chatId || "",
revision,
meta: {
migrationCompletedAt: Date.now(),
migrationSource: "chat_metadata",
},
});
setIndexedDbSnapshotForChat(dbChatId, migratedSnapshot);
runtimeContext.__indexedDbSnapshot =
getIndexedDbSnapshotForChat(dbChatId);
return {
migrated: true,
revision,
imported: {
nodes: runtimeContext.__indexedDbSnapshot?.nodes?.length || 0,
edges: runtimeContext.__indexedDbSnapshot?.edges?.length || 0,
tombstones:
runtimeContext.__indexedDbSnapshot?.tombstones?.length || 0,
},
};
},
async markSyncDirty() {},
};
}
async getCurrentDb(dbChatId = this._currentChatId) {
this._currentChatId = String(dbChatId || this._currentChatId || "");
runtimeContext.__indexedDbSnapshot = getIndexedDbSnapshotForChat(
this._currentChatId,
);
return this._createDb(this._currentChatId);
}
async switchChat(dbChatId = "") {
this._currentChatId = String(dbChatId || "");
runtimeContext.__indexedDbSnapshot = getIndexedDbSnapshotForChat(
this._currentChatId,
);
return this._createDb(this._currentChatId);
}
async closeCurrent() {}
},
};
runtimeContext.globalThis = runtimeContext;
vm.createContext(runtimeContext);
vm.runInContext(
[
persistencePrelude,
persistenceCore,
messageSnippet,
`
result = {
GRAPH_LOAD_STATES,
GRAPH_LOAD_RETRY_DELAYS_MS,
readRuntimeDebugSnapshot,
getGraphPersistenceLiveState,
readGraphShadowSnapshot,
writeGraphShadowSnapshot,
removeGraphShadowSnapshot,
maybeCaptureGraphShadowSnapshot,
loadGraphFromChat,
loadGraphFromIndexedDb,
saveGraphToChat,
syncGraphLoadFromLiveContext,
buildBmeSyncRuntimeOptions,
onMessageReceived,
applyGraphLoadState,
maybeFlushQueuedGraphPersist,
cloneGraphForPersistence,
assertRecoveryChatStillActive,
createAbortError,
isAbortError,
setCurrentGraph(graph) {
currentGraph = graph;
return currentGraph;
},
getCurrentGraph() {
return currentGraph;
},
getLastInjectionContent() {
return lastInjectionContent;
},
getLastRecalledItems() {
return lastRecalledItems;
},
setGraphPersistenceState(patch = {}) {
graphPersistenceState = {
...graphPersistenceState,
...(patch || {}),
updatedAt: new Date().toISOString(),
};
syncGraphPersistenceDebugState();
return graphPersistenceState;
},
getGraphPersistenceState() {
return graphPersistenceState;
},
setChatContext(nextContext) {
globalThis.__chatContext = nextContext;
return globalThis.__chatContext;
},
getChatContext() {
return globalThis.__chatContext;
},
setIndexedDbSnapshot(snapshot) {
const activeChatId =
String(globalThis.__chatContext?.chatId || globalThis.__globalChatId || "");
if (activeChatId) {
globalThis.__indexedDbSnapshots.set(
activeChatId,
structuredClone(snapshot),
);
}
globalThis.__indexedDbSnapshot = structuredClone(snapshot);
},
getIndexedDbSnapshot() {
return globalThis.__indexedDbSnapshot;
},
setIndexedDbSnapshotForChat(chatId, snapshot) {
const normalizedChatId = String(chatId || "");
if (!normalizedChatId) return;
globalThis.__indexedDbSnapshots.set(
normalizedChatId,
structuredClone(snapshot),
);
},
getIndexedDbSnapshotForChat(chatId) {
const normalizedChatId = String(chatId || "");
if (!normalizedChatId) return null;
const snapshot = globalThis.__indexedDbSnapshots.get(normalizedChatId);
return snapshot ? structuredClone(snapshot) : null;
},
};
`,
].join("\n"),
runtimeContext,
{ filename: indexPath },
);
return {
api: runtimeContext.result,
runtimeContext,
sessionStore: storage.__store,
};
}
{
const harness = await createGraphPersistenceHarness({
chatId: "",
globalChatId: "",
chatMetadata: {},
characterId: "",
groupId: null,
chat: [],
});
const result = harness.api.loadGraphFromChat({
attemptIndex: 0,
source: "no-chat-empty-host-state",
});
const live = harness.api.getGraphPersistenceLiveState();
assert.equal(result.loadState, "no-chat");
assert.equal(live.loadState, "no-chat");
assert.equal(live.writesBlocked, true);
}
{
const harness = await createGraphPersistenceHarness({
chatId: "",
globalChatId: "chat-global",
chatMetadata: {
st_bme_graph: createMeaningfulGraph("chat-global", "global"),
},
});
const result = harness.api.loadGraphFromChat({
attemptIndex: 0,
source: "global-chat-id",
});
assert.equal(result.loadState, "loading");
assert.equal(result.reason, "global-chat-id:metadata-compat-provisional");
assert.equal(
harness.api.getCurrentGraph().historyState.chatId,
"chat-global",
);
assert.equal(harness.api.getGraphPersistenceState().dbReady, false);
assert.equal(harness.api.getGraphPersistenceLiveState().writesBlocked, true);
assert.equal(
harness.api.getGraphPersistenceState().dualWriteLastResult?.resultCode,
"graph.load.metadata-compat.provisional",
);
assert.equal(
harness.api.getGraphPersistenceState().dualWriteLastResult?.provisional,
true,
);
assert.equal(
harness.api.getGraphPersistenceState().dualWriteLastResult?.reason,
"global-chat-id:metadata-compat-provisional",
);
}
{
const graph = createMeaningfulGraph("chat-recall-ui", "recall-ui");
graph.nodes[0].id = "restore-node";
graph.lastRecallResult = [{ id: "restore-node" }];
stampPersistedGraph(graph, {
revision: 7,
chatId: "chat-recall-ui",
reason: "recall-ui-restore",
});
const harness = await createGraphPersistenceHarness({
chatId: "chat-recall-ui",
globalChatId: "chat-recall-ui",
indexedDbSnapshot: buildSnapshotFromGraph(graph, {
chatId: "chat-recall-ui",
revision: 7,
}),
chat: [
{
is_user: true,
mes: "用户楼层",
extra: {
bme_recall: buildPersistedRecallRecord({
injectionText: "已持久化的召回注入",
selectedNodeIds: [],
nowIso: "2026-01-01T00:00:00.000Z",
}),
},
},
{
is_user: false,
mes: "assistant",
},
],
});
const result = harness.api.syncGraphLoadFromLiveContext({
source: "indexeddb-recall-ui-restore",
});
await new Promise((resolve) => setTimeout(resolve, 0));
assert.equal(result.synced, true);
assert.equal(harness.api.getGraphPersistenceState().dbReady, true);
assert.equal(harness.api.getLastInjectionContent(), "已持久化的召回注入");
assert.equal(harness.api.getLastRecalledItems().length, 1);
assert.equal(harness.api.getLastRecalledItems()[0]?.id, "restore-node");
}
{
const harness = await createGraphPersistenceHarness({
chatId: "",
globalChatId: "",
chatMetadata: {},
});
const lateGraph = createMeaningfulGraph("chat-late", "late");
harness.api.setChatContext({
chatId: "chat-late",
chatMetadata: {
integrity: "chat-late-ready",
st_bme_graph: lateGraph,
},
characterId: "char-late",
groupId: null,
chat: [{ is_user: true, mes: "late load" }],
updateChatMetadata(patch) {
const base =
this.chatMetadata &&
typeof this.chatMetadata === "object" &&
!Array.isArray(this.chatMetadata)
? this.chatMetadata
: {};
this.chatMetadata = {
...base,
...(patch || {}),
};
},
saveMetadataDebounced() {},
});
harness.api.setIndexedDbSnapshot(
buildSnapshotFromGraph(lateGraph, { chatId: "chat-late", revision: 5 }),
);
const result = harness.api.syncGraphLoadFromLiveContext({
source: "late-context-sync",
});
await new Promise((resolve) => setTimeout(resolve, 0));
assert.equal(result.synced, true);
assert.equal(result.loadState, "loading");
assert.equal(
harness.api.getCurrentGraph().historyState.chatId,
"chat-late-ready",
);
assert.equal(harness.api.getGraphPersistenceState().dbReady, true);
assert.equal(
harness.api.getGraphPersistenceState().storagePrimary,
"indexeddb",
);
}
{
const harness = await createGraphPersistenceHarness({
chatId: "",
globalChatId: "",
chatMetadata: {},
});
harness.api.setChatContext({
chatId: "chat-empty-live",
chatMetadata: {
integrity: "chat-empty-live-ready",
},
characterId: "char-empty-live",
groupId: null,
chat: [{ is_user: true, mes: "hello" }],
updateChatMetadata(patch) {
const base =
this.chatMetadata &&
typeof this.chatMetadata === "object" &&
!Array.isArray(this.chatMetadata)
? this.chatMetadata
: {};
this.chatMetadata = {
...base,
...(patch || {}),
};
},
saveMetadataDebounced() {},
});
const result = harness.api.syncGraphLoadFromLiveContext({
source: "late-empty-sync",
});
await new Promise((resolve) => setTimeout(resolve, 0));
assert.equal(result.synced, true);
assert.equal(result.loadState, "loading");
assert.equal(
harness.api.getGraphPersistenceState().loadState,
"empty-confirmed",
);
assert.equal(harness.api.getGraphPersistenceState().dbReady, true);
}
{
const harness = await createGraphPersistenceHarness({
chatId: "chat-metadata-placeholder",
chatMetadata: {
placeholder: "host-loading",
},
});
const result = harness.api.loadGraphFromChat({
attemptIndex: 0,
source: "metadata-placeholder-not-ready",
});
const live = harness.api.getGraphPersistenceLiveState();
assert.equal(
result.loadState,
"loading",
"无图谱数据时应进入 IndexedDB 探测等待态",
);
assert.equal(
result.reason,
"indexeddb-probe-pending",
"应继续等待 IndexedDB 探测结果",
);
assert.equal(live.writesBlocked, true);
}
{
const harness = await createGraphPersistenceHarness({
chatId: "chat-metadata-chatid-ready",
chatMetadata: {
chatId: "chat-metadata-chatid-ready",
},
});
const result = harness.api.loadGraphFromChat({
attemptIndex: 0,
source: "metadata-chatid-ready",
});
assert.equal(result.loadState, "loading");
assert.equal(
harness.api.getGraphPersistenceLiveState().writesBlocked,
true,
"无 IndexedDB 命中时应维持 loading 等待探测结果",
);
}
{
const harness = await createGraphPersistenceHarness({
chatId: "",
globalChatId: "",
characterId: "char-1",
chatMetadata: undefined,
chat: [{ is_user: true, mes: "hello" }],
});
const result = harness.api.loadGraphFromChat({
attemptIndex: 0,
source: "pending-chat-context",
});
const live = harness.api.getGraphPersistenceLiveState();
assert.equal(result.loadState, "loading");
assert.equal(live.loadState, "loading");
assert.equal(live.reason, "chat-id-missing");
assert.equal(live.writesBlocked, true);
}
{
const harness = await createGraphPersistenceHarness({
chatId: "chat-blocked",
chatMetadata: undefined,
});
const graph = createMeaningfulGraph("chat-blocked", "blocked");
harness.api.setCurrentGraph(graph);
harness.api.setGraphPersistenceState({
loadState: "loading",
chatId: "chat-blocked",
reason: "chat-metadata-missing",
revision: 4,
writesBlocked: true,
});
const result = harness.api.saveGraphToChat({
reason: "blocked-save",
markMutation: false,
});
assert.equal(result.saved, true);
assert.equal(result.queued, false);
assert.equal(result.blocked, false);
assert.equal(result.saveMode, "indexeddb-queued");
assert.equal(harness.runtimeContext.__chatContext.chatMetadata, undefined);
assert.equal(harness.runtimeContext.__contextSaveCalls, 0);
assert.equal(harness.runtimeContext.__globalSaveCalls, 0);
const shadow = harness.api.readGraphShadowSnapshot("chat-blocked");
assert.equal(shadow, null, "IndexedDB 主路径不再依赖会话影子快照");
assert.equal(
harness.api.readRuntimeDebugSnapshot().graphPersistence
?.queuedPersistRevision,
0,
);
}
{
const harness = await createGraphPersistenceHarness({
chatId: "chat-empty",
chatMetadata: undefined,
});
harness.api.setCurrentGraph(
normalizeGraphRuntimeState(createEmptyGraph(), "chat-empty"),
);
harness.api.setGraphPersistenceState({
loadState: "loading",
chatId: "chat-empty",
reason: "chat-metadata-missing",
revision: 0,
writesBlocked: true,
});
const result = harness.api.saveGraphToChat({
reason: "loading-empty-save",
markMutation: false,
});
assert.equal(result.blocked, false);
assert.equal(result.queued, false);
assert.equal(result.reason, "passive-empty-graph-skipped");
assert.equal(
harness.api.readGraphShadowSnapshot("chat-empty"),
null,
"空图不应污染影子快照",
);
}
{
const harness = await createGraphPersistenceHarness({
chatId: "chat-message",
chatMetadata: undefined,
});
harness.api.setCurrentGraph(createMeaningfulGraph("chat-message", "message"));
harness.api.setGraphPersistenceState({
loadState: "loading",
chatId: "chat-message",
reason: "chat-metadata-missing",
revision: 2,
writesBlocked: true,
});
harness.api.onMessageReceived();
assert.equal(
harness.runtimeContext.__chatContext.chatMetadata,
undefined,
"onMessageReceived 不应在 loading 期间写回 chat metadata",
);
assert.equal(harness.runtimeContext.__contextSaveCalls, 0);
assert.equal(
harness.api.readGraphShadowSnapshot("chat-message"),
null,
"onMessageReceived 不再依赖 shadow snapshot 兜底",
);
}
{
const harness = await createGraphPersistenceHarness({
chatId: "chat-late-reconcile",
chatMetadata: undefined,
});
harness.api.setCurrentGraph(
normalizeGraphRuntimeState(createEmptyGraph(), "chat-late-reconcile"),
);
harness.api.setGraphPersistenceState({
loadState: "blocked",
chatId: "chat-late-reconcile",
reason: "chat-metadata-timeout",
revision: 2,
writesBlocked: true,
});
harness.api.setChatContext({
...harness.api.getChatContext(),
chatId: "chat-late-reconcile",
chatMetadata: {
integrity: "chat-late-reconcile-ready",
st_bme_graph: createMeaningfulGraph(
"chat-late-reconcile",
"late-official",
),
},
});
harness.api.setIndexedDbSnapshot(
buildSnapshotFromGraph(
createMeaningfulGraph("chat-late-reconcile", "late-indexeddb"),
{
chatId: "chat-late-reconcile",
revision: 7,
},
),
);
harness.api.onMessageReceived();
await new Promise((resolve) => setTimeout(resolve, 0));
const live = harness.api.getGraphPersistenceLiveState();
assert.equal(live.loadState, "loaded");
assert.equal(live.writesBlocked, false);
assert.equal(live.storagePrimary, "indexeddb");
assert.equal(
harness.api.getCurrentGraph().nodes[0]?.fields?.title,
"事件-late-indexeddb",
);
}
{
const harness = await createGraphPersistenceHarness({
chatId: "chat-sync-refresh",
chatMetadata: {
integrity: "chat-sync-refresh-ready",
},
});
harness.api.setCurrentGraph(
normalizeGraphRuntimeState(
createMeaningfulGraph("chat-sync-refresh", "stale-runtime"),
"chat-sync-refresh",
),
);
harness.api.setGraphPersistenceState({
loadState: "loaded",
chatId: "chat-sync-refresh",
reason: "runtime-stale",
revision: 2,
lastPersistedRevision: 2,
dbReady: true,
writesBlocked: false,
});
harness.api.setIndexedDbSnapshot(
buildSnapshotFromGraph(
createMeaningfulGraph("chat-sync-refresh", "fresh-indexeddb"),
{
chatId: "chat-sync-refresh",
revision: 7,
},
),
);
const runtimeOptions = harness.api.buildBmeSyncRuntimeOptions();
await runtimeOptions.onSyncApplied({
chatId: "chat-sync-refresh",
action: "download",
});
assert.equal(
harness.api.getCurrentGraph().nodes[0]?.fields?.title,
"事件-fresh-indexeddb",
"download/merge 后应刷新当前运行时图谱",
);
}
{
const harness = await createGraphPersistenceHarness({
chatId: "chat-sync-refresh-merge",
chatMetadata: {
integrity: "chat-sync-refresh-merge-ready",
},
});
harness.api.setCurrentGraph(
normalizeGraphRuntimeState(
createMeaningfulGraph("chat-sync-refresh-merge", "stale-runtime-merge"),
"chat-sync-refresh-merge",
),
);
harness.api.setGraphPersistenceState({
loadState: "loaded",
chatId: "chat-sync-refresh-merge",
reason: "runtime-stale",
revision: 3,
lastPersistedRevision: 3,
dbReady: true,
writesBlocked: false,
});
harness.api.setIndexedDbSnapshot(
buildSnapshotFromGraph(
createMeaningfulGraph("chat-sync-refresh-merge", "fresh-indexeddb-merge"),
{
chatId: "chat-sync-refresh-merge",
revision: 8,
},
),
);
const runtimeOptions = harness.api.buildBmeSyncRuntimeOptions();
await runtimeOptions.onSyncApplied({
chatId: "chat-sync-refresh-merge",
action: "merge",
});
assert.equal(
harness.api.getCurrentGraph().nodes[0]?.fields?.title,
"事件-fresh-indexeddb-merge",
"merge 后应刷新当前运行时图谱",
);
}
{
const harness = await createGraphPersistenceHarness({
chatId: "chat-sync-refresh-active",
chatMetadata: {
integrity: "chat-sync-refresh-active-ready",
},
});
harness.api.setCurrentGraph(
normalizeGraphRuntimeState(
createMeaningfulGraph("chat-sync-refresh-active", "active-runtime"),
"chat-sync-refresh-active",
),
);
harness.api.setGraphPersistenceState({
loadState: "loaded",
chatId: "chat-sync-refresh-active",
reason: "runtime-active",
revision: 4,
dbReady: true,
writesBlocked: false,
});
const runtimeOptions = harness.api.buildBmeSyncRuntimeOptions();
await runtimeOptions.onSyncApplied({
chatId: "chat-sync-refresh-other",
action: "download",
});
assert.equal(
harness.api.getCurrentGraph().nodes[0]?.fields?.title,
"事件-active-runtime",
"active chat 与 sync payload chat 不一致时不应覆盖当前运行时图谱",
);
}
{
const harness = await createGraphPersistenceHarness({
chatId: "chat-panel-host",
globalChatId: "chat-panel-host",
chatMetadata: {
integrity: "chat-panel-integrity",
},
});
harness.api.setCurrentGraph(
normalizeGraphRuntimeState(
createMeaningfulGraph("chat-panel-host", "runtime-host"),
"chat-panel-host",
),
);
harness.api.setGraphPersistenceState({
loadState: "loaded",
chatId: "chat-panel-host",
reason: "runtime-host-loaded",
revision: 6,
lastPersistedRevision: 6,
dbReady: true,
writesBlocked: false,
});
const result = harness.api.syncGraphLoadFromLiveContext({
source: "panel-open-sync",
});
assert.equal(
result.synced,
false,
"hostChatId 与 integrity 只是同一聊天的不同身份时,不应误判为需要重新加载",
);
assert.equal(result.reason, "no-sync-needed");
}
{
const harness = await createGraphPersistenceHarness({
chatId: "chat-stale-cache",
globalChatId: "chat-stale-cache",
chatMetadata: {
integrity: "chat-stale-cache-integrity",
},
});
harness.api.setCurrentGraph(
normalizeGraphRuntimeState(
createMeaningfulGraph("chat-stale-cache", "runtime-newer"),
"chat-stale-cache",
),
);
harness.api.setGraphPersistenceState({
loadState: "loaded",
chatId: "chat-stale-cache",
reason: "runtime-newer",
revision: 9,
lastPersistedRevision: 9,
queuedPersistRevision: 9,
dbReady: true,
writesBlocked: false,
});
harness.api.setIndexedDbSnapshotForChat(
"chat-stale-cache-integrity",
buildSnapshotFromGraph(
createMeaningfulGraph("chat-stale-cache", "indexeddb-older"),
{
chatId: "chat-stale-cache-integrity",
revision: 4,
},
),
);
const result = await harness.api.loadGraphFromIndexedDb(
"chat-stale-cache-integrity",
{
source: "sync-post-refresh:download",
allowOverride: true,
applyEmptyState: true,
},
);
assert.equal(result.success, false);
assert.equal(result.loaded, false);
assert.equal(result.reason, "indexeddb-stale-runtime");
assert.equal(
result.staleDetail?.reason,
"runtime-revision-newer",
"同聊天较旧的 IndexedDB 快照应被识别为过期",
);
assert.equal(
harness.api.getCurrentGraph().nodes[0]?.fields?.title,
"事件-runtime-newer",
"较旧的 IndexedDB 快照不得覆盖当前更近的运行时图谱",
);
assert.equal(
harness.api.getGraphPersistenceLiveState().loadState,
"loaded",
"拒绝旧快照后不应把当前图谱重新打回 loading",
);
}
{
const harness = await createGraphPersistenceHarness({
chatId: "chat-stale-cache-panel",
globalChatId: "chat-stale-cache-panel",
chatMetadata: {
integrity: "chat-stale-cache-panel-integrity",
},
});
harness.api.setCurrentGraph(
normalizeGraphRuntimeState(
createMeaningfulGraph("chat-stale-cache-panel", "runtime-newer"),
"chat-stale-cache-panel",
),
);
harness.api.setGraphPersistenceState({
loadState: "loaded",
chatId: "chat-stale-cache-panel",
reason: "runtime-newer",
revision: 9,
lastPersistedRevision: 9,
queuedPersistRevision: 9,
dbReady: true,
writesBlocked: false,
});
const result = harness.api.syncGraphLoadFromLiveContext({
source: "panel-open-sync",
});
assert.equal(
result.synced,
false,
"hostChatId 与 integrity 只是同一聊天的不同身份时,面板打开不应误判成要重新同步",
);
assert.equal(result.reason, "no-sync-needed");
}
{
const metadataGraph = stampPersistedGraph(
createMeaningfulGraph("chat-stale-metadata", "metadata-older"),
{
revision: 3,
integrity: "chat-stale-metadata-integrity",
chatId: "chat-stale-metadata",
reason: "metadata-older",
},
);
const harness = await createGraphPersistenceHarness({
chatId: "chat-stale-metadata",
globalChatId: "chat-stale-metadata",
chatMetadata: {
integrity: "chat-stale-metadata-integrity",
st_bme_graph: metadataGraph,
},
});
harness.api.setCurrentGraph(
normalizeGraphRuntimeState(
createMeaningfulGraph("chat-stale-metadata", "runtime-newer"),
"chat-stale-metadata",
),
);
harness.api.setGraphPersistenceState({
loadState: "loaded",
chatId: "chat-stale-metadata",
reason: "runtime-newer",
revision: 8,
lastPersistedRevision: 8,
dbReady: true,
writesBlocked: false,
});
const result = harness.api.loadGraphFromChat({
attemptIndex: 0,
source: "stale-metadata-runtime-guard",
});
assert.equal(result.reason, "metadata-compat-stale-runtime");
assert.equal(
harness.api.getCurrentGraph().nodes[0]?.fields?.title,
"事件-runtime-newer",
"较旧的 metadata 兼容图不得把当前运行时图谱盖回去",
);
}
{
const sharedSession = new Map();
const writer = await createGraphPersistenceHarness({
chatId: "chat-shadow",
chatMetadata: undefined,
sessionStore: sharedSession,
});
writer.api.writeGraphShadowSnapshot(
"chat-shadow",
createMeaningfulGraph("chat-shadow", "shadow"),
{ revision: 7, reason: "manual-shadow" },
);
const reader = await createGraphPersistenceHarness({
chatId: "chat-shadow",
chatMetadata: undefined,
sessionStore: sharedSession,
});
const result = reader.api.loadGraphFromChat({
attemptIndex: 0,
source: "shadow-test",
});
assert.equal(result.loadState, "shadow-restored");
assert.equal(
reader.api.getCurrentGraph().nodes[0]?.fields?.title,
"事件-shadow",
);
assert.equal(
reader.api.getGraphPersistenceLiveState().shadowSnapshotUsed,
true,
);
assert.equal(reader.api.getGraphPersistenceLiveState().writesBlocked, false);
}
{
const sharedSession = new Map();
const writer = await createGraphPersistenceHarness({
chatId: "chat-official",
chatMetadata: undefined,
sessionStore: sharedSession,
});
writer.api.writeGraphShadowSnapshot(
"chat-official",
createMeaningfulGraph("chat-official", "shadow-stale"),
{ revision: 3, reason: "stale-shadow" },
);
const officialGraph = stampPersistedGraph(
createMeaningfulGraph("chat-official", "official"),
{ revision: 6, integrity: "official-integrity" },
);
const reader = await createGraphPersistenceHarness({
chatId: "chat-official",
chatMetadata: {
integrity: "official-integrity",
st_bme_graph: officialGraph,
},
sessionStore: sharedSession,
});
const result = reader.api.loadGraphFromChat({
attemptIndex: 0,
source: "official-load",
});
assert.equal(result.loadState, "loading");
assert.equal(
reader.api.getCurrentGraph().nodes[0]?.fields?.title,
"事件-official",
);
assert.equal(
reader.api.readGraphShadowSnapshot("chat-official")?.reason,
"stale-shadow",
"metadata 兼容加载时保留影子快照仅作为兼容数据,不参与主链路",
);
}
{
const sharedSession = new Map();
const writer = await createGraphPersistenceHarness({
chatId: "chat-shadow-newer",
chatMetadata: undefined,
sessionStore: sharedSession,
});
const shadowGraph = stampPersistedGraph(
createMeaningfulGraph("chat-shadow-newer", "shadow-newer"),
{
revision: 9,
integrity: "integrity-shadow-mismatch",
chatId: "chat-shadow-newer",
reason: "pagehide-refresh",
},
);
writer.api.writeGraphShadowSnapshot("chat-shadow-newer", shadowGraph, {
revision: 9,
reason: "pagehide-refresh",
integrity: "integrity-shadow-mismatch",
debugReason: "pagehide-refresh",
});
const officialGraph = stampPersistedGraph(
createMeaningfulGraph("chat-shadow-newer", "official-older"),
{ revision: 3, integrity: "integrity-official-older" },
);
const reader = await createGraphPersistenceHarness({
chatId: "chat-shadow-newer",
chatMetadata: {
integrity: "integrity-official-older",
st_bme_graph: officialGraph,
},
sessionStore: sharedSession,
});
const result = reader.api.loadGraphFromChat({
attemptIndex: 0,
source: "official-older-than-shadow",
});
assert.equal(result.loadState, "loading");
assert.equal(
result.reason,
"official-older-than-shadow:metadata-compat-provisional",
);
assert.equal(
reader.api.getCurrentGraph().nodes[0]?.fields?.title,
"事件-official-older",
);
assert.equal(reader.runtimeContext.__contextImmediateSaveCalls, 0);
assert.equal(
reader.runtimeContext.__chatContext.chatMetadata?.st_bme_graph?.nodes?.[0]
?.fields?.title,
"事件-official-older",
);
assert.equal(
reader.api.readGraphShadowSnapshot("chat-shadow-newer")?.reason,
"pagehide-refresh",
"metadata 兼容加载后影子快照可保留,但不作为主链路恢复来源",
);
const live = reader.api.getGraphPersistenceLiveState();
assert.equal(live.shadowSnapshotRevision, 9);
assert.equal(live.shadowSnapshotReason, "shadow-integrity-mismatch");
const compareDecision = shouldPreferShadowSnapshotOverOfficial(
officialGraph,
reader.api.readGraphShadowSnapshot("chat-shadow-newer"),
);
assert.equal(compareDecision.resultCode, "shadow.reject.integrity-mismatch");
}
{
const decision = shouldPreferShadowSnapshotOverOfficial(
stampPersistedGraph(createMeaningfulGraph("chat-self-mismatch"), {
revision: 0,
chatId: "",
integrity: "",
}),
{
chatId: "chat-self-mismatch",
persistedChatId: "chat-other",
revision: 5,
integrity: "",
},
);
assert.equal(decision.prefer, false);
assert.equal(decision.reason, "shadow-self-chat-mismatch");
assert.equal(decision.resultCode, "shadow.reject.self-chat-mismatch");
}
{
const decision = shouldPreferShadowSnapshotOverOfficial(
stampPersistedGraph(createMeaningfulGraph("chat-official-missing"), {
revision: 0,
chatId: "",
integrity: "",
}),
{
chatId: "chat-official-missing",
persistedChatId: "chat-official-missing",
revision: 4,
integrity: "",
},
);
assert.equal(decision.prefer, false);
assert.equal(decision.reason, "shadow-persisted-chat-without-official-chat");
assert.equal(
decision.resultCode,
"shadow.reject.persisted-chat-without-official-chat",
);
}
{
const decision = shouldPreferShadowSnapshotOverOfficial(
stampPersistedGraph(
createMeaningfulGraph("chat-official-integrity-missing"),
{
revision: 0,
chatId: "chat-official-integrity-missing",
integrity: "",
},
),
{
chatId: "chat-official-integrity-missing",
persistedChatId: "chat-official-integrity-missing",
revision: 4,
integrity: "shadow-only-integrity",
},
);
assert.equal(decision.prefer, false);
assert.equal(decision.reason, "shadow-integrity-without-official-integrity");
assert.equal(
decision.resultCode,
"shadow.reject.integrity-without-official-integrity",
);
}
{
const harness = await createGraphPersistenceHarness({
chatId: "chat-empty-confirmed",
chatMetadata: {
integrity: "meta-ready-empty",
},
});
const result = harness.api.loadGraphFromChat({
attemptIndex: 0,
source: "ready-empty",
});
const live = harness.api.getGraphPersistenceLiveState();
assert.equal(result.loadState, "loading");
assert.equal(result.reason, "indexeddb-probe-pending");
assert.equal(live.writesBlocked, true);
assert.equal(harness.api.getCurrentGraph(), null);
assert.equal(
harness.api.readRuntimeDebugSnapshot().graphPersistence?.loadState,
"loading",
);
}
{
const harness = await createGraphPersistenceHarness({
chatId: "chat-empty-confirmed-passive",
chatMetadata: {
integrity: "meta-ready-empty-passive",
},
});
harness.api.loadGraphFromChat({
attemptIndex: 0,
source: "ready-empty-passive",
});
harness.api.onMessageReceived();
assert.equal(
harness.runtimeContext.__contextImmediateSaveCalls,
0,
"空聊天的被动同步不应触发立即保存",
);
assert.equal(
harness.runtimeContext.__contextSaveCalls,
0,
"空聊天的被动同步不应触发防抖保存",
);
assert.equal(
harness.runtimeContext.__chatContext.chatMetadata?.st_bme_graph,
undefined,
"loading 状态下不能把空图被动写回 metadata",
);
}
{
const harness = await createGraphPersistenceHarness({
chatId: "chat-create-first-graph",
chatMetadata: {
integrity: "integrity-before-first-save",
},
});
harness.api.loadGraphFromChat({
attemptIndex: 0,
source: "ready-for-first-save",
});
harness.api.setCurrentGraph(
createMeaningfulGraph("chat-create-first-graph", "first-save"),
);
const result = harness.api.saveGraphToChat({
reason: "first-meaningful-graph",
});
assert.equal(result.saved, true);
assert.equal(result.saveMode, "indexeddb-queued");
assert.equal(harness.runtimeContext.__contextImmediateSaveCalls, 0);
assert.equal(harness.runtimeContext.__contextSaveCalls, 0);
assert.equal(
harness.runtimeContext.__chatContext.chatMetadata?.integrity ===
"integrity-before-first-save",
true,
"插件保存图谱时不能改写宿主 metadata.integrity",
);
assert.equal(
harness.runtimeContext.__chatContext.chatMetadata?.st_bme_graph,
undefined,
);
await new Promise((resolve) => setTimeout(resolve, 0));
assert.equal(
Number(harness.api.getIndexedDbSnapshot()?.meta?.revision) > 0,
true,
);
}
{
const sharedSession = new Map();
const writer = await createGraphPersistenceHarness({
chatId: "chat-promote",
chatMetadata: undefined,
sessionStore: sharedSession,
});
writer.api.writeGraphShadowSnapshot(
"chat-promote",
createMeaningfulGraph("chat-promote", "promote"),
{ revision: 9, reason: "pre-refresh" },
);
const reader = await createGraphPersistenceHarness({
chatId: "chat-promote",
chatMetadata: {
integrity: "meta-ready-promote",
},
sessionStore: sharedSession,
});
const result = reader.api.loadGraphFromChat({
attemptIndex: 0,
source: "promote-when-metadata-ready",
});
const live = reader.api.getGraphPersistenceLiveState();
assert.equal(result.loadState, "shadow-restored");
assert.equal(
reader.runtimeContext.__chatContext.chatMetadata?.st_bme_graph?.nodes
?.length,
undefined,
);
assert.equal(
reader.runtimeContext.__chatContext.chatMetadata?.integrity,
"meta-ready-promote",
);
assert.equal(reader.runtimeContext.__contextImmediateSaveCalls, 0);
assert.equal(reader.runtimeContext.__contextSaveCalls, 0);
assert.equal(live.lastPersistedRevision, 9);
assert.equal(live.pendingPersist, true);
}
{
const harness = await createGraphPersistenceHarness({
chatId: "chat-decouple",
chatMetadata: {
integrity: "meta-decouple",
},
});
const runtimeGraph = createMeaningfulGraph("chat-decouple", "runtime");
harness.api.setCurrentGraph(runtimeGraph);
harness.api.setGraphPersistenceState({
loadState: "loaded",
chatId: "chat-decouple",
revision: 3,
lastPersistedRevision: 0,
writesBlocked: false,
});
const result = harness.api.saveGraphToChat({
reason: "decouple-metadata-runtime",
markMutation: false,
persistMetadata: true,
});
assert.equal(result.saved, true);
const persistedGraph =
harness.runtimeContext.__chatContext.chatMetadata?.st_bme_graph;
assert.notEqual(
persistedGraph,
harness.api.getCurrentGraph(),
"写入 metadata 时必须使用独立 graph 快照",
);
persistedGraph.nodes[0].fields.title = "metadata-mutated";
assert.equal(
harness.api.getCurrentGraph().nodes[0].fields.title,
"事件-runtime",
"metadata 修改不能反向污染运行时 graph",
);
harness.api.getCurrentGraph().nodes[0].fields.title = "runtime-mutated";
assert.equal(
persistedGraph.nodes[0].fields.title,
"metadata-mutated",
"运行时修改不能反向污染已保存 metadata",
);
}
{
const officialGraph = stampPersistedGraph(
createMeaningfulGraph("chat-load-official", "official"),
{
revision: 4,
integrity: "meta-load-official",
chatId: "chat-load-official",
reason: "official-save",
},
);
const harness = await createGraphPersistenceHarness({
chatId: "chat-load-official",
chatMetadata: {
integrity: "meta-load-official",
st_bme_graph: officialGraph,
},
});
const result = harness.api.loadGraphFromChat({
attemptIndex: 0,
source: "load-official-decoupled",
});
assert.equal(result.loadState, "loading");
const runtimeGraph = harness.api.getCurrentGraph();
const persistedGraph =
harness.runtimeContext.__chatContext.chatMetadata.st_bme_graph;
assert.notEqual(
runtimeGraph,
persistedGraph,
"从 official metadata 恢复到运行时必须使用独立对象",
);
runtimeGraph.nodes[0].fields.title = "runtime-after-load";
assert.equal(
persistedGraph.nodes[0].fields.title,
"事件-official",
"official metadata 不应被运行时修改污染",
);
}
{
const sharedSession = new Map();
const writer = await createGraphPersistenceHarness({
chatId: "chat-load-shadow",
chatMetadata: {
integrity: "meta-load-shadow",
st_bme_graph: stampPersistedGraph(
createMeaningfulGraph("chat-load-shadow", "official-older"),
{
revision: 2,
integrity: "meta-load-shadow",
chatId: "chat-load-shadow",
reason: "official-older",
},
),
},
sessionStore: sharedSession,
});
writer.api.writeGraphShadowSnapshot(
"chat-load-shadow",
createMeaningfulGraph("chat-load-shadow", "shadow"),
{
revision: 5,
reason: "shadow-newer",
},
);
const reader = await createGraphPersistenceHarness({
chatId: "chat-load-shadow",
chatMetadata: {
integrity: "meta-load-shadow",
st_bme_graph: stampPersistedGraph(
createMeaningfulGraph("chat-load-shadow", "official-older"),
{
revision: 2,
integrity: "meta-load-shadow",
chatId: "chat-load-shadow",
reason: "official-older",
},
),
},
sessionStore: sharedSession,
});
const result = reader.api.loadGraphFromChat({
attemptIndex: 0,
source: "load-shadow-decoupled",
});
assert.equal(result.loadState, "shadow-restored");
const runtimeGraph = reader.api.getCurrentGraph();
const persistedGraph =
reader.runtimeContext.__chatContext.chatMetadata.st_bme_graph;
assert.notEqual(
runtimeGraph,
persistedGraph,
"从 shadow snapshot 提升后,运行时与 metadata 也必须解耦",
);
runtimeGraph.nodes[0].fields.title = "runtime-shadow-mutated";
assert.equal(
runtimeGraph.nodes[0].fields.title,
"runtime-shadow-mutated",
);
assert.equal(
persistedGraph.nodes[0].fields.title,
"事件-official-older",
"metadata 兼容加载后的运行时修改不能污染已保存 metadata",
);
}
{
const harness = await createGraphPersistenceHarness({
chatId: "chat-two-saves",
chatMetadata: {
integrity: "meta-two-saves",
},
});
harness.api.setCurrentGraph(createMeaningfulGraph("chat-two-saves", "first"));
harness.api.setGraphPersistenceState({
loadState: "loaded",
chatId: "chat-two-saves",
revision: 1,
lastPersistedRevision: 0,
writesBlocked: false,
});
const firstSave = harness.api.saveGraphToChat({
reason: "first-save",
markMutation: false,
persistMetadata: true,
});
assert.equal(firstSave.saved, true);
const firstPersistedGraph =
harness.runtimeContext.__chatContext.chatMetadata.st_bme_graph;
harness.api.getCurrentGraph().nodes[0].fields.title = "runtime-between-saves";
assert.equal(
firstPersistedGraph.nodes[0].fields.title,
"事件-first",
"第一次保存后的 metadata 不应被后续运行时修改污染",
);
harness.api.setGraphPersistenceState({ revision: 2 });
const secondSave = harness.api.saveGraphToChat({
reason: "second-save",
markMutation: false,
persistMetadata: true,
});
assert.equal(secondSave.saved, true);
const secondPersistedGraph =
harness.runtimeContext.__chatContext.chatMetadata.st_bme_graph;
assert.notEqual(
secondPersistedGraph,
firstPersistedGraph,
"第二次保存应生成新的 metadata graph 快照",
);
assert.equal(
secondPersistedGraph.nodes[0].fields.title,
"runtime-between-saves",
"第二次保存应反映第二轮运行时修改",
);
harness.api.getCurrentGraph().nodes[0].fields.title =
"runtime-after-second-save";
assert.equal(
firstPersistedGraph.nodes[0].fields.title,
"事件-first",
"第二轮运行时修改仍不能污染第一次已保存 metadata",
);
assert.equal(
secondPersistedGraph.nodes[0].fields.title,
"runtime-between-saves",
"第二次已保存 metadata 也不能被后续运行时修改污染",
);
}
{
const harness = await createGraphPersistenceHarness({
chatId: "chat-b",
globalChatId: "chat-b",
chatMetadata: {
integrity: "meta-chat-b",
},
});
harness.api.setCurrentGraph(createMeaningfulGraph("chat-a", "queued"));
harness.api.setGraphPersistenceState({
loadState: "loaded",
chatId: "chat-a",
revision: 6,
lastPersistedRevision: 4,
queuedPersistRevision: 6,
queuedPersistChatId: "chat-a",
queuedPersistMode: "immediate",
pendingPersist: true,
writesBlocked: false,
});
const result = harness.api.maybeFlushQueuedGraphPersist("cross-chat-flush");
assert.equal(result.saved, false);
assert.equal(result.blocked, true);
assert.equal(result.reason, "queued-chat-mismatch");
assert.equal(harness.runtimeContext.__contextImmediateSaveCalls, 0);
assert.equal(harness.runtimeContext.__contextSaveCalls, 0);
assert.equal(
harness.runtimeContext.__chatContext.chatMetadata?.st_bme_graph,
undefined,
"跨 chat 的 queued persist 不得 flush 到当前 metadata",
);
assert.equal(
harness.api.getGraphPersistenceLiveState().queuedPersistChatId,
"chat-a",
"发生 chat mismatch 时应保留原始 queued chat 绑定",
);
}
// === Fix 2c: assertRecoveryChatStillActive 跨 chat 守卫 ===
{
const harness = await createGraphPersistenceHarness({
chatId: "chat-recovery-a",
globalChatId: "chat-recovery-a",
chatMetadata: {
integrity: "meta-recovery-a",
},
});
// 同一 chat 不应抛出
harness.api.assertRecoveryChatStillActive("chat-recovery-a", "test-same");
// 切换到 chat-b
harness.runtimeContext.__globalChatId = "chat-recovery-b";
harness.runtimeContext.__chatContext.chatId = "chat-recovery-b";
let abortCaught = false;
try {
harness.api.assertRecoveryChatStillActive("chat-recovery-a", "test-switch");
} catch (e) {
abortCaught = harness.api.isAbortError(e);
}
assert.equal(
abortCaught,
true,
"chat 切换后 assertRecoveryChatStillActive 应抛出 AbortError",
);
// 空 expectedChatId 不应抛出
harness.api.assertRecoveryChatStillActive("", "test-empty");
harness.api.assertRecoveryChatStillActive(undefined, "test-undefined");
}
// === Fix 2e: resolveDirtyFloorFromMutationMeta 候选过滤 ===
// 此测试需要 resolveDirtyFloorFromMutationMeta 与 getAssistantTurns
// 它们均在 persistencePrelude 范围内,通过 vm 上下文执行。
// 这里使用间接方式验证:构造一个只有晚期 assistant 的 chat
// 然后检查 inspectHistoryMutation 不会对早期 floor 误判。
{
const harness = await createGraphPersistenceHarness({
chatId: "chat-dirty-floor",
globalChatId: "chat-dirty-floor",
chatMetadata: {
integrity: "meta-dirty-floor",
},
chat: [
// index 0: user
{ is_user: true, mes: "hello" },
// index 1: user (no assistant before index 4)
{ is_user: true, mes: "second" },
// index 2: user
{ is_user: true, mes: "third" },
// index 3: user
{ is_user: true, mes: "fourth" },
// index 4: first assistant
{ is_user: false, mes: "first reply" },
],
});
const graph = createMeaningfulGraph("chat-dirty-floor", "dirty-floor");
graph.historyState.lastProcessedAssistantFloor = 4;
graph.historyState.extractionCount = 1;
harness.api.setCurrentGraph(graph);
harness.api.setGraphPersistenceState({
loadState: "loaded",
chatId: "chat-dirty-floor",
revision: 2,
writesBlocked: false,
});
// 模拟meta 指向 floor=1早于最小可提取 floor=4的删除事件
// 使用间接方式graph 的 lastProcessedAssistantFloor=4
// 如果 resolveDirtyFloorFromMutationMeta 正确过滤了 floor<4 的候选,
// 那么 inspectHistoryMutation 不会标记为 dirty因为没有有效候选
// 注意:这里不直接测试内部函数,而是验证整体行为。
const graph2 = harness.api.getCurrentGraph();
assert.ok(graph2, "graph 应存在");
assert.equal(
graph2.historyState.lastProcessedAssistantFloor,
4,
"lastProcessedAssistantFloor 应为 4",
);
}
{
const metadataGraph = stampPersistedGraph(
createMeaningfulGraph("chat-indexeddb-priority", "metadata"),
{
revision: 3,
integrity: "meta-indexeddb-priority",
chatId: "chat-indexeddb-priority",
reason: "metadata-seed",
},
);
const indexedDbGraph = stampPersistedGraph(
createMeaningfulGraph("chat-indexeddb-priority", "indexeddb"),
{
revision: 9,
integrity: "idxdb-indexeddb-priority",
chatId: "chat-indexeddb-priority",
reason: "indexeddb-seed",
},
);
const indexedDbSnapshot = buildSnapshotFromGraph(indexedDbGraph, {
chatId: "chat-indexeddb-priority",
revision: 9,
});
const harness = await createGraphPersistenceHarness({
chatId: "chat-indexeddb-priority",
globalChatId: "chat-indexeddb-priority",
chatMetadata: {
integrity: "meta-indexeddb-priority",
[GRAPH_METADATA_KEY]: metadataGraph,
},
indexedDbSnapshot,
});
harness.api.loadGraphFromChat({ source: "indexeddb-priority" });
await new Promise((resolve) => setTimeout(resolve, 0));
assert.equal(harness.api.getCurrentGraph().nodes[0].id, "node-indexeddb");
assert.equal(
harness.api.getGraphPersistenceState().storagePrimary,
"indexeddb",
);
}
{
const sharedSession = new Map();
const writer = await createGraphPersistenceHarness({
chatId: "chat-indexeddb-shadow-restore",
globalChatId: "chat-indexeddb-shadow-restore",
sessionStore: sharedSession,
});
writer.api.writeGraphShadowSnapshot(
"chat-indexeddb-shadow-restore",
createMeaningfulGraph("chat-indexeddb-shadow-restore", "shadow-newer"),
{
revision: 9,
reason: "pagehide-refresh",
},
);
const indexedDbGraph = stampPersistedGraph(
createMeaningfulGraph("chat-indexeddb-shadow-restore", "indexeddb-older"),
{
revision: 4,
integrity: "meta-indexeddb-shadow-restore",
chatId: "chat-indexeddb-shadow-restore",
reason: "indexeddb-older",
},
);
const indexedDbSnapshot = buildSnapshotFromGraph(indexedDbGraph, {
chatId: "chat-indexeddb-shadow-restore",
revision: 4,
});
const harness = await createGraphPersistenceHarness({
chatId: "chat-indexeddb-shadow-restore",
globalChatId: "chat-indexeddb-shadow-restore",
indexedDbSnapshot,
sessionStore: sharedSession,
});
const result = await harness.api.loadGraphFromIndexedDb(
"chat-indexeddb-shadow-restore",
{
source: "indexeddb-shadow-restore",
allowOverride: true,
applyEmptyState: true,
},
);
assert.equal(result.loadState, "shadow-restored");
assert.equal(
harness.api.getCurrentGraph().nodes[0]?.fields?.title,
"事件-shadow-newer",
);
await new Promise((resolve) => setTimeout(resolve, 0));
assert.equal(
harness.api.getIndexedDbSnapshot().meta.revision,
9,
"shadow 恢复后应回补 IndexedDB 修正旧快照",
);
}
{
const legacyGraph = stampPersistedGraph(
createMeaningfulGraph("chat-legacy-migration", "legacy"),
{
revision: 6,
integrity: "meta-legacy-migration",
chatId: "chat-legacy-migration",
reason: "legacy-seed",
},
);
const harness = await createGraphPersistenceHarness({
chatId: "chat-legacy-migration",
globalChatId: "chat-legacy-migration",
chatMetadata: {
integrity: "meta-legacy-migration",
[GRAPH_METADATA_KEY]: legacyGraph,
},
indexedDbSnapshot: {
meta: {
chatId: "chat-legacy-migration",
revision: 0,
migrationCompletedAt: 0,
},
nodes: [],
edges: [],
tombstones: [],
state: {
lastProcessedFloor: -1,
extractionCount: 0,
},
},
});
harness.api.loadGraphFromChat({ source: "legacy-migration-check" });
await new Promise((resolve) => setTimeout(resolve, 0));
assert.ok(harness.runtimeContext.__syncNowCalls.length >= 1);
assert.equal(
harness.runtimeContext.__syncNowCalls[0].options.reason,
"post-migration",
);
assert.equal(harness.api.getCurrentGraph().nodes[0].id, "node-legacy");
assert.equal(
harness.api.getIndexedDbSnapshot().meta.migrationSource,
"chat_metadata",
);
}
console.log("graph-persistence tests passed");