This commit is contained in:
Andras Schmelczer 2026-09-27 11:09:36 +01:00
commit ff1f474324

View file

@ -1,7 +1,8 @@
import { SyncPhase } from "../sync-client/src/sync-operations/sync-scheduler";
import assert from "node:assert/strict";
import { PermanentSyncError } from "../sync-client/src/errors/errors";
import { test } from "node:test";
import { Database, emptyState } from "../sync-client/src/persistence/database";
import { Database, createEmptyDatabase } 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 { hashSnapshot } from "../sync-client/src/snapshot";
@ -13,7 +14,7 @@ import { bytes, head, fixture } from "./sync-fixture";
test("offline content notifications need no saves and restart reads the current bytes", async () => {
const disk = new MemoryDisk();
await disk.userWrite("a.md", bytes("original"));
const initial = emptyState(JSON.stringify(["http://offline.test", "test"]));
const initial = createEmptyDatabase(JSON.stringify(["http://offline.test", "test"]));
initial.lastSeenUpdateId = 0;
initial.local = { a: "a.md" };
initial.documents.a = {
@ -44,14 +45,14 @@ test("offline content notifications need no saves and restart reads the current
const internals = client as unknown as {
database: Database;
syncer: {
stopped: boolean;
scan(): Promise<void>;
preparePush(): Promise<boolean>;
scheduler: { phase: SyncPhase };
refreshLocalFiles(): Promise<void>;
prepareNextUpload(): Promise<boolean>;
};
};
await internals.syncer.scan();
internals.syncer.stopped = false;
assert.equal(await internals.syncer.preparePush(), true);
await internals.syncer.refreshLocalFiles();
internals.syncer.scheduler.phase = SyncPhase.Idle;
assert.equal(await internals.syncer.prepareNextUpload(), true);
const pending = internals.database.state.pending;
assert(pending?.type === "content");
assert.equal(pending.documentId, "a");
@ -71,7 +72,7 @@ test("namespace notifications preserve the acknowledged prefix on restart", asyn
const disk = new MemoryDisk();
await disk.userWrite("d.md", bytes("A"));
await disk.userWrite("b.md", bytes("B"));
const initial = emptyState(JSON.stringify(["http://offline.test", "test"]));
const initial = createEmptyDatabase(JSON.stringify(["http://offline.test", "test"]));
initial.lastSeenUpdateId = 0;
initial.local = { a: "c.md", b: "b.md" };
initial.fileManifest.entries = { ...initial.local };
@ -111,9 +112,9 @@ test("namespace notifications preserve the acknowledged prefix on restart", asyn
try {
const internals = client as unknown as {
database: Database;
syncer: { scan(): Promise<void> };
syncer: { refreshLocalFiles(): Promise<void> };
};
await internals.syncer.scan();
await internals.syncer.refreshLocalFiles();
assert.deepEqual(internals.database.state.local, {
a: "d.md",
b: "b.md"
@ -129,12 +130,12 @@ test("namespace notifications preserve the acknowledged prefix on restart", asyn
test("remote binary replacement and deletion do not create backup artifacts", async () => {
const f = await fixture({ "a.bin": "private edit" });
await f.files.apply(structuredClone(f.database.state), {
await f.files.applyChanges(structuredClone(f.database.state), {
a: { replacement: await hashSnapshot({ content: bytes("remote") }) }
});
assert.deepEqual(await f.disk.listFilesRecursively(), ["a.bin"]);
assert.deepEqual(f.disk.userFiles(), new Map([["a.bin", bytes("remote")]]));
await f.internals.reconcile({
await f.internals.applyRemoteChanges({
fileManifestId: 2,
entries: {}
});
@ -150,7 +151,7 @@ test("rejected content recovery remains idempotent across a rename and network f
return bytes("base");
}
});
const snapshot = (await f.files.snapshot("a.md"))!;
const snapshot = (await f.files.readSnapshot("a.md"))!;
f.database.state.documents.a.base = {
...head("a", 1, "base"),
hash: (await hashSnapshot({ content: bytes("base") })).hash
@ -171,9 +172,9 @@ test("rejected content recovery remains idempotent across a rename and network f
oldPath: "a.md",
relativePath: "renamed.md"
});
await assert.rejects(f.internals.finishPending(), /offline/);
await assert.rejects(f.internals.uploads.completePendingUpload(), /offline/);
offline = false;
await f.internals.finishPending();
await f.internals.uploads.completePendingUpload();
assert.equal(f.database.state.pending, undefined);
assert.equal(f.database.state.local.a, "renamed.md");
});
@ -203,7 +204,7 @@ test("an unmaterialized file with a directory at its path is deconflicted", asyn
f.database.state.fileManifest.entries.a = "folder";
await f.database.save();
f.database.state.documents.a.remote = head("a", 2, "remote");
await f.internals.reconcile(f.database.state.fileManifest);
await f.internals.applyRemoteChanges(f.database.state.fileManifest);
assert(
[...f.disk.userFiles().values()].some(
(value) => Buffer.from(value).toString() === "remote"
@ -219,7 +220,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.reconcile({
await f.internals.applyRemoteChanges({
fileManifestId: 2,
entries: {}
});
@ -227,8 +228,8 @@ test("ignored materialized content survives remote deletion", async () => {
assert.deepEqual(f.database.state.fileManifest.entries, {});
});
test("unfinished bootstrap cannot be rebound to another vault", async () => {
const database = emptyState(JSON.stringify(["http://example.test", "old"]));
test("unfinished initializeFromServerSnapshot cannot be rebound to another vault", async () => {
const database = createEmptyDatabase(JSON.stringify(["http://example.test", "old"]));
// A scan may commit local identities before the first remote application.
database.local = { a: "a.md" };
database.documents.a = { observedHash: "" };
@ -279,7 +280,7 @@ test("a rejected upload pauses all sync across restarts until an explicit retry"
const reads = eventReads;
await f.disk.userWrite("a.md", bytes("small"));
await f.syncer.syncLocallyUpdatedFile({ relativePath: "a.md" });
f.syncer.wake();
f.syncer.requestSync();
await assert.rejects(f.syncer.waitUntilFinished(), /413/);
assert.equal(eventReads, reads);
assert.equal(sent.length, 1);
@ -292,7 +293,7 @@ test("a rejected upload pauses all sync across restarts until an explicit retry"
assert.equal(sent.length, 1);
reject = false;
await f.syncer.retryRejectedRequests();
f.syncer.wake();
f.syncer.requestSync();
await f.syncer.waitUntilFinished();
assert.deepEqual(sent.map(({ id }) => id), ["a", "a", "a", "b"]);
assert.deepEqual(sent[1], sent[0], "resolve the old request before uploading corrected bytes");
@ -311,11 +312,11 @@ test("a transient upload failure retries the exact request and recovers its ackn
return { type: "Accepted", ...head("a", 2, "local") };
}
});
f.internals.stopped = false;
await f.internals.preparePush();
await assert.rejects(f.internals.finishPending(), /lost upload reply/);
f.internals.scheduler.phase = SyncPhase.Idle;
await f.internals.prepareNextUpload();
await assert.rejects(f.internals.uploads.completePendingUpload(), /lost upload reply/);
assert(f.database.state.pending);
await f.internals.finishPending();
await f.internals.uploads.completePendingUpload();
assert.equal(requests.length, 2);
assert.deepEqual(requests[1], requests[0]);
assert.equal(f.database.state.documents.a.base!.vaultUpdateId, 2);
@ -330,13 +331,13 @@ test("a persisted upload rejection pauses without filesystem writes", async () =
throw new PermanentSyncError("HTTP 413");
}
});
f.internals.stopped = false;
await f.internals.preparePush();
f.internals.scheduler.phase = SyncPhase.Idle;
await f.internals.prepareNextUpload();
f.disk.boundary = (label) => {
if (!label.startsWith("read:"))
throw new Error("Unexpected filesystem mutation");
};
await assert.rejects(f.internals.finishPending(), /413/);
await assert.rejects(f.internals.uploads.completePendingUpload(), /413/);
assert.equal(uploads, 1);
assert.equal(f.database.state.pending?.rejection, "HTTP 413");
assert.deepEqual(f.disk.userFiles(), new Map([["a.md", bytes("local")]]));
@ -351,16 +352,16 @@ test("an explicit retry recovers an earlier accepted upload after a permanent re
return { type: "Accepted", ...head("a", 2, "local") };
}
});
f.internals.stopped = false;
await f.internals.preparePush();
await assert.rejects(f.internals.finishPending(), /413/);
f.internals.scheduler.phase = SyncPhase.Idle;
await f.internals.prepareNextUpload();
await assert.rejects(f.internals.uploads.completePendingUpload(), /413/);
await assert.rejects(f.syncer.waitUntilFinished(), /413/);
await f.syncer.retryRejectedRequests();
await f.internals.finishPending();
await f.internals.uploads.completePendingUpload();
assert.deepEqual(requests[1], requests[0]);
assert.equal(f.database.state.documents.a.base?.vaultUpdateId, 2);
assert.equal(f.database.state.pending, undefined);
assert.equal(await f.internals.preparePush(), false);
assert.equal(await f.internals.prepareNextUpload(), false);
await f.syncer.waitUntilFinished();
assert.deepEqual(f.disk.userFiles(), new Map([["a.md", bytes("local")]]));
});
@ -390,22 +391,22 @@ test("rejected manifests are resolved before publishing later namespace changes"
base: { ...head("a", 1, "x"), hash: "hash" }
};
await f.database.save();
await assert.rejects(f.internals.finishPending(), /413/);
f.internals.stopped = false;
assert.equal(await f.internals.preparePush(), false);
await assert.rejects(f.internals.uploads.completePendingUpload(), /413/);
f.internals.scheduler.phase = SyncPhase.Idle;
assert.equal(await f.internals.prepareNextUpload(), false);
f.database.state.local = {};
reject = false;
await f.syncer.retryRejectedRequests();
await f.internals.finishPending();
await f.internals.uploads.completePendingUpload();
assert.deepEqual(f.database.state.fileManifest.entries, { a: "new.md" });
assert(await f.internals.preparePush());
await f.internals.finishPending();
assert(await f.internals.prepareNextUpload());
await f.internals.uploads.completePendingUpload();
assert.deepEqual(f.database.state.fileManifest.entries, {});
});
test("offline delete/recreate identities survive restarting before any network connection", async () => {
const key = JSON.stringify(["http://offline.test", "test"]);
const initial = emptyState(key);
const initial = createEmptyDatabase(key);
initial.lastSeenUpdateId = 0;
initial.local.a = "a.md";
initial.documents.a = { observedHash: "" };
@ -425,10 +426,10 @@ test("offline delete/recreate identities survive restarting before any network c
const second = await SyncClient.create({ fs: disk, persistence });
try {
const internals = second as unknown as {
syncer: { scan(): Promise<void> };
syncer: { refreshLocalFiles(): Promise<void> };
database: Database;
};
await internals.syncer.scan();
await internals.syncer.refreshLocalFiles();
assert.equal(internals.database.state.local.a, undefined);
assert.equal(
Object.values(internals.database.state.local).filter(
@ -466,12 +467,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.reconcile({
await f.internals.applyRemoteChanges({
fileManifestId: 2,
entries: {}
});
await f.settings.setSettings({ ignorePatterns: [] });
await f.internals.reconcile(f.database.state.fileManifest);
await f.internals.applyRemoteChanges(f.database.state.fileManifest);
assert.equal(f.disk.userFiles().size, 0);
assert.deepEqual(f.database.state.local, {});
});
@ -479,7 +480,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.reconcile({
await f.internals.applyRemoteChanges({
fileManifestId: 2,
entries: { a: "c.md", b: "a.md" }
});
@ -490,8 +491,8 @@ test("an ignored file does not move when a remote rename targets its path", asyn
["b.md", bytes("B")]
])
);
f.internals.stopped = false;
assert.equal(await f.internals.preparePush(), false);
f.internals.scheduler.phase = SyncPhase.Idle;
assert.equal(await f.internals.prepareNextUpload(), false);
assert.deepEqual(
f.disk.userFiles(),
new Map([
@ -522,7 +523,7 @@ for (const kind of ["path", "content"] as const) {
relativePath: "c.md"
});
}
await f.internals.incorporateEventBatch({
await f.internals.applyEventBatch({
headEventId: 3,
endEventId: 3,
events: [2, 3].map((id) =>
@ -622,15 +623,15 @@ test("ignoring a locally renamed file defers its move without forgetting the loc
relativePath: "ignored.md"
});
await f.settings.setSettings({ ignorePatterns: ["ignored.md"] });
await f.internals.scan();
f.internals.stopped = false;
assert.equal(await f.internals.preparePush(), false);
await f.internals.reconcile({
await f.internals.refreshLocalFiles();
f.internals.scheduler.phase = SyncPhase.Idle;
assert.equal(await f.internals.prepareNextUpload(), false);
await f.internals.applyRemoteChanges({
fileManifestId: 2,
entries: { a: "a.md" }
});
await f.settings.setSettings({ ignorePatterns: [] });
await f.internals.reconcile(f.database.state.fileManifest);
await f.internals.applyRemoteChanges(f.database.state.fileManifest);
assert.equal(f.database.state.local.a, "ignored.md");
assert.equal(
Buffer.from(
@ -645,25 +646,25 @@ test("a notified deletion of an ignored file does not delete its server membersh
await f.settings.setSettings({ ignorePatterns: ["a.md"] });
await f.disk.userDelete("a.md");
await f.syncer.syncLocallyDeletedFile("a.md");
await f.internals.scan();
f.internals.stopped = false;
assert.equal(await f.internals.preparePush(), false);
await f.internals.refreshLocalFiles();
f.internals.scheduler.phase = SyncPhase.Idle;
assert.equal(await f.internals.prepareNextUpload(), false);
assert.equal(f.database.state.local.a, undefined);
await f.settings.setSettings({ ignorePatterns: [] });
await f.internals.reconcile(f.database.state.fileManifest);
assert(await f.internals.preparePush());
await f.internals.applyRemoteChanges(f.database.state.fileManifest);
assert(await f.internals.prepareNextUpload());
assert.equal(f.database.state.pending?.type, "fileManifest");
});
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.reconcile({
await f.internals.applyRemoteChanges({
fileManifestId: 2,
entries: { a: "b.md", other: "a.md" }
});
f.internals.stopped = false;
await f.internals.preparePush();
f.internals.scheduler.phase = SyncPhase.Idle;
await f.internals.prepareNextUpload();
assert.deepEqual(f.disk.userFiles(), new Map([["a.md", bytes("local")]]));
assert.equal(
new Set(Object.values(f.database.state.local)).size,
@ -677,9 +678,9 @@ test("rejected content is reported to callers and an explicit retry clears the r
throw new PermanentSyncError("HTTP 413");
}
});
f.internals.stopped = false;
await f.internals.preparePush();
await assert.rejects(f.internals.finishPending(), /413/);
f.internals.scheduler.phase = SyncPhase.Idle;
await f.internals.prepareNextUpload();
await assert.rejects(f.internals.uploads.completePendingUpload(), /413/);
await assert.rejects(f.syncer.waitUntilFinished(), /413/);
assert(f.syncer.isBusy);
await f.syncer.retryRejectedRequests();
@ -689,7 +690,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);
const initial = createEmptyDatabase(key);
initial.lastSeenUpdateId = 0;
const disk = new MemoryDisk();
const store = new MemoryPersistence({
@ -784,7 +785,7 @@ for (const edited of [false, true]) {
});
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();
await (f.syncer as unknown as { initializeFromServerSnapshot(): Promise<void> }).initializeFromServerSnapshot();
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");