This commit is contained in:
Andras Schmelczer 2026-09-23 08:23:17 +01:00
commit 4c002f9acd
26 changed files with 14543 additions and 4367 deletions

View file

@ -0,0 +1,57 @@
# Shared sync harness support
Test-only code shared by the deterministic and seeded clients.
| Suite | Coverage |
| --- | --- |
| `harness.test.ts`, `oracle-regressions.test.ts` | Independent content/identity oracles, exercised faults, exclusive operations, persistence ownership, cleanup and seeds |
| `file-application.test.ts` | Swaps, file/directory transitions, visible conflict files, live edits, exclusions, and one metadata save per application |
| `recovery.test.ts` | Interrupt every application filesystem/save boundary, reopen metadata, modify disk, and verify stable ordinary scans; manifest policy and notification identity |
| `engine-regressions.test.ts`, `sync-audit.test.ts` | Offline identities, unknown network outcomes, rejection/retry, exclusions, interrupted in-memory pagination, namespace races and lifecycle changes |
| `event-delivery.test.ts`, `polling.test.ts` | WebSocket hints with HTTP-only event reads, missed notifications, opt-in polling, shutdown and retry |
| `protocol.test.ts` | Abrupt worker termination after accepted requests, exact retries, real server SIGKILL, CAS and event ordering |
| `restart-reconciliation.test.ts` | Interrupted file application followed by user edits, two-client/server convergence and a second restart |
| `server-restore.test.ts` | History checkpoints, server backup restores and interrupted metadata resets |
| `content-limits.test.ts`, `protocol-audit.test.ts` | Content validation and API invariants |
| `real-disk.test.ts` | Production Node adapter direct writes, no-replace moves, unsafe links, and rename cycles |
| `production-adapters.test.ts` | Actual CLI processes, raw watchers, Obsidian adapter, complete metadata and restart after SIGKILL |
The filesystem interruption tests assert the supported contract: complete metadata
remains usable, scans observe the current files, and a second scan is stable.
They do not demand the pre-interruption file contents, identities or a replay of
unfinished operations. Normal-operation tests still check exact bytes and identities.
There is no filesystem journal or hidden recovery archive.
`MemoryDisk` exposes `before:`, `visible:` and `durable:` boundaries. A process
interruption retains visible changes; a simulated power loss rolls unflushed
changes back. This provides different partial-application states for testing; it
is not an APFS/ext4 writeback model or a production durability requirement.
`MemoryPersistence` atomically replaces complete objects and can reject before or
after replacement. Abandoned filesystem sessions are fenced against later writes.
`crash-worker.ts` runs a complete client in a worker, which the parent terminates
at accepted-response checkpoints without graceful shutdown. A new runtime receives
the saved disk and metadata. Real-disk tests exercise the production Node adapter.
The production integration suite launches built CLI processes and an Obsidian
filesystem adapter against a real server. Obsidian unit tests exercise unsaved editor
text, selections and mobile storage contracts; they do not launch the Obsidian app.
Filesystem path checks are not a sandbox against hostile concurrent changes to
ancestor directories.
Scripted and seeded suites run multiple clients against real servers. They check
visible bytes, UUID mappings, canonical server state, content heads and event cursors.
Polling is disabled except in tests specifically exercising it. Faults must fire;
missed injections fail cleanup rather than silently passing. Workload traces,
per-worker results and logs are saved by `scripts/e2e.sh` for reproduction.
Run from the repository root:
```sh
E2E_WORKERS=32 E2E_SEED=12 E2E_TIMEOUT_SECONDS=9000 E2E_ITERATIONS=100 scripts/e2e.sh
```
The command rebuilds peers and both production integrations, runs core and adapter
unit tests plus harness/protocol/scripted suites, and then
runs seeds 12 through 43. `E2E_ARTIFACTS` optionally selects the artifact directory.
The runner tests also verify subprocess exit codes, log errors, spawn failures,
timeouts, workspace dependency consistency and CI artifact wiring.

View file

@ -0,0 +1,157 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import type { StoredDatabase } from "sync-client";
import { assertQuiescent, assertSameFiles } from "./oracles";
export interface CanonicalSnapshot {
headEventId: number;
fileManifest: { fileManifestId: number; entries: Record<string, string> };
documents: { documentId: string; vaultUpdateId: number }[];
}
export interface InspectableClient {
files(): Map<string, Uint8Array>;
database(): Partial<StoredDatabase> | undefined;
}
export async function getJson<T>(url: string, token: string): Promise<T> {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${token}`,
"Device-Id": "harness-observer"
},
signal: AbortSignal.timeout(5_000)
});
assert(response.ok, `Observer GET ${url}: HTTP ${response.status}`);
return (await response.json()) as T;
}
/** Equality alone can pass when every replica loses the same bytes. Check the
* canonical UUIDs, content heads, receipts and event cursor as well.
* Scenario-specific content/identity oracles still run separately.
*/
export async function assertCanonical(
clients: InspectableClient[],
vaultUrl: string,
token: string
): Promise<CanonicalSnapshot> {
assert.equal(
new Set(clients).size,
clients.length,
"Cannot compare a client to itself"
);
const files = clients.map((client) => client.files());
const states = clients.map((client) => client.database());
assertSameFiles(files);
const snapshot = await getJson<CanonicalSnapshot>(
`${vaultUrl}/vault-snapshot`,
token
);
const entries = snapshot.fileManifest.entries;
for (const id of Object.keys(entries))
assert(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
id
),
`Non-UUID document identity: ${id}`
);
const heads = Object.fromEntries(
snapshot.documents.map((doc) => [doc.documentId, doc])
);
assert.equal(
Object.keys(heads).length,
snapshot.documents.length,
"Duplicate document heads"
);
assert.deepEqual(
Object.keys(heads).sort(),
Object.keys(entries).sort(),
"Snapshot heads differ from membership"
);
for (const [i, state] of states.entries()) {
assert(state?.initialized, `Client ${i} never initialized`);
assertQuiescent(state, entries, snapshot.headEventId);
assert.equal(
state.fileManifest?.fileManifestId,
snapshot.fileManifest.fileManifestId,
"Wrong manifest CAS parent"
);
assert.deepEqual(
[...files[i].keys()].sort(),
Object.values(entries).sort(),
"Untracked or missing disk files"
);
for (const id of Object.keys(entries)) {
assert(
state.documents?.[id]?.materialized,
`Unmaterialized document: ${id}`
);
assert.equal(
state.documents?.[id]?.base?.vaultUpdateId,
heads[id]?.vaultUpdateId,
`Wrong content CAS parent: ${id}`
);
}
}
for (const [id, path] of Object.entries(entries)) {
const doc = await getJson<{ contentBase64: string }>(
`${vaultUrl}/documents/${id}`,
token
);
const serverBytes = new Uint8Array(
Buffer.from(doc.contentBase64, "base64")
);
assert.deepEqual(
files[0].get(path),
serverBytes,
`Server bytes differ for ${id} at ${path}`
);
const hash = createHash("sha256").update(serverBytes).digest("hex");
for (const [i, state] of states.entries()) {
assert.equal(
state!.documents?.[id]?.base?.hash,
hash,
`Wrong base hash: client ${i}, ${id}`
);
assert.equal(
state!.documents?.[id]?.observedHash,
hash,
`Wrong observed hash: client ${i}, ${id}`
);
}
}
const events: { eventId: number; requestId: string }[] = [];
let after = 0;
do {
const batch = await getJson<{
headEventId: number;
endEventId?: number;
events: { eventId: number; requestId: string }[];
}>(`${vaultUrl}/events-since?after=${after}`, token);
assert.equal(
batch.headEventId,
snapshot.headEventId,
"Server changed while checking convergence"
);
const end = batch.endEventId ?? batch.headEventId;
assert.equal(
end,
batch.events.at(-1)?.eventId ?? after,
"Event page cursor differs from its contents"
);
if (after < snapshot.headEventId)
assert(end > after, "Event page made no progress");
events.push(...batch.events);
after = end;
} while (after < snapshot.headEventId);
assert.deepEqual(
events.map((e) => e.eventId),
Array.from({ length: snapshot.headEventId }, (_, i) => i + 1),
"Server event log is not contiguous and totally ordered"
);
assert.equal(
new Set(events.map((e) => e.requestId)).size,
events.length,
"An idempotent request emitted multiple events"
);
return snapshot;
}

View file

@ -0,0 +1,103 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { fixture, bytes, randomUUID } from "./sync-server-fixture";
test("stale manifest reconciliation must not fetch excluded remote content", async () => {
const f = await fixture();
try {
const id = randomUUID(),
big = randomUUID();
await f.put("/documents/" + id, f.content("A"));
const m = await f.put("/file-manifest", {
requestId: randomUUID(),
parentFileManifestId: 0,
entries: { [id]: "a.md" }
});
await f.client.setSetting("maxFileSizeMB", 100 / (1024 * 1024));
await f.client.start();
await f.client.setSetting("isSyncEnabled", false);
await f.disk.userRename("a.md", "renamed.md");
await f.client.syncLocallyUpdatedFile({
oldPath: "a.md",
relativePath: "renamed.md"
});
let injected = false,
downloaded = 0;
f.setFetchHook(async (input, init) => {
const path = String(input);
if (
!injected &&
init?.method === "PUT" &&
path.endsWith("/file-manifest")
) {
injected = true;
await f.put("/documents/" + big, f.content("B".repeat(4096)));
await f.put("/file-manifest", {
requestId: randomUUID(),
parentFileManifestId: m.fileManifestId,
entries: { [id]: "a.md", [big]: "big.bin" }
});
}
const response = await fetch(input, init);
if (
path.includes("/documents/" + big) &&
!path.endsWith("/metadata") &&
(!init?.method || init.method === "GET")
)
downloaded += (await response.clone().arrayBuffer()).byteLength;
return response;
});
await f.client.setSetting("isSyncEnabled", true);
assert(injected);
assert(!f.disk.userFiles().has("big.bin"));
assert.equal(downloaded, 0);
} finally {
await f.dispose();
}
});
test("stale content acknowledgements contain metadata without the excluded payload", async () => {
const f = await fixture();
try {
const id = randomUUID();
await f.put("/documents/" + id, f.content("B".repeat(4096)));
const stale = await f.put("/documents/" + id, f.content("LOCAL"));
assert.equal(stale.type, "StaleBase");
assert.equal(stale.contentSize, 4096);
assert.equal(stale.contentBase64, undefined);
const metadata = await f.get("/documents/" + id + "/metadata");
assert.equal(metadata.contentSize, 4096);
assert.equal(metadata.contentBase64, undefined);
} finally {
await f.dispose();
}
});
test("highly fragmented local edits use a snapshot within the server operation budget", async () => {
const { fixture } = await import("./sync-fixture");
const { toStoredSnapshot } = await import(
"../sync-client/src/sync-operations/content"
);
const f = await fixture({});
f.contentCache.resize(1024 * 1024);
const original = Array.from({ length: 12_000 }, (_, i) => `word${i}`).join(
" "
);
const edited = Array.from({ length: 12_000 }, (_, i) =>
i % 2 ? `changed${i}` : `word${i}`
).join(" ");
f.contentCache.put(1, bytes(original));
const syncer = f.syncer as unknown as {
pushContent(
path: string,
version: number,
snapshot: unknown
): Promise<{ type: string }>;
};
const request = await syncer.pushContent(
"a.md",
1,
await toStoredSnapshot({ content: bytes(edited) })
);
assert.equal(request.type, "Snapshot");
});

View file

@ -0,0 +1,757 @@
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 { 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 { 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 = emptyState(JSON.stringify(["http://offline.test", "test"]));
initial.initialized = true;
initial.local = { a: "a.md" };
initial.documents.a = {
materialized: true,
observedHash: (await toStoredSnapshot({ 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: {
stopped: boolean;
scan(): Promise<void>;
preparePush(): Promise<boolean>;
};
};
await internals.syncer.scan();
internals.syncer.stopped = false;
assert.equal(await internals.syncer.preparePush(), true);
const pending = internals.database.state.pending;
assert(pending?.type === "content");
assert.equal(pending.documentId, "a");
assert.equal(
Buffer.from(pending.snapshot.contentBase64, "base64").toString(),
"latest offline bytes"
);
} finally {
store.boundary = () => { };
await client.destroy();
}
});
test("legacy content notifications preserve the acknowledged namespace prefix on restart", async () => {
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"]));
initial.initialized = true;
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] = {
materialized: true,
observedHash: (await toStoredSnapshot({ content: bytes(content) }))
.hash
};
initial.lastAppliedLocalChangeId = "consumed-update";
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: "update", path: "c.md", changeId: "consumed-update" },
{ type: "update", path: "b.md", changeId: "new-update" },
{
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: { scan(): Promise<void> };
};
await internals.syncer.scan();
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.apply(structuredClone(f.database.state), {
a: { replacement: await toStoredSnapshot({ 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.incorporateFileManifest({
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, {
getDocumentVersionContent: async () => {
if (offline) throw new Error("offline");
return bytes("base");
}
});
const snapshot = (await f.files.snapshot("a.md"))!;
f.database.state.documents.a.base = {
...head("a", 1, "base"),
hash: (await toStoredSnapshot({ content: bytes("base") })).hash
};
f.database.state.pending = {
type: "content",
documentId: "a",
snapshot,
request: {
requestId: "request",
parentVersionId: 1,
content: { type: "Snapshot", value: snapshot.contentBase64 }
},
response: {
type: "StaleBase",
...head("a", 2, "remote")
}
};
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.finishPending(), /offline/);
offline = false;
await f.internals.finishPending();
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",
{ other: 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 = { materialized: false };
f.database.state.fileManifest.entries.a = "folder";
await f.database.save();
await f.internals.incorporateContent(head("a", 2, "remote"));
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.incorporateFileManifest({
fileManifestId: 2,
entries: {}
});
assert.deepEqual(f.disk.userFiles(), new Map([["a.md", bytes("local")]]));
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"]));
database.bootstrap = {
headEventId: 1,
fileManifest: { fileManifestId: 1, entries: {} },
documents: []
};
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 is retained locally and does not block another file or a corrected snapshot", async () => {
let reject = true;
const sent: string[] = [];
const f = await fixture(
{ "a.md": "too large", "b.md": "other" },
{
putFileContent: async (id) => {
sent.push(id);
if (id === "a" && reject)
throw new PermanentSyncError("HTTP 413");
return {
type: "Accepted",
...head(id, sent.length + 2, "small")
};
}
}
);
f.internals.stopped = false;
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(await f.internals.preparePush());
await f.internals.finishPending();
assert.deepEqual(sent, ["a", "b"]);
assert.equal(await f.internals.preparePush(), false);
await f.disk.userWrite("a.md", bytes("small"));
reject = false;
assert(await f.internals.preparePush());
await f.internals.finishPending();
assert.deepEqual(sent, ["a", "b", "a"]);
assert.equal(f.database.state.documents.a.rejected, undefined);
});
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.stopped = false;
await f.internals.preparePush();
await assert.rejects(f.internals.finishPending(), /lost upload reply/);
assert(f.database.state.pending);
await f.internals.finishPending();
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.pending, undefined);
});
test("a persisted upload rejection finishes without filesystem writes", async () => {
let uploads = 0;
const f = await fixture(undefined, {
putFileContent: async () => {
uploads++;
throw new PermanentSyncError("HTTP 413");
}
});
f.internals.stopped = false;
await f.internals.preparePush();
f.disk.boundary = (label) => {
if (!label.startsWith("read:"))
throw new Error("Unexpected filesystem mutation");
};
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.deepEqual(f.disk.userFiles(), new Map([["a.md", bytes("local")]]));
});
test("event replay incorporates an earlier accepted upload after a permanent retry rejection", async () => {
const f = await fixture(undefined, {
putFileContent: async () => {
throw new PermanentSyncError(
"HTTP 413 after proxy reconfiguration"
);
},
getDocumentVersionContent: async () => bytes("local")
});
f.internals.stopped = false;
await f.internals.preparePush();
const { pending } = f.database.state;
assert(pending);
const { requestId } = pending.request;
await f.internals.finishPending();
await assert.rejects(f.syncer.waitUntilFinished(), /413/);
f.database.state.lastSeenUpdateId = 1;
await f.internals.incorporateEventBatch({
headEventId: 2,
events: [
{
eventId: 2,
requestId,
type: "content",
document: head("a", 2, "local")
}
]
});
assert.equal(f.database.state.documents.a.base?.vaultUpdateId, 2);
assert.equal(f.database.state.pending, undefined);
assert.equal(await f.internals.preparePush(), false);
await f.syncer.waitUntilFinished();
assert.deepEqual(f.disk.userFiles(), new Map([["a.md", bytes("local")]]));
});
test("rejected manifests can be replaced after editing the local namespace", 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 = {
materialized: false,
base: { ...head("a", 1, "x"), hash: "hash" }
};
await f.database.save();
await f.internals.finishPending();
f.internals.stopped = false;
assert.equal(await f.internals.preparePush(), false);
f.database.state.local = {};
reject = false;
assert(await f.internals.preparePush());
await f.internals.finishPending();
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);
initial.initialized = true;
initial.local.a = "a.md";
initial.documents.a = { materialized: true };
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: { scan(): Promise<void> };
database: Database;
};
await internals.syncer.scan();
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.incorporateFileManifest({
fileManifestId: 2,
entries: {}
});
await f.settings.setSettings({ ignorePatterns: [] });
await f.internals.incorporateFileManifest(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.incorporateFileManifest({
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.stopped = false;
assert.equal(await f.internals.preparePush(), 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 toStoredSnapshot({ 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.incorporateEventBatch({
headEventId: 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.scan();
f.internals.stopped = false;
assert.equal(await f.internals.preparePush(), false);
await f.internals.incorporateFileManifest({
fileManifestId: 2,
entries: { a: "a.md" }
});
await f.settings.setSettings({ ignorePatterns: [] });
await f.internals.incorporateFileManifest(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.scan();
f.internals.stopped = false;
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);
assert(await f.internals.preparePush());
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.incorporateFileManifest({
fileManifestId: 2,
entries: { a: "b.md", other: "a.md" }
});
f.internals.stopped = false;
await f.internals.preparePush();
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.stopped = false;
await f.internals.preparePush();
await f.internals.finishPending();
await assert.rejects(f.syncer.waitUntilFinished(), /413/);
assert(f.syncer.isBusy);
await f.syncer.retryRejectedRequests();
assert(await f.internals.preparePush());
});
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;
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);
}
);

View file

@ -0,0 +1,175 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import {
Database,
emptyState,
type StoredDatabase
} from "../sync-client/src/persistence/database";
import { Settings } from "../sync-client/src/persistence/settings";
import { FileOperations } from "../sync-client/src/file-operations/file-operations";
import type { ServerConfig } from "../sync-client/src/services/server-config";
import type { SyncService } from "../sync-client/src/services/sync-service";
import type { WebSocketManager } from "../sync-client/src/services/websocket-manager";
import type { EventBatch } from "../sync-client/src/services/types/EventBatch";
import type { EventRecord } from "../sync-client/src/services/types/EventRecord";
import { Syncer } from "../sync-client/src/sync-operations/syncer";
import { Logger } from "../sync-client/src/tracing/logger";
import { SyncHistory } from "../sync-client/src/tracing/sync-history";
import { EventListeners } from "../sync-client/src/utils/data-structures/event-listeners";
import { FixedSizeDocumentCache } from "../sync-client/src/utils/data-structures/fix-sized-cache";
import { MemoryDisk, MemoryPersistence } from "./storage";
for (const [count, delivery] of [
[1, "before the loop"],
[9, "before the loop"],
[3, "during the first batch"]
] as const) {
test(
`${count} WS hints delivered ${delivery} catch up through HTTP without polling`,
{ timeout: 5_000 },
async () => {
const logger = new Logger();
const initial = emptyState("event-delivery");
initial.initialized = true;
const persistence = new MemoryPersistence({ database: initial });
const database = new Database(
initial,
async (next) => persistence.save({ database: next }),
initial.vaultKey,
async () =>
(await persistence.load()).database as StoredDatabase
);
const settings = new Settings(
logger,
{ syncIntervalMs: 0 },
async () => {}
);
assert.equal(settings.getSettings().syncIntervalMs, 0);
const disk = new MemoryDisk();
const initializing = Promise.withResolvers<void>();
const initialized = Promise.withResolvers<void>();
const firstBatch = Promise.withResolvers<void>();
const continueBatch = Promise.withResolvers<void>();
let heldBatch = false;
const applied: number[] = [];
persistence.boundary = async (label) => {
if (label !== "durable:save") return;
const cursor =
persistence.snapshot().database!.lastSeenUpdateId;
if (cursor && applied.at(-1) !== cursor) applied.push(cursor);
if (
delivery === "during the first batch" &&
cursor === 1 &&
!heldBatch
) {
heldBatch = true;
firstBatch.resolve();
await continueBatch.promise;
}
};
const config = {
initialize: async () => {
initializing.resolve();
await initialized.promise;
},
getConfig: async () => ({ mergeableFileExtensions: ["md"] }),
reset: () => {}
} as unknown as ServerConfig;
const events: EventRecord[] = Array.from(
{ length: count },
(_, index) => ({
eventId: index + 1,
requestId: `manifest-${index + 1}`,
type: "fileManifest",
fileManifest: { fileManifestId: index + 1, entries: {} }
})
);
let available: number = delivery === "before the loop" ? count : 1;
const fetchedAfter: number[] = [];
const service = {
getEvents: async (after: number): Promise<EventBatch> => {
fetchedAfter.push(after);
return {
headEventId: available,
events: events.filter(
(event) =>
event.eventId > after &&
event.eventId <= available
)
};
}
} as unknown as SyncService;
const websocket = {
onWebSocketStatusChanged: new EventListeners<
(connected: boolean) => unknown
>(),
onRemoteVaultUpdateReceived: new EventListeners<
(batch: EventBatch) => unknown
>()
};
const syncer = new Syncer(
"device",
logger,
database,
settings,
service,
websocket as unknown as WebSocketManager,
new FileOperations(disk.session(), database, config),
config,
new SyncHistory(logger),
new FixedSizeDocumentCache(1024)
);
const deliver = async (event: EventRecord) => {
await websocket.onRemoteVaultUpdateReceived.triggerAsync({
headEventId: event.eventId,
// Payloads are deliberately inconsistent with HTTP. The socket
// only wakes the loop; it cannot supply reconciliation state.
events: [
{
...event,
type: "fileManifest",
fileManifest: { fileManifestId: 999, entries: {} }
}
]
});
};
try {
syncer.start();
await initializing.promise;
if (delivery === "before the loop") {
for (const event of events) await deliver(event);
initialized.resolve();
} else {
await deliver(events[0]);
initialized.resolve();
await firstBatch.promise;
available = count;
for (const event of events.slice(1)) await deliver(event);
continueBatch.resolve();
}
await syncer.waitUntilFinished();
assert.equal(
database.state.lastSeenUpdateId,
count,
"Client became idle with already-delivered events unapplied"
);
assert.deepEqual(
applied,
delivery === "before the loop" ? [count] : [1, count],
"Each HTTP catchup commits its final cursor"
);
assert.deepEqual(
fetchedAfter,
delivery === "before the loop" ? [0] : [0, 1]
);
assert.equal(database.state.fileManifest.fileManifestId, count);
assert.equal(database.state.pending, undefined);
assert.equal(syncer.isBusy, false);
} finally {
initialized.resolve();
continueBatch.resolve();
await syncer.stop();
}
}
);
}

View file

@ -0,0 +1,311 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { fixture, bytes } from "./sync-fixture";
import { toStoredSnapshot } from "../sync-client/src/sync-operations/content";
import { LocalChangesDuringReconciliation } from "../sync-client/src/errors/errors";
for (const equal of [false, true])
test(`swaps preserve identities with ${equal ? "equal" : "distinct"} content and save once`, async () => {
const f = await fixture({ "a.md": "A", "b.md": equal ? "A" : "B" });
let saves = 0;
f.persistence.boundary = (label) => {
if (label === "before:save") saves++;
};
const next = structuredClone(f.database.state);
next.local = { a: "b.md", b: "a.md" };
await f.files.apply(next);
assert.deepEqual(f.database.state.local, next.local);
assert.deepEqual(
f.disk.userFiles(),
new Map([
["a.md", bytes(equal ? "A" : "B")],
["b.md", bytes("A")]
])
);
assert.equal(saves, 1);
assert.deepEqual(await f.disk.listFilesRecursively(), ["a.md", "b.md"]);
});
for (const direction of ["file-to-directory", "directory-to-file"])
test(direction, async () => {
const f = await fixture(
direction === "file-to-directory"
? { folder: "A", "b.md": "B" }
: { "a.md": "A", "folder/b.md": "B" }
);
const ids = Object.keys(f.database.state.local);
const next = structuredClone(f.database.state);
next.local =
direction === "file-to-directory"
? { [ids[0]]: "folder/a.md", [ids[1]]: "b.md" }
: { [ids[0]]: "folder", [ids[1]]: "other.md" };
const expected = structuredClone(next.local);
await f.files.apply(next);
assert.deepEqual(f.database.state.local, expected);
assert.deepEqual(
new Set(f.disk.userFiles().keys()),
new Set(Object.values(f.database.state.local))
);
assert.deepEqual(
[...f.disk.userFiles().values()]
.map((value) => Buffer.from(value).toString())
.sort(),
["A", "B"]
);
});
test("an unexpected occupant is preserved under a visible conflict name", async () => {
const f = await fixture();
await f.disk.userWrite("remote.md", bytes("unexpected"));
const next = structuredClone(f.database.state);
next.local.a = "remote.md";
await f.files.apply(next);
assert.equal(f.database.state.local.a, "remote.md");
assert.equal(f.disk.userFiles().size, 2);
assert.deepEqual(
[...f.disk.userFiles().values()]
.map((value) => Buffer.from(value).toString())
.sort(),
["local", "unexpected"]
);
assert.equal(Object.keys(f.database.state.local).length, 2);
});
test("content edited after planning is merged with the incoming snapshot", async () => {
const f = await fixture({ "a.md": "base\n" });
const expected = (await f.files.snapshot("a.md"))!;
await f.disk.userWrite("a.md", bytes("base\nlocal\n"));
await f.files.apply(structuredClone(f.database.state), {
a: {
expected,
replacement: await toStoredSnapshot({
content: bytes("remote\nbase\n")
})
}
});
assert.deepEqual(
f.disk.userFiles().get("a.md"),
bytes("remote\nbase\nlocal\n")
);
});
test("deleting the source after planning does not resurrect it", async () => {
const f = await fixture();
const next = structuredClone(f.database.state);
next.local.a = "remote.md";
await f.disk.userDelete("a.md");
await f.files.apply(next, {
a: { replacement: await toStoredSnapshot({ content: bytes("remote") }) }
});
assert.deepEqual(f.disk.userFiles(), new Map());
assert.deepEqual(f.database.state.local, {});
});
for (const protect of ["ignore", "size"] as const)
test(`protect ${protect} at application time`, async () => {
const f = await fixture();
const next = structuredClone(f.database.state);
next.local.a = "remote.md";
if (protect === "ignore")
await f.settings.setSetting("ignorePatterns", ["a.md"]);
else await f.settings.setSetting("maxFileSizeMB", 0);
await f.files.apply(next, {
a: {
replacement: await toStoredSnapshot({
content: bytes("remote")
})
}
});
assert.deepEqual(
f.disk.userFiles(),
new Map([["a.md", bytes("local")]])
);
assert.equal(f.database.state.local.a, "a.md");
assert.equal(f.database.state.excluded?.a, "a.md");
});
for (const when of ["planning", "installed"])
test(`notified move during ${when} is attributed to the right identity on rescan`, async () => {
const f = await fixture({ "a.md": "A", "b.md": "B" });
let changed = false;
f.disk.boundary = async (label) => {
if (
changed ||
label !==
(when === "planning"
? "read:stat:a.md"
: "visible:rename:a.md->b.md")
)
return;
changed = true;
const oldPath = when === "planning" ? "a.md" : "b.md";
await f.disk.userRename(oldPath, "editor.md");
await f.syncer.syncLocallyUpdatedFile({
oldPath,
relativePath: "editor.md"
});
};
const next = structuredClone(f.database.state);
next.local = { a: "b.md", b: "a.md" };
if (when === "planning")
await assert.rejects(
f.files.apply(next),
LocalChangesDuringReconciliation
);
else await f.files.apply(next);
assert(changed);
await f.internals.scan();
assert.equal(f.database.state.local.a, "editor.md");
assert.deepEqual(
f.disk.userFiles().get(f.database.state.local.a),
bytes("A")
);
assert.deepEqual(
f.disk.userFiles().get(f.database.state.local.b),
bytes("B")
);
});
for (const action of ["move", "delete"] as const)
test(`notified ${action}/recreate between file operations does not repurpose the replacement file`, async () => {
const f = await fixture({ "a.md": "A", "b.md": "B" });
await f.settings.setSetting("ignorePatterns", ["private.md"]);
let changed = false;
f.disk.boundary = async (label) => {
if (changed || label !== "visible:rename:a.md->remote-a.md") return;
changed = true;
if (action === "move") {
await f.disk.userRename("b.md", "private.md");
await f.syncer.syncLocallyUpdatedFile({
oldPath: "b.md",
relativePath: "private.md"
});
} else {
await f.disk.userDelete("b.md");
await f.syncer.syncLocallyDeletedFile("b.md");
}
await f.disk.userWrite("b.md", bytes("replacement"));
await f.syncer.syncLocallyCreatedFile("b.md");
};
const next = structuredClone(f.database.state);
next.local = { a: "remote-a.md", b: "remote-b.md" };
await f.files.apply(next);
assert(changed);
await f.internals.scan();
assert.deepEqual(f.disk.userFiles().get("b.md"), bytes("replacement"));
assert.equal(f.disk.userFiles().has("remote-b.md"), false);
assert.equal(
f.database.state.local.b,
action === "move" ? "private.md" : undefined
);
if (action === "move")
assert.deepEqual(f.disk.userFiles().get("private.md"), bytes("B"));
});
test("a file and nested directory exchange their exact paths", async () => {
const f = await fixture({ a: "A", "b/deep/child": "B" });
const [a, b] = Object.keys(f.database.state.local);
const next = structuredClone(f.database.state);
next.local = { [a]: "b", [b]: "a/deep/child" };
await f.files.apply(next);
assert.deepEqual(f.database.state.local, { [a]: "b", [b]: "a/deep/child" });
assert.deepEqual(
f.disk.userFiles(),
new Map([
["b", bytes("A")],
["a/deep/child", bytes("B")]
])
);
});
for (const phase of ["before", "durable"])
test(`retrying after a ${phase}-save failure does not merge text twice`, async () => {
const f = await fixture({ "a.md": "base\n" });
const original = structuredClone(f.database.state);
const write = {
expected: (await f.files.snapshot("a.md"))!,
replacement: await toStoredSnapshot({
content: bytes("remote\nbase\n")
})
};
await f.disk.userWrite("a.md", bytes("base\nlocal\n"));
f.persistence.boundary = (label) => {
if (label === `${phase}:save`) {
assert.deepEqual(
f.disk.userFiles().get("a.md"),
bytes("remote\nbase\nlocal\n")
);
throw new Error("save failed");
}
};
await assert.rejects(
f.files.apply(structuredClone(original), { a: write }),
/save failed/
);
f.persistence.boundary = () => {};
await f.database.recoverPersistence();
await f.files.apply(structuredClone(original), { a: write });
assert.deepEqual(
f.disk.userFiles().get("a.md"),
bytes("remote\nbase\nlocal\n")
);
assert.equal(
f.database.state.documents.a.observedHash,
(await f.files.snapshot("a.md"))!.hash
);
});
test("atomic updater includes an edit made after the last snapshot without unlinking the file", async () => {
const f = await fixture({ "a.md": "base\n" });
const expected = (await f.files.snapshot("a.md"))!;
let edited = false;
f.disk.boundary = async (label) => {
assert(
!label.includes("unlink:"),
"Updating text must not delete the file"
);
if (label === "before:write:a.md" && !edited) {
edited = true;
await f.disk.userWrite("a.md", bytes("base\nlatest edit\n"));
}
};
await f.files.apply(structuredClone(f.database.state), {
a: {
expected,
replacement: await toStoredSnapshot({
content: bytes("remote\nbase\n")
})
}
});
assert(edited);
assert.deepEqual(
f.disk.userFiles().get("a.md"),
bytes("remote\nbase\nlatest edit\n")
);
assert.equal(
f.database.state.documents.a.observedHash,
(await f.files.snapshot("a.md"))!.hash
);
});
test("a failed atomic update leaves database metadata unchanged", async () => {
const f = await fixture();
const original = structuredClone(f.database.state);
f.disk.boundary = (label) => {
if (label === "before:write:a.md") throw new Error("update failed");
};
await assert.rejects(
f.files.apply(structuredClone(original), {
a: {
expected: (await f.files.snapshot("a.md"))!,
replacement: await toStoredSnapshot({
content: bytes("remote")
})
}
}),
/update failed/
);
assert.deepEqual(f.database.state, original);
assert.deepEqual(f.persistence.snapshot().database, original);
assert.deepEqual(f.disk.userFiles().get("a.md"), bytes("local"));
});

View file

@ -0,0 +1,317 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { MemoryDisk, MemoryPersistence } from "./storage";
import { NetworkFaults, classifyRequest } from "./network";
import {
ContentLedger,
assertManifest,
assertSameFiles,
pathDecision
} from "./oracles";
import { Random } from "./random";
import { TestRunner } from "../deterministic-tests/src/test-runner";
import type { ServerControl } from "../deterministic-tests/src/server-control";
import { Logger } from "sync-client";
import { ManagedWebSocketFactory } from "../deterministic-tests/src/managed-websocket";
const bytes = (text: string) => new TextEncoder().encode(text);
test("consistency oracle rejects self comparison, divergent paths and invalid UTF-8 bytes", () => {
const first = new Map([["a.bin", new Uint8Array([0xff])]]);
assert.throws(() => assertSameFiles([first, first]), /itself/);
assert.throws(() =>
assertSameFiles([first, new Map([["a.bin", new Uint8Array([0xfe])]])])
);
assert.throws(() => assertSameFiles([first, new Map()]));
assertSameFiles([first, structuredClone(first)]);
});
test("ledger catches shared loss, duplicated bytes, and unrelated loss in delete workloads", () => {
const ledger = new ContentLedger();
ledger.add("first-marker");
ledger.add("second-marker");
ledger.removedBy("first-marker", "explicit delete of doc A");
assert.throws(
() => ledger.assertPreserved(new Map()),
/second-marker.*lost/
);
assert.throws(
() =>
ledger.assertPreserved(
new Map([["a", bytes("second-marker second-marker")]])
),
/duplicated/
);
ledger.assertPreserved(new Map([["a", bytes("second-marker")]]));
});
test("memory adapter enforces v4 operations and snapshot ownership", async () => {
const disk = new MemoryDisk();
await disk.userWrite("folder/a", bytes("A"));
const snap = (await disk.read("folder/a"))!;
snap.content[0] = 0;
assert.equal(
Buffer.from((await disk.read("folder/a"))!.content).toString(),
"A"
);
await disk.write("folder/a", { content: bytes("updated") });
assert.equal(
Buffer.from((await disk.read("folder/a"))!.content).toString(),
"updated"
);
await disk.userWrite("folder/b", bytes("B"));
await assert.rejects(disk.rename("folder/a", "folder/b"), /exists/);
await assert.rejects(disk.deleteDirectory("folder/a"), /regular file/);
await assert.rejects(disk.deleteDirectory("folder"), /regular file/);
await assert.rejects(disk.read("../outside"), /Unsafe/);
await assert.rejects(disk.stat("folder/a/child"), /ancestor/);
await assert.rejects(disk.read("folder"), /directory/);
});
for (const phase of ["before", "visible", "durable"] as const) {
for (const powerLoss of [false, true]) {
test(`${powerLoss ? "power loss" : "process death"} at ${phase} rename boundary`, async () => {
const disk = new MemoryDisk();
await disk.userWrite("a", bytes("A"));
const session = disk.session();
let fired = false;
disk.boundary = (label) => {
if (label === `${phase}:rename:a->b`) {
fired = true;
disk.crash(powerLoss);
throw new Error("crash");
}
};
await assert.rejects(session.rename("a", "b"), /crash/);
assert(fired);
disk.boundary = () => { };
const moved =
phase === "durable" || (phase === "visible" && !powerLoss);
assert.deepEqual([...disk.userFiles().keys()], [moved ? "b" : "a"]);
assert.throws(
() => session.write("stale", { content: bytes("bad") }),
/crashed/
);
});
}
}
test("persistence has deep ownership and both old/new uncertain save outcomes", async () => {
for (const phase of ["before", "durable"]) {
const store = new MemoryPersistence({ settings: { vaultName: "old" } });
const loaded = await store.load();
loaded.settings!.vaultName = "mutated";
assert.equal(store.snapshot().settings!.vaultName, "old");
store.boundary = (label) => {
if (label === `${phase}:save`) throw new Error("save failed");
};
await assert.rejects(
store.save({ settings: { vaultName: "new" } }),
/save failed/
);
assert.equal(
store.snapshot().settings!.vaultName,
phase === "before" ? "old" : "new"
);
}
});
test("CAS hooks match v4 create, update and manifest and preserve retry payload", async () => {
for (const kind of ["create", "content", "manifest"] as const) {
const net = new NetworkFaults();
const url = `http://test/vaults/test/${kind === "manifest" ? "file-manifest" : "documents/uuid"}`;
const body = {
requestId: "same",
parentVersionId: kind === "create" ? null : 3
};
const init = { method: "PUT", body: JSON.stringify(body) };
assert.equal(classifyRequest(url, init)?.kind, kind);
let committed = 0;
const wrapped = net.wrap(async () => {
committed++;
return new Response('{"type":"Accepted"}');
});
net.arm(kind);
await assert.rejects(wrapped(url, init), /Injected after-commit/);
await net.wait();
assert.throws(() => net.assertConsumed(), /never retried/);
assert.equal(committed, 1);
await wrapped(url, init);
net.assertConsumed();
await assert.rejects(
wrapped(url, {
...init,
body: JSON.stringify({ ...body, changed: true })
}),
/Retry changed/
);
}
assert.equal(
classifyRequest("http://test/documents", { method: "POST" }),
undefined
);
});
test("unconsumed and before-send faults cannot silently pass", async () => {
const net = new NetworkFaults();
net.arm("manifest", "before");
assert.throws(() => net.assertConsumed(), /never fired/);
let called = false;
await assert.rejects(
net.wrap(async () => {
called = true;
return new Response();
})("http://test/file-manifest", {
method: "PUT",
body: '{"requestId":"r"}'
})
);
assert(!called);
assert.throws(() => net.assertConsumed(), /never retried/);
await net.wrap(async () => new Response())("http://test/file-manifest", {
method: "PUT",
body: '{"requestId":"r"}'
});
net.assertConsumed();
});
test("HTTP observation checkpoint blocks catchup, resumes, and respects abort", async () => {
const network = new NetworkFaults();
const wrapped = network.wrap(async () => new Response("{}"));
network.pauseObservation();
let delivered = false;
const request = wrapped("http://test/events-since?after=0").then(() => {
delivered = true;
});
await network.waitForObservation();
assert(!delivered, "Catchup bypassed the observation checkpoint");
network.resumeObservation();
await request;
network.assertConsumed();
network.pauseObservation();
const controller = new AbortController();
const interrupted = wrapped("http://test/vault-snapshot", {
signal: controller.signal
});
const rejected = assert.rejects(interrupted, /abort/i);
await network.waitForObservation();
controller.abort();
await rejected;
network.resumeObservation();
network.assertConsumed();
});
test("independent path decision truth table covers create, delete and rename", () => {
for (const base of [undefined, "a", "b"])
for (const local of [undefined, "a", "b"])
for (const remote of [undefined, "a", "b"]) {
const result = pathDecision(base, local, remote);
assert.equal(result, remote !== base ? remote : local);
}
assertManifest({ a: "folder/é.md", b: "other.bin" });
for (const entries of [
{ a: "A", b: "a" },
{ a: "é", b: "e\u0301" },
{ a: "x", b: "x/y" }
])
assert.throws(() => assertManifest(entries));
});
test("seeded actions are repeatable, including seed zero", () => {
for (const seed of [0, 1, 12345, 0xffffffff]) {
const first = new Random(seed);
const second = new Random(seed);
assert.deepEqual(
Array.from({ length: 100 }, () => first.int(100)),
Array.from({ length: 100 }, () => second.int(100))
);
}
});
test("runner cleanup errors fail an otherwise successful test", async () => {
const server = {
isRunning: () => true,
resume: () => { }
} as unknown as ServerControl;
const runner = new TestRunner(
server,
new Logger(),
"unused",
"http://unused"
);
let cleaned = 0;
Object.assign(runner, {
initializeAgents: async () => { },
agents: [
{
clientId: 0,
database: () => ({ evidence: "before cleanup" }),
disk: { image: () => ({ bytes: [0, 255] }) },
network: { requests: [{ body: { requestId: "original" } }] },
cleanup: async () => {
cleaned++;
throw new Error("late background error");
}
}
]
});
const result = await runner.runTest("cleanup-probe", {
clients: 2,
steps: []
});
assert.equal(result.success, false);
assert.match(result.error!, /late background error/);
assert.equal(cleaned, 1, "Failed cleanup was run twice");
assert.deepEqual(result.diagnostics, [
{
client: 0,
database: { evidence: "before cleanup" },
disk: { bytes: [0, 255] },
requests: [{ body: { requestId: "original" } }]
}
]);
});
test("disposed harness rejects late websocket reconnects", async () => {
const factory = new ManagedWebSocketFactory();
const Socket = factory.constructorFn;
await factory.finish();
assert.throws(() => new Socket("ws://unused"), /after harness cleanup/);
});
test("runner preserves primary failure and cleanup diagnostics", async () => {
const server = {
isRunning: () => true,
resume: () => { }
} as unknown as ServerControl;
const runner = new TestRunner(
server,
new Logger(),
"unused",
"http://unused"
);
Object.assign(runner, {
initializeAgents: async () => { },
executeStep: async () => {
throw new Error("primary assertion failed");
},
agents: [
{
clientId: 0,
database: () => ({}),
disk: { image: () => ({}) },
network: { requests: [] },
cleanup: async () => {
throw new Error("cleanup failed too");
}
}
]
});
const result = await runner.runTest("cleanup-probe", {
clients: 2,
steps: [{ type: "barrier" }]
});
assert.equal(result.success, false);
assert.match(result.error!, /primary assertion failed/);
assert.match(result.error!, /cleanup failed too/);
});

View file

@ -0,0 +1,30 @@
import assert from "node:assert/strict";
import { fixture } from "./sync-fixture";
(async () => {
const f = await fixture(
{},
{
getEvents: async () => ({ headEventId: 0, events: [] })
}
);
let delivered = 0;
f.syncer.onRemainingOperationsCountChanged.add(() => {
throw new Error("observer failure");
});
f.syncer.onRemainingOperationsCountChanged.add(async () => {
throw new Error("async observer failure");
});
f.syncer.onRemainingOperationsCountChanged.add(() => {
delivered++;
});
try {
f.syncer.start();
await f.syncer.waitUntilFinished();
const before = delivered;
f.syncer.wake();
await f.syncer.waitUntilFinished();
assert(delivered > before, "Background sync must remain usable");
} finally {
await f.syncer.stop();
}
})();

View file

@ -0,0 +1,243 @@
import assert from "node:assert/strict";
export type RequestKind = "create" | "content" | "manifest";
export interface RequestRecord {
kind: RequestKind;
method: string;
url: string;
body: Record<string, unknown>;
status?: number;
}
interface InjectedFault {
kind: RequestKind;
point: "before" | "after";
fired: boolean;
interrupted?: RequestRecord;
retried: boolean;
reached: Promise<void>;
resolve: () => void;
}
export function classifyRequest(
input: RequestInfo | URL,
init?: RequestInit
): RequestRecord | undefined {
const url = new URL(input instanceof Request ? input.url : String(input));
const method = (
init?.method ?? (input instanceof Request ? input.method : "GET")
).toUpperCase();
if (method !== "PUT") return undefined;
// The engine passes JSON strings. Fail loudly if that contract changes.
if (!/\/(documents\/[^/]+|file-manifest)\/?$/.test(url.pathname))
return undefined;
assert.equal(
typeof init?.body,
"string",
"CAS request body must be observable JSON"
);
const body = JSON.parse(init!.body as string) as Record<string, unknown>;
const kind = /\/file-manifest\/?$/.test(url.pathname)
? "manifest"
: body.parentVersionId === null
? "create"
: "content";
assert.equal(
typeof body.requestId,
"string",
"CAS request missing requestId"
);
return { kind, method, url: url.href, body };
}
export class NetworkFaults {
public readonly requests: RequestRecord[] = [];
private observation?: {
released: boolean;
fired: boolean;
held: Promise<void>;
release: () => void;
reached: Promise<void>;
reach: () => void;
};
public pauseObservation(): void {
assert(
!this.observation || this.observation.released,
"Observation already paused"
);
let release!: () => void, reach!: () => void;
const held = new Promise<void>((resolve) => {
release = resolve;
});
const reached = new Promise<void>((resolve) => {
reach = resolve;
});
this.observation = {
released: false,
fired: false,
held,
release,
reached,
reach
};
}
public async waitForObservation(): Promise<void> {
assert(this.observation, "No observation checkpoint armed");
await this.observation.reached;
}
public resumeObservation(): void {
if (this.observation) {
this.observation.released = true;
this.observation.release();
}
}
private async observationCheckpoint(
signal?: AbortSignal | null
): Promise<void> {
const gate = this.observation;
if (!gate || gate.released) return;
gate.fired = true;
gate.reach();
signal?.throwIfAborted();
let abort = () => {};
try {
await Promise.race([
gate.held,
new Promise<never>((_, reject) => {
abort = () =>
reject(
signal?.reason ?? new Error("Observation aborted")
);
signal?.addEventListener("abort", abort, { once: true });
})
]);
} finally {
signal?.removeEventListener("abort", abort);
}
}
private armed?: InjectedFault;
private readonly faults: InjectedFault[] = [];
public isExpectedFailure(message: string): boolean {
return this.faults.some(
(fault) =>
fault.fired &&
message.includes(
`Injected ${fault.point}-commit ${fault.kind} network failure`
)
);
}
public arm(kind: RequestKind, point: "before" | "after" = "after"): void {
assert(
!this.armed || (this.armed.fired && this.armed.retried),
"Previous network fault was not exercised and retried"
);
let resolve!: () => void;
const reached = new Promise<void>((r) => {
resolve = r;
});
this.armed = {
kind,
point,
fired: false,
retried: false,
reached,
resolve
};
this.faults.push(this.armed);
}
public async wait(): Promise<void> {
assert(this.armed, "No network fault armed");
await this.armed.reached;
}
public assertConsumed(): void {
assert(
!this.observation ||
(this.observation.fired && this.observation.released),
"Observation checkpoint was not exercised and released"
);
for (const fault of this.faults) {
assert(fault.fired, "Armed network fault never fired");
assert(fault.retried, "Interrupted CAS request was never retried");
}
}
public wrap(fetch: typeof globalThis.fetch): typeof globalThis.fetch {
return async (input, init) => {
const url = new URL(
input instanceof Request ? input.url : String(input)
);
const observing =
(
init?.method ??
(input instanceof Request ? input.method : "GET")
).toUpperCase() === "GET" && !url.pathname.endsWith("/ping");
const signal =
init?.signal ??
(input instanceof Request ? input.signal : undefined);
if (observing) await this.observationCheckpoint(signal);
const record = classifyRequest(input, init);
if (record) {
// A client serializes its persisted CAS requests. Its next CAS
// after a lost response must replay the interrupted operation,
// even if it has since observed newer remote state.
const interrupted = this.armed?.interrupted;
if (interrupted && !this.armed!.retried) {
assert.equal(
record.url,
interrupted.url,
"Retry changed target"
);
assert.deepEqual(
record.body,
interrupted.body,
"Retry changed payload/request identity"
);
this.armed!.retried = true;
}
const previous = this.requests.find(
(r) => r.body.requestId === record.body.requestId
);
if (previous) {
assert.equal(
record.url,
previous.url,
"Retry changed target"
);
assert.deepEqual(
record.body,
previous.body,
"Retry changed payload/request identity"
);
}
this.requests.push(record);
}
const fault = this.armed;
const matches =
record && fault && !fault.fired && fault.kind === record.kind;
const fail = () => {
fault!.interrupted = structuredClone(record!);
fault!.fired = true;
fault!.resolve();
throw new TypeError(
`Injected ${fault!.point}-commit ${fault!.kind} network failure`
);
};
if (matches && fault.point === "before") fail();
const response = await fetch(input, init);
if (observing) await this.observationCheckpoint(signal);
if (record) record.status = response.status;
// Only drop accepted replies, never confuse a rejected CAS with commit.
if (
matches &&
!fault.fired &&
fault.point === "after" &&
response.ok &&
((await response.clone().json()) as { type?: string }).type ===
"Accepted"
) {
await response.body?.cancel();
fail();
}
return response;
};
}
}

View file

@ -0,0 +1,15 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { spawnSync } from "node:child_process";
import { resolve } from "node:path";
test("a status observer exception must not terminate a background sync process", () => {
const result = spawnSync(
process.execPath,
[
resolve(__dirname, "../node_modules/tsx/dist/cli.mjs"),
resolve(__dirname, "listener-crash-child.ts")
],
{ encoding: "utf8", timeout: 10000 }
);
assert.equal(result.status, 0, result.stderr);
});

View file

@ -0,0 +1,351 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { test } from "node:test";
import type { StoredDatabase } from "sync-client";
import { assertCanonical } from "./canonical";
import { assertManifest } from "./oracles";
import { NetworkFaults } from "./network";
import { MemoryDisk } from "./storage";
import { DeterministicAgent } from "../deterministic-tests/src/deterministic-agent";
import { AssertableState } from "../deterministic-tests/src/utils/assertable-state";
import type { ExpectedDocument } from "../deterministic-tests/src/test-definition";
const bytes = (text: string) => new TextEncoder().encode(text);
const id = "00000000-0000-4000-8000-000000000001";
test("canonical checks reject repeated clients before reading snapshots", async () => {
const client = { files: () => new Map(), database: () => ({}) };
await assert.rejects(
assertCanonical([client, client], "http://unused", "unused"),
/itself/
);
});
test("canonical checks independently verify both persisted content hashes", async (context) => {
const content = bytes("\ufeffexact bytes\r\n");
const hash = createHash("sha256").update(content).digest("hex");
const document = {
documentId: id,
vaultUpdateId: 1,
contentSize: content.length,
userId: "user",
deviceId: "device",
updatedDate: "2026-09-20T00:00:00Z"
};
const manifest = { fileManifestId: 2, entries: { [id]: "a.md" } };
const state: Partial<StoredDatabase> = {
initialized: true,
local: manifest.entries,
fileManifest: manifest,
lastSeenUpdateId: 2,
documents: {
[id]: {
materialized: true,
observedHash: hash,
base: { ...document, hash }
}
}
};
context.mock.method(
globalThis,
"fetch",
async (url: string) =>
new Response(
JSON.stringify(
url.endsWith("/vault-snapshot")
? {
headEventId: 2,
fileManifest: manifest,
documents: [document]
}
: url.includes("/events-since")
? {
headEventId: 2,
events: [
{ eventId: 1, requestId: "create" },
{ eventId: 2, requestId: "manifest" }
]
}
: {
contentBase64:
Buffer.from(content).toString("base64")
}
)
)
);
const client = (database = state) => ({
files: () => new Map([["a.md", content.slice()]]),
database: () => structuredClone(database)
});
await assertCanonical([client(), client()], "http://test", "token");
for (const field of ["base", "observed"] as const) {
const corrupt = structuredClone(state);
if (field === "base") corrupt.documents![id].base!.hash = "wrong";
else corrupt.documents![id].observedHash = "wrong";
await assert.rejects(
assertCanonical(
[client(), client(corrupt)],
"http://test",
"token"
),
new RegExp(`Wrong ${field} hash`)
);
}
});
test("lost responses require an identical retry, including request identity and target", async () => {
for (const point of ["before", "after"] as const) {
const faults = new NetworkFaults();
let sent = 0;
const send = faults.wrap(async () => {
sent++;
return new Response('{"type":"Accepted"}');
});
const url = "http://test/documents/a";
const body = {
requestId: "original",
parentVersionId: 3,
content: { type: "Snapshot", value: "YQ==" }
};
const init = { method: "PUT", body: JSON.stringify(body) };
faults.arm("content", point);
await assert.rejects(send(url, init), /Injected/);
assert.throws(() => faults.assertConsumed(), /never retried/);
assert.throws(
() => faults.arm("manifest"),
/not exercised and retried/
);
const before = sent;
await assert.rejects(
send(url, {
...init,
body: JSON.stringify({ ...body, requestId: "regenerated" })
}),
/Retry changed/
);
await assert.rejects(
send(`${url}-different`, init),
/Retry changed target/
);
await assert.rejects(
send(url, {
...init,
body: JSON.stringify({ ...body, parentVersionId: 4 })
}),
/Retry changed/
);
assert.equal(sent, before, "Invalid retries must not reach the server");
assert.throws(() => faults.assertConsumed(), /never retried/);
await send(url, init);
faults.assertConsumed();
}
});
test("an update fault cannot be consumed by creating a new UUID", async () => {
const faults = new NetworkFaults();
const send = faults.wrap(async () => new Response('{"type":"Accepted"}'));
faults.arm("content");
await send("http://test/documents/new", {
method: "PUT",
body: '{"requestId":"new","parentVersionId":null}'
});
assert.throws(() => faults.assertConsumed(), /never fired/);
const update = {
method: "PUT",
body: '{"requestId":"update","parentVersionId":1}'
};
await assert.rejects(
send("http://test/documents/existing", update),
/Injected/
);
await send("http://test/documents/existing", update);
faults.assertConsumed();
});
test("unlink is idempotent even after an uncertain visible deletion", async () => {
const disk = new MemoryDisk();
await disk.userWrite("a", bytes("A"));
disk.boundary = (label) => {
if (label === "visible:unlink:a") throw new Error("uncertain unlink");
};
await assert.rejects(disk.deleteFile("a"), /uncertain/);
disk.boundary = () => {};
await disk.deleteFile("a");
disk.crash(true);
assert.equal(await disk.exists("a"), false);
await disk.deleteFile("a");
await disk.createDirectory("directory");
await assert.rejects(disk.deleteFile("directory"), /directory/);
});
test("text assertions preserve BOM, CRLF, empty content and missing final newline", async () => {
const agent = new DeterministicAgent(0, {}, () => {});
for (const content of [
"",
"\ufeffhello",
"\ufeffa\r\nb\r\n",
"last line"
]) {
await agent.disk.userWrite("a.md", bytes(content));
assert.equal(await agent.getFileContent("a.md"), content);
}
});
test("durably pruning a directory also removes its already-unlinked durable children", async () => {
const disk = new MemoryDisk();
await disk.userWrite("transaction/source", bytes("staged bytes"));
disk.boundary = (label) => {
if (label === "visible:unlink:transaction/source") {
disk.crash(false);
throw new Error("process died after unlink");
}
};
await assert.rejects(disk.deleteFile("transaction/source"), /process died/);
disk.boundary = () => {};
await disk.deleteDirectory("transaction");
disk.crash(true);
assert.deepEqual(disk.userFiles(), new Map());
assert.equal(await disk.exists("transaction"), false);
});
test("portable components are limited by UTF-8 bytes, not characters or total path length", () => {
for (const component of ["a".repeat(255), "é".repeat(127) + "a"])
assertManifest({ [id]: `${component}/${component}/${component}` });
for (const component of ["a".repeat(256), "é".repeat(128)])
assert.throws(
() => assertManifest({ [id]: `nested/${component}` }),
/Oversized component/
);
});
function state(documents: { path: string; content: string; id: string }[]) {
return new AssertableState({
files: new Map(documents.map((doc) => [doc.path, doc.content])),
clientFiles: [],
manifests: [],
canonical: Object.fromEntries(
documents.map((doc) => [doc.id, doc.path])
),
bytes: new Map(documents.map((doc) => [doc.path, bytes(doc.content)]))
});
}
test("complete scenario assertions reject collapsed creates, forgotten edits, reverted moves and recycled UUIDs", () => {
const identities = new Map<string, string>();
const expected: ExpectedDocument[] = [
{ key: "a", path: "a.md", content: "A\nimportant edit" },
{ key: "b", path: "b.md", content: "B" }
];
const original = [
{ id: "id-a", path: "a.md", content: "A\nimportant edit" },
{ id: "id-b", path: "b.md", content: "B" }
];
state(original).assertDocuments(expected, identities);
assert.throws(
() =>
state([
{ ...original[0], content: "A\nimportant edit\nB" }
]).assertDocuments(expected, identities),
/Expected 2 file/
);
assert.throws(
() =>
state([
{ ...original[0], content: "A" },
original[1]
]).assertDocuments(expected, identities),
/Byte mismatch/
);
const moved = [
{ ...expected[0], path: "b.md" },
{ ...expected[1], path: "a.md" }
];
assert.throws(
() => state(original).assertDocuments(moved, identities),
/Byte mismatch/
);
assert.throws(
() =>
state([
{ ...original[0], id: "id-b" },
{ ...original[1], id: "id-a" }
]).assertDocuments(expected, identities),
/identity changed/
);
state([original[1]]).assertDocuments([expected[1]], identities);
assert.throws(
() =>
state(original).assertDocuments(
[{ ...expected[0], key: "recreated" }, expected[1]],
identities
),
/reused.*deleted UUID/
);
assert.equal(identities.has("recreated"), false);
const conflict = [
{ key: "one", path: "same.md", content: "one" },
{ key: "two", path: "same.md", conflict: true, content: "two" }
];
state([
{ id: "one", path: "same.md", content: "one" },
{ id: "two", path: "same (conflict two).md", content: "two" }
]).assertDocuments(conflict, new Map());
});
test("canonical checks follow pages without weakening gap or duplicate-request detection", async (context) => {
const manifest = { fileManifestId: 3, entries: {} };
const client = () => ({
files: () => new Map<string, Uint8Array>(),
database: () => ({
initialized: true,
local: {},
documents: {},
fileManifest: manifest,
lastSeenUpdateId: 3
})
});
let fault = "none";
context.mock.method(globalThis, "fetch", async (input: string) => {
if (input.endsWith("/vault-snapshot"))
return Response.json({
headEventId: 3,
fileManifest: manifest,
documents: []
});
const after = Number(new URL(input).searchParams.get("after"));
if (after === 0)
return Response.json({
headEventId: 3,
endEventId: 1,
events: [{ eventId: 1, requestId: "first" }]
});
assert.equal(after, 1);
return Response.json({
headEventId: 3,
endEventId: fault === "empty" ? 1 : 3,
events:
fault === "empty"
? []
: [
...(fault === "gap"
? []
: [
{
eventId: 2,
requestId:
fault === "duplicate"
? "first"
: "second"
}
]),
{ eventId: 3, requestId: "third" }
]
});
});
await assertCanonical([client(), client()], "http://test", "token");
for (fault of ["gap", "duplicate", "empty"])
await assert.rejects(
assertCanonical([client(), client()], "http://test", "token"),
/contiguous|multiple events|progress/
);
});

View file

@ -0,0 +1,163 @@
import assert from "node:assert/strict";
import type { StoredDatabase } from "sync-client";
export function assertSameFiles(
clients: readonly Map<string, Uint8Array>[]
): void {
assert(
clients.length >= 2,
"Consistency requires at least two distinct clients"
);
assert.equal(
new Set(clients).size,
clients.length,
"Cannot compare a client to itself"
);
const sorted = (files: Map<string, Uint8Array>) =>
[...files].sort(([a], [b]) => a.localeCompare(b));
for (let i = 1; i < clients.length; i++)
assert.deepEqual(
sorted(clients[i]),
sorted(clients[0]),
`Client ${i} differs from client 0 (paths or bytes)`
);
}
export function assertManifest(entries: Record<string, string>): void {
const paths = Object.values(entries);
const folded = paths.map((p) =>
p.normalize("NFC").toUpperCase().normalize("NFC")
);
const directorySpellings = new Map<string, string>();
for (const path of paths) {
assert.equal(
path,
path.normalize("NFC"),
`Noncanonical Unicode path: ${path}`
);
const parts = path.split("/");
parts.forEach((part, i) => {
assert(
Buffer.byteLength(part, "utf8") <= 255,
`Oversized component: ${part}`
);
assert(
!/[<>:"\\|?*\p{Cc}]|[. ]$/u.test(part),
`Nonportable component: ${part}`
);
assert(
!/^(CON|PRN|AUX|NUL|CONIN\$|CONOUT\$|COM[1-9¹²³]|LPT[1-9¹²³])$/u.test(
part.split(".")[0].trimEnd().toUpperCase()
),
`Reserved component: ${part}`
);
if (i < parts.length - 1) {
const prefix = parts.slice(0, i + 1).join("/");
const key = prefix.toUpperCase().normalize("NFC");
const previous = directorySpellings.get(key);
assert(
previous === undefined || previous === prefix,
`Aliased directory spellings: ${prefix} and ${previous}`
);
directorySpellings.set(key, prefix);
}
});
}
assert.equal(
new Set(folded).size,
paths.length,
"Manifest contains aliased/duplicate paths"
);
for (const path of folded) {
assert(
path && !path.startsWith("/") && !path.includes("\\"),
`Invalid path ${path}`
);
assert(
path.split("/").every((p) => p && p !== "." && p !== ".."),
`Invalid component ${path}`
);
assert(
path.split("/")[0] !== ".VAULT-LINK-SYNC",
"Internal path in manifest"
);
assert(
!folded.some(
(other) => other !== path && other.startsWith(`${path}/`)
),
`File/ancestor conflict: ${path}`
);
}
}
/** Independent per-ID policy oracle, intentionally not importing merge code. */
export function pathDecision(
base: string | undefined,
local: string | undefined,
remote: string | undefined
): string | undefined {
if (local === base) return remote;
if (remote === base) return local;
return remote;
}
export function assertQuiescent(
state: Partial<StoredDatabase>,
entries: Record<string, string>,
head: number
): void {
assert.equal(
state.pending,
undefined,
"Pending CAS request at convergence"
);
assert.deepEqual(
state.local,
entries,
"Local UUID/path map differs from server"
);
assert.deepEqual(
state.fileManifest?.entries,
entries,
"Client manifest differs from server"
);
assert.equal(
state.lastSeenUpdateId,
head,
"Client event watermark differs from server"
);
assertManifest(entries);
}
/** Removal exemptions name exact markers and the explicit destructive action.
* Deletion mode never disables preservation for unrelated documents. */
export class ContentLedger {
private readonly markers = new Map<string, string | undefined>();
public add(marker: string): void {
assert(!this.markers.has(marker), "Duplicate marker");
this.markers.set(marker, undefined);
}
public removedBy(content: string, action: string): void {
assert(action.length > 0);
for (const marker of this.markers.keys())
if (content.includes(marker)) this.markers.set(marker, action);
}
public assertPreserved(files: Map<string, Uint8Array>): void {
const texts = [...files.values()].map((bytes) =>
Buffer.from(bytes).toString("utf8")
);
for (const [marker, deletion] of this.markers) {
const count = texts.reduce(
(n, text) => n + text.split(marker).length - 1,
0
);
assert(count <= 1, `Marker ${marker} duplicated ${count} times`);
if (!deletion)
assert.equal(
count,
1,
`Marker ${marker} lost without an explicit delete/overwrite`
);
}
}
}

View file

@ -0,0 +1,316 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import {
Database,
emptyState,
type StoredDatabase
} from "../sync-client/src/persistence/database";
import { Settings } from "../sync-client/src/persistence/settings";
import { FileOperations } from "../sync-client/src/file-operations/file-operations";
import { Syncer } from "../sync-client/src/sync-operations/syncer";
import { toStoredSnapshot } from "../sync-client/src/sync-operations/content";
import { Logger } from "../sync-client/src/tracing/logger";
import { SyncHistory } from "../sync-client/src/tracing/sync-history";
import { FixedSizeDocumentCache } from "../sync-client/src/utils/data-structures/fix-sized-cache";
import { EventListeners } from "../sync-client/src/utils/data-structures/event-listeners";
import type { SyncService } from "../sync-client/src/services/sync-service";
import type { ServerConfig } from "../sync-client/src/services/server-config";
import type { WebSocketManager } from "../sync-client/src/services/websocket-manager";
import type { EventBatch } from "../sync-client/src/services/types/EventBatch";
import type { EventRecord } from "../sync-client/src/services/types/EventRecord";
import type { PutFileContent } from "../sync-client/src/services/types/PutFileContent";
import { MemoryDisk, MemoryPersistence } from "./storage";
const documentId = "00000000-0000-4000-8000-000000000001";
const interval = 1_000;
const retryInterval = 75;
const bytes = (text: string) => new TextEncoder().encode(text);
/** Real sync loop over in-memory disk/persistence and an observable version
* store. The transport never wakes the engine itself. */
async function fixture(syncIntervalMs: number | undefined) {
const disk = new MemoryDisk();
await disk.userWrite("note.bin", bytes("base"));
let headEventId = 2; // Initial content (1), then manifest (2).
const versions = new Map([[1, bytes("base")]]);
const head = (version: number) => ({
documentId,
vaultUpdateId: version,
contentSize: versions.get(version)!.length,
updatedDate: "2026-09-20T00:00:00Z",
userId: "user",
deviceId: "device"
});
let latest = head(1);
const events: EventRecord[] = [];
const writes: PutFileContent[] = [];
let eventReads = 0;
let failNextRead = false;
const publishContent = (content: Uint8Array, requestId: string) => {
const eventId = ++headEventId;
versions.set(eventId, new Uint8Array(content));
latest = head(eventId);
events.push({ eventId, requestId, type: "content", document: latest });
return latest;
};
const batch = (after = 2): EventBatch =>
structuredClone({
headEventId,
events: events.filter((event) => event.eventId > after)
});
const service: Partial<SyncService> = {
getEvents: async (after) => {
eventReads++;
if (failNextRead) {
failNextRead = false;
throw new Error("offline");
}
return batch(after);
},
getDocumentVersionContent: async ({ vaultUpdateId }) => {
const content = versions.get(vaultUpdateId);
assert(content, `Missing immutable version ${vaultUpdateId}`);
return content.slice();
},
putFileContent: async (id, request) => {
assert.equal(id, documentId);
assert.equal(request.parentVersionId, latest.vaultUpdateId);
assert(request.content.type === "Snapshot");
writes.push(structuredClone(request));
return {
type: "Accepted",
...publishContent(
Buffer.from(request.content.value, "base64"),
request.requestId
)
};
}
};
const initial = emptyState("polling-test");
initial.initialized = true;
initial.lastSeenUpdateId = 2;
initial.local = { [documentId]: "note.bin" };
initial.fileManifest = { fileManifestId: 2, entries: { ...initial.local } };
initial.remoteHeads = { [documentId]: latest };
const hash = (await toStoredSnapshot({ content: bytes("base") })).hash;
initial.documents[documentId] = {
materialized: true,
observedHash: hash,
base: { ...latest, hash }
};
const persistence = new MemoryPersistence({ database: initial });
const logger = new Logger();
const database = new Database(
initial,
async (next) => persistence.save({ database: next }),
initial.vaultKey,
async () => (await persistence.load()).database as StoredDatabase
);
const settings = new Settings(
logger,
{ syncIntervalMs, networkRetryIntervalMs: retryInterval },
async () => {}
);
const config = {
initialize: async () => {},
getConfig: async () => ({ mergeableFileExtensions: [] }),
reset: () => {}
} as unknown as ServerConfig;
const websocket = {
onWebSocketStatusChanged: new EventListeners<
(connected: boolean) => unknown
>(),
onRemoteVaultUpdateReceived: new EventListeners<
(batch: EventBatch) => Promise<void>
>()
};
const syncer = new Syncer(
"device",
logger,
database,
settings,
service as SyncService,
websocket as unknown as WebSocketManager,
new FileOperations(disk.session(), database, config),
config,
new SyncHistory(logger),
new FixedSizeDocumentCache(0)
);
return {
disk,
database,
syncer,
writes,
batch,
websocket,
reads: () => eventReads,
serverContent: () => versions.get(latest.vaultUpdateId),
failNextRead: () => {
failNextRead = true;
},
changeRemote: () => {
publishContent(bytes("remote edit"), "remote-content");
const eventId = ++headEventId;
events.push({
eventId,
requestId: "remote-rename",
type: "fileManifest",
fileManifest: {
fileManifestId: eventId,
entries: { [documentId]: "renamed.bin" }
}
});
}
};
}
for (const syncIntervalMs of [undefined, 0, interval]) {
for (const side of ["local", "remote"] as const) {
test(`${side} changes without notifications ${syncIntervalMs ? "are recovered by explicit polling" : `remain unobserved when polling is ${syncIntervalMs === 0 ? "zero" : "unset"}`}`, async (context) => {
context.mock.timers.enable({ apis: ["setTimeout"] });
const f = await fixture(syncIntervalMs);
try {
f.syncer.start();
await f.syncer.waitUntilFinished();
const reads = f.reads();
if (side === "local")
await f.disk.userWrite("note.bin", bytes("local edit"));
else f.changeRemote();
const assertUnobserved = () => {
assert.equal(
f.reads(),
reads,
"A hidden wakeup fetched events"
);
assert.equal(f.database.state.lastSeenUpdateId, 2);
assert.equal(f.writes.length, 0);
assert.deepEqual(
f.serverContent(),
bytes(side === "local" ? "base" : "remote edit")
);
assert.deepEqual(
f.disk.userFiles(),
new Map([
[
"note.bin",
bytes(side === "local" ? "local edit" : "base")
]
])
);
};
context.mock.timers.tick(
syncIntervalMs ? interval - 1 : 60_000
);
await f.syncer.waitUntilFinished();
assertUnobserved();
if (syncIntervalMs) context.mock.timers.tick(1);
else if (side === "local")
await f.syncer.syncLocallyUpdatedFile({
relativePath: "note.bin"
});
else
await f.websocket.onRemoteVaultUpdateReceived.triggerAsync(
f.batch()
);
await f.syncer.waitUntilFinished();
const content = bytes(
side === "local" ? "local edit" : "remote edit"
);
const path = side === "local" ? "note.bin" : "renamed.bin";
assert.deepEqual(f.serverContent(), content);
assert.deepEqual(
f.disk.userFiles(),
new Map([[path, content]])
);
assert.deepEqual(f.database.state.local, {
[documentId]: path
});
assert.equal(
f.database.state.lastSeenUpdateId,
side === "local" ? 3 : 4
);
assert.equal(f.syncer.isBusy, false);
assert.equal(f.writes.length, side === "local" ? 1 : 0);
if (syncIntervalMs) {
const beforeNextPoll = f.reads();
context.mock.timers.tick(interval);
await f.syncer.waitUntilFinished();
assert.equal(
f.reads(),
beforeNextPoll + 1,
"Polling stopped after its first successful run"
);
assert.equal(
f.writes.length,
side === "local" ? 1 : 0,
"An idle poll resubmitted unchanged content"
);
}
await f.syncer.stop();
const stoppedReads = f.reads();
context.mock.timers.tick(60_000);
await f.syncer.waitUntilFinished();
assert.equal(
f.reads(),
stoppedReads,
"Polling continued after stop"
);
} finally {
await f.syncer.stop();
context.mock.timers.reset();
}
});
}
}
test("transient failures retry with polling disabled, then leave the client idle", async (context) => {
context.mock.timers.enable({ apis: ["setTimeout"] });
const f = await fixture(0);
try {
f.changeRemote();
f.failNextRead();
f.syncer.start();
await assert.rejects(f.syncer.waitUntilFinished(), /offline/);
const reads = f.reads();
context.mock.timers.tick(retryInterval - 1);
await assert.rejects(f.syncer.waitUntilFinished(), /offline/);
assert.equal(f.reads(), reads);
context.mock.timers.tick(1);
await f.syncer.waitUntilFinished();
assert.equal(f.reads(), reads + 1);
assert.deepEqual(
f.disk.userFiles(),
new Map([["renamed.bin", bytes("remote edit")]])
);
assert.equal(f.database.state.lastSeenUpdateId, 4);
assert.equal(f.syncer.isBusy, false);
context.mock.timers.tick(60_000);
await f.syncer.waitUntilFinished();
assert.equal(
f.reads(),
reads + 1,
"Network retries became clean-state polling"
);
} finally {
await f.syncer.stop();
context.mock.timers.reset();
}
});
test("a failed WebSocket connection catches up over HTTP even when polling is disabled", async () => {
const f = await fixture(0);
try {
f.syncer.start();
await f.syncer.waitUntilFinished();
f.changeRemote();
f.websocket.onWebSocketStatusChanged.trigger(false);
await f.syncer.waitUntilFinished();
assert.equal(f.database.state.lastSeenUpdateId, 4);
assert.deepEqual(
f.disk.userFiles(),
new Map([["renamed.bin", bytes("remote edit")]])
);
} finally {
await f.syncer.stop();
}
});

View file

@ -0,0 +1,207 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { spawn, type ChildProcess } from "node:child_process";
import { once } from "node:events";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { tmpdir } from "node:os";
import { Logger, SyncClient } from "sync-client";
import { ServerControl } from "../deterministic-tests/src/server-control";
import { ClientPersistence } from "../local-client-cli/src/client-persistence";
import { NodeFileSystemOperations } from "../local-client-cli/src/node-filesystem";
import { ObsidianFileSystemOperations } from "../obsidian-plugin/src/obsidian-file-system";
import { getJson, type CanonicalSnapshot } from "./canonical";
const root = path.resolve(__dirname, "../..");
const token = "test-token-change-me";
const pause = () => new Promise((resolve) => setTimeout(resolve, 50));
async function until(check: () => Promise<boolean>, message: string) {
const deadline = Date.now() + 15_000;
while (!(await check())) {
assert(Date.now() < deadline, message);
await pause();
}
}
async function contents(file: string) {
try {
return await fs.readFile(file, "utf8");
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT")
return undefined;
throw error;
}
}
async function stop(child: ChildProcess, signal: NodeJS.Signals) {
if (child.exitCode !== null || child.signalCode !== null) return;
const exited = once(child, "exit");
child.kill(signal);
await exited;
}
test(
"production CLI watchers, Obsidian adapter, and full metadata survive process replacement",
{ timeout: 90_000 },
async (t) => {
const directory = await fs.mkdtemp(
path.join(tmpdir(), "vaultlink-production-")
);
const server = new ServerControl(
path.join(root, "sync-server/target/release/sync_server"),
path.join(root, "sync-server/config-e2e.yml"),
new Logger()
);
const children: ChildProcess[] = [];
let obsidian: SyncClient | undefined;
t.after(async () => {
await obsidian?.destroy();
await Promise.all(children.map((child) => stop(child, "SIGKILL")));
await server.stop();
await fs.rm(directory, { recursive: true, force: true });
});
await server.start();
const a = path.join(directory, "a"),
b = path.join(directory, "b"),
c = path.join(directory, "obsidian");
await Promise.all([a, b, c].map((p) => fs.mkdir(p)));
await fs.writeFile(path.join(a, "note.md"), "initial\n");
async function launch(vault: string) {
const child = spawn(
process.execPath,
[
path.join(root, "frontend/local-client-cli/dist/cli.js"),
"-l",
vault,
"-r",
server.remoteUri,
"-t",
token,
"-v",
"production",
"--ignore-pattern",
"**/*.tmp"
],
{ stdio: ["ignore", "pipe", "pipe"] }
);
children.push(child);
let log = "";
child.stdout!.on("data", (data: Buffer) => {
log += data.toString();
});
child.stderr!.on("data", (data: Buffer) => {
log += data.toString();
});
await until(async () => {
assert(
child.exitCode === null && child.signalCode === null,
`CLI exited: ${log}`
);
return log.includes("File watcher started");
}, `CLI did not start: ${log}`);
return child;
}
let first = await launch(a);
await launch(b);
await until(
async () =>
(await contents(path.join(b, "note.md"))) === "initial\n",
"CLI initial download failed"
);
const persistence = (vault: string) =>
new ClientPersistence(
path.join(vault, ".vaultlink/sync-data.json"),
{}
);
const original = (await persistence(a).load()).database!;
const [documentId] = Object.keys(original.local!);
assert(documentId);
const settings = {
remoteUri: server.remoteUri,
token,
vaultName: "production",
isSyncEnabled: true,
enableTelemetry: false,
ignorePatterns: [".vaultlink/**"],
syncIntervalMs: 0
};
obsidian = await SyncClient.create({
fs: new ObsidianFileSystemOperations(
new NodeFileSystemOperations(c),
() => undefined
),
persistence: new ClientPersistence(
path.join(c, ".vaultlink/sync-data.json"),
settings
)
});
await obsidian.start();
await obsidian.waitUntilFinished();
assert.equal(await contents(path.join(c, "note.md")), "initial\n");
await fs.writeFile(
path.join(a, "replacement.tmp"),
"atomic editor save\n"
);
await fs.rename(
path.join(a, "replacement.tmp"),
path.join(a, "note.md")
);
await until(
async () =>
(await contents(path.join(b, "note.md"))) ===
"atomic editor save\n" &&
(await contents(path.join(c, "note.md"))) ===
"atomic editor save\n",
"Editor replacement did not converge"
);
assert.equal(
(await persistence(a).load()).database!.local![documentId],
"note.md"
);
await fs.rename(path.join(b, "note.md"), path.join(b, "renamed.md"));
await until(
async () =>
(await contents(path.join(a, "renamed.md"))) ===
"atomic editor save\n" &&
(await contents(path.join(a, "note.md"))) === undefined,
"CLI rename did not converge"
);
await until(
async () =>
(await persistence(a).load()).database!.local![documentId] ===
"renamed.md",
"Rename changed document identity"
);
const saved = await persistence(a).load();
assert(
saved.historyCheckpoint?.checkpoint,
"CLI dropped the history checkpoint"
);
await stop(first, "SIGKILL");
await fs.writeFile(path.join(a, "renamed.md"), "edited after kill\n");
first = await launch(a);
await until(
async () =>
(await contents(path.join(b, "renamed.md"))) ===
"edited after kill\n" &&
(await contents(path.join(c, "renamed.md"))) ===
"edited after kill\n",
"Restart failed to reconcile current files"
);
const snapshot = await getJson<CanonicalSnapshot>(
`${server.remoteUri}/vaults/production/vault-snapshot`,
token
);
assert.deepEqual(snapshot.fileManifest.entries, {
[documentId]: "renamed.md"
});
await fs.unlink(path.join(b, "renamed.md"));
await until(
async () =>
(await contents(path.join(a, "renamed.md"))) === undefined &&
(await contents(path.join(c, "renamed.md"))) === undefined,
"Delete did not converge"
);
await stop(first, "SIGTERM");
assert.equal(first.exitCode, 0, "CLI shutdown failed");
}
);

View file

@ -0,0 +1,390 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { randomUUID } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import { ServerControl } from "../deterministic-tests/src/server-control";
import { Logger } from "sync-client";
import { SyncClient } from "../sync-client/src/sync-client";
import { MemoryDisk, MemoryPersistence } from "./storage";
import { resolve, join } from "node:path";
import { tmpdir } from "node:os";
import { mkdtemp, rm } from "node:fs/promises";
const root = resolve(__dirname, "../..");
const token = "test-token-change-me";
const bytes = (s: string) => new TextEncoder().encode(s);
const headers = {
Authorization: `Bearer ${token}`,
"Device-Id": "audit",
"Content-Type": "application/json"
};
async function put(url: string, body: unknown) {
const r = await fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body)
});
assert(r.ok, await r.clone().text());
return r.json();
}
const content = (s: string) => ({
requestId: randomUUID(),
parentVersionId: null,
content: { type: "Snapshot", value: Buffer.from(s).toString("base64") }
});
test(
"connection admission rejects excess clients and releases disconnected slots",
{ timeout: 15000 },
async () => {
const dir = await mkdtemp(join(tmpdir(), "vault-link-admission-"));
const config = join(dir, "config.yml");
await writeFile(
config,
(
await readFile(root + "/sync-server/config-e2e.yml", "utf8")
).replace("max_clients_per_vault: 256", "max_clients_per_vault: 1")
);
const server = new ServerControl(
root + "/sync-server/target/release/sync_server",
config,
new Logger()
);
const sockets: WebSocket[] = [];
try {
await server.start();
const url = server.remoteUri + "/vaults/" + randomUUID();
await put(url + "/documents/" + randomUUID(), content("event"));
const connect = async () => {
const ws = new WebSocket(url.replace("http:", "ws:") + "/ws");
sockets.push(ws);
const accepted = await new Promise<boolean>((resolve) => {
ws.onopen = () =>
ws.send(
JSON.stringify({
type: "handshake",
token,
deviceId: randomUUID()
})
);
ws.onmessage = () => resolve(true);
ws.onerror = () => resolve(false);
ws.onclose = () => resolve(false);
});
return { ws, accepted };
};
const first = await connect();
assert(first.accepted);
assert.equal((await connect()).accepted, false);
await new Promise<void>((resolve) => {
first.ws.onclose = () => resolve();
first.ws.close();
});
// A completed close releases the server's owned connection permit.
assert.equal((await connect()).accepted, true);
} finally {
for (const ws of sockets) ws.close();
await server.stop();
await rm(dir, { recursive: true, force: true });
}
}
);
test(
"an unauthenticated upgraded socket expires without a handshake",
{ timeout: 10000 },
async () => {
const server = new ServerControl(
root + "/sync-server/target/release/sync_server",
root + "/sync-server/config-e2e.yml",
new Logger()
);
let ws: WebSocket | undefined;
try {
await server.start();
ws = new WebSocket(
server.remoteUri.replace("http:", "ws:") +
"/vaults/" +
randomUUID() +
"/ws"
);
const socket = ws;
const closed = await new Promise<boolean>((resolve) => {
const timer = setTimeout(() => resolve(false), 6500);
socket.onclose = () => {
clearTimeout(timer);
resolve(true);
};
socket.onerror = () => { };
});
assert(closed, "handshake deadline must release idle sockets");
} finally {
ws?.close();
await server.stop();
}
}
);
test(
"bootstrap notifications adopt the existing remote document identity",
{ timeout: 15000 },
async () => {
const server = new ServerControl(
root + "/sync-server/target/release/sync_server",
root + "/sync-server/config-e2e.yml",
new Logger()
);
let client: SyncClient | undefined;
try {
await server.start();
const vault = randomUUID(),
id = randomUUID();
const url = server.remoteUri + "/vaults/" + vault;
await put(url + "/documents/" + id, content("REMOTE"));
await put(url + "/file-manifest", {
requestId: randomUUID(),
parentFileManifestId: 0,
entries: { [id]: "a.md" }
});
const disk = new MemoryDisk();
const store = new MemoryPersistence({
settings: {
remoteUri: server.remoteUri,
vaultName: vault,
token,
isSyncEnabled: true,
syncIntervalMs: 0
}
});
client = await SyncClient.create({ fs: disk, persistence: store });
await disk.userWrite("a.md", bytes("LOCAL"));
await client.syncLocallyCreatedFile("a.md");
await disk.userWrite("b.md", bytes("B"));
await client.syncLocallyCreatedFile("b.md");
await client.start();
const files = [...disk.userFiles().keys()];
assert.equal(files.length, 2);
} finally {
await client?.destroy();
await server.stop();
}
}
);
test(
"local undo survives a lost accepted reply followed by a rejected retry",
{ timeout: 15000 },
async () => {
const server = new ServerControl(
root + "/sync-server/target/release/sync_server",
root + "/sync-server/config-e2e.yml",
new Logger()
);
let client: SyncClient | undefined;
class SilentWebSocket {
static OPEN = 1;
readyState = 1;
onopen: ((event: object) => void) | null = null;
onclose:
| ((event: { code: number; reason: string }) => void)
| null = null;
onmessage: ((event: { data: string }) => void) | null = null;
onerror: ((event: object) => void) | null = null;
constructor(_url: string | URL) {
queueMicrotask(() => this.onopen?.({}));
}
send(_data: string) { }
close() {
this.readyState = 3;
this.onclose?.({ code: 1000, reason: "" });
}
}
try {
await server.start();
const vault = randomUUID(),
id = randomUUID();
const url = server.remoteUri + "/vaults/" + vault;
await put(url + "/documents/" + id, content("base"));
await put(url + "/file-manifest", {
requestId: randomUUID(),
parentFileManifestId: 0,
entries: { [id]: "a.md" }
});
const disk = new MemoryDisk();
const store = new MemoryPersistence({
settings: {
remoteUri: server.remoteUri,
vaultName: vault,
token,
isSyncEnabled: true,
syncIntervalMs: 0,
networkRetryIntervalMs: 0
}
});
let mode: "normal" | "lose-reply" | "reject-retry" = "normal";
let lostId: string | undefined;
client = await SyncClient.create({
fs: disk,
persistence: store,
webSocket: SilentWebSocket as unknown as typeof WebSocket,
fetch: async (input, init) => {
if (init?.method === "PUT" && mode === "reject-retry") {
assert.equal(
JSON.parse(String(init.body)).requestId,
lostId
);
mode = "normal";
return new Response("proxy body limit changed", {
status: 413
});
}
const response = await fetch(input, init);
if (init?.method === "PUT" && mode === "lose-reply") {
assert.equal(
(await response.clone().json()).type,
"Accepted"
);
lostId = JSON.parse(String(init.body)).requestId;
mode = "normal";
throw new Error("injected lost accepted response");
}
return response;
}
});
await client.start();
mode = "lose-reply";
await disk.userWrite("a.md", bytes("base A"));
await client.syncLocallyUpdatedFile({ relativePath: "a.md" });
await assert.rejects(client.waitUntilFinished(), /lost accepted/);
const original = await (
await fetch(url + "/documents/" + id, { headers })
).json();
await put(url + "/documents/" + id, {
...content("base A REMOTE"),
parentVersionId: original.vaultUpdateId
});
mode = "reject-retry";
await disk.userWrite("a.md", bytes("base"));
await client.syncLocallyUpdatedFile({ relativePath: "a.md" });
await client.waitUntilFinished();
const result = Buffer.from(
(await disk.read("a.md"))!.content
).toString();
assert.equal(result, "base REMOTE");
} finally {
await client?.destroy();
await server.stop();
}
}
);
test(
"closing an old connection cannot erase its replacement's cursors",
{ timeout: 15000 },
async () => {
const server = new ServerControl(
root + "/sync-server/target/release/sync_server",
root + "/sync-server/config-e2e.yml",
new Logger()
);
const sockets: WebSocket[] = [];
try {
await server.start();
const url = server.remoteUri + "/vaults/" + randomUUID();
const id = randomUUID();
await put(url + "/documents/" + id, content("example"));
const connect = async (deviceId: string) => {
const ws = new WebSocket(url.replace("http:", "ws:") + "/ws");
sockets.push(ws);
await new Promise<void>((resolve, reject) => {
ws.onopen = () =>
ws.send(
JSON.stringify({
type: "handshake",
token,
deviceId
})
);
ws.onmessage = () => resolve();
ws.onerror = () => reject(new Error("connection failed"));
});
return ws;
};
const observer = await connect("observer");
const first = await connect("same-device");
const sendCursor = (ws: WebSocket, start: number) =>
ws.send(
JSON.stringify({
type: "cursorPositions",
documentsWithCursors: [
{
document_id: id,
relative_path: "note.md",
vault_update_id: 1,
cursors: [{ start, end: start }]
}
]
})
);
const seen = (start: number) =>
new Promise<void>((resolve) => {
observer.onmessage = (event) => {
const message = JSON.parse(String(event.data));
if (
message.type === "cursorPositions" &&
message.clients.some(
(client: {
deviceId: string;
documentsWithCursors: {
cursors: { start: number }[];
}[];
}) =>
client.deviceId === "same-device" &&
client.documentsWithCursors[0]?.cursors[0]
?.start === start
)
)
resolve();
};
});
let observed = seen(1);
sendCursor(first, 1);
await observed;
const replacement = await connect("same-device");
observed = seen(2);
sendCursor(replacement, 2);
await observed;
await new Promise<void>((resolve) => {
first.onclose = () => resolve();
first.close();
});
await new Promise((resolve) => setTimeout(resolve, 100));
const current = new Promise<
{
deviceId: string;
documentsWithCursors: { cursors: { start: number }[] }[];
}[]
>((resolve) => {
observer.onmessage = (event) => {
const message = JSON.parse(String(event.data));
if (message.type === "cursorPositions")
resolve(message.clients);
};
});
observer.send(
JSON.stringify({
type: "cursorPositions",
documentsWithCursors: []
})
);
const clients = await current;
assert.equal(
clients.find((client) => client.deviceId === "same-device")
?.documentsWithCursors[0]?.cursors[0]?.start,
2
);
} finally {
for (const ws of sockets) ws.close();
await server.stop();
}
}
);

View file

@ -0,0 +1,431 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { randomUUID } from "node:crypto";
import { Worker } from "node:worker_threads";
import * as path from "node:path";
import { Logger } from "sync-client";
import { ServerControl } from "../deterministic-tests/src/server-control";
import { MemoryDisk, type DiskImage, type StoredClient } from "./storage";
import { getJson, type CanonicalSnapshot } from "./canonical";
import type { RequestRecord } from "./network";
import type { CrashWorkerInput } from "../deterministic-tests/src/crash-worker";
const root = path.resolve(__dirname, "../..");
const token = "test-token-change-me";
const headers = {
Authorization: `Bearer ${token}`,
"Device-Id": "protocol-test",
"Content-Type": "application/json"
};
const makeServer = () =>
new ServerControl(
path.join(root, "sync-server/target/release/sync_server"),
path.join(root, "sync-server/config-e2e.yml"),
new Logger()
);
async function put(url: string, body: unknown) {
const response = await fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
signal: AbortSignal.timeout(5_000)
});
assert(
response.ok,
`HTTP ${response.status}: ${await response.clone().text()}`
);
return (await response.json()) as {
type: string;
vaultUpdateId: number;
fileManifestId: number;
};
}
const content = (text: string, parentVersionId: number | null = null) => ({
requestId: randomUUID(),
parentVersionId,
content: { type: "Snapshot", value: Buffer.from(text).toString("base64") }
});
interface WorkerSnapshot {
type: string;
disk: DiskImage;
stored: StoredClient;
requests: RequestRecord[];
editedBase?: { documentId: string; vaultUpdateId: number };
}
async function runWorker(input: CrashWorkerInput): Promise<WorkerSnapshot> {
const worker = new Worker(
path.join(root, "frontend/deterministic-tests/dist/crash-worker.js"),
{ workerData: input }
);
let timeout: ReturnType<typeof setTimeout> | undefined;
try {
return await new Promise<WorkerSnapshot>((resolve, reject) => {
timeout = setTimeout(
() =>
reject(new Error("Worker checkpoint/completion timed out")),
15_000
);
worker.once("error", reject);
worker.once("exit", (code) =>
reject(
new Error(
`Worker exited (${code}) before expected checkpoint`
)
)
);
worker.on(
"message",
(message: WorkerSnapshot & { error?: string }) => {
if (message.type === "error")
reject(new Error(message.error));
else {
assert.equal(
message.type,
input.stopAt ? "checkpoint" : "finished"
);
resolve(message);
}
}
);
});
} finally {
clearTimeout(timeout);
// This is the fault injection, not graceful shutdown masquerading as it.
await worker.terminate();
}
}
for (const [kind, format, originalPath] of [
["create", "Snapshot", "original.md"],
["content", "Snapshot", "original.bin"],
["content", "Diff", "original.md"],
["manifest", undefined, "original.md"]
] as const) {
test(
`abrupt client death after accepted ${kind} ${format ?? ""} reply, newer head, server SIGKILL, durable retry`,
{ timeout: 30_000 },
async () => {
const server = makeServer();
try {
await server.start();
const vault = `crash-${randomUUID()}`;
const url = `${server.remoteUri}/vaults/${vault}`;
const disk = new MemoryDisk();
await disk.userWrite(
originalPath,
new TextEncoder().encode("preserve-original\n")
);
let input: CrashWorkerInput = {
disk: disk.image(),
stored: {
settings: {
isSyncEnabled: true,
enableTelemetry: false,
remoteUri: server.remoteUri,
vaultName: vault,
token,
requestTimeoutMs: 5_000,
syncIntervalMs: 0
}
} as StoredClient
};
let originalId: string | undefined;
let originalParent: number | undefined;
let uploaded = "preserve-original\n";
if (kind === "content" && format === "Diff") {
// Keep the established base in the worker's real content
// cache; a cold restart intentionally falls back to Snapshot.
uploaded += "local-update\n";
input.editAfterSync = {
path: originalPath,
content: uploaded
};
} else if (kind === "content") {
const initialized = await runWorker(input);
originalId = Object.keys(
initialized.stored.database!.local!
)[0];
originalParent =
initialized.stored.database!.documents![originalId]
.base!.vaultUpdateId;
disk.restore(initialized.disk);
uploaded += "local-update\n";
await disk.userWrite(
originalPath,
new TextEncoder().encode(uploaded)
);
input = { disk: disk.image(), stored: initialized.stored };
}
const checkpoint = await runWorker({ ...input, stopAt: kind });
if (format === "Diff") {
assert(
checkpoint.editedBase,
"Warm update baseline was not recorded"
);
originalId = checkpoint.editedBase.documentId;
originalParent = checkpoint.editedBase.vaultUpdateId;
}
const pending = checkpoint.stored.database!.pending!;
assert(
pending,
"The uncertain request was not persisted before sending"
);
const interrupted = checkpoint.requests.at(-1)!;
assert.equal(
interrupted.kind,
kind,
"Wrong CAS was interrupted"
);
if (kind !== "manifest") {
assert.equal(pending.type, "content");
assert(pending.type === "content");
assert.equal(pending.request.content.type, format);
assert.equal(
pending.request.parentVersionId,
kind === "create" ? null : originalParent
);
if (originalId)
assert.equal(
pending.documentId,
originalId,
"Update created a replacement UUID"
);
}
const accepted = await getJson<CanonicalSnapshot>(
`${url}/vault-snapshot`,
token
);
// Unpublished content has no manifest membership yet, so it is
// deliberately absent from /vault-snapshot's document list.
const doc =
pending.type === "content"
? await getJson<{
documentId: string;
vaultUpdateId: number;
}>(`${url}/documents/${pending.documentId}`, token)
: accepted.documents[0];
assert(doc);
const remote = await put(
`${url}/documents/${doc.documentId}`,
content(`${uploaded}remote-update\n`, doc.vaultUpdateId)
);
assert.equal(remote.type, "Accepted");
const remotePath = originalPath.replace("original", "remote");
const renamed = await put(`${url}/file-manifest`, {
requestId: randomUUID(),
parentFileManifestId: accepted.fileManifest.fileManifestId,
entries: { [doc.documentId]: remotePath }
});
assert.equal(renamed.type, "Accepted");
const before = await getJson<CanonicalSnapshot>(
`${url}/vault-snapshot`,
token
);
await server.crash();
await server.restart();
assert.deepEqual(
await getJson(`${url}/vault-snapshot`, token),
before,
"Server restart lost acknowledged state"
);
const restarted = await runWorker({
disk: checkpoint.disk,
stored: checkpoint.stored
});
const replay = restarted.requests.find(
(r) => r.body.requestId === pending.request.requestId
);
assert(replay, "Restart never retried the uncertain request");
assert.equal(
replay.url,
interrupted.url,
"Restart changed the request target"
);
assert.deepEqual(
replay.body,
pending.request,
"Restart changed the persisted request"
);
assert.equal(restarted.stored.database!.pending, undefined);
const recovered = new MemoryDisk();
recovered.restore(restarted.disk);
assert.deepEqual(
recovered.userFiles(),
new Map([
[
remotePath,
new TextEncoder().encode(
`${uploaded}remote-update\n`
)
]
])
);
assert.deepEqual(restarted.stored.database!.local, {
[doc.documentId]: remotePath
});
const events = await getJson<{
events: { requestId: string }[];
}>(`${url}/events-since?after=0`, token);
assert.equal(
events.events.filter(
(e) => e.requestId === pending.request.requestId
).length,
1,
"Retry emitted a duplicate event"
);
} finally {
await server.stop();
}
}
);
}
test(
"WebSocket hints trigger HTTP catchup in commit order, including after reconnect",
{ timeout: 20_000 },
async () => {
const server = makeServer();
let ws: WebSocket | undefined;
try {
await server.start();
const url = `${server.remoteUri}/vaults/order-${randomUUID()}`;
const received: number[] = [];
let catchup = Promise.resolve();
let catchupError: unknown;
const connect = async () => {
ws = new WebSocket(`${url.replace(/^http/, "ws")}/ws`);
ws.addEventListener("message", (message) => {
const hint = JSON.parse(String(message.data));
if (hint.type !== "vaultChanged") return;
catchup = catchup
.then(async () => {
assert.deepEqual(hint, { type: "vaultChanged" });
const batch = await getJson<{
events: { eventId: number }[];
}>(
`${url}/events-since?after=${received.at(-1) ?? 0}`,
token
);
received.push(
...batch.events.map((event) => event.eventId)
);
})
.catch((error) => {
catchupError = error;
});
});
await new Promise<void>((resolve, reject) => {
ws!.onopen = () => resolve();
ws!.onerror = () => reject(new Error("WebSocket failed"));
});
ws.send(
JSON.stringify({
type: "handshake",
token,
deviceId: "order-observer"
})
);
};
await connect();
const ids = Array.from({ length: 12 }, () => randomUUID());
const requests = ids.map((id, i) => ({
id,
body: content(`bytes-${i}`)
}));
const replies = await Promise.all(
requests.map(({ id, body }) =>
put(`${url}/documents/${id}`, body)
)
);
assert.deepEqual(
replies.map((r) => r.vaultUpdateId).sort((a, b) => a - b),
ids.map((_, i) => i + 1)
);
const emptyManifest = await getJson<{
entries: Record<string, string>;
}>(`${url}/file-manifest`, token);
assert.deepEqual(
emptyManifest.entries,
{},
"Content-only CAS changed membership"
);
const entries = Object.fromEntries(
ids.map((id, i) => [id, `${i}.md`])
);
await put(`${url}/file-manifest`, {
requestId: randomUUID(),
parentFileManifestId: 0,
entries
});
// All retries must return original receipts without emitting events.
assert.deepEqual(
await Promise.all(
requests.map(({ id, body }) =>
put(`${url}/documents/${id}`, body)
)
),
replies
);
const waitForEvents = async (head: number) => {
const deadline = Date.now() + 5_000;
while (received.length < head && Date.now() < deadline)
await new Promise((resolve) => setTimeout(resolve, 10));
if (catchupError) throw catchupError;
assert.deepEqual(
received,
Array.from({ length: head }, (_, i) => i + 1),
"HTTP catchup after WebSocket hints lost or duplicated an event"
);
};
await waitForEvents(13);
await new Promise<void>((resolve) => {
ws!.onclose = () => resolve();
ws!.close();
});
await put(
`${url}/documents/${ids[0]}`,
content("next", replies[0].vaultUpdateId)
);
await connect();
await waitForEvents(14);
const log = await getJson<{ events: { eventId: number }[] }>(
`${url}/events-since?after=0`,
token
);
assert.deepEqual(
received,
log.events.map((event) => event.eventId)
);
const stale = await put(`${url}/file-manifest`, {
requestId: randomUUID(),
parentFileManifestId: 0,
entries: {}
});
assert.equal(stale.type, "StaleBase");
const invalid = await fetch(`${url}/file-manifest`, {
method: "PUT",
headers,
body: JSON.stringify({
requestId: randomUUID(),
parentFileManifestId: 13,
entries: { [ids[0]]: "same", [ids[1]]: "same" }
})
});
assert.equal(invalid.status, 422);
assert.equal(
(
await getJson<CanonicalSnapshot>(
`${url}/vault-snapshot`,
token
)
).headEventId,
14,
"Rejected CAS emitted an event"
);
} finally {
ws?.close();
await server.stop();
}
}
);

View file

@ -0,0 +1,25 @@
/** A seed controls harness choices; production UUIDs and OS scheduling remain
* nondeterministic. The concrete action trace is the stronger replay artifact. */
export class Random {
private state: number;
public constructor(seed: number) {
if (!Number.isInteger(seed) || seed < 0 || seed > 0xffffffff)
throw new Error("Seed must be a uint32");
this.state = seed;
}
public next(): number {
this.state = (this.state + 0x6d2b79f5) >>> 0;
let n = this.state;
n = Math.imul(n ^ (n >>> 15), n | 1);
n ^= n + Math.imul(n ^ (n >>> 7), n | 61);
return ((n ^ (n >>> 14)) >>> 0) / 4294967296;
}
public int(max: number): number {
if (!Number.isSafeInteger(max) || max < 1)
throw new Error("Empty random choice");
return Math.floor(this.next() * max);
}
public pick<T>(values: readonly T[]): T {
return values[this.int(values.length)];
}
}

View file

@ -0,0 +1,139 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { tmpdir } from "node:os";
import { NodeFileSystemOperations } from "../local-client-cli/src/node-filesystem";
import { Database, emptyState } from "../sync-client/src/persistence/database";
import { FileOperations } from "../sync-client/src/file-operations/file-operations";
import type { ServerConfig } from "../sync-client/src/services/server-config";
import { toStoredSnapshot } from "../sync-client/src/sync-operations/content";
test(
"real filesystem: direct updates, unsafe entries, and file swaps",
{ timeout: 20_000 },
async () => {
const directory = await fs.mkdtemp(
path.join(tmpdir(), "vault-link-real-fs-")
);
try {
const root = path.join(directory, "vault");
await fs.mkdir(root);
const disk = new NodeFileSystemOperations(root);
const snapshot = (text: string) => ({
content: new TextEncoder().encode(text)
});
await disk.write("updated.bin", snapshot("first"));
await disk.write("updated.bin", snapshot("second"));
assert.equal(
Buffer.from(
(await disk.read("updated.bin"))!.content
).toString(),
"second"
);
for (const unsafe of [
"../outside",
"/absolute",
"folder/../outside"
]) {
await assert.rejects(
disk.write(unsafe, snapshot("blocked")),
/Unsafe path/
);
await assert.rejects(disk.read(unsafe), /Unsafe path/);
}
// POSIX filenames can contain colons. The engine, not the adapter,
// allocates their portable names for sync.
if (process.platform !== "win32") {
await fs.writeFile(
path.join(root, "local:name.md"),
"local name"
);
assert.equal(
Buffer.from(
(await disk.read("local:name.md"))!.content
).toString(),
"local name"
);
await disk.rename("local:name.md", "portable-name.md");
}
await disk.write("a.md", snapshot("A"));
await disk.write("b.md", snapshot("B"));
// A file in an ancestor position makes descendants absent.
assert.equal(await disk.stat("a.md/absent.md"), undefined);
assert.equal(await disk.read("a.md/absent.md"), undefined);
await assert.rejects(disk.rename("a.md", "b.md"));
await assert.rejects(disk.deleteDirectory("a.md"), /regular file/);
await fs.symlink("a.md", path.join(root, "symlink"));
await assert.rejects(disk.read("symlink"), /Symlink/);
await fs.unlink(path.join(root, "symlink"));
await fs.link(path.join(root, "a.md"), path.join(root, "hardlink"));
await assert.rejects(disk.read("a.md"), /hardlink/);
await fs.unlink(path.join(root, "hardlink"));
// No-replace renames also protect case/normalization aliases.
for (const [original, alias] of [
["Case.bin", "case.bin"],
["é.bin", "e\u0301.bin"]
]) {
await disk.write(original, snapshot("original bytes"));
await disk.write("alias-source.bin", snapshot("alias bytes"));
if (await disk.exists(alias))
await assert.rejects(
disk.rename("alias-source.bin", alias)
);
else {
await disk.rename("alias-source.bin", alias);
assert.equal(
Buffer.from(
(await disk.read(alias))!.content
).toString(),
"alias bytes"
);
}
assert.equal(
Buffer.from(
(await disk.read(original))!.content
).toString(),
"original bytes"
);
}
const initial = emptyState("real");
initial.local = { a: "a.md", b: "b.md" };
for (const [id, relative] of Object.entries(initial.local))
initial.documents[id] = {
materialized: true,
observedHash: (
await toStoredSnapshot(
(await disk.read(relative))!
)
).hash
};
const database = new Database(
initial,
async () => { },
"real",
async () => initial
);
const operations = new FileOperations(disk, database, {
getConfig: async () => ({ mergeableFileExtensions: ["md"] })
} as ServerConfig);
const next = structuredClone(initial);
next.local = { a: "b.md", b: "a.md" };
await operations.apply(next);
assert.equal(
Buffer.from(
(await disk.read("a.md"))!.content
).toString(),
"B"
);
assert.equal(
Buffer.from(
(await disk.read("b.md"))!.content
).toString(),
"A"
);
} finally {
await fs.rm(directory, { recursive: true });
}
}
);

View file

@ -0,0 +1,580 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { MemoryDisk, MemoryPersistence } from "./storage";
import {
Database,
emptyState,
type StoredDatabase
} from "../sync-client/src/persistence/database";
import { FileOperations } from "../sync-client/src/file-operations/file-operations";
import { toStoredSnapshot } from "../sync-client/src/sync-operations/content";
import type { ServerConfig } from "../sync-client/src/services/server-config";
import { mergeFileManifests } from "../sync-client/src/sync-operations/file-manifest";
import { assertManifest, pathDecision } from "./oracles";
import {
scanLocalFiles,
type LocalChange
} from "../sync-client/src/sync-operations/scan";
const bytes = (text: string) => new TextEncoder().encode(text);
const config = {
getConfig: async () => ({ mergeableFileExtensions: ["md"] })
} as ServerConfig;
async function fixture(
contents: Record<string, { path: string; text: string }> = {
a: { path: "a.md", text: "A" },
b: { path: "b.md", text: "B" }
}
) {
const disk = new MemoryDisk();
const initial = emptyState("recovery-test");
for (const [id, { path, text }] of Object.entries(contents)) {
await disk.userWrite(path, bytes(text));
initial.local[id] = path;
}
for (const [id, path] of Object.entries(initial.local))
initial.documents[id] = {
materialized: true,
observedHash: (
await toStoredSnapshot((await disk.read(path))!)
).hash
};
const persistence = new MemoryPersistence({ database: initial });
const restart = (changes: LocalChange[] = []) => {
const database = new Database(
persistence.snapshot().database,
async (next) => persistence.save({ database: next }),
"recovery-test",
async () => (await persistence.load()).database as StoredDatabase
);
return {
database,
files: new FileOperations(
disk.session(),
database,
config,
undefined,
{ entries: () => changes, flush: async () => { } }
)
};
};
return {
disk,
persistence,
restart,
initial,
permanentFault: (_label: string): void => { },
expectedLocal: undefined as Record<string, string> | undefined
};
}
async function swapFixture() {
const current = await fixture();
const next = structuredClone(current.initial);
next.local = { a: "b.md", b: "a.md", c: "nested/c.md" };
next.documents.c = { materialized: false };
const writes = {
c: { replacement: await toStoredSnapshot({ content: bytes("C") }) }
};
const expected = new Map([
["a.md", bytes("B")],
["b.md", bytes("A")],
["nested/c.md", bytes("C")]
]);
return { ...current, next, writes, expected };
}
async function directoryCycleFixture() {
const current = await fixture({
a: { path: "a", text: "A" },
b: { path: "b/deep/child", text: "B" }
});
const next = structuredClone(current.initial);
next.local = { a: "b", b: "a/deep/child" };
return {
...current,
next,
writes: {},
expected: new Map([
["b", bytes("A")],
["a/deep/child", bytes("B")]
])
};
}
async function crossDirectoryFixture() {
const current = await fixture({
a: { path: "src/a.md", text: "A" },
// Keep src nonempty: deleting an empty source directory would mask an
// unflushed source unlink in the staging rename.
b: { path: "src/keep.md", text: "untouched sibling" }
});
const next = structuredClone(current.initial);
next.local.a = "dst/a.md";
const expected = new Map([
["dst/a.md", bytes("A")],
["src/keep.md", bytes("untouched sibling")]
]);
return { ...current, next, writes: {}, expected };
}
async function mergeFixture() {
const base = "first paragraph\n\nlast paragraph\n";
const local = "local first paragraph\n\nlast paragraph\n";
const remote = "first paragraph\n\nremote last paragraph\n";
const current = await fixture({ a: { path: "note.md", text: local } });
return {
...current,
next: structuredClone(current.initial),
writes: {
a: {
expected: await toStoredSnapshot({ content: bytes(base) }),
replacement: await toStoredSnapshot({ content: bytes(remote) })
}
},
expected: new Map([
[
"note.md",
bytes("local first paragraph\n\nremote last paragraph\n")
]
])
};
}
async function deletionFixture() {
const current = await fixture({
a: { path: "a.bin", text: "private binary edit\0" },
b: { path: "deleted.md", text: "unsent deleted bytes" }
});
const next = structuredClone(current.initial);
delete next.local.b;
return {
...current,
next,
writes: {
a: {
replacement: await toStoredSnapshot({
content: bytes("remote binary\0")
})
}
},
expected: new Map([["a.bin", bytes("remote binary\0")]])
};
}
async function permanentDestinationFixture() {
const current = await fixture();
const next = structuredClone(current.initial);
next.local = { a: "unsupported/a.md", b: "good.md" };
return {
...current,
next,
permanentFault: (label: string) => {
if (label === "before:mkdir:unsupported")
throw Object.assign(new Error("Unsupported destination"), {
code: "ENAMETOOLONG"
});
},
writes: {
a: {
expected: await toStoredSnapshot({ content: bytes("A") }),
replacement: await toStoredSnapshot({
content: bytes("REMOTE")
})
}
},
expectedLocal: { a: "Recovered a.md", b: "good.md" },
expected: new Map([
["Recovered a.md", bytes("REMOTE")],
["good.md", bytes("B")]
])
};
}
for (const [name, create] of [
["swap and create", swapFixture],
["file/directory cycle", directoryCycleFixture],
["cross-directory move", crossDirectoryFixture],
["content merge", mergeFixture],
["binary replacement and deletion", deletionFixture],
["permanent destination fallback", permanentDestinationFixture]
] as const) {
test(`${name}: interrupted operations leave metadata usable by ordinary scans`, async (t) => {
const baseline = await create();
const labels: string[] = [];
const trace = (label: string) => {
if (!label.startsWith("read:")) labels.push(label);
};
baseline.disk.boundary = (label) => {
baseline.permanentFault(label);
trace(label);
};
baseline.persistence.boundary = trace;
await baseline.restart().files.apply(baseline.next, baseline.writes);
assert.deepEqual(baseline.disk.userFiles(), baseline.expected);
assert.deepEqual(
baseline.persistence.snapshot().database!.local,
baseline.expectedLocal ?? baseline.next.local
);
assert(labels.length > 0);
for (const powerLoss of [false, true])
for (let cut = 0; cut < labels.length; cut++) {
await t.test(
`${powerLoss ? "power" : "process"} interruption at ${labels[cut]}`,
async () => {
const current = await create();
let seen = 0;
let fired = false;
const interrupt = (label: string) => {
if (label.startsWith("read:")) return;
if (seen++ === cut) {
fired = true;
current.disk.crash(powerLoss);
throw new Error("interrupted");
}
};
current.disk.boundary = (label) => {
current.permanentFault(label);
interrupt(label);
};
current.persistence.boundary = interrupt;
await assert.rejects(
current
.restart()
.files.apply(current.next, current.writes),
/interrupted/
);
assert(fired);
current.disk.boundary = () => { };
current.persistence.boundary = () => { };
// The user may edit after the interruption, including a partial
// write or a file whose metadata was never saved.
await current.disk.userWrite(
"after-restart.md",
bytes("user edit")
);
const runtime = current.restart();
const observed = current.disk.userFiles();
await scanChanges(runtime, []);
assert.deepEqual(
current.disk.userFiles(),
observed,
"A scan must not replay old filesystem intentions"
);
assertManifest(runtime.database.state.local);
assert.deepEqual(
new Set(
Object.values(runtime.database.state.local)
),
new Set(observed.keys())
);
for (const [id, path] of Object.entries(
runtime.database.state.local
)) {
assert(
runtime.database.state.documents[id]
.materialized
);
assert.equal(
runtime.database.state.documents[id]
.observedHash,
(await runtime.files.snapshot(path))!.hash
);
}
assert(
(await runtime.files.listFilesRecursively()).every(
(path) => !path.startsWith(".vault-link-sync/")
)
);
const state = structuredClone(runtime.database.state);
await scanChanges(current.restart(), []);
assert.deepEqual(
current.persistence.snapshot().database,
state,
"Second startup must be stable"
);
}
);
}
});
}
test("production manifest merge obeys independently specified per-document policy", () => {
const paths = [undefined, "a.md", "b.md", "nested/c.md"];
const map = (path: string | undefined): Record<string, string> =>
path === undefined
? {}
: { "00000000-0000-4000-8000-000000000001": path };
for (const base of paths)
for (const local of paths)
for (const remote of paths) {
assert.deepEqual(
mergeFileManifests(map(base), map(local), map(remote)),
map(pathDecision(base, local, remote))
);
}
const conflicts = mergeFileManifests(
{},
{ a: "folder" },
{ b: "folder/note.md" }
);
assertManifest(conflicts);
assert.equal(
conflicts.b,
"folder/note.md",
"Canonical path must win collision"
);
assert.equal(
Object.keys(conflicts).length,
2,
"Collision must not discard an ID"
);
});
test("exhaustive two-document manifest states preserve IDs and canonical collision priority", () => {
const ids = [
"00000000-0000-4000-8000-000000000001",
"00000000-0000-4000-8000-000000000002"
];
const states: Record<string, string>[] = [];
for (const a of [undefined, "a.md", "b.md"])
for (const b of [undefined, "a.md", "b.md"]) {
if (a && a === b) continue;
states.push({
...(a ? { [ids[0]]: a } : {}),
...(b ? { [ids[1]]: b } : {})
});
}
for (const base of states)
for (const local of states)
for (const remote of states) {
const expected = Object.fromEntries(
ids
.map((id) => [
id,
pathDecision(base[id], local[id], remote[id])
])
.filter(([, value]) => value !== undefined)
);
const merged = mergeFileManifests(base, local, remote);
assertManifest(merged);
assert.deepEqual(
Object.keys(merged).sort(),
Object.keys(expected).sort(),
"Merge discarded or invented an identity"
);
for (const id of ids)
if (
expected[id] !== undefined &&
remote[id] === expected[id]
)
assert.equal(
merged[id],
remote[id],
"Canonical claimant lost its path"
);
assert.deepEqual(
mergeFileManifests(remote, merged, remote),
merged,
"Manifest merge is not idempotent"
);
}
});
test("scan failures cannot masquerade as deletions", async () => {
for (const point of ["read:list:", "read:snapshot:a.md"]) {
const current = await fixture();
const runtime = current.restart();
const original = structuredClone(runtime.database.state);
let fired = false;
current.disk.boundary = (label) => {
if (label === point) {
fired = true;
throw new Error("permission denied");
}
};
await assert.rejects(
scanLocalFiles({
next: structuredClone(original),
changes: [],
files: runtime.files,
guard: () => { },
ignored: () => false,
oversized: () => false,
commit: (next) => runtime.database.commit(next)
}),
/permission denied/
);
assert(fired);
assert.deepEqual(runtime.database.state, original);
assert.deepEqual(current.persistence.snapshot().database, original);
}
});
for (const phase of ["before", "durable"]) {
test(`delete/recreate identity survives ${phase}-save failure and retry`, async () => {
const current = await fixture();
await current.disk.userDelete("a.md");
await current.disk.userWrite("a.md", bytes("new incarnation"));
const changes: LocalChange[] = [
{ type: "delete", path: "a.md" },
{ type: "create", path: "a.md" }
];
const scan = async () => {
const runtime = current.restart();
await scanLocalFiles({
next: structuredClone(runtime.database.state),
changes,
files: runtime.files,
guard: () => { },
ignored: () => false,
oversized: () => false,
commit: (next) => runtime.database.commit(next)
});
return runtime.database.state;
};
current.persistence.boundary = (label) => {
if (label === `${phase}:save`) throw new Error("save failure");
};
await assert.rejects(scan(), /save failure/);
current.persistence.boundary = () => { };
const state = await scan();
assert.equal(
state.local.a,
undefined,
"Retry reused the deleted document's UUID"
);
assert.equal(
Object.values(state.local).filter((path) => path === "a.md").length,
1
);
});
}
async function scanChanges(
runtime: { database: Database; files: FileOperations },
changes: LocalChange[]
): Promise<void> {
await runtime.database.recoverPersistence();
await scanLocalFiles({
next: structuredClone(runtime.database.state),
changes,
files: runtime.files,
guard: () => { },
ignored: () => false,
oversized: () => false,
commit: (next) => runtime.database.commit(next)
});
}
for (const editedContent of ["new bytes", "B"]) {
test(`explicit rename then edit to ${JSON.stringify(editedContent)} preserves both UUIDs`, async () => {
const current = await fixture();
const runtime = current.restart();
await current.disk.userRename("a.md", "moved.md");
await current.disk.userWrite("moved.md", bytes(editedContent));
const changes: LocalChange[] = [
{ type: "move", oldPath: "a.md", relativePath: "moved.md" }
];
await scanChanges(runtime, changes);
assert.deepEqual(
runtime.database.state.local,
{ a: "moved.md", b: "b.md" },
"Matching another live document's bytes must not steal its UUID"
);
assert.deepEqual(
current.disk.userFiles(),
new Map([
["moved.md", bytes(editedContent)],
["b.md", bytes("B")]
])
);
assert.equal(changes.length, 0);
});
}
for (const equalContent of [false, true]) {
for (const phase of ["before", "durable"]) {
test(`${equalContent ? "equal" : "different"}-content swap retains UUIDs after ${phase}-save rejection`, async () => {
const current = await fixture({
a: { path: "a.md", text: "A" },
b: { path: "b.md", text: equalContent ? "A" : "B" }
});
const runtime = current.restart();
const changes: LocalChange[] = [];
for (const [oldPath, relativePath] of [
["a.md", "temporary.md"],
["b.md", "a.md"],
["temporary.md", "b.md"]
]) {
await current.disk.userRename(oldPath, relativePath);
changes.push({ type: "move", oldPath, relativePath });
}
const expected = { a: "b.md", b: "a.md" };
const expectedFiles = new Map([
["a.md", bytes(equalContent ? "A" : "B")],
["b.md", bytes("A")]
]);
let fired = false;
current.persistence.boundary = (label) => {
if (!fired && label === `${phase}:save`) {
fired = true;
throw new Error("uncertain save");
}
};
await assert.rejects(
scanChanges(runtime, changes),
/uncertain save/
);
assert(fired, "The requested save failure must be exercised");
assert.deepEqual(
current.persistence.snapshot().database!.local,
phase === "durable" ? expected : current.initial.local,
"The fault must occur on the intended side of the durable save"
);
current.persistence.boundary = () => { };
// Model an uncertain save in a live client: durable state reloads,
// but the original editor notifications remain in memory. Do not
// clear, reconstruct or deduplicate that queue in the harness.
await scanChanges(runtime, changes);
assert.deepEqual(
runtime.database.state.local,
expected,
"Retry replayed an already committed swap and reversed its identities"
);
assert.equal(changes.length, 0);
await scanChanges(runtime, changes);
assert.deepEqual(runtime.database.state.local, expected);
const restarted = current.restart();
await scanChanges(restarted, []);
assert.deepEqual(restarted.database.state.local, expected);
assert.deepEqual(current.disk.userFiles(), expectedFiles);
});
}
}
test("normalization-sensitive disk does not lose a distinct decomposed filename", async () => {
const current = await fixture();
await current.disk.userWrite("é.md", bytes("composed"));
const runtime = current.restart();
const next = structuredClone(runtime.database.state);
next.local.composed = "é.md";
next.documents.composed = { materialized: true };
await runtime.database.commit(next);
await current.disk.userWrite("e\u0301.md", bytes("decomposed"));
await scanLocalFiles({
next: structuredClone(runtime.database.state),
changes: [],
files: runtime.files,
guard: () => { },
ignored: () => false,
oversized: () => false,
commit: (next) => runtime.database.commit(next)
});
assert.equal(
Object.keys(runtime.database.state.local).length,
4,
"Distinct Unicode file vanished from the scan"
);
});

View file

@ -0,0 +1,231 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { fixture, bytes, randomUUID } from "./sync-server-fixture";
import { assertCanonical } from "./canonical";
import { SyncClient } from "../sync-client/src/sync-client";
import { MemoryDisk, MemoryPersistence } from "./storage";
for (const powerLoss of [false, true])
for (const operation of ["create", "replace", "move", "metadata"] as const)
test(
`${operation}: a ${powerLoss ? "power" : "process"} interruption and later user edits converge without replay`,
{ timeout: 20_000 },
async () => {
const f = await fixture();
let peer: SyncClient | undefined;
try {
const id = randomUUID();
const head = await f.put(
`/documents/${id}`,
f.content("BASE\n")
);
const manifest = await f.put("/file-manifest", {
requestId: randomUUID(),
parentFileManifestId: 0,
entries: { [id]: "note.md" }
});
if (operation !== "create") {
await f.client.start();
await f.client.setSetting("isSyncEnabled", false);
if (operation === "move")
await f.put("/file-manifest", {
requestId: randomUUID(),
parentFileManifestId: manifest.fileManifestId,
entries: { [id]: "moved.md" }
});
else
await f.put(
`/documents/${id}`,
f.content("REMOTE\nBASE\n", head.vaultUpdateId)
);
}
let fired = false;
let wrote = false;
const interrupt = () => {
fired = true;
f.disk.crash(powerLoss);
throw new Error("injected interruption");
};
f.disk.boundary = (label) => {
if (fired) return;
if (label === "durable:write:note.md") wrote = true;
if (
operation === "move" &&
label === "visible:rename:note.md->moved.md"
)
interrupt();
if (
(operation === "create" ||
operation === "replace") &&
label === "visible:write:note.md"
)
interrupt();
};
f.store.boundary = (label) => {
if (
!fired &&
operation === "metadata" &&
wrote &&
label === "before:save"
)
interrupt();
};
await assert.rejects(
operation === "create"
? f.client.start()
: f.client.setSetting("isSyncEnabled", true),
/injected interruption/
);
assert(
fired,
"The intended filesystem/save boundary must be reached"
);
f.disk.boundary = () => {};
f.store.boundary = () => {};
// Model torn content and further independent edits. Neither is
// recoverable from an old filesystem intention, nor should it be.
await f.disk.userWrite(
operation === "move" ? "moved.md" : "note.md",
bytes("partial\nUSER EDIT\n")
);
await f.disk.userWrite(
"after.md",
bytes("AFTER INTERRUPTION\n")
);
await f.reopenClient();
await f.client.start();
const inspectable = {
files: () => f.disk.userFiles(),
database: () => f.store.snapshot().database
};
const peerDisk = new MemoryDisk();
const peerStore = new MemoryPersistence({
settings: f.store.snapshot().settings
});
peer = await SyncClient.create({
fs: peerDisk,
persistence: peerStore
});
await peer.start();
const clients = [
inspectable,
{
files: () => peerDisk.userFiles(),
database: () => peerStore.snapshot().database
}
];
await assertCanonical(
clients,
f.url,
"test-token-change-me"
);
assert.deepEqual(
f.disk.userFiles().get("after.md"),
bytes("AFTER INTERRUPTION\n")
);
assert(
(await f.disk.listFilesRecursively()).every(
(path) => !path.startsWith(".vault-link-sync/")
)
);
const files = f.disk.userFiles();
await f.reopenClient();
await f.client.start();
await assertCanonical(
clients,
f.url,
"test-token-change-me"
);
assert.deepEqual(
f.disk.userFiles(),
files,
"A second restart must not replay old writes"
);
} finally {
f.disk.boundary = () => {};
f.store.boundary = () => {};
await peer?.destroy();
await f.dispose();
}
}
);
for (const operation of ["create", "replace", "move"] as const)
test(
`${operation}: metadata can survive while completed user-file writes are lost`,
{ timeout: 20_000 },
async () => {
const f = await fixture();
let peer: SyncClient | undefined;
try {
const id = randomUUID();
const head = await f.put(
`/documents/${id}`,
f.content("BASE\n")
);
const manifest = await f.put("/file-manifest", {
requestId: randomUUID(),
parentFileManifestId: 0,
entries: { [id]: "note.md" }
});
if (operation !== "create") {
await f.client.start();
await f.client.setSetting("isSyncEnabled", false);
if (operation === "replace")
await f.put(
`/documents/${id}`,
f.content("REMOTE\nBASE\n", head.vaultUpdateId)
);
else
await f.put("/file-manifest", {
requestId: randomUUID(),
parentFileManifestId: manifest.fileManifestId,
entries: { [id]: "moved.md" }
});
}
const oldDisk = f.disk.image();
if (operation === "create") await f.client.start();
else await f.client.setSetting("isSyncEnabled", true);
await f.client.destroy();
// Metadata is newer than disk: completed filesystem calls have no
// flush contract. This is not covered by failing a call mid-flight.
f.disk.restore(oldDisk);
await f.disk.userWrite(
"after.md",
bytes("USER AFTER POWER LOSS\n")
);
await f.reopenClient();
await f.client.start();
const peerDisk = new MemoryDisk();
const peerStore = new MemoryPersistence({
settings: f.store.snapshot().settings
});
peer = await SyncClient.create({
fs: peerDisk,
persistence: peerStore
});
await peer.start();
await assertCanonical(
[
{
files: () => f.disk.userFiles(),
database: () => f.store.snapshot().database
},
{
files: () => peerDisk.userFiles(),
database: () => peerStore.snapshot().database
}
],
f.url,
"test-token-change-me"
);
assert.deepEqual(
f.disk.userFiles().get("after.md"),
bytes("USER AFTER POWER LOSS\n")
);
} finally {
await peer?.destroy();
await f.dispose();
}
}
);

Some files were not shown because too many files have changed in this diff Show more