refactor(rebirth): extract identity resolver core

This commit is contained in:
youzini
2026-05-30 13:38:01 +00:00
parent 4e04b07306
commit 10ec3b7b77
5 changed files with 641 additions and 188 deletions

277
index.js
View File

@@ -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(

View File

@@ -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",

View File

@@ -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 };
}

View File

@@ -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,

198
tests/identity-resolver.mjs Normal file
View File

@@ -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");