219 lines
8.3 KiB
TypeScript
219 lines
8.3 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import { fixture, bytes } from "./sync-fixture";
|
|
import { toStoredSnapshot } from "../sync-client/src/sync-operations/content";
|
|
import { LocalChangesDuringReconciliation } from "../sync-client/src/errors/errors";
|
|
|
|
for (const equal of [false, true])
|
|
test(`swaps preserve identities with ${equal ? "equal" : "distinct"} content and save once`, async () => {
|
|
const f = await fixture({ "a.md": "A", "b.md": equal ? "A" : "B" });
|
|
let saves = 0;
|
|
f.persistence.boundary = (label) => {
|
|
if (label === "before:save") saves++;
|
|
};
|
|
const next = structuredClone(f.database.state);
|
|
next.local = { a: "b.md", b: "a.md" };
|
|
await f.files.apply(next);
|
|
assert.deepEqual(f.database.state.local, next.local);
|
|
assert.deepEqual(
|
|
f.disk.userFiles(),
|
|
new Map([
|
|
["a.md", bytes(equal ? "A" : "B")],
|
|
["b.md", bytes("A")]
|
|
])
|
|
);
|
|
assert.equal(saves, 1);
|
|
assert.deepEqual(await f.disk.listFilesRecursively(), ["a.md", "b.md"]);
|
|
});
|
|
|
|
for (const direction of ["file-to-directory", "directory-to-file"])
|
|
test(direction, async () => {
|
|
const f = await fixture(
|
|
direction === "file-to-directory"
|
|
? { folder: "A", "b.md": "B" }
|
|
: { "a.md": "A", "folder/b.md": "B" }
|
|
);
|
|
const ids = Object.keys(f.database.state.local);
|
|
const next = structuredClone(f.database.state);
|
|
next.local =
|
|
direction === "file-to-directory"
|
|
? { [ids[0]]: "folder/a.md", [ids[1]]: "b.md" }
|
|
: { [ids[0]]: "folder", [ids[1]]: "other.md" };
|
|
const expected = structuredClone(next.local);
|
|
await f.files.apply(next);
|
|
assert.deepEqual(f.database.state.local, expected);
|
|
assert.deepEqual(
|
|
new Set(f.disk.userFiles().keys()),
|
|
new Set(Object.values(f.database.state.local))
|
|
);
|
|
assert.deepEqual(
|
|
[...f.disk.userFiles().values()]
|
|
.map((value) => Buffer.from(value).toString())
|
|
.sort(),
|
|
["A", "B"]
|
|
);
|
|
});
|
|
|
|
test("an unexpected occupant is preserved under a visible conflict name", async () => {
|
|
const f = await fixture();
|
|
await f.disk.userWrite("remote.md", bytes("unexpected"));
|
|
const next = structuredClone(f.database.state);
|
|
next.local.a = "remote.md";
|
|
await f.files.apply(next);
|
|
assert.equal(f.database.state.local.a, "remote.md");
|
|
assert.equal(f.disk.userFiles().size, 2);
|
|
assert.deepEqual(
|
|
[...f.disk.userFiles().values()]
|
|
.map((value) => Buffer.from(value).toString())
|
|
.sort(),
|
|
["local", "unexpected"]
|
|
);
|
|
assert.equal(Object.keys(f.database.state.local).length, 2);
|
|
});
|
|
|
|
test("content edited after planning is merged with the incoming snapshot", async () => {
|
|
const f = await fixture({ "a.md": "base\n" });
|
|
const expected = (await f.files.snapshot("a.md"))!;
|
|
await f.disk.userWrite("a.md", bytes("base\nlocal\n"));
|
|
await f.files.apply(structuredClone(f.database.state), {
|
|
a: {
|
|
expected,
|
|
replacement: await toStoredSnapshot({
|
|
content: bytes("remote\nbase\n")
|
|
})
|
|
}
|
|
});
|
|
assert.deepEqual(
|
|
f.disk.userFiles().get("a.md"),
|
|
bytes("remote\nbase\nlocal\n")
|
|
);
|
|
});
|
|
|
|
test("deleting the source after planning does not resurrect it", async () => {
|
|
const f = await fixture();
|
|
const next = structuredClone(f.database.state);
|
|
next.local.a = "remote.md";
|
|
await f.disk.userDelete("a.md");
|
|
await f.files.apply(next, {
|
|
a: { replacement: await toStoredSnapshot({ content: bytes("remote") }) }
|
|
});
|
|
assert.deepEqual(f.disk.userFiles(), new Map());
|
|
assert.deepEqual(f.database.state.local, {});
|
|
});
|
|
|
|
for (const protect of ["ignore", "size"] as const)
|
|
test(`protect ${protect} at application time`, async () => {
|
|
const f = await fixture();
|
|
const next = structuredClone(f.database.state);
|
|
next.local.a = "remote.md";
|
|
if (protect === "ignore")
|
|
await f.settings.setSetting("ignorePatterns", ["a.md"]);
|
|
else await f.settings.setSetting("maxFileSizeMB", 0);
|
|
await f.files.apply(next, {
|
|
a: {
|
|
replacement: await toStoredSnapshot({
|
|
content: bytes("remote")
|
|
})
|
|
}
|
|
});
|
|
assert.deepEqual(
|
|
f.disk.userFiles(),
|
|
new Map([["a.md", bytes("local")]])
|
|
);
|
|
assert.equal(f.database.state.local.a, "a.md");
|
|
assert.equal(f.database.state.excluded?.a, "a.md");
|
|
});
|
|
|
|
for (const when of ["planning", "installed"])
|
|
test(`notified move during ${when} is attributed to the right identity on rescan`, async () => {
|
|
const f = await fixture({ "a.md": "A", "b.md": "B" });
|
|
let changed = false;
|
|
f.disk.boundary = async (label) => {
|
|
if (
|
|
changed ||
|
|
label !==
|
|
(when === "planning"
|
|
? "read:stat:a.md"
|
|
: "visible:rename:a.md->b.md")
|
|
)
|
|
return;
|
|
changed = true;
|
|
const oldPath = when === "planning" ? "a.md" : "b.md";
|
|
await f.disk.userRename(oldPath, "editor.md");
|
|
await f.syncer.syncLocallyUpdatedFile({
|
|
oldPath,
|
|
relativePath: "editor.md"
|
|
});
|
|
};
|
|
const next = structuredClone(f.database.state);
|
|
next.local = { a: "b.md", b: "a.md" };
|
|
if (when === "planning")
|
|
await assert.rejects(
|
|
f.files.apply(next),
|
|
LocalChangesDuringReconciliation
|
|
);
|
|
else await f.files.apply(next);
|
|
assert(changed);
|
|
await f.internals.scan();
|
|
assert.equal(f.database.state.local.a, "editor.md");
|
|
assert.deepEqual(
|
|
f.disk.userFiles().get(f.database.state.local.a),
|
|
bytes("A")
|
|
);
|
|
assert.deepEqual(
|
|
f.disk.userFiles().get(f.database.state.local.b),
|
|
bytes("B")
|
|
);
|
|
});
|
|
|
|
for (const action of ["move", "delete"] as const)
|
|
test(`notified ${action}/recreate between file operations does not repurpose the replacement file`, async () => {
|
|
const f = await fixture({ "a.md": "A", "b.md": "B" });
|
|
await f.settings.setSetting("ignorePatterns", ["private.md"]);
|
|
let changed = false;
|
|
f.disk.boundary = async (label) => {
|
|
if (changed || label !== "visible:rename:a.md->remote-a.md") return;
|
|
changed = true;
|
|
if (action === "move") {
|
|
await f.disk.userRename("b.md", "private.md");
|
|
await f.syncer.syncLocallyUpdatedFile({
|
|
oldPath: "b.md",
|
|
relativePath: "private.md"
|
|
});
|
|
} else {
|
|
await f.disk.userDelete("b.md");
|
|
await f.syncer.syncLocallyDeletedFile("b.md");
|
|
}
|
|
await f.disk.userWrite("b.md", bytes("replacement"));
|
|
await f.syncer.syncLocallyCreatedFile("b.md");
|
|
};
|
|
const next = structuredClone(f.database.state);
|
|
next.local = { a: "remote-a.md", b: "remote-b.md" };
|
|
await f.files.apply(next);
|
|
assert(changed);
|
|
await f.internals.scan();
|
|
assert.deepEqual(f.disk.userFiles().get("b.md"), bytes("replacement"));
|
|
assert.equal(f.disk.userFiles().has("remote-b.md"), false);
|
|
assert.equal(
|
|
f.database.state.local.b,
|
|
action === "move" ? "private.md" : undefined
|
|
);
|
|
if (action === "move")
|
|
assert.deepEqual(f.disk.userFiles().get("private.md"), bytes("B"));
|
|
});
|
|
|
|
test("a file and nested directory exchange their exact paths", async () => {
|
|
const f = await fixture({ a: "A", "b/deep/child": "B" });
|
|
const [a, b] = Object.keys(f.database.state.local);
|
|
const next = structuredClone(f.database.state);
|
|
next.local = { [a]: "b", [b]: "a/deep/child" };
|
|
await f.files.apply(next);
|
|
assert.deepEqual(f.database.state.local, { [a]: "b", [b]: "a/deep/child" });
|
|
assert.deepEqual(
|
|
f.disk.userFiles(),
|
|
new Map([
|
|
["b", bytes("A")],
|
|
["a/deep/child", bytes("B")]
|
|
])
|
|
);
|
|
});
|