.
This commit is contained in:
parent
4c002f9acd
commit
b07aa589ff
1 changed files with 728 additions and 530 deletions
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue