mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
fix: backend vector state marked dirty on sync/restore import and query failure
- sync/bme-sync.js: conservatively clear backend hash mappings and mark vectorIndexState dirty before importing remote snapshots via download, merge, and cloud backup restore, preventing stale clean-looking state after cross-device sync or restore - vector/vector-index.js: mark backend vector state dirty on real backend query failures (HTTP/network) instead of silently returning empty results - regression: indexeddb-sync.mjs covers download/restore/merge import dirty marking; p0-regressions.mjs covers backend query failure dirtying
This commit is contained in:
@@ -332,6 +332,23 @@ async function testDownloadImport() {
|
||||
nodeCount: 1,
|
||||
edgeCount: 0,
|
||||
tombstoneCount: 0,
|
||||
runtimeVectorIndexState: {
|
||||
mode: "backend",
|
||||
collectionId: "st-bme::chat-download",
|
||||
source: "openai",
|
||||
hashToNodeId: {
|
||||
"hash-remote-node": "remote-node",
|
||||
},
|
||||
nodeToHash: {
|
||||
"remote-node": "hash-remote-node",
|
||||
},
|
||||
lastStats: {
|
||||
total: 1,
|
||||
indexed: 1,
|
||||
stale: 0,
|
||||
pending: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
nodes: [{ id: "remote-node", updatedAt: 400 }],
|
||||
edges: [],
|
||||
@@ -348,6 +365,17 @@ async function testDownloadImport() {
|
||||
assert.equal(result.downloaded, true);
|
||||
assert.equal(db.lastImportPayload.meta.revision, 12);
|
||||
assert.equal(db.lastImportPayload.nodes[0].id, "remote-node");
|
||||
assert.equal(db.lastImportPayload.meta.runtimeVectorIndexState.dirty, true);
|
||||
assert.equal(
|
||||
db.lastImportPayload.meta.runtimeVectorIndexState.dirtyReason,
|
||||
"backend-sync-download-unverified",
|
||||
);
|
||||
assert.deepEqual(db.lastImportPayload.meta.runtimeVectorIndexState.hashToNodeId, {});
|
||||
assert.deepEqual(db.lastImportPayload.meta.runtimeVectorIndexState.nodeToHash, {});
|
||||
assert.equal(
|
||||
db.lastImportPayload.meta.runtimeVectorIndexState.pendingRepairFromFloor,
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
async function testLegacyRemoteFilenameFallbackAndReuse() {
|
||||
@@ -648,6 +676,23 @@ async function testManualBackupAndRestoreFlow() {
|
||||
{ id: "journal-5", processedRange: [4, 4], createdAt: 55 },
|
||||
{ id: "journal-6", processedRange: [5, 5], createdAt: 66 },
|
||||
],
|
||||
runtimeVectorIndexState: {
|
||||
mode: "backend",
|
||||
collectionId: "st-bme::chat-backup-flow",
|
||||
source: "openai",
|
||||
hashToNodeId: {
|
||||
"hash-local-node": "local-node",
|
||||
},
|
||||
nodeToHash: {
|
||||
"local-node": "hash-local-node",
|
||||
},
|
||||
lastStats: {
|
||||
total: 1,
|
||||
indexed: 1,
|
||||
stale: 0,
|
||||
pending: 0,
|
||||
},
|
||||
},
|
||||
maintenanceJournal: [
|
||||
{ id: "maintenance-a", updatedAt: 70 },
|
||||
{ id: "maintenance-b", updatedAt: 80 },
|
||||
@@ -768,6 +813,13 @@ async function testManualBackupAndRestoreFlow() {
|
||||
assert.equal(db.snapshot.meta.runtimeHistoryState.lastMutationReason, "");
|
||||
assert.equal(db.snapshot.meta.runtimeHistoryState.lastMutationSource, "");
|
||||
assert.equal(db.snapshot.meta.runtimeHistoryState.lastRecoveryResult, null);
|
||||
assert.equal(db.snapshot.meta.runtimeVectorIndexState.dirty, true);
|
||||
assert.equal(
|
||||
db.snapshot.meta.runtimeVectorIndexState.dirtyReason,
|
||||
"backend-backup-restore-unverified",
|
||||
);
|
||||
assert.deepEqual(db.snapshot.meta.runtimeVectorIndexState.hashToNodeId, {});
|
||||
assert.deepEqual(db.snapshot.meta.runtimeVectorIndexState.nodeToHash, {});
|
||||
assert.ok(Number(db.meta.get("lastBackupRestoredAt")) > 0);
|
||||
const safetyStatus = await getRestoreSafetySnapshotStatus(
|
||||
"chat-backup-flow",
|
||||
@@ -1247,6 +1299,23 @@ async function testSyncAppliedHook() {
|
||||
nodeCount: 1,
|
||||
edgeCount: 0,
|
||||
tombstoneCount: 0,
|
||||
runtimeVectorIndexState: {
|
||||
mode: "backend",
|
||||
collectionId: "st-bme::chat-hook-merge",
|
||||
source: "openai",
|
||||
hashToNodeId: {
|
||||
"hash-local-merge": "local-merge",
|
||||
},
|
||||
nodeToHash: {
|
||||
"local-merge": "hash-local-merge",
|
||||
},
|
||||
lastStats: {
|
||||
total: 1,
|
||||
indexed: 1,
|
||||
stale: 0,
|
||||
pending: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
nodes: [{ id: "local-merge", updatedAt: 20 }],
|
||||
edges: [],
|
||||
@@ -1263,7 +1332,33 @@ async function testSyncAppliedHook() {
|
||||
state: { lastProcessedFloor: 2, extractionCount: 1 },
|
||||
});
|
||||
remoteFiles.set("ST-BME_sync_chat-hook-merge.json", {
|
||||
meta: { schemaVersion: 1, chatId: "chat-hook-merge", revision: 4, lastModified: 25, deviceId: "remote", nodeCount: 1, edgeCount: 0, tombstoneCount: 0 },
|
||||
meta: {
|
||||
schemaVersion: 1,
|
||||
chatId: "chat-hook-merge",
|
||||
revision: 4,
|
||||
lastModified: 25,
|
||||
deviceId: "remote",
|
||||
nodeCount: 1,
|
||||
edgeCount: 0,
|
||||
tombstoneCount: 0,
|
||||
runtimeVectorIndexState: {
|
||||
mode: "backend",
|
||||
collectionId: "st-bme::chat-hook-merge",
|
||||
source: "openai",
|
||||
hashToNodeId: {
|
||||
"hash-remote-merge": "remote-merge",
|
||||
},
|
||||
nodeToHash: {
|
||||
"remote-merge": "hash-remote-merge",
|
||||
},
|
||||
lastStats: {
|
||||
total: 1,
|
||||
indexed: 1,
|
||||
stale: 0,
|
||||
pending: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
nodes: [{ id: "remote-merge", updatedAt: 25 }],
|
||||
edges: [],
|
||||
tombstones: [],
|
||||
@@ -1284,6 +1379,22 @@ async function testSyncAppliedHook() {
|
||||
|
||||
assert.equal(downloadResult.revision, 3);
|
||||
assert.equal(mergeResult.revision, 5);
|
||||
assert.equal(
|
||||
dbByChatId.get("chat-hook-merge").lastImportPayload.meta.runtimeVectorIndexState.dirty,
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
dbByChatId.get("chat-hook-merge").lastImportPayload.meta.runtimeVectorIndexState.dirtyReason,
|
||||
"backend-sync-merge-unverified",
|
||||
);
|
||||
assert.deepEqual(
|
||||
dbByChatId.get("chat-hook-merge").lastImportPayload.meta.runtimeVectorIndexState.hashToNodeId,
|
||||
{},
|
||||
);
|
||||
assert.deepEqual(
|
||||
dbByChatId.get("chat-hook-merge").lastImportPayload.meta.runtimeVectorIndexState.nodeToHash,
|
||||
{},
|
||||
);
|
||||
|
||||
assert.deepEqual(hookCalls.map((item) => item.action), ["download", "merge"]);
|
||||
assert.deepEqual(hookCalls.map((item) => item.chatId), ["chat-hook-download", "chat-hook-merge"]);
|
||||
|
||||
@@ -155,7 +155,10 @@ const {
|
||||
removeNode,
|
||||
} = await import("../graph/graph.js");
|
||||
const { compressType } = await import("../maintenance/compressor.js");
|
||||
const { syncGraphVectorIndex } = await import("../vector/vector-index.js");
|
||||
const {
|
||||
findSimilarNodesByText,
|
||||
syncGraphVectorIndex,
|
||||
} = await import("../vector/vector-index.js");
|
||||
const {
|
||||
extractMemories,
|
||||
generateReflection,
|
||||
@@ -1984,6 +1987,55 @@ async function testVectorIndexKeepsDirtyOnDirectPartialEmbeddingFailure() {
|
||||
}
|
||||
}
|
||||
|
||||
async function testBackendVectorQueryFailureMarksStateDirty() {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const graph = normalizeGraphRuntimeState(createEmptyGraph(), "chat-backend-query");
|
||||
const node = makeEvent(1, "后端向量节点");
|
||||
addNode(graph, node);
|
||||
graph.vectorIndexState.mode = "backend";
|
||||
graph.vectorIndexState.source = "openai";
|
||||
graph.vectorIndexState.collectionId = "st-bme::chat-backend-query";
|
||||
graph.vectorIndexState.hashToNodeId = {
|
||||
"hash-backend-node": node.id,
|
||||
};
|
||||
graph.vectorIndexState.nodeToHash = {
|
||||
[node.id]: "hash-backend-node",
|
||||
};
|
||||
graph.vectorIndexState.lastStats = {
|
||||
total: 1,
|
||||
indexed: 1,
|
||||
stale: 0,
|
||||
pending: 0,
|
||||
};
|
||||
|
||||
globalThis.fetch = async () => {
|
||||
throw new Error("backend-down");
|
||||
};
|
||||
|
||||
try {
|
||||
await assert.rejects(
|
||||
findSimilarNodesByText(
|
||||
graph,
|
||||
"测试后端向量失败",
|
||||
{
|
||||
mode: "backend",
|
||||
source: "openai",
|
||||
model: "text-embedding-3-small",
|
||||
},
|
||||
5,
|
||||
[node],
|
||||
),
|
||||
/backend-down/,
|
||||
);
|
||||
assert.equal(graph.vectorIndexState.dirty, true);
|
||||
assert.equal(graph.vectorIndexState.dirtyReason, "backend-query-failed");
|
||||
assert.equal(graph.vectorIndexState.pendingRepairFromFloor, 0);
|
||||
assert.match(graph.vectorIndexState.lastWarning, /后端向量查询失败/);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
}
|
||||
|
||||
async function testCompressTypeAcceptsTopLevelFieldsResult() {
|
||||
const graph = createEmptyGraph();
|
||||
const typeDef = {
|
||||
@@ -6725,6 +6777,7 @@ async function testManualSleepExplainsThatItIsLocalOnlyWhenNothingChanges() {
|
||||
|
||||
await testCompressorMigratesEdgesToCompressedNode();
|
||||
await testVectorIndexKeepsDirtyOnDirectPartialEmbeddingFailure();
|
||||
await testBackendVectorQueryFailureMarksStateDirty();
|
||||
await testCompressTypeAcceptsTopLevelFieldsResult();
|
||||
await testExtractorFailsOnUnknownOperation();
|
||||
await testExtractorNormalizesFlatCreateOperation();
|
||||
|
||||
Reference in New Issue
Block a user