vault-link/frontend/test-support/engine-regressions.test.ts
2026-09-27 11:09:36 +01:00

798 lines
31 KiB
TypeScript

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, 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";
import { allocatePortablePath } from "../sync-client/src/utils/portable-path";
import { MemoryDisk, MemoryPersistence } from "./storage";
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 = createEmptyDatabase(JSON.stringify(["http://offline.test", "test"]));
initial.lastSeenUpdateId = 0;
initial.local = { a: "a.md" };
initial.documents.a = {
observedHash: (await hashSnapshot({ content: bytes("original") }))
.hash
};
const store = new MemoryPersistence({
database: initial,
settings: { remoteUri: "http://offline.test", vaultName: "test" }
});
let client = await SyncClient.create({ fs: disk, persistence: store });
try {
const saved = store.snapshot();
store.boundary = () => {
throw new Error("content notifications must not save");
};
for (let edit = 0; edit < 20; edit++) {
await disk.userWrite("a.md", bytes(`edit ${edit}`));
await client.syncLocallyUpdatedFile({ relativePath: "a.md" });
}
await client.destroy();
assert.deepEqual(store.snapshot(), saved);
store.boundary = () => { };
// A final edit has no notification at all.
await disk.userWrite("a.md", bytes("latest offline bytes"));
client = await SyncClient.create({ fs: disk, persistence: store });
const internals = client as unknown as {
database: Database;
syncer: {
scheduler: { phase: SyncPhase };
refreshLocalFiles(): Promise<void>;
prepareNextUpload(): Promise<boolean>;
};
};
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");
assert.equal(pending.request.content.type, "Snapshot");
assert(pending.request.content.type === "Snapshot");
assert.equal(
Buffer.from(pending.request.content.value, "base64").toString(),
"latest offline bytes"
);
} finally {
store.boundary = () => { };
await client.destroy();
}
});
test("namespace notifications preserve the acknowledged prefix on restart", async () => {
const disk = new MemoryDisk();
await disk.userWrite("d.md", bytes("A"));
await disk.userWrite("b.md", bytes("B"));
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 };
for (const [id, content] of [
["a", "A"],
["b", "B"]
])
initial.documents[id] = {
observedHash: (await hashSnapshot({ content: bytes(content) }))
.hash
};
initial.lastAppliedLocalChangeId = "consumed-move";
const saved: StoredClient = {
database: initial,
settings: { remoteUri: "http://offline.test", vaultName: "test" },
localChangesVaultKey: initial.vaultKey,
localChanges: [
{
type: "move",
oldPath: "a.md",
relativePath: "b.md",
identities: { a: "a.md" },
changeId: "consumed-move"
},
{
type: "move",
oldPath: "c.md",
relativePath: "d.md",
identities: { a: "c.md" },
changeId: "new-move"
}
]
};
const store = new MemoryPersistence(saved);
const client = await SyncClient.create({ fs: disk, persistence: store });
try {
const internals = client as unknown as {
database: Database;
syncer: { refreshLocalFiles(): Promise<void> };
};
await internals.syncer.refreshLocalFiles();
assert.deepEqual(internals.database.state.local, {
a: "d.md",
b: "b.md"
});
assert.equal(
internals.database.state.lastAppliedLocalChangeId,
"new-move"
);
} finally {
await client.destroy();
}
});
test("remote binary replacement and deletion do not create backup artifacts", async () => {
const f = await fixture({ "a.bin": "private edit" });
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.applyRemoteChanges({
fileManifestId: 2,
entries: {}
});
assert.deepEqual(await f.disk.listFilesRecursively(), []);
});
test("rejected content recovery remains idempotent across a rename and network failure", async () => {
let offline = true;
const f = await fixture(undefined, {
putFileContent: async () => ({ type: "StaleBase", ...head("a", 2, "remote") }),
getDocumentVersionContent: async () => {
if (offline) throw new Error("offline");
return bytes("base");
}
});
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
};
f.database.state.pending = {
type: "content",
documentId: "a",
hash: snapshot.hash,
request: {
requestId: "request",
parentVersionId: 1,
content: { type: "Snapshot", value: Buffer.from(snapshot.content).toString("base64") }
}
};
await f.database.save();
await f.disk.userRename("a.md", "renamed.md");
await f.syncer.syncLocallyUpdatedFile({
oldPath: "a.md",
relativePath: "renamed.md"
});
await assert.rejects(f.internals.uploads.completePendingUpload(), /offline/);
offline = false;
await f.internals.uploads.completePendingUpload();
assert.equal(f.database.state.pending, undefined);
assert.equal(f.database.state.local.a, "renamed.md");
});
test("conflict names respect component limits without imposing a total path limit", () => {
const wanted = "a".repeat(235) + ".md";
const allocated = allocatePortablePath(
wanted,
"00000000-0000-4000-8000-000000000001",
[wanted]
);
assert(Buffer.byteLength(allocated) <= 255);
assert(allocated.endsWith(".md"));
assert.notEqual(allocated, wanted);
const longPath = "folder/".repeat(100) + "note.md";
assert.equal(allocatePortablePath(longPath, "id", []), longPath);
});
test("an unmaterialized file with a directory at its path is deconflicted", async () => {
const f = await fixture(
{},
{ getDocumentVersionContent: async () => bytes("remote") }
);
await f.disk.userWrite("folder/note.md", bytes("local"));
f.database.state.local.a = "folder";
f.database.state.documents.a = {};
f.database.state.fileManifest.entries.a = "folder";
await f.database.save();
f.database.state.documents.a.remote = head("a", 2, "remote");
await f.internals.applyRemoteChanges(f.database.state.fileManifest);
assert(
[...f.disk.userFiles().values()].some(
(value) => Buffer.from(value).toString() === "remote"
)
);
assert(
[...f.disk.userFiles().values()].some(
(value) => Buffer.from(value).toString() === "local"
)
);
});
test("ignored materialized content survives remote deletion", async () => {
const f = await fixture();
await f.settings.setSettings({ ignorePatterns: ["a.md"] });
await f.internals.applyRemoteChanges({
fileManifestId: 2,
entries: {}
});
assert.deepEqual(f.disk.userFiles(), new Map([["a.md", bytes("local")]]));
assert.deepEqual(f.database.state.fileManifest.entries, {});
});
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: "" };
const persistence = new MemoryPersistence({
settings: { remoteUri: "http://example.test", vaultName: "old" },
database
});
const client = await SyncClient.create({
fs: new MemoryDisk(),
persistence
});
try {
await assert.rejects(
client.setSettings({ vaultName: "new" }),
/separate state store/
);
assert.equal(persistence.snapshot().settings!.vaultName, "old");
} finally {
await client.destroy();
}
});
test("a rejected upload pauses all sync across restarts until an explicit retry", async (context) => {
let reject = true;
let eventReads = 0;
const sent: { id: string; request: unknown }[] = [];
const service = {
getEvents: async () => {
eventReads++;
return { headEventId: 0, endEventId: 0, events: [] };
},
putFileContent: async (id: string, request: unknown) => {
sent.push({ id, request: structuredClone(request) });
if (reject) throw new PermanentSyncError("HTTP 413");
return { type: "Accepted" as const, ...head(id, sent.length + 2, "small") };
}
};
let f = await fixture({ "a.md": "too large", "b.md": "other" }, service);
context.after(async () => f.syncer.stop());
for (const doc of Object.values(f.database.state.documents))
doc.base = { vaultUpdateId: 1, hash: "previous" };
await f.database.save();
f.syncer.start();
await assert.rejects(f.syncer.waitUntilFinished(), /413/);
const pending = structuredClone(f.database.state.pending!);
assert.equal(pending.rejection, "HTTP 413");
assert.deepEqual(sent.map(({ id }) => id), ["a"]);
const reads = eventReads;
await f.disk.userWrite("a.md", bytes("small"));
await f.syncer.syncLocallyUpdatedFile({ relativePath: "a.md" });
f.syncer.requestSync();
await assert.rejects(f.syncer.waitUntilFinished(), /413/);
assert.equal(eventReads, reads);
assert.equal(sent.length, 1);
await f.syncer.stop();
f = await fixture({}, service, f);
f.syncer.start();
await assert.rejects(f.syncer.waitUntilFinished(), /413/);
assert.equal(eventReads, reads);
assert.equal(sent.length, 1);
reject = false;
await f.syncer.retryRejectedRequests();
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");
assert.notDeepEqual(sent[2].request, sent[0].request);
assert.equal(f.database.state.pending, undefined);
assert.deepEqual((await f.disk.read("a.md"))?.content, bytes("small"));
await f.syncer.stop();
});
test("a transient upload failure retries the exact request and recovers its acknowledgement", async () => {
const requests: unknown[] = [];
const f = await fixture(undefined, {
putFileContent: async (_id, request) => {
requests.push(structuredClone(request));
if (requests.length === 1) throw new Error("lost upload reply");
return { type: "Accepted", ...head("a", 2, "local") };
}
});
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.uploads.completePendingUpload();
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.pending, undefined);
});
test("a persisted upload rejection pauses without filesystem writes", async () => {
let uploads = 0;
const f = await fixture(undefined, {
putFileContent: async () => {
uploads++;
throw new PermanentSyncError("HTTP 413");
}
});
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.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")]]));
});
test("an explicit retry recovers an earlier accepted upload after a permanent rejection", async () => {
const requests: unknown[] = [];
const f = await fixture(undefined, {
putFileContent: async (_id, request) => {
requests.push(structuredClone(request));
if (requests.length === 1) throw new PermanentSyncError("HTTP 413 after proxy reconfiguration");
return { type: "Accepted", ...head("a", 2, "local") };
}
});
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.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.prepareNextUpload(), false);
await f.syncer.waitUntilFinished();
assert.deepEqual(f.disk.userFiles(), new Map([["a.md", bytes("local")]]));
});
test("rejected manifests are resolved before publishing later namespace changes", async () => {
let reject = true;
const f = await fixture(
{},
{
pushFileManifest: async () => {
if (reject) throw new PermanentSyncError("HTTP 413");
return { type: "Accepted", fileManifestId: 3 };
}
}
);
f.database.state.fileManifest.entries = { a: "old.md" };
f.database.state.pending = {
type: "fileManifest",
request: {
requestId: "request",
parentFileManifestId: 1,
entries: { a: "new.md" }
}
};
f.database.state.local = { a: "new.md" };
f.database.state.documents.a = {
base: { ...head("a", 1, "x"), hash: "hash" }
};
await f.database.save();
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.uploads.completePendingUpload();
assert.deepEqual(f.database.state.fileManifest.entries, { a: "new.md" });
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 = createEmptyDatabase(key);
initial.lastSeenUpdateId = 0;
initial.local.a = "a.md";
initial.documents.a = { observedHash: "" };
initial.fileManifest = { fileManifestId: 1, entries: { a: "a.md" } };
const disk = new MemoryDisk();
await disk.userWrite("a.md", bytes("same"));
const persistence = new MemoryPersistence({
database: initial,
settings: { remoteUri: "http://offline.test", vaultName: "test" }
});
const first = await SyncClient.create({ fs: disk, persistence });
await disk.userDelete("a.md");
await first.syncLocallyDeletedFile("a.md");
await disk.userWrite("a.md", bytes("same"));
await first.syncLocallyCreatedFile("a.md");
await first.destroy();
const second = await SyncClient.create({ fs: disk, persistence });
try {
const internals = second as unknown as {
syncer: { refreshLocalFiles(): Promise<void> };
database: Database;
};
await internals.syncer.refreshLocalFiles();
assert.equal(internals.database.state.local.a, undefined);
assert.equal(
Object.values(internals.database.state.local).filter(
(path) => path === "a.md"
).length,
1
);
} finally {
await second.destroy();
}
});
test("offline notifications bind an otherwise empty client to its vault", async () => {
const disk = new MemoryDisk();
await disk.userWrite("a.md", bytes("local"));
const persistence = new MemoryPersistence({
settings: { remoteUri: "http://offline.test", vaultName: "test" }
});
const client = await SyncClient.create({ fs: disk, persistence });
await client.syncLocallyCreatedFile("a.md");
await assert.rejects(
client.setSettings({ vaultName: "other" }),
/separate state store/
);
await client.destroy();
const stored = persistence.snapshot();
stored.settings!.vaultName = "other";
await persistence.save(stored);
await assert.rejects(
SyncClient.create({ fs: disk, persistence }),
/another vault/
);
});
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.applyRemoteChanges({
fileManifestId: 2,
entries: {}
});
await f.settings.setSettings({ ignorePatterns: [] });
await f.internals.applyRemoteChanges(f.database.state.fileManifest);
assert.equal(f.disk.userFiles().size, 0);
assert.deepEqual(f.database.state.local, {});
});
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.applyRemoteChanges({
fileManifestId: 2,
entries: { a: "c.md", b: "a.md" }
});
assert.deepEqual(
f.disk.userFiles(),
new Map([
["a.md", bytes("A")],
["b.md", bytes("B")]
])
);
f.internals.scheduler.phase = SyncPhase.Idle;
assert.equal(await f.internals.prepareNextUpload(), false);
assert.deepEqual(
f.disk.userFiles(),
new Map([
["a.md", bytes("A")],
["b.md", bytes("B")]
])
);
});
for (const kind of ["path", "content"] as const) {
test(`a remote ${kind} change that was reverted before observation preserves the local edit`, async () => {
const f = await fixture(
{ "a.md": "local" },
{
getDocumentVersionContent: async ({ vaultUpdateId }) =>
bytes(vaultUpdateId === 2 ? "temporary" : "base")
}
);
f.database.state.lastSeenUpdateId = 1;
f.database.state.documents.a.base = {
...head("a", 1, "base"),
hash: (await hashSnapshot({ content: bytes("base") })).hash
};
if (kind === "path") {
await f.disk.userRename("a.md", "c.md");
await f.syncer.syncLocallyUpdatedFile({
oldPath: "a.md",
relativePath: "c.md"
});
}
await f.internals.applyEventBatch({
headEventId: 3,
endEventId: 3,
events: [2, 3].map((id) =>
kind === "path"
? {
eventId: id,
requestId: String(id),
type: "fileManifest",
fileManifest: {
fileManifestId: id,
entries: { a: id === 2 ? "b.md" : "a.md" }
}
}
: {
eventId: id,
requestId: String(id),
type: "content",
document: head(
"a",
id,
id === 2 ? "temporary" : "base"
)
}
)
});
assert.equal(
f.database.state.local.a,
kind === "path" ? "c.md" : "a.md"
);
assert.equal(
Buffer.from(
(await f.disk.read(f.database.state.local.a))!.content
).toString(),
"local"
);
assert.equal(f.database.state.lastSeenUpdateId, 3);
});
}
test("concurrent settings updates cannot bind the engine to a different vault than its settings", async () => {
const disk = new MemoryDisk();
const persistence = new MemoryPersistence();
const client = await SyncClient.create({ fs: disk, persistence });
try {
await Promise.all([
client.setSettings({ vaultName: "changed" }),
client.setSettings({ maxFileSizeMB: 20 })
]);
const stored = persistence.snapshot();
assert.equal(
stored.database!.vaultKey,
JSON.stringify([
stored.settings!.remoteUri,
stored.settings!.vaultName
])
);
assert.equal(stored.settings!.maxFileSizeMB, 20);
} finally {
await client.destroy();
}
});
test("destroy waits for an in-progress offline notification to become durable", async () => {
const disk = new MemoryDisk();
const persistence = new MemoryPersistence();
const client = await SyncClient.create({ fs: disk, persistence });
await disk.userWrite("a.md", bytes("local"));
const reached = Promise.withResolvers<void>();
const release = Promise.withResolvers<void>();
persistence.boundary = async (label) => {
if (label === "before:save") {
reached.resolve();
await release.promise;
}
};
const notification = client.syncLocallyCreatedFile("a.md");
await reached.promise;
let destroyed = false;
const destroying = client.destroy().then(() => {
destroyed = true;
});
await new Promise((resolve) => setImmediate(resolve));
assert.equal(destroyed, false);
release.resolve();
await Promise.all([notification, destroying]);
});
test("ignoring a locally renamed file defers its move without forgetting the local rename", async () => {
const f = await fixture();
f.database.state.documents.a.base = {
...head("a", 1, "local"),
hash: f.database.state.documents.a.observedHash!
};
await f.disk.userRename("a.md", "ignored.md");
await f.syncer.syncLocallyUpdatedFile({
oldPath: "a.md",
relativePath: "ignored.md"
});
await f.settings.setSettings({ ignorePatterns: ["ignored.md"] });
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.applyRemoteChanges(f.database.state.fileManifest);
assert.equal(f.database.state.local.a, "ignored.md");
assert.equal(
Buffer.from(
(await f.disk.read("ignored.md"))!.content
).toString(),
"local"
);
});
test("a notified deletion of an ignored file does not delete its server membership", async () => {
const f = await fixture();
await f.settings.setSettings({ ignorePatterns: ["a.md"] });
await f.disk.userDelete("a.md");
await f.syncer.syncLocallyDeletedFile("a.md");
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.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.applyRemoteChanges({
fileManifestId: 2,
entries: { a: "b.md", other: "a.md" }
});
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,
Object.keys(f.database.state.local).length
);
});
test("rejected content is reported to callers and an explicit retry clears the rejection", async () => {
const f = await fixture(undefined, {
putFileContent: async () => {
throw new PermanentSyncError("HTTP 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();
assert.equal(f.database.state.pending?.rejection, undefined);
assert(f.database.state.pending, "explicit retry must retain the exact request");
});
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 = createEmptyDatabase(key);
initial.lastSeenUpdateId = 0;
const disk = new MemoryDisk();
const store = new MemoryPersistence({
database: initial,
settings: { remoteUri: "http://offline.test", vaultName: "test" }
});
let failLoad = false;
const persistence = {
load: async () => {
if (failLoad) {
failLoad = false;
throw new Error("reload failed");
}
return store.load();
},
save: async (state: Parameters<typeof store.save>[0]) =>
store.save(state)
};
const client = await SyncClient.create({ fs: disk, persistence });
try {
const internals = client as unknown as {
database: Database;
syncer: Syncer;
};
const next = structuredClone(initial);
next.lastSeenUpdateId = 2;
store.boundary = (label) => {
if (label === "durable:save") {
store.boundary = () => { };
throw new Error("durable save failed");
}
};
await assert.rejects(
internals.database.commit(next),
/durable save failed/
);
await disk.userWrite("a.md", bytes("new"));
failLoad = true;
await assert.rejects(
client.syncLocallyCreatedFile("a.md"),
/reload failed/
);
await internals.syncer.flushLocalChanges();
assert.equal(store.snapshot().database!.lastSeenUpdateId, 2);
await internals.database.recoverPersistence();
assert.equal(internals.database.state.lastSeenUpdateId, 2);
} finally {
await client.destroy();
}
});
test(
"a settings listener can await client shutdown without deadlocking the lifecycle lock",
{ timeout: 1_000 },
async () => {
const client = await SyncClient.create({
fs: new MemoryDisk(),
persistence: new MemoryPersistence()
});
let notified = false;
client.onSettingsChanged.add(async () => {
await client.destroy();
notified = true;
});
await client.setSettings({ maxFileSizeMB: 20 });
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 { 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");
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);
}
});
}