import { describe, it, expect } from "vitest" import { runScenario, runCycle, localMutate, remoteMutate } from "../harness/runner" import { writeLocal, rmLocal } from "../harness/mutations" /** * Category ZH — a directory deletion must not cascade over live content the OTHER side did not delete. * * When one side removes a directory while the other adds (or keeps a modified) child inside it in the same * cycle, the raw delta set holds both `deleteXDirectory ` and the child's own add. The dir-delete used * to win — collapse subsumed the child's sibling deletes under it and the dir-delete then wiped the * brand-new file at execution time, before it was ever propagated (silent data loss). Newer content beats a * delete (the same rule the per-file passes apply), so the directory survives: its delete is dropped and * the surviving child's own add re-creates it. A child that is merely RENAMED out of the directory does NOT * keep it alive (that is a normal dir rename, which deletes the now-empty old directory). */ describe("Category ZH — directory delete vs a live child", () => { it("ZH1: remote deletes a dir while local adds a new child — the child survives, the dir is kept (twoWay)", async () => { const result = await runScenario({ name: "ZH1", mode: "twoWay", initialLocal: { "/local/dir/keep.txt": "keep", "/local/other.txt": "o" }, steps: [ runCycle(), remoteMutate(world => world.cloud.controls.trashPath("/dir")), localMutate(world => writeLocal(world, "dir/new.txt", "new-child")), runCycle(), runCycle(), runCycle() ] }) // The new child survives and is uploaded; the dir is re-asserted; the unmodified base child the // remote deleted is gone; both sides converge. expect(result.finalLocal["/dir/new.txt"]).toMatchObject({ type: "file", size: "new-child".length }) expect(result.finalRemote["/dir/new.txt"]).toMatchObject({ type: "file" }) expect(result.finalLocal["/dir/keep.txt"]).toBeUndefined() expect(result.finalLocal).toEqual(result.finalRemote) }) it("ZH2: local deletes a dir while remote adds a new child — the child survives (symmetric)", async () => { const result = await runScenario({ name: "ZH2", mode: "twoWay", initialLocal: { "/local/dir/keep.txt": "keep", "/local/other.txt": "o" }, steps: [ runCycle(), localMutate(world => rmLocal(world, "dir")), remoteMutate(world => world.cloud.controls.addFile("/dir/new.txt", "remote-new")), runCycle(), runCycle(), runCycle() ] }) expect(result.finalRemote["/dir/new.txt"]).toMatchObject({ type: "file", size: "remote-new".length }) expect(result.finalLocal["/dir/new.txt"]).toMatchObject({ type: "file" }) expect(result.finalRemote["/dir/keep.txt"]).toBeUndefined() expect(result.finalLocal).toEqual(result.finalRemote) }) it("ZH3: remote deletes a dir while local MODIFIES a base child — the modified child wins, dir kept", async () => { const result = await runScenario({ name: "ZH3", mode: "twoWay", initialLocal: { "/local/dir/keep.txt": "v1", "/local/other.txt": "o" }, steps: [ runCycle(), remoteMutate(world => world.cloud.controls.trashPath("/dir")), localMutate(world => writeLocal(world, "dir/keep.txt", "v2-modified-bigger")), runCycle(), runCycle(), runCycle() ] }) // Newer-modify-wins: the locally-modified child is resurrected (re-uploaded), keeping its directory. expect(result.finalRemote["/dir/keep.txt"]).toMatchObject({ type: "file", size: "v2-modified-bigger".length }) expect(result.finalLocal["/dir/keep.txt"]).toMatchObject({ type: "file" }) expect(result.finalLocal).toEqual(result.finalRemote) }) it("ZH4: deleting a dir whose ONLY child is moved out still deletes the dir (no false-keep on rename)", async () => { const result = await runScenario({ name: "ZH4", mode: "twoWay", initialLocal: { "/local/dir/only.txt": "x", "/local/other.txt": "o" }, steps: [ runCycle(), localMutate(world => { // Move the only child OUT to the root, then remove the now-empty directory. writeLocal(world, "moved.txt", "x") rmLocal(world, "dir") }), runCycle(), runCycle(), runCycle() ] }) expect(result.finalRemote["/dir"]).toBeUndefined() expect(result.finalRemote["/moved.txt"]).toMatchObject({ type: "file" }) expect(result.finalLocal).toEqual(result.finalRemote) }) })