mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-21 16:52:32 +00:00
Align multi-device tests with retained content evidence
This commit is contained in:
@@ -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,38 @@ describe("file provenance across multiple devices and real CouchDB", () => {
|
||||
}
|
||||
}, 60_000);
|
||||
|
||||
it.each(["missing record", "compacted base"] as const)(
|
||||
it("reflects the selected revision after compaction when saved content evidence proves the file unchanged", 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 storage = device.getStorage();
|
||||
expect(await storage.body.text()).not.toBe(content);
|
||||
expect(device.reflection.get(path)).toMatchObject({
|
||||
revision: f.revisions[index],
|
||||
contentHash: expect.stringMatching(/^sha256:[0-9a-f]{64}$/u),
|
||||
contentSize: storage.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 expect(device.handler.storeFileToDB(path)).resolves.toBe(true);
|
||||
expect((await device.db.info()).update_seq).toBe(before);
|
||||
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);
|
||||
}
|
||||
expect((await leaves(f.remote)).map((doc) => doc._rev)).toEqual([keep]);
|
||||
}, 60_000);
|
||||
|
||||
it.each(["missing record", "compacted base without content evidence"] as const)(
|
||||
"preserves uncertain storage as one independent conflict with four devices: %s",
|
||||
async (reason) => {
|
||||
const f = await conflictedDevices(4);
|
||||
@@ -299,6 +331,9 @@ describe("file provenance across multiple devices and real CouchDB", () => {
|
||||
// Matching an old ancestor must not be mistaken for an unchanged current branch.
|
||||
device.setStorage(makeFile(old));
|
||||
} else {
|
||||
// Legacy records retain the revision but have no saved content hash or byte length.
|
||||
const { revision, observedStorageMtime } = device.reflection.get(path)!;
|
||||
device.reflection.set(path, { revision, observedStorageMtime });
|
||||
await device.db.compact();
|
||||
await expect(device.db.get(path, { rev: f.revisions[index] })).rejects.toMatchObject({ status: 404 });
|
||||
}
|
||||
|
||||
+3
-2
@@ -98,7 +98,7 @@ npm run test:docker-couchdb:start
|
||||
NODE_OPTIONS=--max-old-space-size=3072 npm run test:integration -- src/serviceModules/FileHandler.multidevice.integration.spec.ts --maxWorkers=1
|
||||
```
|
||||
|
||||
The expected result is five passing tests. Each case creates a uniquely named remote database and removes it during teardown. Stop the fixture after the run, including when the test command fails:
|
||||
The expected result is six passing tests. Each case creates a uniquely named remote database and removes it during teardown. Stop the fixture after the run, including when the test command fails:
|
||||
|
||||
```bash
|
||||
npm run test:docker-couchdb:stop
|
||||
@@ -116,7 +116,8 @@ Each simulated device owns a separate real PouchDB database, `LiveSyncLocalDB` m
|
||||
| Four editing devices | Repeat the same sequence with four independently edited branches. | All four contents are initially preserved, and the same unchanged-save and convergence guarantees hold. |
|
||||
| A genuine edit on a losing device | After three-way resolution reaches its DB, a device adds content to its still-unreflected losing file, then saves and replicates it. | The new revision extends that device's recorded branch. The selected result and the new edit remain readable on every replica. |
|
||||
| Missing provenance | After four-way resolution, remove a losing device's provenance record and set its file to the original historical ancestor's content. Save, remove provenance again, and repeat the save. | The file becomes one fresh independent root, distinct from the historical root. Both contents remain readable after replication, and repeated saves create no duplicates. CLI `resolve` can select the independent root and propagate its resolution. |
|
||||
| Compacted base | After four-way resolution, compact a losing device's local DB and confirm that its recorded revision body is unavailable. Save the remaining file, then repeat after removing provenance. | The file is preserved as one independent conflict rather than discarded. Both contents remain readable on every replica, repeated saves create no duplicates, and CLI `resolve` can select the independent root and propagate its resolution. |
|
||||
| Compacted base with saved content evidence | After four-way resolution, retain the saved content hash and byte length, compact a losing device's local DB, and confirm that its recorded revision body is unavailable. Process its unchanged file twice. | The selected revision is reflected without a database write or a new conflict. Provenance advances to that revision, and every replica retains only the selected non-deleted branch. |
|
||||
| Compacted base without content evidence | After four-way resolution, retain a legacy provenance record with only its revision and modification time, compact a losing device's local DB, and confirm that its recorded revision body is unavailable. Save the remaining file, then repeat after removing provenance. | The file is preserved as one independent conflict rather than discarded. Both contents remain readable on every replica, repeated saves create no duplicates, and CLI `resolve` can select the independent root and propagate its resolution. |
|
||||
|
||||
### Coverage boundaries
|
||||
|
||||
|
||||
@@ -17,6 +17,9 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi
|
||||
- We can opt into NFC path identity for CouchDB synchronisation between the CLI and Obsidian. This prevents decomposed or mixed Unicode file names from becoming inaccessible through a receiving device's normalised path. (#1259)
|
||||
- Existing Vaults retain their current identity rules. Migration requires updated clients, a Rebuild from a complete authoritative Vault, and a Fetch on the other devices.
|
||||
- Reconstruction stops when the supported host file listing or local Metadata exposes ambiguous files or parent folders under NFC and the configured case handling. The CLI retains existing physical file names. P2P and Object Storage do not support the new mode.
|
||||
- We can receive incoming updates to unchanged files after their recorded database revision has been pruned, without creating false conflicts. (#1207)
|
||||
- Saved content evidence establishes that the local file is unchanged. Genuine edits and older records without this evidence retain conservative handling.
|
||||
- We can receive deletions and renames when the source removes the file's Metadata.
|
||||
|
||||
## 1.0.37
|
||||
|
||||
|
||||
Reference in New Issue
Block a user