This commit is contained in:
Andras Schmelczer 2026-09-26 20:22:16 +01:00
commit b07aa589ff

View file

@ -4,7 +4,7 @@ import { test } from "node:test";
import { Database, emptyState } from "../sync-client/src/persistence/database";
import { Syncer } from "../sync-client/src/sync-operations/syncer";
import { SyncClient, type StoredClient } from "../sync-client/src/sync-client";
import { toStoredSnapshot } from "../sync-client/src/sync-operations/content";
import { toStoredSnapshot } from "../sync-client/src/snapshot";
import { allocatePortablePath } from "../sync-client/src/utils/portable-path";
import { MemoryDisk, MemoryPersistence } from "./storage";
@ -14,10 +14,10 @@ test("offline content notifications need no saves and restart reads the current
const disk = new MemoryDisk();
await disk.userWrite("a.md", bytes("original"));
const initial = emptyState(JSON.stringify(["http://offline.test", "test"]));
initial.initialized = true;
initial.lastSeenUpdateId = 0;
initial.local = { a: "a.md" };
initial.documents.a = {
materialized: true,
observedHash: (await toStoredSnapshot({ content: bytes("original") }))
.hash
};
@ -70,7 +70,7 @@ test("legacy content notifications preserve the acknowledged namespace prefix on
await disk.userWrite("d.md", bytes("A"));
await disk.userWrite("b.md", bytes("B"));
const initial = emptyState(JSON.stringify(["http://offline.test", "test"]));
initial.initialized = true;
initial.lastSeenUpdateId = 0;
initial.local = { a: "c.md", b: "b.md" };
initial.fileManifest.entries = { ...initial.local };
for (const [id, content] of [
@ -78,7 +78,7 @@ test("legacy content notifications preserve the acknowledged namespace prefix on
["b", "B"]
])
initial.documents[id] = {
materialized: true,
observedHash: (await toStoredSnapshot({ content: bytes(content) }))
.hash
};
@ -134,7 +134,7 @@ test("remote binary replacement and deletion do not create backup artifacts", as
});
assert.deepEqual(await f.disk.listFilesRecursively(), ["a.bin"]);
assert.deepEqual(f.disk.userFiles(), new Map([["a.bin", bytes("remote")]]));
await f.internals.incorporateFileManifest({
await f.internals.reconcile({
fileManifestId: 2,
entries: {}
});
@ -202,10 +202,11 @@ test("an unmaterialized file with a directory at its path is deconflicted", asyn
);
await f.disk.userWrite("folder/note.md", bytes("local"));
f.database.state.local.a = "folder";
f.database.state.documents.a = { materialized: false };
f.database.state.documents.a = {};
f.database.state.fileManifest.entries.a = "folder";
await f.database.save();
await f.internals.incorporateContent(head("a", 2, "remote"));
f.database.state.documents.a.remote = head("a", 2, "remote");
await f.internals.reconcile(f.database.state.fileManifest);
assert(
[...f.disk.userFiles().values()].some(
(value) => Buffer.from(value).toString() === "remote"
@ -221,7 +222,7 @@ test("an unmaterialized file with a directory at its path is deconflicted", asyn
test("ignored materialized content survives remote deletion", async () => {
const f = await fixture();
await f.settings.setSettings({ ignorePatterns: ["a.md"] });
await f.internals.incorporateFileManifest({
await f.internals.reconcile({
fileManifestId: 2,
entries: {}
});
@ -231,11 +232,9 @@ test("ignored materialized content survives remote deletion", async () => {
test("unfinished bootstrap cannot be rebound to another vault", async () => {
const database = emptyState(JSON.stringify(["http://example.test", "old"]));
database.bootstrap = {
headEventId: 1,
fileManifest: { fileManifestId: 1, entries: {} },
documents: []
};
// A scan may commit local identities before the first remote application.
database.local = { a: "a.md" };
database.documents.a = { observedHash: "" };
const persistence = new MemoryPersistence({
settings: { remoteUri: "http://example.test", vaultName: "old" },
database
@ -276,7 +275,7 @@ test("a rejected upload is retained locally and does not block another file or a
assert(await f.internals.preparePush());
await f.internals.finishPending();
assert.equal(f.database.state.pending, undefined);
assert.match(f.database.state.documents.a.rejected!.message, /413/);
assert.match(f.database.state.unconfirmed![0].rejection!, /413/);
assert(await f.internals.preparePush());
await f.internals.finishPending();
assert.deepEqual(sent, ["a", "b"]);
@ -286,7 +285,7 @@ test("a rejected upload is retained locally and does not block another file or a
assert(await f.internals.preparePush());
await f.internals.finishPending();
assert.deepEqual(sent, ["a", "b", "a"]);
assert.equal(f.database.state.documents.a.rejected, undefined);
assert.equal(f.database.state.unconfirmed?.find((request) => request.rejection), undefined);
});
test("a transient upload failure retries the exact request and recovers its acknowledgement", async () => {
@ -306,7 +305,7 @@ test("a transient upload failure retries the exact request and recovers its ackn
assert.equal(requests.length, 2);
assert.deepEqual(requests[1], requests[0]);
assert.equal(f.database.state.documents.a.base!.vaultUpdateId, 2);
assert.equal(f.database.state.documents.a.rejected, undefined);
assert.equal(f.database.state.unconfirmed?.find((request) => request.rejection), undefined);
assert.equal(f.database.state.pending, undefined);
});
@ -327,7 +326,7 @@ test("a persisted upload rejection finishes without filesystem writes", async ()
await f.internals.finishPending();
assert.equal(uploads, 1);
assert.equal(f.database.state.pending, undefined);
assert.equal(f.database.state.documents.a.rejected?.message, "HTTP 413");
assert.equal(f.database.state.unconfirmed![0].rejection, "HTTP 413");
assert.deepEqual(f.disk.userFiles(), new Map([["a.md", bytes("local")]]));
});
@ -388,7 +387,6 @@ test("rejected manifests can be replaced after editing the local namespace", asy
};
f.database.state.local = { a: "new.md" };
f.database.state.documents.a = {
materialized: false,
base: { ...head("a", 1, "x"), hash: "hash" }
};
await f.database.save();
@ -405,9 +403,9 @@ test("rejected manifests can be replaced after editing the local namespace", asy
test("offline delete/recreate identities survive restarting before any network connection", async () => {
const key = JSON.stringify(["http://offline.test", "test"]);
const initial = emptyState(key);
initial.initialized = true;
initial.lastSeenUpdateId = 0;
initial.local.a = "a.md";
initial.documents.a = { materialized: true };
initial.documents.a = { observedHash: "" };
initial.fileManifest = { fileManifestId: 1, entries: { a: "a.md" } };
const disk = new MemoryDisk();
await disk.userWrite("a.md", bytes("same"));
@ -465,12 +463,12 @@ test("offline notifications bind an otherwise empty client to its vault", async
test("unignoring a deleted file resumes the deferred remote deletion without resurrection", async () => {
const f = await fixture();
await f.settings.setSettings({ ignorePatterns: ["a.md"] });
await f.internals.incorporateFileManifest({
await f.internals.reconcile({
fileManifestId: 2,
entries: {}
});
await f.settings.setSettings({ ignorePatterns: [] });
await f.internals.incorporateFileManifest(f.database.state.fileManifest);
await f.internals.reconcile(f.database.state.fileManifest);
assert.equal(f.disk.userFiles().size, 0);
assert.deepEqual(f.database.state.local, {});
});
@ -478,7 +476,7 @@ test("unignoring a deleted file resumes the deferred remote deletion without res
test("an ignored file does not move when a remote rename targets its path", async () => {
const f = await fixture({ "a.md": "A", "b.md": "B" });
await f.settings.setSettings({ ignorePatterns: ["a.md"] });
await f.internals.incorporateFileManifest({
await f.internals.reconcile({
fileManifestId: 2,
entries: { a: "c.md", b: "a.md" }
});
@ -623,12 +621,12 @@ test("ignoring a locally renamed file defers its move without forgetting the loc
await f.internals.scan();
f.internals.stopped = false;
assert.equal(await f.internals.preparePush(), false);
await f.internals.incorporateFileManifest({
await f.internals.reconcile({
fileManifestId: 2,
entries: { a: "a.md" }
});
await f.settings.setSettings({ ignorePatterns: [] });
await f.internals.incorporateFileManifest(f.database.state.fileManifest);
await f.internals.reconcile(f.database.state.fileManifest);
assert.equal(f.database.state.local.a, "ignored.md");
assert.equal(
Buffer.from(
@ -648,7 +646,7 @@ test("a notified deletion of an ignored file does not delete its server membersh
assert.equal(await f.internals.preparePush(), false);
assert.equal(f.database.state.local.a, undefined);
await f.settings.setSettings({ ignorePatterns: [] });
await f.internals.incorporateFileManifest(f.database.state.fileManifest);
await f.internals.reconcile(f.database.state.fileManifest);
assert(await f.internals.preparePush());
assert.equal(f.database.state.pending?.type, "fileManifest");
});
@ -656,7 +654,7 @@ test("a notified deletion of an ignored file does not delete its server membersh
test("an incoming ignored document cannot force an existing ignored file to move", async () => {
const f = await fixture();
await f.settings.setSettings({ ignorePatterns: ["a.md", "*conflict*"] });
await f.internals.incorporateFileManifest({
await f.internals.reconcile({
fileManifestId: 2,
entries: { a: "b.md", other: "a.md" }
});
@ -687,7 +685,7 @@ test("rejected content is reported to callers and an explicit retry clears the r
test("notification saves cannot roll back an uncertain engine commit, even if the first reload fails", async () => {
const key = JSON.stringify(["http://offline.test", "test"]);
const initial = emptyState(key);
initial.initialized = true;
initial.lastSeenUpdateId = 0;
const disk = new MemoryDisk();
const store = new MemoryPersistence({
database: initial,
@ -755,3 +753,40 @@ test(
assert(notified);
}
);
for (const edited of [false, true]) {
test(`hash-only history recovery preserves ${edited ? "an edit made after reset" : "a clean file"} across restart`, async () => {
const remote = head("a", 2, "restored");
const service = {
getVaultSnapshot: async () => ({
headEventId: 3,
fileManifest: { fileManifestId: 3, entries: { a: "a.md" } },
documents: [remote]
}),
getDocumentVersionContent: async ({ vaultUpdateId }: { vaultUpdateId: number }) => {
assert.equal(vaultUpdateId, 2, "never read a version from the abandoned history");
return bytes("restored");
}
};
let f = await fixture({ "a.md": "old branch" }, service);
const hash = f.database.state.documents.a.observedHash!;
f.database.state.documents.a.base = { vaultUpdateId: 50, hash };
await (f.syncer as unknown as { recoverServerHistory(): Promise<void> }).recoverServerHistory();
assert.equal(f.database.state.lastSeenUpdateId, undefined);
assert.deepEqual(f.database.state.documents.a, {
observedHash: hash,
base: { vaultUpdateId: null, hash }
});
if (edited) await f.disk.userWrite("a.md", bytes("my new edit"));
f = await fixture({}, service, f);
await (f.syncer as unknown as { bootstrap(): Promise<void> }).bootstrap();
assert.equal(f.database.state.lastSeenUpdateId, 3);
assert.deepEqual((await f.disk.read("a.md"))?.content, bytes("restored"));
const other = Object.keys(f.database.state.local).filter((id) => id !== "a");
assert.equal(other.length, edited ? 1 : 0);
if (edited) {
assert.deepEqual((await f.disk.read(f.database.state.local[other[0]]))?.content, bytes("my new edit"));
assert.equal(f.database.state.documents[other[0]].base, undefined);
}
});
}