407 lines
15 KiB
TypeScript
407 lines
15 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import { fixture, bytes, head } from "./sync-fixture";
|
|
import { toStoredSnapshot } from "../sync-client/src/sync-operations/content";
|
|
|
|
for (const origin of ["bootstrap", "history recovery"])
|
|
test(`cancelling a replacement retains the ${origin} merge context`, async () => {
|
|
const f = await fixture(
|
|
{ "a.md": "BASE\n" },
|
|
{ getDocumentVersionContent: async () => bytes("REMOTE\nBASE\n") }
|
|
);
|
|
const recoveryBase = await toStoredSnapshot({
|
|
content: bytes("BASE\n")
|
|
});
|
|
if (origin === "bootstrap")
|
|
f.database.state.documents.a.bootstrap = true;
|
|
else f.database.state.documents.a.recoveryBase = recoveryBase;
|
|
await f.database.save();
|
|
await f.settings.setSetting("ignorePatterns", ["private.md"]);
|
|
let injected = false;
|
|
f.disk.boundary = async (label) => {
|
|
if (
|
|
injected ||
|
|
!label.startsWith("before:mkdir:.vault-link-sync/transactions/")
|
|
)
|
|
return;
|
|
injected = true;
|
|
await f.disk.userRename("a.md", "private.md");
|
|
await f.disk.userWrite("private.md", bytes("BASE\nLOCAL\n"));
|
|
await f.syncer.syncLocallyUpdatedFile({
|
|
oldPath: "a.md",
|
|
relativePath: "private.md"
|
|
});
|
|
};
|
|
await f.internals.incorporateContent(head("a", 2, "REMOTE\nBASE\n"));
|
|
assert(injected);
|
|
const deferred = f.database.state.documents.a;
|
|
assert.equal(deferred.base, undefined);
|
|
if (origin === "bootstrap") assert.equal(deferred.bootstrap, true);
|
|
else assert.deepEqual(deferred.recoveryBase, recoveryBase);
|
|
f.disk.boundary = () => {};
|
|
await f.settings.setSetting("ignorePatterns", []);
|
|
await f.internals.incorporateFileManifest(
|
|
f.database.state.fileManifest
|
|
);
|
|
const content = Buffer.from(
|
|
f.disk.userFiles().get("private.md")!
|
|
).toString();
|
|
assert.equal(content.match(/REMOTE/g)?.length, 1);
|
|
assert.equal(content.match(/LOCAL/g)?.length, 1);
|
|
assert.equal(f.database.state.documents.a.base?.vaultUpdateId, 2);
|
|
assert.equal(f.database.state.documents.a.recoveryBase, undefined);
|
|
});
|
|
|
|
for (const timing of ["before", "durable"])
|
|
test(`a save that exceeds the size limit ${timing} staging keeps its exact bytes`, async () => {
|
|
const f = await fixture({ "a.bin": "PUBLIC" });
|
|
await f.settings.setSetting("maxFileSizeMB", 100 / (1024 * 1024));
|
|
const large = "PRIVATE".repeat(100);
|
|
let injected = false;
|
|
f.disk.boundary = async (label) => {
|
|
if (
|
|
injected ||
|
|
!label.startsWith(
|
|
`${timing}:rename:a.bin->.vault-link-sync/transactions/`
|
|
)
|
|
)
|
|
return;
|
|
injected = true;
|
|
await f.disk.userWrite("a.bin", bytes(large));
|
|
await f.syncer.syncLocallyUpdatedFile({ relativePath: "a.bin" });
|
|
};
|
|
await f.files.apply(structuredClone(f.database.state), {
|
|
a: {
|
|
expected: await toStoredSnapshot({ content: bytes("PUBLIC") }),
|
|
replacement: await toStoredSnapshot({
|
|
content: bytes("REMOTE")
|
|
})
|
|
}
|
|
});
|
|
assert(injected);
|
|
assert.equal(
|
|
Buffer.from(f.disk.userFiles().get("a.bin")!).toString(),
|
|
large
|
|
);
|
|
assert.equal(f.database.state.excluded?.a, "a.bin");
|
|
assert.deepEqual(f.database.state.local, { a: "a.bin" });
|
|
});
|
|
|
|
test("an excluded move after installation survives an uncertain journal acknowledgement", async () => {
|
|
const f = await fixture({ "a.md": "PUBLIC" });
|
|
await f.settings.setSetting("ignorePatterns", ["private.md"]);
|
|
let moved = false,
|
|
crashed = false;
|
|
f.disk.boundary = async (label) => {
|
|
if (
|
|
moved ||
|
|
!label.startsWith("durable:rename:") ||
|
|
!label.endsWith(".output->a.md")
|
|
)
|
|
return;
|
|
moved = true;
|
|
await f.disk.userRename("a.md", "private.md");
|
|
await f.disk.userWrite("private.md", bytes("SECRET"));
|
|
await f.syncer.syncLocallyUpdatedFile({
|
|
oldPath: "a.md",
|
|
relativePath: "private.md"
|
|
});
|
|
};
|
|
f.persistence.boundary = (label) => {
|
|
if (!moved || crashed || label !== "durable:save") return;
|
|
crashed = true;
|
|
throw new Error("lost journal acknowledgement");
|
|
};
|
|
await assert.rejects(
|
|
f.files.apply(structuredClone(f.database.state), {
|
|
a: {
|
|
expected: await toStoredSnapshot({ content: bytes("PUBLIC") }),
|
|
replacement: await toStoredSnapshot({
|
|
content: bytes("PUBLIC REMOTE")
|
|
})
|
|
}
|
|
}),
|
|
/lost journal acknowledgement/
|
|
);
|
|
assert(moved && crashed);
|
|
f.disk.boundary = () => {};
|
|
f.persistence.boundary = () => {};
|
|
await f.database.recoverPersistence();
|
|
await f.files.recover();
|
|
assert.deepEqual(f.database.state.local, { a: "private.md" });
|
|
assert.equal(
|
|
Buffer.from(f.disk.userFiles().get("private.md")!).toString(),
|
|
"SECRET"
|
|
);
|
|
});
|
|
|
|
test("moving a tracked file into an ignored path during a journal leaves it protected", async () => {
|
|
const f = await fixture({ "a.md": "PUBLIC", "b.md": "OTHER" });
|
|
await f.settings.setSetting("ignorePatterns", ["private.md"]);
|
|
const next = structuredClone(f.database.state);
|
|
next.local.b = "remote-b.md";
|
|
let injected = false;
|
|
f.disk.boundary = async (label) => {
|
|
if (
|
|
injected ||
|
|
!label.startsWith("before:mkdir:.vault-link-sync/transactions/")
|
|
)
|
|
return;
|
|
injected = true;
|
|
await f.disk.userRename("a.md", "private.md");
|
|
await f.disk.userWrite("private.md", bytes("SECRET"));
|
|
await f.syncer.syncLocallyUpdatedFile({
|
|
oldPath: "a.md",
|
|
relativePath: "private.md"
|
|
});
|
|
};
|
|
await f.files.apply(next);
|
|
assert(injected);
|
|
assert.equal(
|
|
Buffer.from(f.disk.userFiles().get("private.md") ?? []).toString(),
|
|
"SECRET"
|
|
);
|
|
assert.equal(f.database.state.local.a, "private.md");
|
|
assert.equal(f.database.state.excluded?.a, "a.md");
|
|
});
|
|
|
|
test("a directory move protects each ignored descendant during a journal", async () => {
|
|
const f = await fixture({ "folder/a.md": "PUBLIC", "b.md": "OTHER" });
|
|
await f.settings.setSetting("ignorePatterns", ["private/**"]);
|
|
const next = structuredClone(f.database.state);
|
|
next.local.b = "remote-b.md";
|
|
let injected = false;
|
|
f.disk.boundary = async (label) => {
|
|
if (
|
|
injected ||
|
|
!label.startsWith("before:mkdir:.vault-link-sync/transactions/")
|
|
)
|
|
return;
|
|
injected = true;
|
|
await f.disk.userRename("folder/a.md", "private/a.md");
|
|
await f.disk.userWrite("private/a.md", bytes("SECRET"));
|
|
await f.syncer.syncLocallyUpdatedFile({
|
|
oldPath: "folder",
|
|
relativePath: "private"
|
|
});
|
|
};
|
|
await f.files.apply(next);
|
|
assert(injected);
|
|
assert.equal(
|
|
Buffer.from(f.disk.userFiles().get("private/a.md") ?? []).toString(),
|
|
"SECRET"
|
|
);
|
|
assert.equal(f.database.state.local["folder/a"], "private/a.md");
|
|
assert.equal(f.database.state.excluded?.["folder/a"], "folder/a.md");
|
|
});
|
|
|
|
test("a later editor save replaces earlier unsynced text after a merge was prepared", async () => {
|
|
const f = await fixture({ "a.md": "base" });
|
|
const next = structuredClone(f.database.state);
|
|
let saves = 0;
|
|
f.disk.boundary = async (label) => {
|
|
if (saves === 0 && label.startsWith("before:rename:a.md->")) {
|
|
saves++;
|
|
await f.disk.userWrite("a.md", bytes("base ONE"));
|
|
await f.syncer.syncLocallyUpdatedFile({ relativePath: "a.md" });
|
|
} else if (
|
|
saves === 1 &&
|
|
label.startsWith("before:write:") &&
|
|
label.endsWith(".output")
|
|
) {
|
|
saves++;
|
|
await f.disk.userWrite("a.md", bytes("base TWO"));
|
|
await f.syncer.syncLocallyUpdatedFile({ relativePath: "a.md" });
|
|
}
|
|
};
|
|
await f.files.apply(next, {
|
|
a: {
|
|
expected: await toStoredSnapshot({ content: bytes("base") }),
|
|
replacement: await toStoredSnapshot({
|
|
content: bytes("base REMOTE")
|
|
})
|
|
}
|
|
});
|
|
assert.equal(saves, 2);
|
|
assert.equal(
|
|
Buffer.from(f.disk.userFiles().get("a.md")!).toString(),
|
|
"base TWO REMOTE"
|
|
);
|
|
assert.deepEqual(f.database.state.local, { a: "a.md" });
|
|
});
|
|
|
|
test("a content save during remote application remains on the same document", async () => {
|
|
const f = await fixture({ "a.md": "base" });
|
|
f.database.state.documents.a.base = {
|
|
...head("a", 1, "base"),
|
|
hash: f.database.state.documents.a.observedHash!
|
|
};
|
|
const next = structuredClone(f.database.state);
|
|
next.documents.a.base = {
|
|
...head("a", 2, "base REMOTE"),
|
|
hash: (await toStoredSnapshot({ content: bytes("base REMOTE") })).hash
|
|
};
|
|
let injected = false;
|
|
f.disk.boundary = async (label) => {
|
|
if (
|
|
injected ||
|
|
!label.startsWith(
|
|
"durable:rename:a.md->.vault-link-sync/transactions/"
|
|
)
|
|
)
|
|
return;
|
|
injected = true;
|
|
await f.disk.userWrite("a.md", bytes("base LOCAL"));
|
|
await f.syncer.syncLocallyUpdatedFile({ relativePath: "a.md" });
|
|
};
|
|
await f.files.apply(next, {
|
|
a: {
|
|
expected: await toStoredSnapshot({ content: bytes("base") }),
|
|
replacement: await toStoredSnapshot({
|
|
content: bytes("base REMOTE")
|
|
})
|
|
}
|
|
});
|
|
|
|
assert.equal(f.disk.userFiles().size, 1);
|
|
const result = Buffer.from(
|
|
(await f.disk.readSnapshot("a.md"))!.content
|
|
).toString();
|
|
assert(result.includes("LOCAL") && result.includes("REMOTE"));
|
|
});
|
|
|
|
async function setup() {
|
|
const f = await fixture({ "a.md": "ORIGINAL", "b.md": "OTHER" });
|
|
f.database.state.documents.a.base = {
|
|
...head("a", 1, "ORIGINAL"),
|
|
hash: f.database.state.documents.a.observedHash!
|
|
};
|
|
const next = structuredClone(f.database.state);
|
|
next.local = { a: "remote.md", b: "remote-b.md" };
|
|
next.fileManifest = { fileManifestId: 3, entries: { ...next.local } };
|
|
const expected = await toStoredSnapshot({ content: bytes("ORIGINAL") });
|
|
const replacement = await toStoredSnapshot({
|
|
content: bytes("ORIGINAL REMOTE")
|
|
});
|
|
next.documents.a.base = {
|
|
...head("a", 2, "ORIGINAL REMOTE"),
|
|
hash: replacement.hash
|
|
};
|
|
return { f, next, writes: { a: { expected, replacement } } };
|
|
}
|
|
test("editor changes preserve exact bytes and identities at every filesystem read, mutation and persistence boundary", async (t) => {
|
|
const { f, next, writes } = await setup();
|
|
const labels: string[] = [];
|
|
const trace = (l: string) => {
|
|
labels.push(l);
|
|
};
|
|
f.disk.boundary = trace;
|
|
f.persistence.boundary = trace;
|
|
await f.files.apply(next, writes);
|
|
const failures = [];
|
|
let exercised = 0;
|
|
for (const kind of ["move", "move-recreate", "delete-recreate", "save"])
|
|
for (let target = 0; target < labels.length; target++) {
|
|
const { f, next, writes } = await setup();
|
|
let count = 0,
|
|
injected = false,
|
|
from: string | undefined;
|
|
const boundary = async (l: string) => {
|
|
if (injected) return;
|
|
if (count++ !== target) return;
|
|
const files = f.disk.userFiles();
|
|
from = files.has("a.md")
|
|
? "a.md"
|
|
: files.has("remote.md")
|
|
? "remote.md"
|
|
: undefined;
|
|
if (kind === "save" && !from) from = "a.md";
|
|
if (!from) return;
|
|
injected = true;
|
|
exercised++;
|
|
if (kind === "save") {
|
|
await f.disk.userWrite(
|
|
from,
|
|
bytes(
|
|
Buffer.from(
|
|
files.get(from) ?? bytes("ORIGINAL")
|
|
).toString() + " LOCAL"
|
|
)
|
|
);
|
|
await f.syncer.syncLocallyUpdatedFile({
|
|
relativePath: from
|
|
});
|
|
} else if (kind === "delete-recreate") {
|
|
await f.disk.userDelete(from);
|
|
await f.syncer.syncLocallyDeletedFile(from);
|
|
} else {
|
|
await f.disk.userRename(from, "mine.md");
|
|
await f.syncer.syncLocallyUpdatedFile({
|
|
oldPath: from,
|
|
relativePath: "mine.md"
|
|
});
|
|
}
|
|
if (kind !== "move" && kind !== "save") {
|
|
await f.disk.userWrite(from, bytes("REPLACEMENT"));
|
|
await f.syncer.syncLocallyCreatedFile(from);
|
|
}
|
|
};
|
|
f.disk.boundary = boundary;
|
|
f.persistence.boundary = boundary;
|
|
let error;
|
|
try {
|
|
await f.files.apply(next, writes);
|
|
} catch (e) {
|
|
error = String(e);
|
|
}
|
|
f.disk.boundary = () => {};
|
|
f.persistence.boundary = () => {};
|
|
try {
|
|
await f.database.recoverPersistence();
|
|
await f.files.recover();
|
|
await f.internals.scan();
|
|
} catch (e) {
|
|
error += ";retry=" + String(e);
|
|
}
|
|
if (!injected) continue;
|
|
const fs = Object.fromEntries(
|
|
[...f.disk.userFiles()].map(([p, b]) => [
|
|
p,
|
|
Buffer.from(b).toString()
|
|
])
|
|
);
|
|
const bad =
|
|
kind === "save"
|
|
? ![
|
|
"ORIGINAL LOCAL REMOTE",
|
|
"ORIGINAL REMOTE LOCAL"
|
|
].includes(fs[f.database.state.local.a]) ||
|
|
Object.keys(f.database.state.local).length !== 2
|
|
: kind === "delete-recreate"
|
|
? f.database.state.local.a !== undefined
|
|
: f.database.state.local.a !== "mine.md" ||
|
|
fs["mine.md"] !== "ORIGINAL REMOTE";
|
|
if (
|
|
bad ||
|
|
(kind !== "move" &&
|
|
kind !== "save" &&
|
|
fs[from!] !== "REPLACEMENT")
|
|
)
|
|
failures.push({
|
|
kind,
|
|
target,
|
|
label: labels[target],
|
|
error,
|
|
ids: f.database.state.local,
|
|
files: fs
|
|
});
|
|
}
|
|
assert(
|
|
exercised > 50,
|
|
"The sweep must actually exercise the injected actions"
|
|
);
|
|
t.diagnostic(
|
|
`${labels.length} boundaries; ${exercised} editor interleavings`
|
|
);
|
|
assert.deepEqual(failures, []);
|
|
});
|