Strengthen conflict resolution regression coverage

This commit is contained in:
vorotamoroz
2026-09-04 08:16:14 +00:00
parent 56a1a19d2c
commit f06f33cbf4
2 changed files with 402 additions and 11 deletions
@@ -170,6 +170,66 @@ describe("conflict resolution serviceFeature", () => {
expect(harness.fileHandler.deleteRevisionFromDB).not.toHaveBeenCalled();
});
it("deletes the older conflict leaf when matching current content is newer", async () => {
const harness = createOperationsHarness();
const path = "same-newer-current.md" as FilePathWithPrefix;
const leftLeaf = leaf("2-left", "Same content\n", 3000);
const rightLeaf = leaf("1-right", "Same content\n", 2000);
harness.tryAutoMerge.mockResolvedValue({
leftRev: leftLeaf.rev,
rightRev: rightLeaf.rev,
leftLeaf,
rightLeaf,
});
const result = await harness.operations.checkConflictAndPerformAutoMerge(path);
expect(result).toBe(AUTO_MERGED);
expect(harness.resolveByDeletingRevision).toHaveBeenCalledWith(path, "1-right", "same");
});
it.each([
{
description: "binary content",
path: "different.png" as FilePathWithPrefix,
resolveConflictsByNewerFile: false,
expectedSubtitle: "binary",
},
{
description: "the newer-file policy",
path: "different.md" as FilePathWithPrefix,
resolveConflictsByNewerFile: true,
expectedSubtitle: "alwaysNewer",
},
])(
"retains automatic resolution for $description",
async ({ path, resolveConflictsByNewerFile, expectedSubtitle }) => {
const harness = createOperationsHarness();
const leftLeaf = leaf("1-left", "Left content\n", 1000);
const rightLeaf = leaf("2-right", "Right content\n", 2000);
harness.tryAutoMerge.mockResolvedValue({
leftRev: leftLeaf.rev,
rightRev: rightLeaf.rev,
leftLeaf,
rightLeaf,
});
const operations = createConflictResolutionOperations({
...harness.dependencies,
currentSettings: () => ({
disableMarkdownAutoMerge: false,
resolveConflictsByNewerFile,
syncAfterMerge: false,
showMergeDialogOnlyOnActive: false,
}),
});
const result = await operations.checkConflictAndPerformAutoMerge(path);
expect(result).toBe(AUTO_MERGED);
expect(harness.resolveByDeletingRevision).toHaveBeenCalledWith(path, "1-left", expectedSubtitle);
}
);
it("acquires the active local database for each resolution attempt", async () => {
const harness = createOperationsHarness();
const path = "database-reset.md" as FilePathWithPrefix;
@@ -254,6 +314,34 @@ describe("conflict resolution serviceFeature", () => {
expect(harness.fileHandler.dbToStorage).not.toHaveBeenCalled();
});
it("returns a safe error and logs when reflecting the resolved revision fails", async () => {
const harness = createOperationsHarness();
const path = "failed-reflection.md" as FilePathWithPrefix;
harness.fileHandler.dbToStorage.mockResolvedValue(false);
const result = await harness.operations.resolveByDeletingRevision(path, "2-right", "UI Selected");
expect(result).toBe(MISSING_OR_ERROR);
expect(harness.fileHandler.dbToStorage).toHaveBeenCalledWith(path, path, true);
expect(harness.dependencies.log).toHaveBeenCalledWith(
`Could not write the resolved content to the storage: ${path}`,
LOG_LEVEL_NOTICE
);
});
it.each([
["plugin metadata", "ps:plugin-metadata" as FilePathWithPrefix],
["customisation metadata", "ix:customisation-metadata" as FilePathWithPrefix],
])("does not reflect %s into the Vault after deleting its revision", async (_description, path) => {
const harness = createOperationsHarness();
const result = await harness.operations.resolveByDeletingRevision(path, "2-right", "UI Selected");
expect(result).toBe(AUTO_MERGED);
expect(harness.fileHandler.deleteRevisionFromDB).toHaveBeenCalledWith(path, "2-right");
expect(harness.fileHandler.dbToStorage).not.toHaveBeenCalled();
});
it("rechecks a remaining manual pair after committing a sensible merge", async () => {
const harness = createOperationsHarness();
const path = "three-versions.md" as FilePathWithPrefix;
@@ -333,6 +421,35 @@ describe("conflict resolution serviceFeature", () => {
);
});
it("opens a manual merge when the active-only setting matches the current file", async () => {
const harness = createOperationsHarness();
const path = "active.md" as FilePathWithPrefix;
const activePath = "active.md" as FilePath;
harness.tryAutoMerge.mockResolvedValue({
leftRev: "2-left",
rightRev: "2-right",
leftLeaf: leaf("2-left", "Left\n", 1),
rightLeaf: leaf("2-right", "Right\n", 2),
});
const operations = createConflictResolutionOperations({
...harness.dependencies,
currentSettings: () => ({
disableMarkdownAutoMerge: false,
resolveConflictsByNewerFile: false,
syncAfterMerge: false,
showMergeDialogOnlyOnActive: true,
}),
vault: { getActiveFilePath: () => activePath },
});
await operations.resolve(path);
expect(harness.resolveByUserInteraction).toHaveBeenCalledWith(
path,
expect.objectContaining({ left: expect.anything(), right: expect.anything() })
);
});
it("cancels an active same-file dialogue before serialising a repeated resolution", async () => {
const harness = createOperationsHarness();
const path = "repeated.md" as FilePathWithPrefix;
@@ -472,6 +589,21 @@ describe("conflict resolution serviceFeature", () => {
);
});
it.each([
["the active-file gate is disabled", { checkConflictOnlyOnOpen: false }, "other.md" as FilePathWithPrefix],
["the requested path is active", { checkConflictOnlyOnOpen: true }, "active.md" as FilePathWithPrefix],
])("reaches the resolver when %s", async (_description, settings, activeFile) => {
const path = "active.md" as FilePathWithPrefix;
const harness = createHarness({ settings, activeFile });
harness.tryAutoMerge.mockResolvedValue({ ok: NOT_CONFLICTED });
await harness.conflict.queueCheckForIfOpen(path);
await harness.conflict.ensureAllProcessed();
expect(harness.tryAutoMerge).toHaveBeenCalledOnce();
expect(harness.tryAutoMerge).toHaveBeenCalledWith(path, true);
});
it("limits concurrent conflict checks and reports queued and active work", async () => {
const paths = Array.from({ length: 11 }, (_, index) => `concurrent-${index}.md` as FilePathWithPrefix);
const harness = createHarness();
@@ -632,6 +764,27 @@ describe("conflict resolution serviceFeature", () => {
expect(MISSING_OR_ERROR).toBeDefined();
});
it("preserves revisions and returns a safe error when the current leaf is unreadable", async () => {
const harness = createOperationsHarness();
const path = "unreadable-current.md" as FilePathWithPrefix;
harness.tryAutoMerge.mockResolvedValue({
leftRev: "3-current",
rightRev: "2-conflict",
leftLeaf: false,
rightLeaf: leaf("2-conflict", "Conflict body\n", 2),
});
const result = await harness.operations.checkConflictAndPerformAutoMerge(path);
expect(result).toBe(MISSING_OR_ERROR);
expect(harness.fileHandler.deleteRevisionFromDB).not.toHaveBeenCalled();
expect(harness.fileHandler.dbToStorage).not.toHaveBeenCalled();
expect(harness.dependencies.log).toHaveBeenCalledWith(
`could not get current revisions:${path}`,
LOG_LEVEL_NOTICE
);
});
it("resolves an identical pair, emits cancellation, and rechecks after merging", async () => {
const path = "independently-created.md" as FilePathWithPrefix;
const harness = createHarness({ settings: { syncAfterMerge: false } });
@@ -704,4 +857,27 @@ describe("conflict resolution serviceFeature", () => {
LOG_LEVEL_NOTICE
);
});
it.each([
{ description: "no conflicts remain", conflicts: [] as string[], expectedDeletes: [] as string[] },
{
description: "conflict metadata is unreadable",
conflicts: ["2-unreadable"],
expectedDeletes: ["2-unreadable"],
},
])("handles the case where $description while resolving by newest", async ({ conflicts, expectedDeletes }) => {
const harness = createOperationsHarness();
const path = "newest-policy.md" as FilePathWithPrefix;
harness.databaseFileAccess.fetchEntryMeta.mockImplementation(
async (_filename: FilePathWithPrefix, revision?: string) =>
revision === undefined ? metadata(path, "3-current", 1000) : false
);
harness.databaseFileAccess.getConflictedRevs.mockResolvedValueOnce(conflicts).mockResolvedValueOnce([]);
await expect(harness.operations.resolveByNewest(path)).resolves.toBe(true);
expect(harness.fileHandler.deleteRevisionFromDB).toHaveBeenCalledTimes(expectedDeletes.length);
for (const [index, revision] of expectedDeletes.entries()) {
expect(harness.fileHandler.deleteRevisionFromDB).toHaveBeenNthCalledWith(index + 1, path, revision);
}
});
});