mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-08-27 05:47:07 +00:00
Protect conflict chunks during garbage collection
This commit is contained in:
@@ -24,7 +24,12 @@ vi.mock("@/common/events", () => ({
|
||||
onEvent: vi.fn(),
|
||||
},
|
||||
}));
|
||||
import { DEFAULT_SETTINGS, REMOTE_COUCHDB, REMOTE_MINIO } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import {
|
||||
DEFAULT_SETTINGS,
|
||||
REMOTE_COUCHDB,
|
||||
REMOTE_MINIO,
|
||||
REMOTE_P2P,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { LocalDatabaseMaintenance } from "./CmdLocalDatabaseMainte";
|
||||
import { ensureLocalDatabaseMaintenancePrerequisites } from "./maintenancePrerequisites";
|
||||
|
||||
@@ -87,6 +92,9 @@ describe("LocalDatabaseMaintenance prerequisites", () => {
|
||||
settings.useEdgeCaseMode = true;
|
||||
expect(garbageCollect?.checkCallback?.(true)).toBe(true);
|
||||
|
||||
settings.remoteType = REMOTE_P2P;
|
||||
expect(garbageCollect?.checkCallback?.(true)).toBe(false);
|
||||
|
||||
settings.remoteType = REMOTE_MINIO;
|
||||
expect(garbageCollect?.checkCallback?.(true)).toBe(false);
|
||||
});
|
||||
@@ -176,4 +184,129 @@ describe("LocalDatabaseMaintenance prerequisites", () => {
|
||||
expect(askSelectStringDialogue).not.toHaveBeenCalled();
|
||||
expect(applyPartial).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("describes the current chunk-recreation action without promising historical recovery", async () => {
|
||||
const maintenance = Object.create(LocalDatabaseMaintenance.prototype) as LocalDatabaseMaintenance;
|
||||
const askSelectStringDialogue = vi.fn().mockResolvedValue("Cancel");
|
||||
Object.assign(maintenance, {
|
||||
core: {
|
||||
confirm: {
|
||||
askSelectStringDialogue,
|
||||
},
|
||||
},
|
||||
_log: vi.fn(),
|
||||
});
|
||||
vi.spyOn(maintenance, "ensureAvailable").mockResolvedValue(true);
|
||||
vi.spyOn(maintenance, "trackChanges").mockResolvedValue(undefined);
|
||||
|
||||
await maintenance.performGC();
|
||||
|
||||
const message = vi.mocked(askSelectStringDialogue).mock.calls[0]?.[0] as string;
|
||||
expect(message).toContain("Hatch -> Recreate chunks for current Vault files");
|
||||
expect(message).toContain("only from files currently present in the Vault");
|
||||
expect(message).not.toContain("Recreate missing chunks for all files");
|
||||
});
|
||||
});
|
||||
|
||||
describe("LocalDatabaseMaintenance Garbage Collection V3", () => {
|
||||
it("keeps chunks referenced by a live conflict revision and deletes only unreachable chunks", async () => {
|
||||
const maintenance = Object.create(LocalDatabaseMaintenance.prototype) as LocalDatabaseMaintenance;
|
||||
const pushModes: string[] = [];
|
||||
const deletedChunks: Array<{ _id: string; _rev?: string; _deleted?: boolean }> = [];
|
||||
const allChunks = vi.fn(async () => ({
|
||||
used: new Set(["h:winner", "h:conflict"]),
|
||||
existing: new Map([
|
||||
["h:winner", { _id: "h:winner", _rev: "1-winner", type: "leaf", data: "winner" }],
|
||||
["h:conflict", { _id: "h:conflict", _rev: "1-conflict", type: "leaf", data: "conflict" }],
|
||||
["h:obsolete", { _id: "h:obsolete", _rev: "1-obsolete", type: "leaf", data: "obsolete" }],
|
||||
]),
|
||||
}));
|
||||
const rawDocuments = new Map<string, object>([
|
||||
[
|
||||
"note.md",
|
||||
{
|
||||
_id: "note.md",
|
||||
_rev: "2-winner",
|
||||
_conflicts: ["2-conflict"],
|
||||
type: "plain",
|
||||
children: ["h:winner"],
|
||||
},
|
||||
],
|
||||
["h:winner", { _id: "h:winner", _rev: "1-winner", type: "leaf", data: "winner" }],
|
||||
["h:conflict", { _id: "h:conflict", _rev: "1-conflict", type: "leaf", data: "conflict" }],
|
||||
["h:obsolete", { _id: "h:obsolete", _rev: "1-obsolete", type: "leaf", data: "obsolete" }],
|
||||
]);
|
||||
const findEntryNames = vi.fn(async function* () {
|
||||
yield* rawDocuments.keys();
|
||||
});
|
||||
const getRaw = vi.fn(async (id: string) => rawDocuments.get(id));
|
||||
const localDatabase = {
|
||||
allChunks,
|
||||
localDatabase: {
|
||||
info: vi.fn(async () => ({ doc_count: rawDocuments.size })),
|
||||
bulkDocs: vi.fn(async (docs: Array<{ _id: string; _rev?: string; _deleted?: boolean }>) => {
|
||||
deletedChunks.push(...docs);
|
||||
return docs.map(({ _id }) => ({ ok: true, id: _id, rev: "2-deleted" }));
|
||||
}),
|
||||
},
|
||||
findEntryNames,
|
||||
getRaw,
|
||||
};
|
||||
const replicator = {
|
||||
openOneShotReplication: vi.fn(
|
||||
async (
|
||||
_settings: typeof DEFAULT_SETTINGS,
|
||||
_showResult: boolean,
|
||||
_ignoreCleanLock: boolean,
|
||||
mode: string
|
||||
) => {
|
||||
pushModes.push(mode);
|
||||
return true;
|
||||
}
|
||||
),
|
||||
getConnectedDeviceList: vi.fn(async () => ({
|
||||
accepted_nodes: ["device-a"],
|
||||
node_info: {
|
||||
"device-a": {
|
||||
progress: "10-local",
|
||||
device_name: "Device A",
|
||||
app_version: "1.12.7",
|
||||
plugin_version: "1.0.0-beta.0",
|
||||
},
|
||||
},
|
||||
})),
|
||||
};
|
||||
Object.assign(maintenance, {
|
||||
core: {
|
||||
settings: {
|
||||
...DEFAULT_SETTINGS,
|
||||
remoteType: REMOTE_COUCHDB,
|
||||
},
|
||||
replicator,
|
||||
confirm: {
|
||||
askSelectStringDialogue: vi.fn(async () => "Proceed Garbage Collection"),
|
||||
},
|
||||
},
|
||||
localDatabase,
|
||||
_notice: vi.fn(),
|
||||
});
|
||||
vi.spyOn(maintenance, "ensureAvailable").mockResolvedValue(true);
|
||||
vi.spyOn(maintenance, "compactDatabase").mockResolvedValue(undefined);
|
||||
vi.spyOn(maintenance, "clearHash").mockImplementation(() => undefined);
|
||||
|
||||
await maintenance.gcv3();
|
||||
|
||||
expect(allChunks).toHaveBeenCalledOnce();
|
||||
expect(findEntryNames).not.toHaveBeenCalled();
|
||||
expect(getRaw).not.toHaveBeenCalled();
|
||||
expect(deletedChunks).toEqual([
|
||||
{
|
||||
_id: "h:obsolete",
|
||||
_rev: "1-obsolete",
|
||||
_deleted: true,
|
||||
},
|
||||
]);
|
||||
expect(pushModes).toEqual(["sync", "pushOnly"]);
|
||||
expect(maintenance.compactDatabase).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user