Merge pull request #31 from Hao19911125/main

手动存储的大小修剪
This commit is contained in:
youzini
2026-04-12 16:16:35 +08:00
committed by GitHub
5 changed files with 302 additions and 12 deletions

View File

@@ -22,6 +22,7 @@ import {
syncNow,
upload,
} from "../sync/bme-sync.js";
import { MANUAL_BACKUP_BATCH_JOURNAL_COVERAGE_KEY } from "../runtime/runtime-state.js";
const PREFIX = "[ST-BME][indexeddb-sync]";
@@ -159,6 +160,7 @@ function createMockFetchEnvironment() {
remoteFiles.set(body.name, payload);
logs.uploadedPayloads.push({
name: body.name,
decoded,
payload,
});
return createJsonResponse(200, { path: `/user/files/${body.name}` });
@@ -605,7 +607,7 @@ async function testManualCloudModeGuards() {
}
async function testManualBackupAndRestoreFlow() {
const { fetch, remoteFiles } = createMockFetchEnvironment();
const { fetch, remoteFiles, logs } = createMockFetchEnvironment();
const dbByChatId = new Map();
const db = new FakeDb("chat-backup-flow", {
meta: {
@@ -617,6 +619,39 @@ async function testManualBackupAndRestoreFlow() {
nodeCount: 1,
edgeCount: 0,
tombstoneCount: 0,
runtimeHistoryState: {
chatId: "chat-backup-flow",
lastProcessedAssistantFloor: 4,
extractionCount: 2,
processedMessageHashVersion: 2,
processedMessageHashes: {
0: "hash-0",
1: "hash-1",
2: "hash-2",
3: "hash-3",
4: "hash-4",
},
processedMessageHashesNeedRefresh: false,
historyDirtyFrom: 2,
lastMutationReason: "hash-recheck",
lastMutationSource: "event:message-received",
lastRecoveryResult: {
status: "pending",
fromFloor: 2,
},
},
runtimeBatchJournal: [
{ id: "journal-1", processedRange: [0, 0], createdAt: 11 },
{ id: "journal-2", processedRange: [1, 1], createdAt: 22 },
{ id: "journal-3", processedRange: [2, 2], createdAt: 33 },
{ id: "journal-4", processedRange: [3, 3], createdAt: 44 },
{ id: "journal-5", processedRange: [4, 4], createdAt: 55 },
{ id: "journal-6", processedRange: [5, 5], createdAt: 66 },
],
maintenanceJournal: [
{ id: "maintenance-a", updatedAt: 70 },
{ id: "maintenance-b", updatedAt: 80 },
],
},
nodes: [{ id: "local-node", updatedAt: 80 }],
edges: [],
@@ -642,10 +677,50 @@ async function testManualBackupAndRestoreFlow() {
assert.equal(db.meta.get("syncDirty"), false);
assert.ok(Number(db.meta.get("lastBackupUploadedAt")) > 0);
assert.ok(String(db.meta.get("lastBackupFilename") || "").startsWith("ST-BME_backup_"));
const backupPayload = remoteFiles.get(backupResult.filename);
assert.ok(backupPayload, "manual backup should be written to remote files");
assert.equal(backupPayload.snapshot.meta.runtimeBatchJournal.length, 4);
assert.deepEqual(
backupPayload.snapshot.meta.runtimeBatchJournal.map((entry) => entry.id),
["journal-3", "journal-4", "journal-5", "journal-6"],
);
assert.equal(backupPayload.snapshot.meta.maintenanceJournal.length, 0);
assert.deepEqual(
backupPayload.snapshot.meta.runtimeHistoryState[MANUAL_BACKUP_BATCH_JOURNAL_COVERAGE_KEY],
{
truncated: true,
earliestRetainedFloor: 2,
retainedCount: 4,
},
);
assert.deepEqual(
backupPayload.snapshot.meta.runtimeHistoryState.processedMessageHashes,
{
0: "hash-0",
1: "hash-1",
2: "hash-2",
3: "hash-3",
4: "hash-4",
},
);
assert.equal(
backupPayload.snapshot.meta.runtimeHistoryState.processedMessageHashesNeedRefresh,
false,
);
const backupUploadLog = logs.uploadedPayloads.find(
(entry) => entry.name === backupResult.filename,
);
assert.ok(backupUploadLog);
assert.equal(backupUploadLog.decoded.includes("\n"), false);
const manifestResult = await listServerBackups(runtime);
assert.equal(manifestResult.entries.length, 1);
assert.equal(manifestResult.entries[0].chatId, "chat-backup-flow");
const manifestUploadLog = logs.uploadedPayloads.find(
(entry) => entry.name === "ST-BME_BackupManifest.json",
);
assert.ok(manifestUploadLog);
assert.equal(manifestUploadLog.decoded.includes("\n"), false);
db.snapshot = {
meta: {
@@ -670,6 +745,29 @@ async function testManualBackupAndRestoreFlow() {
const restoreResult = await restoreFromServer("chat-backup-flow", runtime);
assert.equal(restoreResult.restored, true);
assert.equal(db.snapshot.nodes[0].id, "local-node");
assert.equal(db.snapshot.meta.runtimeBatchJournal.length, 4);
assert.equal(db.snapshot.meta.maintenanceJournal.length, 0);
assert.deepEqual(
db.snapshot.meta.runtimeHistoryState[MANUAL_BACKUP_BATCH_JOURNAL_COVERAGE_KEY],
{
truncated: true,
earliestRetainedFloor: 2,
retainedCount: 4,
},
);
assert.deepEqual(db.snapshot.meta.runtimeHistoryState.processedMessageHashes, {});
assert.equal(
db.snapshot.meta.runtimeHistoryState.processedMessageHashesNeedRefresh,
true,
);
assert.equal(
db.snapshot.meta.runtimeHistoryState.lastProcessedAssistantFloor,
4,
);
assert.equal(db.snapshot.meta.runtimeHistoryState.historyDirtyFrom, null);
assert.equal(db.snapshot.meta.runtimeHistoryState.lastMutationReason, "");
assert.equal(db.snapshot.meta.runtimeHistoryState.lastMutationSource, "");
assert.equal(db.snapshot.meta.runtimeHistoryState.lastRecoveryResult, null);
assert.ok(Number(db.meta.get("lastBackupRestoredAt")) > 0);
const safetyStatus = await getRestoreSafetySnapshotStatus(
"chat-backup-flow",

View File

@@ -1,5 +1,6 @@
import assert from "node:assert/strict";
import {
MANUAL_BACKUP_BATCH_JOURNAL_COVERAGE_KEY,
appendBatchJournal,
clearHistoryDirty,
cloneGraphSnapshot,
@@ -264,6 +265,32 @@ assert.ok(recoveryPoint);
assert.equal(recoveryPoint.path, "reverse-journal");
assert.equal(recoveryPoint.affectedJournals[0].processedRange[1], 3);
const truncatedCoverageGraph = createEmptyGraph();
truncatedCoverageGraph.historyState.chatId = "chat-truncated-history-test";
truncatedCoverageGraph.historyState[MANUAL_BACKUP_BATCH_JOURNAL_COVERAGE_KEY] = {
truncated: true,
earliestRetainedFloor: 4,
retainedCount: 4,
};
truncatedCoverageGraph.batchJournal = [
{ id: "journal-4", journalVersion: 2, processedRange: [4, 4] },
{ id: "journal-5", journalVersion: 2, processedRange: [5, 5] },
{ id: "journal-6", journalVersion: 2, processedRange: [6, 6] },
{ id: "journal-7", journalVersion: 2, processedRange: [7, 7] },
];
assert.equal(
findJournalRecoveryPoint(truncatedCoverageGraph, 3),
null,
"dirty floor earlier than retained backup coverage should reject partial rollback",
);
const retainedCoverageRecoveryPoint = findJournalRecoveryPoint(
truncatedCoverageGraph,
5,
);
assert.ok(retainedCoverageRecoveryPoint);
assert.equal(retainedCoverageRecoveryPoint.path, "reverse-journal");
assert.equal(retainedCoverageRecoveryPoint.affectedJournals.length, 3);
rollbackBatch(graph, recoveryPoint.affectedJournals[0]);
assert.equal(graph.nodes.length, 0);
assert.equal(graph.historyState.lastProcessedAssistantFloor, -1);