mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-08-22 03:17:05 +00:00
229 lines
7.7 KiB
TypeScript
229 lines
7.7 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import {
|
|
discardLiveBranch,
|
|
discardUnreadableLiveRevision,
|
|
inspectFileRepair,
|
|
} from "./fileRepair";
|
|
|
|
function createCore() {
|
|
const current = {
|
|
_id: "f:note",
|
|
_rev: "3-current",
|
|
_conflicts: ["2-conflict"],
|
|
_revs_info: [{ rev: "3-current", status: "available" }],
|
|
path: "note.md",
|
|
ctime: 1,
|
|
mtime: 3,
|
|
size: 7,
|
|
type: "plain",
|
|
children: ["h:current"],
|
|
deleted: false,
|
|
eden: {},
|
|
};
|
|
const conflict = {
|
|
...current,
|
|
_rev: "2-conflict",
|
|
_conflicts: undefined,
|
|
_revs_info: [{ rev: "2-conflict", status: "available" }],
|
|
mtime: 2,
|
|
children: ["h:missing-conflict"],
|
|
};
|
|
const deleteRevisionFromDB = vi.fn(async () => true);
|
|
const core = {
|
|
settings: {
|
|
syncInternalFiles: false,
|
|
syncInternalFilesIgnorePatterns: "",
|
|
syncInternalFilesTargetPatterns: "",
|
|
},
|
|
storageAccess: {
|
|
isExistsIncludeHidden: vi.fn(async () => true),
|
|
statHidden: vi.fn(async () => ({
|
|
ctime: 1,
|
|
mtime: 3,
|
|
size: 7,
|
|
type: "file",
|
|
})),
|
|
readHiddenFileBinary: vi.fn(async () => new TextEncoder().encode("current").buffer),
|
|
getFileNames: vi.fn(async () => ["note.md"]),
|
|
getFilesIncludeHidden: vi.fn(async () => ["note.md"]),
|
|
},
|
|
localDatabase: {
|
|
localDatabase: {
|
|
get: vi.fn(async (_id: string, options?: { rev?: string }) =>
|
|
options?.rev === "2-conflict" ? conflict : current
|
|
),
|
|
},
|
|
allDocsRaw: vi.fn(async ({ keys }: { keys: string[] }) => ({
|
|
rows: keys.includes("h:current")
|
|
? [
|
|
{
|
|
id: "h:current",
|
|
key: "h:current",
|
|
value: { rev: "1-current" },
|
|
},
|
|
]
|
|
: [],
|
|
})),
|
|
getDBEntryFromMeta: vi.fn(async (meta: typeof current) => ({
|
|
...meta,
|
|
data: [meta._rev === "3-current" ? "current" : "conflict"],
|
|
})),
|
|
getDBEntry: vi.fn(async () => false),
|
|
findAllDocs: vi.fn(async function* () {
|
|
yield current;
|
|
}),
|
|
},
|
|
fileHandler: {
|
|
deleteRevisionFromDB,
|
|
},
|
|
services: {
|
|
path: {
|
|
path2id: vi.fn(async () => "f:note"),
|
|
},
|
|
UI: {
|
|
confirm: {},
|
|
},
|
|
},
|
|
};
|
|
return {
|
|
conflict,
|
|
core,
|
|
current,
|
|
deleteRevisionFromDB,
|
|
};
|
|
}
|
|
|
|
describe("file repair inspection", () => {
|
|
it("shows the winner and every conflict revision independently", async () => {
|
|
const { core } = createCore();
|
|
|
|
const inspection = await inspectFileRepair(core as never, "note.md");
|
|
|
|
expect(inspection.revisions).toEqual([
|
|
expect.objectContaining({
|
|
role: "winner",
|
|
contentReadable: true,
|
|
contentMatchesStorage: true,
|
|
metadata: expect.objectContaining({
|
|
revision: "3-current",
|
|
}),
|
|
}),
|
|
expect.objectContaining({
|
|
role: "conflict",
|
|
contentReadable: false,
|
|
contentMatchesStorage: null,
|
|
metadata: expect.objectContaining({
|
|
revision: "2-conflict",
|
|
}),
|
|
}),
|
|
]);
|
|
expect(inspection.requiresAttention).toBe(true);
|
|
});
|
|
|
|
it("omits a logical deletion which already matches an absent Vault file", async () => {
|
|
const { core, current } = createCore();
|
|
current.deleted = true;
|
|
current._conflicts = [];
|
|
current.children = [];
|
|
core.storageAccess.isExistsIncludeHidden.mockResolvedValue(false);
|
|
core.storageAccess.statHidden.mockResolvedValue(null as never);
|
|
|
|
const inspection = await inspectFileRepair(core as never, "note.md");
|
|
|
|
expect(inspection.revisions).toEqual([
|
|
expect.objectContaining({
|
|
role: "winner",
|
|
contentReadable: true,
|
|
metadata: expect.objectContaining({
|
|
deleted: true,
|
|
revision: "3-current",
|
|
}),
|
|
}),
|
|
]);
|
|
expect(inspection.requiresAttention).toBe(false);
|
|
});
|
|
|
|
it("rechecks liveness and readability before discarding an exact revision", async () => {
|
|
const { core, deleteRevisionFromDB } = createCore();
|
|
|
|
await expect(
|
|
discardUnreadableLiveRevision(core as never, "note.md", "2-conflict")
|
|
).resolves.toBe("discarded");
|
|
await expect(
|
|
discardUnreadableLiveRevision(core as never, "note.md", "3-current")
|
|
).resolves.toBe("revision-is-readable");
|
|
|
|
expect(deleteRevisionFromDB).toHaveBeenCalledOnce();
|
|
expect(deleteRevisionFromDB).toHaveBeenCalledWith("note.md", "2-conflict");
|
|
});
|
|
|
|
it("allows an exact unreadable generation-one winner to be discarded explicitly", async () => {
|
|
const { core, current, deleteRevisionFromDB } = createCore();
|
|
current._rev = "1-root";
|
|
current._conflicts = [];
|
|
current.children = ["h:missing-root"];
|
|
core.localDatabase.allDocsRaw.mockResolvedValue({ rows: [] });
|
|
|
|
const inspection = await inspectFileRepair(core as never, "note.md");
|
|
|
|
expect(inspection.revisions).toEqual([
|
|
expect.objectContaining({
|
|
role: "winner",
|
|
contentReadable: false,
|
|
metadata: expect.objectContaining({
|
|
revision: "1-root",
|
|
}),
|
|
}),
|
|
]);
|
|
await expect(
|
|
discardUnreadableLiveRevision(core as never, "note.md", "1-root")
|
|
).resolves.toBe("discarded");
|
|
expect(deleteRevisionFromDB).toHaveBeenCalledWith("note.md", "1-root");
|
|
});
|
|
|
|
it("refuses to discard a revision which stopped being a live leaf", async () => {
|
|
const { core, current, deleteRevisionFromDB } = createCore();
|
|
core.localDatabase.localDatabase.get.mockResolvedValue({
|
|
...current,
|
|
_conflicts: [],
|
|
});
|
|
|
|
await expect(
|
|
discardUnreadableLiveRevision(core as never, "note.md", "2-conflict")
|
|
).resolves.toBe("no-longer-live");
|
|
|
|
expect(deleteRevisionFromDB).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("discards an exact readable winner while another live branch remains", async () => {
|
|
const { core, deleteRevisionFromDB } = createCore();
|
|
|
|
await expect(
|
|
discardLiveBranch(core as never, "note.md", "3-current")
|
|
).resolves.toBe("discarded");
|
|
|
|
expect(deleteRevisionFromDB).toHaveBeenCalledWith("note.md", "3-current");
|
|
});
|
|
|
|
it("refuses to discard the only live branch", async () => {
|
|
const { core, current, deleteRevisionFromDB } = createCore();
|
|
current._conflicts = [];
|
|
|
|
await expect(
|
|
discardLiveBranch(core as never, "note.md", "3-current")
|
|
).resolves.toBe("only-live-revision");
|
|
|
|
expect(deleteRevisionFromDB).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("refuses to discard a branch which is no longer live", async () => {
|
|
const { core, deleteRevisionFromDB } = createCore();
|
|
|
|
await expect(
|
|
discardLiveBranch(core as never, "note.md", "1-stale")
|
|
).resolves.toBe("no-longer-live");
|
|
|
|
expect(deleteRevisionFromDB).not.toHaveBeenCalled();
|
|
});
|
|
});
|