import { describe, expect, it } from "vitest"; import { compareDirectoryManifests, createDirectoryManifest, parseDirectoryManifest, serializeDirectoryManifest, } from "../../src/core/directory-manifest"; import { mergeThreeWay } from "../../src/core/merge"; function file(path: string, value: string): File { const item = new File([value], path.split("/").at(-1)!, { lastModified: Date.UTC(2026, 0, 1), }); Object.defineProperty(item, "webkitRelativePath", { value: `chosen-root/${path}`, }); return item; } describe("directory manifests", () => { it("hashes bounded files deterministically and strips the selected root", async () => { const first = await createDirectoryManifest([ file("z.txt", "last"), file("nested/a.txt", "first"), ]); const second = await createDirectoryManifest([ file("nested/a.txt", "first"), file("z.txt", "last"), ]); expect(first.entries.map((entry) => entry.path)).toEqual([ "nested/a.txt", "z.txt", ]); expect(first.entries).toEqual(second.entries); expect(parseDirectoryManifest(serializeDirectoryManifest(first))).toEqual( first, ); }); it("compares content digests rather than timestamps", async () => { const left = await createDirectoryManifest([ file("same.txt", "same"), file("changed.txt", "before"), file("removed.txt", "gone"), ]); const right = await createDirectoryManifest([ file("same.txt", "same"), file("changed.txt", "after"), file("added.txt", "new"), ]); expect( compareDirectoryManifests(left, right).map(({ path, status }) => ({ path, status, })), ).toEqual([ { path: "added.txt", status: "added" }, { path: "changed.txt", status: "modified" }, { path: "removed.txt", status: "removed" }, { path: "same.txt", status: "same" }, ]); }); it("rejects traversal and inconsistent imported totals", async () => { await expect( createDirectoryManifest([file("../escape.txt", "bad")]), ).rejects.toThrow(/unsafe/iu); const manifest = await createDirectoryManifest([file("safe.txt", "ok")]); const serialized = JSON.stringify({ ...manifest, totals: { files: 1, bytes: 999 }, }); expect(() => parseDirectoryManifest(serialized)).toThrow(/totals/iu); }); it("rejects case-normalized collisions and invalid timestamps on import", async () => { const manifest = await createDirectoryManifest([file("safe.txt", "ok")]); const duplicate = { ...manifest, entries: [ manifest.entries[0], { ...manifest.entries[0], path: "SAFE.txt" }, ], totals: { files: 2, bytes: manifest.totals.bytes * 2 }, }; expect(() => parseDirectoryManifest(JSON.stringify(duplicate))).toThrow( /collision/iu, ); expect(() => parseDirectoryManifest( JSON.stringify({ ...manifest, entries: [{ ...manifest.entries[0], lastModified: "not-a-date" }], }), ), ).toThrow(/fields/iu); }); }); describe("bounded three-way merge", () => { it("combines non-overlapping line changes without conflict", () => { const result = mergeThreeWay({ base: "one\ntwo\nthree\n", ours: "ONE\ntwo\nthree\n", theirs: "one\ntwo\nTHREE\n", }); expect(result.clean).toBe(true); expect(result.text).toBe("ONE\ntwo\nTHREE\n"); }); it("emits explicit ours/base/theirs markers for overlapping changes", () => { const result = mergeThreeWay({ base: "same\nvalue\n", ours: "same\nours\n", theirs: "same\ntheirs\n", oursName: "working.txt", theirsName: "incoming.txt", }); expect(result.clean).toBe(false); expect(result.conflicts).toHaveLength(1); expect(result.text).toContain("<<<<<<< working.txt"); expect(result.text).toContain("||||||| base"); expect(result.text).toContain(">>>>>>> incoming.txt"); expect(JSON.parse(result.report)).toMatchObject({ schema: "de.add-ideas.diff-tools.merge-report.v1", clean: false, }); }); it("keeps conflict markers on their own lines when inputs lack final newlines", () => { const result = mergeThreeWay({ base: "base", ours: "ours", theirs: "theirs", }); expect(result.text).toBe( "<<<<<<< ours\nours\n||||||| base\nbase\n=======\ntheirs\n>>>>>>> theirs\n", ); }); });