From 10ec3b7b773b792270193b9d2595033b75287def Mon Sep 17 00:00:00 2001 From: youzini Date: Sat, 30 May 2026 13:38:01 +0000 Subject: [PATCH] refactor(rebirth): extract identity resolver core --- index.js | 277 ++++++++++------------------- package.json | 1 + runtime/identity-resolver.js | 333 +++++++++++++++++++++++++++++++++++ tests/graph-persistence.mjs | 20 +++ tests/identity-resolver.mjs | 198 +++++++++++++++++++++ 5 files changed, 641 insertions(+), 188 deletions(-) create mode 100644 runtime/identity-resolver.js create mode 100644 tests/identity-resolver.mjs diff --git a/index.js b/index.js index fc7b1b4..c1ebb5e 100644 --- a/index.js +++ b/index.js @@ -131,6 +131,17 @@ import { debugDebug, debugLog, } from "./runtime/debug-logging.js"; +import { + areChatIdsEquivalentForIdentityCore, + canMutateRuntimeGraphForIdentityCore, + doesChatIdMatchIdentityCore, + planRuntimeGraphIdentityRepairCore, + resolveActiveHostChatIdCore, + resolveCurrentChatIdentityCore, + resolveGraphOwnerIdentityCore, + resolvePersistenceChatIdCore, + resolveRuntimeGraphFallbackIdentityCore, +} from "./runtime/identity-resolver.js"; import { extractMemories, generateReflection, @@ -540,55 +551,17 @@ function getChatCommitMarker(context = getContext()) { } function resolveCurrentHostChatId(context = getContext()) { - const candidates = [ - context?.chatId, - context?.getCurrentChatId?.(), - readGlobalCurrentChatId(), - context?.chatMetadata?.chat_id, - context?.chatMetadata?.chatId, - context?.chatMetadata?.session_id, - context?.chatMetadata?.sessionId, - ]; - - return ( - candidates - .map((candidate) => normalizeChatIdCandidate(candidate)) - .find(Boolean) || "" - ); + return resolveActiveHostChatIdCore({ context, readGlobalCurrentChatId }); } function resolveCurrentChatIdentity(context = getContext()) { - const hostChatId = resolveCurrentHostChatId(context); - const integrity = - typeof getChatMetadataIntegrity === "function" - ? getChatMetadataIntegrity(context) - : normalizeChatIdCandidate( - context?.chatMetadata?.integrity || - context?.chatMetadata?.chat_id || - context?.chatMetadata?.chatId || - "", - ); - const aliasedChatId = - !integrity && - hostChatId && - typeof resolveGraphIdentityAliasByHostChatId === "function" - ? resolveGraphIdentityAliasByHostChatId(hostChatId) - : ""; - const chatId = integrity || aliasedChatId || hostChatId; - - return { - chatId, - hostChatId, - integrity, - identitySource: integrity - ? "integrity" - : aliasedChatId - ? "alias" - : hostChatId - ? "host-chat-id" - : "", - hasLikelySelectedChat: hasLikelySelectedChatContext(context), - }; + return resolveCurrentChatIdentityCore({ + context, + readGlobalCurrentChatId, + resolveAliasByHostChatId: resolveGraphIdentityAliasByHostChatId, + resolveIntegrity: getChatMetadataIntegrity, + hasLikelySelectedChat: hasLikelySelectedChatContext, + }); } function getCurrentChatId(context = getContext()) { @@ -597,29 +570,16 @@ function getCurrentChatId(context = getContext()) { function getRuntimeGraphChatIdFallback(graph = currentGraph) { const graphMeta = getGraphPersistenceMeta(graph) || {}; - const fallbackCandidates = [ - graph?.historyState?.chatId, - graphMeta.chatId, - graphPersistenceState.chatId, - graphPersistenceState.queuedPersistChatId, - graphPersistenceState.commitMarker?.chatId, - ]; - - return ( - fallbackCandidates - .map((candidate) => normalizeChatIdCandidate(candidate)) - .find(Boolean) || "" - ); + return resolveRuntimeGraphFallbackIdentityCore({ + graph, + graphMeta, + persistenceState: graphPersistenceState, + }).chatId; } function getGraphOwnedChatId(graph = currentGraph) { const graphMeta = getGraphPersistenceMeta(graph) || {}; - const ownedCandidates = [graph?.historyState?.chatId, graphMeta.chatId]; - return ( - ownedCandidates - .map((candidate) => normalizeChatIdCandidate(candidate)) - .find(Boolean) || "" - ); + return resolveGraphOwnerIdentityCore({ graph, graphMeta }).chatId; } function resolveOperationalChatId( @@ -639,34 +599,16 @@ function resolvePersistenceChatId( graph = currentGraph, explicitChatId = "", ) { - const directChatId = normalizeChatIdCandidate(explicitChatId); - if (directChatId) return directChatId; - - const resolvedIdentity = resolveCurrentChatIdentity(context); - const resolvedChatId = normalizeChatIdCandidate(resolvedIdentity.chatId); - if (resolvedChatId) return resolvedChatId; - - const graphMeta = getGraphPersistenceMeta(graph) || {}; - const fallbackCandidates = [ - graph?.historyState?.chatId, - graphMeta.chatId, - currentGraph?.historyState?.chatId, - getGraphPersistenceMeta(currentGraph)?.chatId, - graphPersistenceState.chatId, - graphPersistenceState.queuedPersistChatId, - graphPersistenceState.commitMarker?.chatId, - context?.chatMetadata?.integrity, - context?.chatMetadata?.chat_id, - context?.chatMetadata?.chatId, - context?.chatMetadata?.session_id, - context?.chatMetadata?.sessionId, - ]; - - return ( - fallbackCandidates - .map((candidate) => normalizeChatIdCandidate(candidate)) - .find(Boolean) || "" - ); + return resolvePersistenceChatIdCore({ + explicitChatId, + activeIdentity: resolveCurrentChatIdentity(context), + graph, + graphMeta: getGraphPersistenceMeta(graph) || {}, + currentGraph, + currentGraphMeta: getGraphPersistenceMeta(currentGraph) || {}, + persistenceState: graphPersistenceState, + context, + }); } function rememberResolvedGraphIdentityAlias( @@ -689,32 +631,14 @@ function doesChatIdMatchResolvedGraphIdentity( candidateChatId, identity = resolveCurrentChatIdentity(getContext()), ) { - const normalizedCandidate = normalizeChatIdCandidate(candidateChatId); - if (!normalizedCandidate || !identity || typeof identity !== "object") { - return false; - } - - const knownChatIds = new Set(); - const addKnownChatId = (value) => { - const normalized = normalizeChatIdCandidate(value); - if (normalized) { - knownChatIds.add(normalized); - } - }; - - addKnownChatId(identity.chatId); - addKnownChatId(identity.hostChatId); - addKnownChatId(identity.integrity); - - for (const aliasCandidate of getGraphIdentityAliasCandidates({ - integrity: identity.integrity, - hostChatId: identity.hostChatId, - persistenceChatId: identity.chatId, - })) { - addKnownChatId(aliasCandidate); - } - - return knownChatIds.has(normalizedCandidate); + return doesChatIdMatchIdentityCore(candidateChatId, { + identity, + aliasCandidates: getGraphIdentityAliasCandidates({ + integrity: identity?.integrity, + hostChatId: identity?.hostChatId, + persistenceChatId: identity?.chatId, + }), + }); } function areChatIdsEquivalentForResolvedIdentity( @@ -722,18 +646,14 @@ function areChatIdsEquivalentForResolvedIdentity( referenceChatId, identity = resolveCurrentChatIdentity(getContext()), ) { - const normalizedCandidate = normalizeChatIdCandidate(candidateChatId); - const normalizedReference = normalizeChatIdCandidate(referenceChatId); - if (!normalizedCandidate || !normalizedReference) { - return normalizedCandidate === normalizedReference; - } - if (normalizedCandidate === normalizedReference) { - return true; - } - return ( - doesChatIdMatchResolvedGraphIdentity(normalizedCandidate, identity) && - doesChatIdMatchResolvedGraphIdentity(normalizedReference, identity) - ); + return areChatIdsEquivalentForIdentityCore(candidateChatId, referenceChatId, { + identity, + aliasCandidates: getGraphIdentityAliasCandidates({ + integrity: identity?.integrity, + hostChatId: identity?.hostChatId, + persistenceChatId: identity?.chatId, + }), + }); } function syncCommitMarkerToPersistenceState(context = getContext()) { @@ -4472,34 +4392,24 @@ function hasRuntimeGraphMutationContext( } const identity = resolveCurrentChatIdentity(context); - const liveChatId = normalizeChatIdCandidate(identity.chatId); const graphOwnedChatId = getGraphOwnedChatId(graph); - if (!graphOwnedChatId) return false; - - if (liveChatId) { - return ( - areChatIdsEquivalentForResolvedIdentity(graphOwnedChatId, liveChatId, identity) || - areChatIdsEquivalentForResolvedIdentity(liveChatId, graphOwnedChatId, identity) - ); - } - - const stateChatId = normalizeChatIdCandidate(graphPersistenceState.chatId); - if (!stateChatId || stateChatId !== graphOwnedChatId) { - return false; - } - - const markerChatId = normalizeChatIdCandidate(graphPersistenceState.commitMarker?.chatId); - if (markerChatId && markerChatId !== graphOwnedChatId) return false; - - if ( - graphPersistenceState.loadState === GRAPH_LOAD_STATES.LOADED || - graphPersistenceState.loadState === GRAPH_LOAD_STATES.EMPTY_CONFIRMED || - graphPersistenceState.dbReady === true - ) { - return true; - } - - return allowNoChatState === true && graphPersistenceState.loadState === GRAPH_LOAD_STATES.NO_CHAT; + return canMutateRuntimeGraphForIdentityCore({ + graph, + activeIdentity: identity, + graphOwnedChatId, + persistenceState: graphPersistenceState, + aliasCandidates: getGraphIdentityAliasCandidates({ + integrity: identity.integrity, + hostChatId: identity.hostChatId, + persistenceChatId: identity.chatId, + }), + loadedStates: [ + GRAPH_LOAD_STATES.LOADED, + GRAPH_LOAD_STATES.EMPTY_CONFIRMED, + ], + allowNoChatState, + noChatState: GRAPH_LOAD_STATES.NO_CHAT, + }); } function repairRuntimeGraphIdentityFromPersistence( @@ -4522,55 +4432,46 @@ function repairRuntimeGraphIdentityFromPersistence( } const graphOwnedChatId = getGraphOwnedChatId(graph); - if (graphOwnedChatId) { - return { repaired: false, reason: "graph-identity-present", chatId: graphOwnedChatId }; - } - const stateChatId = normalizeChatIdCandidate(graphPersistenceState.chatId); - if (!stateChatId) { - return { repaired: false, reason: "missing-persistence-chat-id" }; - } - const identity = resolveCurrentChatIdentity(context); - const liveChatId = normalizeChatIdCandidate(identity.chatId); - if ( - liveChatId && - !areChatIdsEquivalentForResolvedIdentity(stateChatId, liveChatId, identity) && - !areChatIdsEquivalentForResolvedIdentity(liveChatId, stateChatId, identity) - ) { - return { - repaired: false, - reason: "live-chat-mismatch", - chatId: stateChatId, - liveChatId, - }; - } - const markerChatId = normalizeChatIdCandidate(graphPersistenceState.commitMarker?.chatId); - if (markerChatId && markerChatId !== stateChatId) { + const repairPlan = planRuntimeGraphIdentityRepairCore({ + graph, + graphOwnedChatId, + stateChatId, + activeIdentity: identity, + markerChatId, + aliasCandidates: getGraphIdentityAliasCandidates({ + integrity: identity.integrity, + hostChatId: identity.hostChatId, + persistenceChatId: identity.chatId, + }), + }); + if (!repairPlan.shouldRepair) { return { repaired: false, - reason: "commit-marker-chat-mismatch", - chatId: stateChatId, - markerChatId, + reason: repairPlan.reason, + chatId: repairPlan.chatId, + liveChatId: repairPlan.liveChatId, + markerChatId: repairPlan.markerChatId, }; } - graph.historyState.chatId = stateChatId; + graph.historyState.chatId = repairPlan.chatId; stampGraphPersistenceMeta(graph, { revision: graphPersistenceState.revision || graph?.meta?.revision || graph?.revision || 0, reason: String(reason || operationLabel || "runtime-graph-identity-repair"), - chatId: stateChatId, + chatId: repairPlan.chatId, integrity: normalizeChatIdCandidate(graphPersistenceState.commitMarker?.integrity) || getChatMetadataIntegrity(context), }); debugDebug("[ST-BME] 已补齐运行时图谱聊天身份", { operationLabel, - chatId: stateChatId, + chatId: repairPlan.chatId, reason, }); - return { repaired: true, reason: "repaired", chatId: stateChatId }; + return { repaired: true, reason: "repaired", chatId: repairPlan.chatId }; } function isGraphReadableForRecall( diff --git a/package.json b/package.json index 29550d0..3ae975a 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "test:runtime-history": "node tests/runtime-history.mjs", "test:graph-persistence": "node tests/graph-persistence.mjs", "test:rebirth-phase0": "node tests/rebirth-phase0.mjs", + "test:identity-resolver": "node tests/identity-resolver.mjs", "test:hide-engine": "node tests/hide-engine.mjs", "test:maintenance-journal": "node tests/maintenance-journal.mjs", "test:indexeddb-persistence": "node tests/indexeddb-persistence.mjs", diff --git a/runtime/identity-resolver.js b/runtime/identity-resolver.js new file mode 100644 index 0000000..0cb1e4a --- /dev/null +++ b/runtime/identity-resolver.js @@ -0,0 +1,333 @@ +// ST-BME identity resolver core. +// +// Phase 1 keeps this module pure: callers provide context-like objects, +// graph-owned metadata, alias callbacks, and persistence state snapshots. +// The module separates active identity, graph-owner identity, queued/runtime +// fallback identity, marker identity, and equivalence checks so later phases +// can stop promoting recovery evidence into the active chat identity. + +export function normalizeIdentityValue(value = "") { + return String(value ?? "").trim(); +} + +export function hasLikelySelectedChatContextCore(context = null) { + if (!context || typeof context !== "object") return false; + const metadata = context.chatMetadata; + const hasMeaningfulChatMetadata = Boolean( + metadata && + typeof metadata === "object" && + Object.keys(metadata).some((key) => metadata[key] != null && metadata[key] !== ""), + ); + const hasChatMessages = Array.isArray(context.chat) && context.chat.length > 0; + const hasCharacterId = + context.characterId !== undefined && + context.characterId !== null && + String(context.characterId).trim() !== ""; + const hasGroupId = + context.groupId !== undefined && + context.groupId !== null && + String(context.groupId).trim() !== ""; + return hasMeaningfulChatMetadata || hasChatMessages || hasCharacterId || hasGroupId; +} + +export function resolveActiveHostChatIdCore({ + context = null, + readGlobalCurrentChatId = null, +} = {}) { + const candidates = [ + context?.chatId, + typeof context?.getCurrentChatId === "function" ? context.getCurrentChatId() : "", + typeof readGlobalCurrentChatId === "function" ? readGlobalCurrentChatId() : "", + context?.chatMetadata?.chat_id, + context?.chatMetadata?.chatId, + context?.chatMetadata?.session_id, + context?.chatMetadata?.sessionId, + ]; + + return candidates.map((candidate) => normalizeIdentityValue(candidate)).find(Boolean) || ""; +} + +export function getContextIntegrityCore(context = null) { + return normalizeIdentityValue(context?.chatMetadata?.integrity); +} + +export function resolveActiveChatIdentityCore({ + context = null, + hostChatId = "", + integrity = "", + resolveAliasByHostChatId = null, + hasLikelySelectedChat = null, +} = {}) { + const normalizedHostChatId = normalizeIdentityValue(hostChatId); + const normalizedIntegrity = normalizeIdentityValue(integrity); + const aliasedChatId = + !normalizedIntegrity && + normalizedHostChatId && + typeof resolveAliasByHostChatId === "function" + ? normalizeIdentityValue(resolveAliasByHostChatId(normalizedHostChatId)) + : ""; + const chatId = normalizedIntegrity || aliasedChatId || normalizedHostChatId; + const hasLikely = + typeof hasLikelySelectedChat === "function" + ? hasLikelySelectedChat(context) + : hasLikelySelectedChatContextCore(context); + + return { + chatId, + hostChatId: normalizedHostChatId, + integrity: normalizedIntegrity, + identitySource: normalizedIntegrity + ? "integrity" + : aliasedChatId + ? "alias" + : normalizedHostChatId + ? "host-chat-id" + : "", + hasLikelySelectedChat: hasLikely, + }; +} + +export function resolveCurrentChatIdentityCore({ + context = null, + readGlobalCurrentChatId = null, + resolveAliasByHostChatId = null, + resolveIntegrity = null, + hasLikelySelectedChat = null, +} = {}) { + const hostChatId = resolveActiveHostChatIdCore({ context, readGlobalCurrentChatId }); + const integrity = + typeof resolveIntegrity === "function" + ? normalizeIdentityValue(resolveIntegrity(context)) + : getContextIntegrityCore(context) || + normalizeIdentityValue( + context?.chatMetadata?.chat_id || context?.chatMetadata?.chatId || "", + ); + return resolveActiveChatIdentityCore({ + context, + hostChatId, + integrity, + resolveAliasByHostChatId, + hasLikelySelectedChat, + }); +} + +export function resolveGraphOwnerIdentityCore({ graph = null, graphMeta = null } = {}) { + const ownedCandidates = [graph?.historyState?.chatId, graphMeta?.chatId]; + const chatId = ownedCandidates.map((candidate) => normalizeIdentityValue(candidate)).find(Boolean) || ""; + return { + chatId, + source: normalizeIdentityValue(graph?.historyState?.chatId) + ? "history-state" + : normalizeIdentityValue(graphMeta?.chatId) + ? "graph-meta" + : "", + integrity: normalizeIdentityValue(graphMeta?.integrity), + }; +} + +export function resolveRuntimeGraphFallbackIdentityCore({ + graph = null, + graphMeta = null, + persistenceState = null, +} = {}) { + const fallbackCandidates = [ + graph?.historyState?.chatId, + graphMeta?.chatId, + persistenceState?.chatId, + persistenceState?.queuedPersistChatId, + persistenceState?.commitMarker?.chatId, + ]; + const chatId = fallbackCandidates.map((candidate) => normalizeIdentityValue(candidate)).find(Boolean) || ""; + return { + chatId, + source: chatId ? "runtime-fallback" : "", + }; +} + +export function resolvePersistenceChatIdCore({ + explicitChatId = "", + activeIdentity = null, + graph = null, + graphMeta = null, + currentGraph = null, + currentGraphMeta = null, + persistenceState = null, + context = null, +} = {}) { + const directChatId = normalizeIdentityValue(explicitChatId); + if (directChatId) return directChatId; + + const resolvedChatId = normalizeIdentityValue(activeIdentity?.chatId); + if (resolvedChatId) return resolvedChatId; + + const fallbackCandidates = [ + graph?.historyState?.chatId, + graphMeta?.chatId, + currentGraph?.historyState?.chatId, + currentGraphMeta?.chatId, + persistenceState?.chatId, + persistenceState?.queuedPersistChatId, + persistenceState?.commitMarker?.chatId, + context?.chatMetadata?.integrity, + context?.chatMetadata?.chat_id, + context?.chatMetadata?.chatId, + context?.chatMetadata?.session_id, + context?.chatMetadata?.sessionId, + ]; + + return fallbackCandidates.map((candidate) => normalizeIdentityValue(candidate)).find(Boolean) || ""; +} + +export function getKnownChatIdsForIdentityCore({ identity = null, aliasCandidates = [] } = {}) { + const knownChatIds = new Set(); + const addKnownChatId = (value) => { + const normalized = normalizeIdentityValue(value); + if (normalized) knownChatIds.add(normalized); + }; + addKnownChatId(identity?.chatId); + addKnownChatId(identity?.hostChatId); + addKnownChatId(identity?.integrity); + for (const aliasCandidate of Array.isArray(aliasCandidates) ? aliasCandidates : []) { + addKnownChatId(aliasCandidate); + } + return knownChatIds; +} + +export function doesChatIdMatchIdentityCore(candidateChatId, { identity = null, aliasCandidates = [] } = {}) { + const normalizedCandidate = normalizeIdentityValue(candidateChatId); + if (!normalizedCandidate || !identity || typeof identity !== "object") return false; + return getKnownChatIdsForIdentityCore({ identity, aliasCandidates }).has(normalizedCandidate); +} + +export function areChatIdsEquivalentForIdentityCore( + candidateChatId, + referenceChatId, + { identity = null, aliasCandidates = [] } = {}, +) { + const normalizedCandidate = normalizeIdentityValue(candidateChatId); + const normalizedReference = normalizeIdentityValue(referenceChatId); + if (!normalizedCandidate || !normalizedReference) { + return normalizedCandidate === normalizedReference; + } + if (normalizedCandidate === normalizedReference) return true; + return ( + doesChatIdMatchIdentityCore(normalizedCandidate, { identity, aliasCandidates }) && + doesChatIdMatchIdentityCore(normalizedReference, { identity, aliasCandidates }) + ); +} + +export function canMutateRuntimeGraphForIdentityCore({ + graph = null, + activeIdentity = null, + graphOwnedChatId = "", + persistenceState = null, + aliasCandidates = [], + loadedStates = ["loaded", "empty-confirmed"], + allowNoChatState = false, + noChatState = "no-chat", +} = {}) { + if ( + !graph || + typeof graph !== "object" || + !graph.historyState || + typeof graph.historyState !== "object" || + Array.isArray(graph.historyState) + ) { + return false; + } + + const ownedChatId = normalizeIdentityValue(graphOwnedChatId); + if (!ownedChatId) return false; + + const liveChatId = normalizeIdentityValue(activeIdentity?.chatId); + if (liveChatId) { + return ( + areChatIdsEquivalentForIdentityCore(ownedChatId, liveChatId, { + identity: activeIdentity, + aliasCandidates, + }) || + areChatIdsEquivalentForIdentityCore(liveChatId, ownedChatId, { + identity: activeIdentity, + aliasCandidates, + }) + ); + } + + const stateChatId = normalizeIdentityValue(persistenceState?.chatId); + if (!stateChatId || stateChatId !== ownedChatId) return false; + + const markerChatId = normalizeIdentityValue(persistenceState?.commitMarker?.chatId); + if (markerChatId && markerChatId !== ownedChatId) return false; + + const loadState = String(persistenceState?.loadState || ""); + if ( + loadedStates.includes(loadState) || + persistenceState?.dbReady === true + ) { + return true; + } + + return allowNoChatState === true && loadState === noChatState; +} + +export function planRuntimeGraphIdentityRepairCore({ + graph = null, + graphOwnedChatId = "", + stateChatId = "", + activeIdentity = null, + markerChatId = "", + aliasCandidates = [], +} = {}) { + if ( + !graph || + typeof graph !== "object" || + Array.isArray(graph) || + !graph.historyState || + typeof graph.historyState !== "object" || + Array.isArray(graph.historyState) + ) { + return { shouldRepair: false, reason: "missing-runtime-graph" }; + } + + const ownedChatId = normalizeIdentityValue(graphOwnedChatId); + if (ownedChatId) { + return { shouldRepair: false, reason: "graph-identity-present", chatId: ownedChatId }; + } + + const normalizedStateChatId = normalizeIdentityValue(stateChatId); + if (!normalizedStateChatId) { + return { shouldRepair: false, reason: "missing-persistence-chat-id" }; + } + + const liveChatId = normalizeIdentityValue(activeIdentity?.chatId); + if ( + liveChatId && + !areChatIdsEquivalentForIdentityCore(normalizedStateChatId, liveChatId, { + identity: activeIdentity, + aliasCandidates, + }) && + !areChatIdsEquivalentForIdentityCore(liveChatId, normalizedStateChatId, { + identity: activeIdentity, + aliasCandidates, + }) + ) { + return { + shouldRepair: false, + reason: "live-chat-mismatch", + chatId: normalizedStateChatId, + liveChatId, + }; + } + + const normalizedMarkerChatId = normalizeIdentityValue(markerChatId); + if (normalizedMarkerChatId && normalizedMarkerChatId !== normalizedStateChatId) { + return { + shouldRepair: false, + reason: "commit-marker-chat-mismatch", + chatId: normalizedStateChatId, + markerChatId: normalizedMarkerChatId, + }; + } + + return { shouldRepair: true, reason: "repair", chatId: normalizedStateChatId }; +} diff --git a/tests/graph-persistence.mjs b/tests/graph-persistence.mjs index c51da58..bb9bf1d 100644 --- a/tests/graph-persistence.mjs +++ b/tests/graph-persistence.mjs @@ -101,6 +101,17 @@ import { getPersistedSettingsSnapshot, mergePersistedSettings, } from "../runtime/settings-defaults.js"; +import { + areChatIdsEquivalentForIdentityCore, + canMutateRuntimeGraphForIdentityCore, + doesChatIdMatchIdentityCore, + planRuntimeGraphIdentityRepairCore, + resolveActiveHostChatIdCore, + resolveCurrentChatIdentityCore, + resolveGraphOwnerIdentityCore, + resolvePersistenceChatIdCore, + resolveRuntimeGraphFallbackIdentityCore, +} from "../runtime/identity-resolver.js"; import { createDefaultAuthorityCapabilityState, normalizeAuthoritySettings, @@ -882,7 +893,9 @@ async function createGraphPersistenceHarness({ return serializeBmeChatStateTarget(target); }, readPersistedRecallFromUserMessage, + areChatIdsEquivalentForIdentityCore, cloneGraphForPersistence, + canMutateRuntimeGraphForIdentityCore, buildGraphCommitMarker, buildGraphChatStateSnapshot, buildLukerGraphCheckpointV2, @@ -892,6 +905,7 @@ async function createGraphPersistenceHarness({ canUseGraphChatState, cloneRuntimeDebugValue, deleteGraphChatStateNamespace, + doesChatIdMatchIdentityCore, detectIndexedDbSnapshotCommitMarkerMismatch, onMessageReceivedController, GRAPH_CHAT_STATE_NAMESPACE, @@ -916,6 +930,7 @@ async function createGraphPersistenceHarness({ GRAPH_SHADOW_SNAPSHOT_STORAGE_PREFIX, GRAPH_STARTUP_RECONCILE_DELAYS_MS, MODULE_NAME, + planRuntimeGraphIdentityRepairCore, findGraphShadowSnapshotByIntegrity, normalizeGraphCommitMarker, readGraphChatStateNamespaces, @@ -925,6 +940,11 @@ async function createGraphPersistenceHarness({ readGraphShadowSnapshot, rememberGraphIdentityAlias, removeGraphShadowSnapshot, + resolveActiveHostChatIdCore, + resolveCurrentChatIdentityCore, + resolveGraphOwnerIdentityCore, + resolvePersistenceChatIdCore, + resolveRuntimeGraphFallbackIdentityCore, resolveGraphIdentityAliasByHostChatId, shouldPreferShadowSnapshotOverOfficial, stampGraphPersistenceMeta, diff --git a/tests/identity-resolver.mjs b/tests/identity-resolver.mjs new file mode 100644 index 0000000..8742a93 --- /dev/null +++ b/tests/identity-resolver.mjs @@ -0,0 +1,198 @@ +// ST-BME restrained rebirth — Phase 1 identity resolver characterization. + +import assert from "node:assert/strict"; +import { + areChatIdsEquivalentForIdentityCore, + canMutateRuntimeGraphForIdentityCore, + doesChatIdMatchIdentityCore, + planRuntimeGraphIdentityRepairCore, + resolveActiveHostChatIdCore, + resolveCurrentChatIdentityCore, + resolveGraphOwnerIdentityCore, + resolvePersistenceChatIdCore, + resolveRuntimeGraphFallbackIdentityCore, +} from "../runtime/identity-resolver.js"; + +const context = { + chatId: "host-chat", + chatMetadata: { + integrity: "integrity-chat", + }, + chat: [{ mes: "hello" }], +}; + +assert.equal(resolveActiveHostChatIdCore({ context }), "host-chat"); + +const activeIdentity = resolveCurrentChatIdentityCore({ + context, + resolveAliasByHostChatId: () => "alias-chat", +}); +assert.deepEqual(activeIdentity, { + chatId: "integrity-chat", + hostChatId: "host-chat", + integrity: "integrity-chat", + identitySource: "integrity", + hasLikelySelectedChat: true, +}); + +const aliasIdentity = resolveCurrentChatIdentityCore({ + context: { chatId: "host-only", chatMetadata: {}, characterId: "1" }, + resolveAliasByHostChatId: () => "persisted-by-alias", +}); +assert.equal(aliasIdentity.chatId, "persisted-by-alias"); +assert.equal(aliasIdentity.identitySource, "alias"); + +console.log(" ✓ active identity is resolved from context and aliases only"); + +const graph = { historyState: { chatId: "graph-chat" } }; +const graphMeta = { chatId: "meta-chat", integrity: "meta-integrity" }; +assert.deepEqual(resolveGraphOwnerIdentityCore({ graph, graphMeta }), { + chatId: "graph-chat", + source: "history-state", + integrity: "meta-integrity", +}); + +assert.deepEqual( + resolveRuntimeGraphFallbackIdentityCore({ + graph: { historyState: {} }, + graphMeta: {}, + persistenceState: { + chatId: "state-chat", + queuedPersistChatId: "queued-chat", + commitMarker: { chatId: "marker-chat" }, + }, + }), + { chatId: "state-chat", source: "runtime-fallback" }, +); + +assert.equal( + resolvePersistenceChatIdCore({ + explicitChatId: "", + activeIdentity: { chatId: "" }, + graph: { historyState: { chatId: "graph-owned" } }, + graphMeta: {}, + persistenceState: { chatId: "state-chat" }, + }), + "graph-owned", +); + +console.log(" ✓ graph-owner and runtime fallback identities stay separate"); + +const identity = { + chatId: "integrity-chat", + hostChatId: "host-chat", + integrity: "integrity-chat", +}; +const aliasCandidates = ["alias-chat", "old-host-chat"]; +assert.equal(doesChatIdMatchIdentityCore("old-host-chat", { identity, aliasCandidates }), true); +assert.equal(doesChatIdMatchIdentityCore("other-chat", { identity, aliasCandidates }), false); +assert.equal( + areChatIdsEquivalentForIdentityCore("host-chat", "old-host-chat", { + identity, + aliasCandidates, + }), + true, +); + +console.log(" ✓ equivalence uses explicit identity evidence and aliases"); + +assert.equal( + canMutateRuntimeGraphForIdentityCore({ + graph: { historyState: { chatId: "integrity-chat" } }, + activeIdentity: identity, + graphOwnedChatId: "integrity-chat", + persistenceState: { loadState: "loaded" }, + }), + true, +); + +assert.equal( + canMutateRuntimeGraphForIdentityCore({ + graph: { historyState: { chatId: "graph-chat" } }, + activeIdentity: { chatId: "" }, + graphOwnedChatId: "graph-chat", + persistenceState: { + chatId: "graph-chat", + commitMarker: { chatId: "other-chat" }, + loadState: "no-chat", + dbReady: false, + }, + allowNoChatState: true, + }), + false, + "wrong-chat commit marker must block no-chat mutation fallback", +); + +assert.equal( + canMutateRuntimeGraphForIdentityCore({ + graph: { historyState: { chatId: "graph-chat" } }, + activeIdentity: { chatId: "" }, + graphOwnedChatId: "graph-chat", + persistenceState: { + chatId: "graph-chat", + commitMarker: { chatId: "graph-chat" }, + loadState: "no-chat", + dbReady: false, + }, + allowNoChatState: true, + }), + true, +); + +console.log(" ✓ runtime mutation fallback preserves no-chat safety checks"); + +assert.deepEqual( + planRuntimeGraphIdentityRepairCore({ + graph: { historyState: {} }, + graphOwnedChatId: "", + stateChatId: "state-chat", + activeIdentity: { chatId: "" }, + markerChatId: "state-chat", + }), + { shouldRepair: true, reason: "repair", chatId: "state-chat" }, +); + +assert.equal( + planRuntimeGraphIdentityRepairCore({ + graph: { historyState: {} }, + graphOwnedChatId: "", + stateChatId: "state-chat", + activeIdentity: { chatId: "live-chat" }, + markerChatId: "state-chat", + }).reason, + "live-chat-mismatch", +); + +assert.equal( + planRuntimeGraphIdentityRepairCore({ + graph: { historyState: {} }, + graphOwnedChatId: "", + stateChatId: "state-chat", + activeIdentity: { chatId: "" }, + markerChatId: "other-chat", + }).reason, + "commit-marker-chat-mismatch", +); + +assert.deepEqual( + planRuntimeGraphIdentityRepairCore({ + graph: { historyState: { chatId: "already-owned" } }, + graphOwnedChatId: "already-owned", + stateChatId: "state-chat", + activeIdentity: { chatId: "" }, + }), + { shouldRepair: false, reason: "graph-identity-present", chatId: "already-owned" }, +); + +assert.deepEqual( + planRuntimeGraphIdentityRepairCore({ + graph: { historyState: {} }, + graphOwnedChatId: "", + stateChatId: "", + activeIdentity: { chatId: "" }, + }), + { shouldRepair: false, reason: "missing-persistence-chat-id" }, +); + +console.log(" ✓ graph identity repair is planned only with non-conflicting evidence"); +console.log("identity-resolver tests passed");