Cover content evidence after conflict-base compaction

This commit is contained in:
vorotamoroz committed 2026-10-17 13:49:11 +00:00
1 parent f47db4f256
commit bf8e899bf7
2 files changed
+44 -5

No files matched your search

@@ -13,6 +13,7 @@ import {
type ServiceDatabaseFileAccessDependencies,
} from "@vrtmrz/livesync-commonlib/compat/serviceModules/ServiceDatabaseFileAccessBase";
import type { ServiceFileHandlerDependencies } from "@vrtmrz/livesync-commonlib/compat/serviceModules/ServiceFileHandlerBase";
import type { FileReflectionProvenanceRecord } from "@vrtmrz/livesync-commonlib/compat/interfaces/FileReflectionProvenance";
import { ServiceFileHandler } from "./FileHandler";
import {
createConflictResolutionOperations,
@@ -38,7 +39,7 @@ function makeFile(body: string, mtime = oldTime): UXFileInfo {
async function makeDevice(name: string) {
const db = new PouchDB<EntryDoc>(name, { adapter: "memory" });
const reflection = new Map<FilePathWithPrefix, { revision: string; observedStorageMtime?: number }>();
const reflection = new Map<FilePathWithPrefix, FileReflectionProvenanceRecord>();
let storage = makeFile(old);
const settings = { ...DEFAULT_SETTINGS, useOnlyLocalChunk: true, writeDocumentsIfConflicted: false };
const setting = { currentSettings: () => settings };
@@ -94,7 +95,7 @@ async function makeDevice(name: string) {
conflict,
fileReflectionProvenance: {
get: (value: FilePathWithPrefix) => Promise.resolve(reflection.get(value)),
set: (value: FilePathWithPrefix, record: { revision: string }) => {
set: (value: FilePathWithPrefix, record: FileReflectionProvenanceRecord) => {
reflection.set(value, record);
return Promise.resolve();
},
@@ -287,7 +288,32 @@ describe("file provenance across multiple devices and real CouchDB", () => {
}
}, 60_000);
it.each(["missing record", "compacted base"] as const)(
it("reflects the selected content after compacting an unchanged losing branch", async () => {
const f = await conflictedDevices(4);
const { keep, content } = await resolveAndReplicate(f);
const index = f.revisions.findIndex((rev, i) => i > 0 && rev !== keep);
const device = f.devices[index];
const record = device.reflection.get(path);
expect(record?.revision).toBe(f.revisions[index]);
expect(record?.contentHash).toMatch(/^sha256:[0-9a-f]{64}$/u);
expect(record?.contentSize).toBe(device.getStorage().body.size);
await device.db.compact();
await expect(device.db.get(path, { rev: f.revisions[index] })).rejects.toMatchObject({ status: 404 });
const before = (await device.db.info()).update_seq;
await expect(device.handler.storeFileToDB(path)).resolves.toBe(true);
expect((await device.db.info()).update_seq).toBe(before);
expect(await device.getStorage().body.text()).toBe(content);
expect(device.reflection.get(path)?.revision).toBe(keep);
await device.db.replicate.to(f.remote);
for (const peer of f.devices) {
await peer.db.replicate.from(f.remote);
expect((await leaves(peer.db)).map((doc) => doc._rev)).toEqual([keep]);
expect(await revisionContent(peer, keep)).toBe(content);
}
}, 60_000);
it.each(["missing record", "compacted legacy base", "compacted hashed base with local edits"] as const)(
"preserves uncertain storage as one independent conflict with four devices: %s",
async (reason) => {
const f = await conflictedDevices(4);
@@ -301,6 +327,17 @@ describe("file provenance across multiple devices and real CouchDB", () => {
} else {
await device.db.compact();
await expect(device.db.get(path, { rev: f.revisions[index] })).rejects.toMatchObject({ status: 404 });
if (reason === "compacted legacy base") {
// Revision-only records still require protection when their recorded body is unavailable.
const { revision, observedStorageMtime } = device.reflection.get(path)!;
device.reflection.set(path, { revision, observedStorageMtime });
} else {
expect(device.reflection.get(path)?.contentHash).toMatch(/^sha256:/u);
const original = device.getStorage();
const edited = (await original.body.text()).replace("Edited", "EDITED");
device.setStorage(makeFile(edited, original.stat.mtime));
expect(device.getStorage().body.size).toBe(original.body.size);
}
}
const uncertainContent = await device.getStorage().body.text();
await expect(device.handler.storeFileToDB(path)).resolves.toBe(true);