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(); }); });