mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-08-29 06:47:06 +00:00
Handle multiple conflict revisions deterministically
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
AUTO_MERGED,
|
||||
CANCELLED,
|
||||
DEFAULT_SETTINGS,
|
||||
LEAVE_TO_SUBSEQUENT,
|
||||
LOG_LEVEL_NOTICE,
|
||||
type FilePathWithPrefix,
|
||||
type diff_result,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
@@ -40,6 +43,12 @@ const conflict: diff_result = {
|
||||
diff: [],
|
||||
};
|
||||
|
||||
async function* documents(items: unknown[]) {
|
||||
for (const item of items) {
|
||||
yield item;
|
||||
}
|
||||
}
|
||||
|
||||
function createModule(conflictedRevisions: string[] = ["2-right"]) {
|
||||
const handlers = {
|
||||
unresolvedMessages: undefined as undefined | (() => Promise<string[]>),
|
||||
@@ -64,24 +73,28 @@ function createModule(conflictedRevisions: string[] = ["2-right"]) {
|
||||
},
|
||||
conflict: {
|
||||
resolveByUserInteraction: { addHandler: vi.fn() },
|
||||
resolveByDeletingRevision: vi.fn(),
|
||||
resolveByDeletingRevision: vi.fn(async () => AUTO_MERGED),
|
||||
queueCheckFor: vi.fn(async () => undefined),
|
||||
ensureAllProcessed: vi.fn(async () => true),
|
||||
},
|
||||
replication: { replicateByEvent: vi.fn(async () => true) },
|
||||
vault: { getActiveFilePath: vi.fn(() => path) },
|
||||
path: { getPath: vi.fn(), getPathWithoutPrefix: vi.fn() },
|
||||
path: { getPath: vi.fn((entry: { path: FilePathWithPrefix }) => entry.path) },
|
||||
};
|
||||
const core = {
|
||||
_services: services,
|
||||
services,
|
||||
settings: { ...DEFAULT_SETTINGS, syncAfterMerge: false },
|
||||
localDatabase: {
|
||||
getDBEntry: vi.fn(async () => false),
|
||||
findAllDocs: vi.fn(),
|
||||
getDBEntry: vi.fn(async (): Promise<false | { _rev: string; _conflicts?: string[] }> => false),
|
||||
findAllDocs: vi.fn(() => documents([])),
|
||||
},
|
||||
databaseFileAccess: {
|
||||
getConflictedRevs: vi.fn(async () => conflictedRevisions),
|
||||
storeContent: vi.fn(async () => true),
|
||||
},
|
||||
confirm: {
|
||||
askSelectString: vi.fn(async (): Promise<string | undefined> => undefined),
|
||||
},
|
||||
};
|
||||
const plugin = { app: {} };
|
||||
@@ -158,4 +171,111 @@ describe("ModuleInteractiveConflictResolver postponement", () => {
|
||||
conflictedRevisions.splice(0);
|
||||
await expect(handlers.unresolvedMessages?.()).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it("reports the number of live versions and reduces it after each resolved pair", async () => {
|
||||
const conflictedRevisions = ["2-second", "2-third"];
|
||||
const { core, handlers, module, services } = createModule(conflictedRevisions);
|
||||
module.onBindFunction(core as never, services as never);
|
||||
|
||||
await expect(handlers.unresolvedMessages?.()).resolves.toEqual([
|
||||
"This file has 3 unresolved versions. They will be reviewed one pair at a time.",
|
||||
]);
|
||||
|
||||
conflictedRevisions.shift();
|
||||
await (module as any).refreshConflictState(path);
|
||||
await expect(handlers.unresolvedMessages?.()).resolves.toEqual(["This file has unresolved conflicts."]);
|
||||
|
||||
conflictedRevisions.shift();
|
||||
await (module as any).refreshConflictState(path);
|
||||
await expect(handlers.unresolvedMessages?.()).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it("reconstructs the remaining pair after a postponed session is restarted", async () => {
|
||||
const conflictedRevisions = ["2-second", "2-third"];
|
||||
const firstSession = createModule(conflictedRevisions);
|
||||
|
||||
await firstSession.module._anyResolveConflictByUI(path, conflict);
|
||||
conflictedRevisions.shift();
|
||||
|
||||
const restartedSession = createModule(conflictedRevisions);
|
||||
restartedSession.module.onBindFunction(restartedSession.core as never, restartedSession.services as never);
|
||||
await expect(restartedSession.handlers.unresolvedMessages?.()).resolves.toEqual([
|
||||
"This file has unresolved conflicts.",
|
||||
]);
|
||||
|
||||
await restartedSession.module._anyResolveConflictByUI(path, {
|
||||
left: { rev: "3-merged", data: "merged", ctime: 1, mtime: 3 },
|
||||
right: { rev: "2-third", data: "third", ctime: 1, mtime: 2 },
|
||||
diff: [],
|
||||
});
|
||||
|
||||
expect(modalState.constructed).toBe(2);
|
||||
});
|
||||
|
||||
it("deletes the compared right leaf when concatenating a deterministically selected pair", async () => {
|
||||
const { core, module, services } = createModule(["2-unrelated", "2-right"]);
|
||||
modalState.result = LEAVE_TO_SUBSEQUENT;
|
||||
core.localDatabase.getDBEntry.mockResolvedValue({
|
||||
_rev: "2-left",
|
||||
_conflicts: ["2-unrelated", "2-right"],
|
||||
});
|
||||
|
||||
await module._anyResolveConflictByUI(path, conflict);
|
||||
|
||||
expect(core.databaseFileAccess.storeContent).toHaveBeenCalledWith(path, "");
|
||||
expect(services.conflict.resolveByDeletingRevision).toHaveBeenCalledWith(path, "2-right", "UI Concatenated");
|
||||
});
|
||||
|
||||
it("rechecks the live leaves instead of applying a stale dialogue selection", async () => {
|
||||
const { core, module, services } = createModule(["2-other"]);
|
||||
modalState.result = "2-right";
|
||||
core.localDatabase.getDBEntry.mockResolvedValue({
|
||||
_rev: "3-new-winner",
|
||||
_conflicts: ["2-other"],
|
||||
});
|
||||
|
||||
await module._anyResolveConflictByUI(path, conflict);
|
||||
|
||||
expect(services.conflict.resolveByDeletingRevision).not.toHaveBeenCalled();
|
||||
expect(services.conflict.queueCheckFor).toHaveBeenCalledWith(path);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ModuleInteractiveConflictResolver file selection", () => {
|
||||
beforeEach(() => {
|
||||
modalState.constructed = 0;
|
||||
modalState.result = modalState.postponed;
|
||||
});
|
||||
|
||||
it("does not show a no-conflicts notice when an automatic repeat reaches its normal end", async () => {
|
||||
const { core, module } = createModule();
|
||||
core.localDatabase.findAllDocs
|
||||
.mockImplementationOnce(() =>
|
||||
documents([
|
||||
{
|
||||
_id: "note-id",
|
||||
_rev: "2-left",
|
||||
_conflicts: ["2-right"],
|
||||
path,
|
||||
mtime: 2,
|
||||
},
|
||||
])
|
||||
)
|
||||
.mockImplementationOnce(() => documents([]));
|
||||
core.confirm.askSelectString.mockResolvedValue(path);
|
||||
|
||||
await module.allConflictCheck();
|
||||
|
||||
expect(core.confirm.askSelectString).toHaveBeenCalledOnce();
|
||||
expect(module._log).not.toHaveBeenCalledWith("There are no conflicted documents", LOG_LEVEL_NOTICE);
|
||||
});
|
||||
|
||||
it("shows one no-conflicts notice for an explicit selection request which starts empty", async () => {
|
||||
const { module } = createModule();
|
||||
|
||||
await module.pickFileForResolve();
|
||||
|
||||
expect(module._log).toHaveBeenCalledTimes(1);
|
||||
expect(module._log).toHaveBeenCalledWith("There are no conflicted documents", LOG_LEVEL_NOTICE);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user