This commit is contained in:
Andras Schmelczer 2026-09-27 08:29:17 +01:00
commit 3dab48f58b

View file

@ -5,7 +5,7 @@ import { test } from "node:test";
import type { Database } from "../sync-client/src/persistence/database";
import { emptyState } from "../sync-client/src/persistence/database";
import { SyncClient } from "../sync-client/src/sync-client";
import { toStoredSnapshot } from "../sync-client/src/snapshot";
import { hashSnapshot } from "../sync-client/src/snapshot";
import { EventListeners } from "../sync-client/src/utils/data-structures/event-listeners";
import type { SyncService } from "../sync-client/src/services/sync-service";
import type { WebSocketManager } from "../sync-client/src/services/websocket-manager";
@ -81,11 +81,13 @@ test("oversized untracked file must remain in place", async () => {
});
test("rejected unknown success must use submitted content as merge base", async () => {
let reject = true;
const f = await fixture(
{ "a.md": "base" },
{
putFileContent: async () => {
throw new PermanentSyncError("HTTP 413");
if (reject) throw new PermanentSyncError("HTTP 413");
return { type: "Accepted", ...head("a", 2, "base A") };
},
getDocumentVersionContent: async ({ vaultUpdateId }) =>
bytes(
@ -106,10 +108,14 @@ test("rejected unknown success must use submitted content as merge base", async
f.internals.stopped = false;
await f.internals.preparePush();
const req = f.database.state.pending!.request.requestId;
await f.internals.finishPending();
await assert.rejects(f.internals.finishPending(), /413/);
await f.disk.userWrite("a.md", bytes("base"));
reject = false;
await f.syncer.retryRejectedRequests();
await f.internals.finishPending();
await f.internals.incorporateEventBatch({
headEventId: 3,
endEventId: 3,
events: [
{
eventId: 2,
@ -166,7 +172,7 @@ test("oversized remote head must not cause identical stale uploads forever", asy
);
f.database.state.documents.a.base = {
...head("a", 1, "base"),
hash: (await toStoredSnapshot({ content: bytes("base") })).hash
hash: (await hashSnapshot({ content: bytes("base") })).hash
};
(f.database.state.documents.a ??= {}).remote = head("a", 2, remoteText);
await f.settings.setSettings({ maxFileSizeMB: 10 / (1024 * 1024) });
@ -178,11 +184,13 @@ test("oversized remote head must not cause identical stale uploads forever", asy
});
test("local undo of accepted-but-rejected manifest must survive replay", async () => {
let reject = true;
const f = await fixture(
{ "a.md": "A" },
{
pushFileManifest: async () => {
throw new PermanentSyncError("HTTP 413");
if (reject) throw new PermanentSyncError("HTTP 413");
return { type: "Accepted", fileManifestId: 2 };
},
getDocumentVersionContent: async () => bytes("A")
}
@ -202,14 +210,18 @@ test("local undo of accepted-but-rejected manifest must survive replay", async (
await f.internals.preparePush();
const req = f.database.state.pending!.request.requestId;
f.internals.stopped = true;
await f.internals.finishPending();
await assert.rejects(f.internals.finishPending(), /413/);
await f.disk.userRename("b.md", "a.md");
await f.syncer.syncLocallyUpdatedFile({
oldPath: "b.md",
relativePath: "a.md"
});
reject = false;
await f.syncer.retryRejectedRequests();
await f.internals.finishPending();
await f.internals.incorporateEventBatch({
headEventId: 2,
endEventId: 2,
events: [
{
eventId: 2,
@ -244,9 +256,6 @@ test("a dirty cursor update must invalidate old precise positions", async () =>
const { CursorTracker } = await import(
"../sync-client/src/sync-operations/cursor-tracker"
);
const { FileChangeNotifier } = await import(
"../sync-client/src/sync-operations/file-change-notifier"
);
const f = await fixture({ "a.md": "base" });
f.database.state.documents.a.base = {
...head("a", 1, "base"),
@ -261,7 +270,7 @@ test("a dirty cursor update must invalidate old precise positions", async () =>
f.database,
ws as unknown as WebSocketManager,
f.files,
new FileChangeNotifier()
new EventListeners()
);
let latest: import("../sync-client/src/types/maybe-outdated-client-cursors").MaybeOutdatedClientCursors[] =
[];
@ -335,21 +344,35 @@ test("silent open WebSocket must eventually trigger catchup or reconnect", async
fetch: async (input) => {
const path = String(input);
if (path.endsWith("/ping"))
return Response.json({
supportedApiVersion: SUPPORTED_API_VERSION,
isAuthenticated: true,
mergeableFileExtensions: ["md"],
serverVersion: "test"
});
return Response.json(
{
supportedApiVersion: SUPPORTED_API_VERSION,
isAuthenticated: true,
mergeableFileExtensions: ["md"],
serverVersion: "test"
},
{ headers: { "X-Vault-Link-History": "0:empty" } }
);
if (path.endsWith("/file-manifest"))
return Response.json(
{ type: "Accepted", fileManifestId: 0 },
{ headers: { "X-Vault-Link-History": "0:empty" } }
);
if (path.endsWith("/vault-snapshot"))
return Response.json({
headEventId: 0,
fileManifest: { fileManifestId: 0, entries: {} },
documents: []
});
return Response.json(
{
headEventId: 0,
fileManifest: { fileManifestId: 0, entries: {} },
documents: []
},
{ headers: { "X-Vault-Link-History": "0:empty" } }
);
if (path.includes("/events-since")) {
eventReads++;
return Response.json({ headEventId: 0, events: [] });
return Response.json(
{ headEventId: 0, endEventId: 0, events: [] },
{ headers: { "X-Vault-Link-History": "0:empty" } }
);
}
throw new Error("Unexpected request " + path);
}
@ -478,7 +501,7 @@ for (const interrupted of [false, true]) {
f = await fixture({ "a.md": "base LOCAL" }, service);
f.database.state.documents.a.base = {
...head("a", 1, "base"),
hash: (await toStoredSnapshot({ content: bytes("base") })).hash
hash: (await hashSnapshot({ content: bytes("base") })).hash
};
(f.database.state.documents.a ??= {}).remote = head("a", 1, "base");
f.database.state.lastSeenUpdateId = 1;
@ -554,14 +577,16 @@ test("content notifications during every snapshot do not starve a complete names
});
for (const boundary of ["before:save", "durable:save"]) {
test(`receipt incorporation after paged replay survives ${boundary} and process replacement`, async () => {
test(`retried acknowledgement before paged replay survives ${boundary} and process replacement`, async () => {
let reject = true;
const service: Partial<SyncService> = {
getEvents: async (after) => {
assert.equal(after, 2);
return page;
},
putFileContent: async () => {
throw new PermanentSyncError("HTTP 413");
if (reject) throw new PermanentSyncError("HTTP 413");
return { type: "Accepted", ...head("a", 2, "base A") };
},
getDocumentVersionContent: async ({ vaultUpdateId }) =>
bytes(
@ -582,7 +607,7 @@ for (const boundary of ["before:save", "durable:save"]) {
f.internals.stopped = false;
await f.internals.preparePush();
const { requestId } = f.database.state.pending!.request;
await f.internals.finishPending();
await assert.rejects(f.internals.finishPending(), /413/);
await f.disk.userWrite("a.md", bytes("base"));
const first = {
headEventId: 3,
@ -608,17 +633,20 @@ for (const boundary of ["before:save", "durable:save"]) {
}
]
};
reject = false;
await f.syncer.retryRejectedRequests();
f.persistence.boundary = (label) => {
if (label === boundary) throw new Error("injected crash");
};
await assert.rejects(
f.internals.incorporateEventBatch(first),
f.internals.finishPending(),
/injected crash/
);
f.persistence.boundary = () => { };
f.disk.crash(true);
f = await fixture({}, service, f);
assert.equal(f.database.state.lastSeenUpdateId, 1);
await f.internals.finishPending();
await f.internals.incorporateEventBatch(first);
assert.equal(f.database.state.lastSeenUpdateId, 3);
assert.deepEqual(
@ -653,19 +681,28 @@ test("initial vault settings bind the engine and survive restart", async () => {
webSocket: Socket as unknown as typeof WebSocket,
fetch: async (input: RequestInfo | URL) => {
if (String(input).endsWith("/ping"))
return Response.json({
supportedApiVersion: SUPPORTED_API_VERSION,
isAuthenticated: true,
mergeableFileExtensions: ["md"],
serverVersion: "test"
});
return Response.json(
{
supportedApiVersion: SUPPORTED_API_VERSION,
isAuthenticated: true,
mergeableFileExtensions: ["md"],
serverVersion: "test"
},
{ headers: { "X-Vault-Link-History": "0:empty" } }
);
if (String(input).endsWith("/vault-snapshot"))
return Response.json({
headEventId: 0,
fileManifest: { fileManifestId: 0, entries: {} },
documents: []
});
return Response.json({ headEventId: 0, events: [] });
return Response.json(
{
headEventId: 0,
fileManifest: { fileManifestId: 0, entries: {} },
documents: []
},
{ headers: { "X-Vault-Link-History": "0:empty" } }
);
return Response.json(
{ headEventId: 0, endEventId: 0, events: [] },
{ headers: { "X-Vault-Link-History": "0:empty" } }
);
}
};
let client = await SyncClient.create(options);
@ -695,13 +732,14 @@ test("disabling sync persists across reset without resuming the transport", asyn
}
});
for (const receipt of [false, true]) {
test(`${receipt ? "replayed" : "direct"} upload acknowledgement survives a local deletion`, async () => {
for (const retry of [false, true]) {
test(`${retry ? "retried" : "direct"} upload acknowledgement survives a local deletion`, async () => {
let reject = retry;
const f = await fixture(
{ "a.md": "base" },
{
putFileContent: async () => {
if (receipt)
if (reject)
throw new PermanentSyncError(
"HTTP 413 after lost success"
);
@ -717,30 +755,19 @@ for (const receipt of [false, true]) {
await f.disk.userWrite("a.md", bytes("edited"));
f.internals.stopped = false;
await f.internals.preparePush();
const { requestId } = f.database.state.pending!.request;
if (receipt) await f.internals.finishPending();
if (retry) await assert.rejects(f.internals.finishPending(), /413/);
// Recovery folds queued deletions into the durable namespace before
// an outstanding request or its delayed receipt is handled.
// an outstanding request is retried.
f.internals.stopped = true;
await f.disk.userDelete("a.md");
await f.syncer.syncLocallyDeletedFile("a.md");
await f.internals.scan();
assert.deepEqual(f.database.state.local, {});
if (receipt) {
await f.internals.incorporateEventBatch({
headEventId: 2,
events: [
{
eventId: 2,
requestId,
type: "content",
document: head("a", 2, "edited")
}
]
});
assert.equal(f.database.state.lastSeenUpdateId, 2);
assert.equal(f.database.state.unconfirmed?.length ?? 0, 0);
} else await f.internals.finishPending();
if (retry) {
reject = false;
await f.syncer.retryRejectedRequests();
}
await f.internals.finishPending();
assert.equal(f.database.state.pending, undefined);
assert.deepEqual(f.database.state.local, {});
assert.equal(await f.disk.read("a.md"), undefined);
@ -781,21 +808,35 @@ async function settingsLifecycleFixture() {
fetch: async (input) => {
const path = String(input);
if (path.endsWith("/ping"))
return Response.json({
supportedApiVersion: SUPPORTED_API_VERSION,
isAuthenticated: true,
mergeableFileExtensions: ["md"],
serverVersion: "test"
});
return Response.json(
{
supportedApiVersion: SUPPORTED_API_VERSION,
isAuthenticated: true,
mergeableFileExtensions: ["md"],
serverVersion: "test"
},
{ headers: { "X-Vault-Link-History": "0:empty" } }
);
if (path.endsWith("/file-manifest"))
return Response.json(
{ type: "Accepted", fileManifestId: 0 },
{ headers: { "X-Vault-Link-History": "0:empty" } }
);
if (path.endsWith("/vault-snapshot"))
return Response.json({
headEventId: 0,
fileManifest: { fileManifestId: 0, entries: {} },
documents: []
});
return Response.json(
{
headEventId: 0,
fileManifest: { fileManifestId: 0, entries: {} },
documents: []
},
{ headers: { "X-Vault-Link-History": "0:empty" } }
);
if (path.includes("/events-since")) {
eventReads++;
return Response.json({ headEventId: 0, events: [] });
return Response.json(
{ headEventId: 0, endEventId: 0, events: [] },
{ headers: { "X-Vault-Link-History": "0:empty" } }
);
}
throw new Error("Unexpected request " + path);
}
@ -818,12 +859,11 @@ for (const boundary of ["before:save", "durable:save"]) {
const { database } = f.client as unknown as { database: Database };
await database.commit({
...database.state,
unconfirmed: [{
pending: {
type: "fileManifest",
requestId: "rejected",
entries: { retired: "old.md" },
rejection: "retired rejection"
}]
request: { requestId: "rejected", parentFileManifestId: 0, entries: {} },
rejection: "upload rejected"
}
});
let saves = 0,
failed = false;
@ -840,6 +880,10 @@ for (const boundary of ["before:save", "durable:save"]) {
);
assert(failed);
assert.equal(f.client.getSettings().maxFileSizeMB, 20);
if (boundary === "before:save") {
await assert.rejects(f.client.waitUntilFinished(), /upload rejected/);
await f.client.reset();
}
const before = f.reads();
await f.client.syncLocallyUpdatedFile({
relativePath: "absent.md"