vault-link/frontend/test-support/file-application.test.ts
2026-09-27 15:59:35 +01:00

354 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import assert from "node:assert/strict";
import { test } from "node:test";
import { fixture, bytes } from "./sync-fixture";
import { hashSnapshot } from "../sync-client/src/snapshot";
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.applyChanges(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.applyChanges(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.applyChanges(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.readSnapshot("a.md"))!;
await f.disk.userWrite("a.md", bytes("base\nlocal\n"));
await f.files.applyChanges(structuredClone(f.database.state), {
a: {
expected,
replacement: await hashSnapshot({
content: bytes("remote\nbase\n")
})
}
});
assert.deepEqual(
f.disk.userFiles().get("a.md"),
bytes("remote\nbase\nlocal\n")
);
});
test("repeated large text replacements reuse WebAssembly memory", async () => {
// The replacement from fuzz-193329 exhausted reconcile-text 0.8.0's
// allocator over repeated merges, eventually stalling sync and trapping.
const original = (
" مرحباéשלום\t日本語\ncaféalphaalphaनमस्ते🦊🌍café\n" +
"🌍مرحبامرحباनमस्तेשלום\talphaalphacaféمرحبا🌍\r\n日本語مرحبا"
).repeat(128);
const replacement = await hashSnapshot({
content: bytes(
(
"alpha🌍éनमस्ते日本語שלום\uFEFFשלום\t\r\n" +
"\uFEFF🦊 🦊🌍नमस्ते\uFEFFéمرحبا\r\ncafé🌍é🌍"
).repeat(128)
)
});
const f = await fixture({ "a.md": original });
const expected = (await f.files.readSnapshot("a.md"))!;
const apply = async () => {
await f.disk.userWrite("a.md", bytes(original));
await f.files.applyChanges(structuredClone(f.database.state), {
a: { expected, replacement }
});
assert.deepEqual(f.disk.userFiles().get("a.md"), replacement.content);
assert.equal(
f.database.state.documents.a.observedHash,
replacement.hash
);
};
await apply(); // Warm up the module and its allocator before measuring.
const warmedMemory = process.memoryUsage.rss();
for (let repeat = 0; repeat < 4; repeat++) {
await apply();
// Measure resident memory because V8's external counter can omit WASM
// memory after GC. The old allocator grew ~150 MiB on every call.
const growth = process.memoryUsage.rss() - warmedMemory;
assert(
growth < 64 * 1024 * 1024,
`Resident memory grew by ${growth} bytes`
);
}
});
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.applyChanges(next, {
a: { replacement: await hashSnapshot({ 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.applyChanges(next, {
a: {
replacement: await hashSnapshot({
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.applyChanges(next),
LocalChangesDuringReconciliation
);
else await f.files.applyChanges(next);
assert(changed);
await f.internals.refreshLocalFiles();
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.applyChanges(next);
assert(changed);
await f.internals.refreshLocalFiles();
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.applyChanges(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")]
])
);
});
for (const phase of ["before", "durable"])
test(`retrying after a ${phase}-save failure does not merge text twice`, async () => {
const f = await fixture({ "a.md": "base\n" });
const original = structuredClone(f.database.state);
const write = {
expected: (await f.files.readSnapshot("a.md"))!,
replacement: await hashSnapshot({
content: bytes("remote\nbase\n")
})
};
await f.disk.userWrite("a.md", bytes("base\nlocal\n"));
f.persistence.boundary = (label) => {
if (label === `${phase}:save`) {
assert.deepEqual(
f.disk.userFiles().get("a.md"),
bytes("remote\nbase\nlocal\n")
);
throw new Error("save failed");
}
};
await assert.rejects(
f.files.applyChanges(structuredClone(original), { a: write }),
/save failed/
);
f.persistence.boundary = () => {};
await f.database.recoverPersistence();
await f.files.applyChanges(structuredClone(original), { a: write });
assert.deepEqual(
f.disk.userFiles().get("a.md"),
bytes("remote\nbase\nlocal\n")
);
assert.equal(
f.database.state.documents.a.observedHash,
(await f.files.readSnapshot("a.md"))!.hash
);
});
test("atomic updater includes an edit made after the last snapshot without unlinking the file", async () => {
const f = await fixture({ "a.md": "base\n" });
const expected = (await f.files.readSnapshot("a.md"))!;
let edited = false;
f.disk.boundary = async (label) => {
assert(
!label.includes("unlink:"),
"Updating text must not delete the file"
);
if (label === "before:write:a.md" && !edited) {
edited = true;
await f.disk.userWrite("a.md", bytes("base\nlatest edit\n"));
}
};
await f.files.applyChanges(structuredClone(f.database.state), {
a: {
expected,
replacement: await hashSnapshot({
content: bytes("remote\nbase\n")
})
}
});
assert(edited);
assert.deepEqual(
f.disk.userFiles().get("a.md"),
bytes("remote\nbase\nlatest edit\n")
);
assert.equal(
f.database.state.documents.a.observedHash,
(await f.files.readSnapshot("a.md"))!.hash
);
});
test("a failed atomic update leaves database metadata unchanged", async () => {
const f = await fixture();
const original = structuredClone(f.database.state);
f.disk.boundary = (label) => {
if (label === "before:write:a.md") throw new Error("update failed");
};
await assert.rejects(
f.files.applyChanges(structuredClone(original), {
a: {
expected: (await f.files.readSnapshot("a.md"))!,
replacement: await hashSnapshot({
content: bytes("remote")
})
}
}),
/update failed/
);
assert.deepEqual(f.database.state, original);
assert.deepEqual(f.persistence.snapshot().database, original);
assert.deepEqual(f.disk.userFiles().get("a.md"), bytes("local"));
});