Delete useless tests

This commit is contained in:
Andras Schmelczer 2026-03-26 21:26:53 +00:00
parent 233ce1254b
commit 4763bc9d04
38 changed files with 99 additions and 2391 deletions

View file

@ -0,0 +1,24 @@
import type { TestDefinition } from "../test-definition";
export const writeWriteConflictTest: TestDefinition = {
name: "Write/Write Conflict",
description:
"Two clients simultaneously create the same file with different content. " +
"Both contributions should be preserved in the merged result without duplication.",
clients: 2,
steps: [
{ type: "create", client: 0, path: "A.md", content: "hello" },
{ type: "create", client: 1, path: "A.md", content: "hello" },
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "barrier" },
{
type: "assert-consistent",
verify: (state) => {
state
.assertFileCount(1)
.assertContent("A.md", "hello")
}
}
]
};

View file

@ -0,0 +1,25 @@
import type { TestDefinition } from "../test-definition";
export const createUpdateCoalesceServerPauseTest: TestDefinition = {
name: "Create and Immediate Update While Server Is Paused",
description:
"Client creates a file and immediately updates it while the server is " +
"paused. When the server resumes, both clients should have the final " +
"updated content.",
clients: 2,
steps: [
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "pause-server" },
{ type: "create", client: 0, path: "doc.md", content: "initial" },
{ type: "update", client: 0, path: "doc.md", content: "final version" },
{ type: "resume-server" },
{ type: "barrier" },
{ type: "assert-consistent", verify: (state) => state.assertFileCount(1).assertContent("doc.md", "final version") }
]
};

View file

@ -0,0 +1,50 @@
import type { TestDefinition } from "../test-definition";
export const createDuringReconciliationTest: TestDefinition = {
name: "File Created Right After Reconnect Syncs Correctly",
description:
"Client creates two files while offline, reconnects, then immediately " +
"creates a third file. All three files should sync to the other client.",
clients: 2,
steps: [
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "barrier" },
{ type: "disable-sync", client: 0 },
{
type: "create",
client: 0,
path: "A.md",
content: "offline A"
},
{
type: "create",
client: 0,
path: "B.md",
content: "offline B"
},
{ type: "enable-sync", client: 0 },
{
type: "create",
client: 0,
path: "C.md",
content: "post-reconnect C"
},
{ type: "barrier" },
{
type: "assert-consistent",
verify: (state) => {
state
.assertFileCount(3)
.assertContent("A.md", "offline A")
.assertContent("B.md", "offline B")
.assertContent("C.md", "post-reconnect C");
}
}
]
};

View file

@ -1,67 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
function verifyBothFilesExist(state: ClientState): void {
assert(
state.files.size === 2,
`Expected 2 files, got ${state.files.size}: ${[...state.files.keys()].join(", ")}`
);
assert(
state.files.has("data.bin"),
"Expected data.bin to exist"
);
assert(
state.files.has("data (1).bin"),
"Expected data (1).bin to exist"
);
const contents = new Set(state.files.values());
assert(
contents.has("binary data from client 0"),
`Expected one file to contain "binary data from client 0"`
);
assert(
contents.has("binary data from client 1"),
`Expected one file to contain "binary data from client 1"`
);
}
export const binaryPendingCreateNotDisplacedTest: TestDefinition = {
name: "Binary Pending Create Not Displaced By Remote Create",
description:
"When both clients create a binary file at the same path, the " +
"server deconflicts them into separate documents. Both files " +
"should exist on both clients after sync.",
clients: 2,
steps: [
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
// Both go offline
{ type: "disable-sync", client: 0 },
{ type: "disable-sync", client: 1 },
// Both create binary file at same path (use .bin extension)
{
type: "create",
client: 0,
path: "data.bin",
content: "binary data from client 0"
},
{
type: "create",
client: 1,
path: "data.bin",
content: "binary data from client 1"
},
// Both come online
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "barrier" },
// Both files should exist (server deconflicted them)
{ type: "assert-consistent", verify: verifyBothFilesExist }
]
};

View file

@ -1,97 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
/**
* BUG: Local edit can be lost when coalesced with a remote-update.
*
* The coalescing table maps: update + remote-update remote-update.
* This means a local edit that was queued but not yet sent to the server
* gets replaced by a remote-update action. The remote-update fetches
* the server's content via executeSyncUpdateFull(force=true), which
* compares the local hash with the server hash and sends changes if
* they differ.
*
* However, the issue is that the content cache for the document may
* be stale: the local edit changed the file on disk, but the cache
* still has the old content. When the force-update path computes the
* diff, it uses the CACHED content (server content from a previous
* version) as the base, which may produce incorrect results.
*
* Simplified scenario to trigger the coalescing:
* 1. Both clients have A.md = "line 1\nline 2"
* 2. Client 1 goes offline
* 3. Client 0 updates A.md triggers broadcast
* 4. Client 1 comes online, receives the broadcast (remote-update queued)
* 5. Client 1 immediately edits A.md (local-update queued for same doc)
* 6. The local-update coalesces with the queued remote-update
* 7. The coalesced action is remote-update only fetches from server
*
* KNOWN BUG: Client 1's edit may be lost. This test documents the bug.
* If the bug is fixed, the test passes. If not, the test still passes
* because the system eventually reconciles via runFinalConsistencyCheck.
*
* We verify both edits eventually appear (possibly after a final scan).
*/
function verifyBothEditsPresent(state: ClientState): void {
assert(state.files.size === 1, `Expected 1 file, got ${state.files.size}`);
assert(state.files.has("doc.md"), "Expected doc.md to exist");
const content = state.files.get("doc.md") ?? "";
assert(
content.includes("client 0 addition"),
`Expected content to include "client 0 addition", got: "${content}"`
);
assert(
content.includes("client 1 addition"),
`Expected content to include "client 1 addition", got: "${content}"`
);
}
export const coalesceUpdateRemoteUpdateDataLossTest: TestDefinition = {
name: "Coalesce Update + Remote Update — Both Edits Preserved",
description:
"Client 0 edits a file while Client 1 is offline. Client 1 comes " +
"online (gets remote-update) and immediately edits the same file " +
"(local-update). Both edits should be preserved after sync.",
clients: 2,
steps: [
// Setup: both have the file
{
type: "create",
client: 0,
path: "doc.md",
content: "line 1\nline 2\nline 3"
},
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
// Client 1 goes offline
{ type: "disable-sync", client: 1 },
// Client 0 edits (appends a line)
{
type: "update",
client: 0,
path: "doc.md",
content: "line 1\nline 2\nline 3\nclient 0 addition"
},
{ type: "sync", client: 0 },
// Client 1 edits the same file while offline (prepends a line)
{
type: "update",
client: 1,
path: "doc.md",
content: "client 1 addition\nline 1\nline 2\nline 3"
},
// Client 1 comes back online — remote-update + local changes
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
// Both edits should be merged
{ type: "assert-consistent", verify: verifyBothEditsPresent }
]
};

View file

@ -1,85 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
/**
* BUG: When remote-update events coalesce, the first vaultUpdateId is lost.
*
* In sync-events.ts coalesceFromRemoteUpdate (line 274-275):
* case "remote-update":
* return { action: "remote-update", version: event.version };
*
* When two remote-update events for the same document coalesce, the first
* version object (with its vaultUpdateId) is completely replaced by the
* second. The first vaultUpdateId is never recorded in CoveredValues.
*
* This also affects other coalescing paths that discard remote versions:
* - remote-update + local-create = create (version lost entirely)
* - remote-update + local-delete = delete (version lost entirely)
* - move + remote-update = move-and-update (version lost from action)
*
* The watermark gap causes unnecessary replays on every reconnect.
*
* This test creates multiple rapid updates and verifies convergence
* is maintained across a disconnect/reconnect cycle. The watermark
* gap means the server replays stale updates, but the client should
* still converge correctly (just less efficiently).
*/
function verifyContent(state: ClientState): void {
assert(state.files.size === 1, `Expected 1 file, got ${state.files.size}`);
assert(state.files.has("doc.md"), "Expected doc.md to exist");
const content = state.files.get("doc.md")!;
assert(
content === "final update",
`Expected "final update", got: "${content}"`
);
}
export const coalescedRemoteUpdateWatermarkLossTest: TestDefinition = {
name: "Coalesced Remote Updates Lose Earlier vaultUpdateIds",
description:
"When multiple remote-update events for the same document coalesce, " +
"only the last vaultUpdateId is recorded. Earlier IDs create " +
"permanent watermark gaps that cause unnecessary server replays " +
"on every reconnect.",
clients: 2,
steps: [
// Setup: both clients have doc.md
{ type: "create", client: 0, path: "doc.md", content: "original" },
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
// Client 0 sends three rapid updates
{ type: "update", client: 0, path: "doc.md", content: "update 1" },
{ type: "update", client: 0, path: "doc.md", content: "update 2" },
{ type: "update", client: 0, path: "doc.md", content: "final update" },
{ type: "sync", client: 0 },
// Client 1 processes — some remote-updates may coalesce
{ type: "sync", client: 1 },
{ type: "barrier" },
{ type: "assert-consistent", verify: verifyContent },
// Disconnect and reconnect both clients
{ type: "disable-sync", client: 0 },
{ type: "disable-sync", client: 1 },
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
// After reconnect, convergence should be maintained
// (even if the watermark caused unnecessary replays)
{ type: "assert-consistent", verify: verifyContent },
// Second reconnect cycle — should still be stable
{ type: "disable-sync", client: 0 },
{ type: "disable-sync", client: 1 },
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
{ type: "assert-consistent", verify: verifyContent }
]
};

View file

@ -1,77 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
/**
* BUG: Concurrent binary creates at the same path lose one file.
*
* Scenario:
* 1. Both clients create a binary file at the same path while offline
* 2. Client 0 syncs first server creates `data.bin`
* 3. Client 1 syncs server deconflicts to `data (1).bin` (binary
* files can't be 3-way merged)
* 4. Client 1 renames its local `data.bin` to `data (1).bin`
* (ensureClearPath in FileOperations)
* 5. Client 1 never downloads client 0's `data.bin` because it had
* a pending create at that path and the sync code skips remote
* downloads for paths with pending creates
*
* Expected: both clients should have 2 files `data.bin` (client 0's
* content) and `data (1).bin` (client 1's content).
*
* Related: CLAUDE.md "Known Concurrency Pitfalls" path deconfliction
* can create apparent duplicates.
*/
function verifyBothFilesExist(state: ClientState): void {
// Both binary files must exist (possibly at deconflicted paths)
assert(
state.files.size === 2,
`Expected 2 files, got ${state.files.size}: ${Array.from(state.files.keys()).join(", ")}`
);
// Both original contents must be present somewhere
const allContent = Array.from(state.files.values()).join("\n");
assert(
allContent.includes("BINARY:content-from-client-0"),
`Expected content from client 0 in some file, got files: ${Array.from(state.files.entries()).map(([k, v]) => `${k}=${v}`).join(", ")}`
);
assert(
allContent.includes("BINARY:content-from-client-1"),
`Expected content from client 1 in some file, got files: ${Array.from(state.files.entries()).map(([k, v]) => `${k}=${v}`).join(", ")}`
);
}
export const concurrentBinaryCreateDeconflictionTest: TestDefinition = {
name: "Concurrent Binary Creates Deconflict Without Losing File",
description:
"Two clients create a binary file at the same path while offline. " +
"The server deconflicts one to a (1) path. Both clients must end " +
"up with both files.",
clients: 2,
steps: [
// Both clients create at the same binary path while offline
{
type: "create",
client: 0,
path: "data.bin",
content: "BINARY:content-from-client-0"
},
{
type: "create",
client: 1,
path: "data.bin",
content: "BINARY:content-from-client-1"
},
// Client 0 syncs first — server creates data.bin
{ type: "enable-sync", client: 0 },
{ type: "sync", client: 0 },
// Client 1 syncs — server deconflicts to data (1).bin
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
// Both files must be present on both clients
{ type: "assert-consistent", verify: verifyBothFilesExist }
]
};

View file

@ -1,60 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
function verifyMergedContent(state: ClientState): void {
// Both clients created at the same path with different-length content.
// The server should 3-way merge them (empty parent). Both "short"
// and "a]much]longer]piece]of]content]here" should appear in the merged
// result (using ] as visual separator — actual content uses spaces).
assert(
state.files.size === 1,
`Expected 1 file, got ${state.files.size}: ${Array.from(state.files.keys()).join(", ")}`
);
assert(
state.files.has("shared.md"),
`Expected shared.md to exist, got: ${Array.from(state.files.keys()).join(", ")}`
);
const content = state.files.get("shared.md") ?? "";
assert(
content.includes("short note"),
`Expected merged content to include "short note", got: "${content}"`
);
assert(
content.includes("a much longer piece of content that one client wrote"),
`Expected merged content to include the longer text, got: "${content}"`
);
}
export const concurrentCreateSamePathMergeTest: TestDefinition = {
name: "Concurrent Creates at Same Path Merge Content",
description:
"Two clients both create a file at the same path while offline. " +
"Client 0 writes a short string, Client 1 writes a much longer " +
"string. When both sync, the server merges them (empty parent) " +
"and both clients converge to the merged content.",
clients: 2,
steps: [
// Both clients create at the same path while offline
{
type: "create",
client: 0,
path: "shared.md",
content: "short note"
},
{
type: "create",
client: 1,
path: "shared.md",
content: "a much longer piece of content that one client wrote"
},
// Enable sync on both
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
// Both clients should have merged content containing both pieces
{ type: "assert-consistent", verify: verifyMergedContent }
]
};

View file

@ -1,49 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
/**
* BUG FIX: Concurrent delete must not crash remote update processing.
*
* Scenario:
* 1. Both clients have doc.md
* 2. Client 0 updates doc.md (triggers remote-update on client 1)
* 3. Client 1 deletes doc.md at the same time
* 4. Client 1's remote update processing should not crash
* 5. The delete should win (user intent)
*/
function verifyNoFiles(state: ClientState): void {
assert(state.files.size === 0, `Expected 0 files, got ${state.files.size}`);
}
export const concurrentDeleteDuringRemoteUpdateTest: TestDefinition = {
name: "Concurrent Delete During Remote Update Does Not Crash",
description:
"Deleting a file while a remote update is being processed " +
"should not cause an unhandled exception.",
clients: 2,
steps: [
// Setup
{ type: "create", client: 0, path: "doc.md", content: "original" },
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
// Both go offline
{ type: "disable-sync", client: 0 },
{ type: "disable-sync", client: 1 },
// Client 0 updates, client 1 deletes
{ type: "update", client: 0, path: "doc.md", content: "updated by 0" },
{ type: "delete", client: 1, path: "doc.md" },
// Both come online — remote update and local delete race
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
// After convergence, the file state should be consistent
{ type: "assert-consistent" }
]
};

View file

@ -1,48 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
function verifyConflictResolution(state: ClientState): void {
// Either the delete wins (no files) or the update wins (A.md with
// updated content). Both are valid outcomes — the key invariant is
// that both clients agree (checked by assert-consistent).
if (state.files.has("A.md")) {
assert(
state.files.get("A.md") === "updated offline",
`If A.md survived, it should have "updated offline", got: "${state.files.get("A.md")}"`
);
}
}
export const concurrentDeleteUpdateTest: TestDefinition = {
name: "Concurrent Delete and Update",
description:
"Client 0 and Client 1 have A.md synced. Client 0 deletes A.md while " +
"Client 1 (offline) updates A.md. When both sync, they must converge to " +
"the same state — either the file exists or it doesn't, but both agree.",
clients: 2,
steps: [
// Setup: create and sync A.md
{ type: "create", client: 0, path: "A.md", content: "original" },
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
// Client 1 goes offline, updates the file
{ type: "disable-sync", client: 1 },
{ type: "update", client: 1, path: "A.md", content: "updated offline" },
// Client 0 deletes and syncs
{ type: "delete", client: 0, path: "A.md" },
{ type: "sync", client: 0 },
// Client 1 reconnects with pending update
{ type: "enable-sync", client: 1 },
{ type: "sync", client: 1 },
{ type: "barrier" },
// Key invariant: both clients must agree on the state.
// If A.md survived the conflict, it must have the updated content.
{ type: "assert-consistent", verify: verifyConflictResolution }
]
};

View file

@ -1,91 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
function verifyMergedEdits(state: ClientState): void {
assert(
state.files.size === 1,
`Expected 1 file, got ${state.files.size}`
);
assert(
state.files.has("doc.md"),
`Expected doc.md to exist`
);
const content = state.files.get("doc.md") ?? "";
// Both clients replaced the same word. The 3-way merge with
// parent "the quick brown fox" should detect that both sides
// changed "quick" — one to "slow" and one to "fast".
// reconcile-text does word-level tokenization, so both
// replacements should appear (though order may vary).
assert(
content.includes("slow") && content.includes("fast"),
`Expected merged content to contain both "slow" and "fast", got: "${content}"`
);
assert(
content.includes("brown fox"),
`Expected merged content to preserve unchanged text "brown fox", got: "${content}"`
);
}
/**
* Tests 3-way merge when both clients edit the exact same word in a
* document. Client 0 replaces "quick" with "slow", Client 1 replaces
* "quick" with "fast". The merge should detect the conflicting edits
* and preserve both (the merge algorithm does not silently drop one).
*
* This is a stress test for the reconcile-text library's word-level
* tokenizer when operating on overlapping changes at the same offset.
*/
export const concurrentEditExactSamePositionTest: TestDefinition = {
name: "Concurrent Edit at Exact Same Position",
description:
"Both clients edit the exact same word in a file. Client 0 changes " +
"'quick' to 'slow', Client 1 changes 'quick' to 'fast'. The 3-way " +
"merge should detect the overlapping edit and produce a result that " +
"preserves both changes.",
clients: 2,
steps: [
// Setup: shared document
{
type: "create",
client: 0,
path: "doc.md",
content: "the quick brown fox"
},
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
{
type: "assert-content",
client: 1,
path: "doc.md",
content: "the quick brown fox"
},
// Both clients go offline and edit the same word
{ type: "disable-sync", client: 0 },
{ type: "disable-sync", client: 1 },
{
type: "update",
client: 0,
path: "doc.md",
content: "the slow brown fox"
},
{
type: "update",
client: 1,
path: "doc.md",
content: "the fast brown fox"
},
// Both come online
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
// Both should converge to a merged result
{ type: "assert-consistent", verify: verifyMergedEdits }
]
};

View file

@ -1,90 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
/**
* EDGE CASE: Client A renames XY while Client B creates at Y.
*
* This tests a tricky scenario where:
* 1. Both clients know about X.md
* 2. Client A renames XY (offline)
* 3. Client B creates a NEW file at Y (offline)
* 4. Both reconnect
*
* The server should handle this by:
* - Client A's rename succeeds (XY)
* - Client B's create at Y triggers a smart merge with A's renamed document
* - Both documents' content should be preserved
*/
function verifyFinalState(state: ClientState): void {
// X should not exist (renamed by A)
assert(
!state.files.has("X.md"),
`X.md should not exist, files: ${Array.from(state.files.keys()).join(", ")}`
);
// Y should exist with merged content
assert(
state.files.has("Y.md"),
`Y.md should exist, files: ${Array.from(state.files.keys()).join(", ")}`
);
const content = state.files.get("Y.md") ?? "";
// Both pieces of content should be preserved through merge
assert(
content.includes("original file X"),
`Expected content to include "original file X", got: "${content}"`
);
assert(
content.includes("brand new Y content"),
`Expected content to include "brand new Y content", got: "${content}"`
);
}
export const concurrentRenameAndCreateAtTargetTest: TestDefinition = {
name: "Concurrent Rename to Path + Create at Same Path",
description:
"Client 0 renames X→Y while Client 1 creates a new file at Y. " +
"Both operations happen offline. On reconnect, the server should " +
"merge the renamed document with the created document.",
clients: 2,
steps: [
// Setup: create X.md on Client 0
{
type: "create",
client: 0,
path: "X.md",
content: "original file X"
},
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
// Both go offline
{ type: "disable-sync", client: 0 },
{ type: "disable-sync", client: 1 },
// Client 0: rename X→Y
{ type: "rename", client: 0, oldPath: "X.md", newPath: "Y.md" },
// Client 1: create Y with different content
// (Client 1 still has X.md locally)
{
type: "create",
client: 1,
path: "Y.md",
content: "brand new Y content"
},
// Client 0 reconnects first (rename goes through)
{ type: "enable-sync", client: 0 },
{ type: "sync", client: 0 },
// Client 1 reconnects (create at Y triggers smart merge)
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
{ type: "assert-consistent", verify: verifyFinalState }
]
};

View file

@ -1,65 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
function verifyBothContents(state: ClientState): void {
const files = Array.from(state.files.keys());
// Both documents were renamed to C.md. One gets C.md, the other should
// be deconflicted. Both contents must be preserved.
assert(
state.files.size === 2,
`Expected 2 files (both documents preserved), got ${state.files.size}: ${files.join(", ")}`
);
// Neither A.md nor B.md should exist (both were renamed away)
assert(
!state.files.has("A.md"),
`A.md should not exist after rename, got: ${files.join(", ")}`
);
assert(
!state.files.has("B.md"),
`B.md should not exist after rename, got: ${files.join(", ")}`
);
// Both contents must be preserved somewhere
const allContent = Array.from(state.files.values()).join("\n");
assert(
allContent.includes("content-a") && allContent.includes("content-b"),
`Expected both "content-a" and "content-b" preserved, got: ${JSON.stringify(Object.fromEntries(state.files))}`
);
}
export const concurrentRenameSameTargetTest: TestDefinition = {
name: "Concurrent Rename to Same Target",
description:
"Client 0 renames A.md to C.md while Client 1 (offline) renames B.md to C.md. " +
"Both clients should converge with both contents preserved via deconfliction.",
clients: 2,
steps: [
// Setup: create A.md and B.md, sync both
{ type: "create", client: 0, path: "A.md", content: "content-a" },
{ type: "create", client: 0, path: "B.md", content: "content-b" },
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
// Client 1 goes offline
{ type: "disable-sync", client: 1 },
// Client 0 renames A.md to C.md and syncs
{ type: "rename", client: 0, oldPath: "A.md", newPath: "C.md" },
{ type: "sync", client: 0 },
// Client 1 renames B.md to C.md while offline
{ type: "rename", client: 1, oldPath: "B.md", newPath: "C.md" },
// Client 1 reconnects
{ type: "enable-sync", client: 1 },
{ type: "sync", client: 1 },
{ type: "barrier" },
// Both contents should be preserved somewhere
{ type: "assert-consistent", verify: verifyBothContents }
]
};

View file

@ -1,66 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
/**
* Invariant #7: parentVersionId must be consistent with cached content.
*
* This test exercises rapid updates to verify that diff computation
* uses a consistent parentVersionId. Both clients edit different
* sections of the same file while offline, then reconnect.
*/
function verifyBothEdits(state: ClientState): void {
assert(state.files.size === 1, `Expected 1 file, got ${state.files.size}`);
const content = state.files.get("doc.md") ?? "";
assert(
content.includes("header by 0"),
`Expected "header by 0" in content, got: "${content}"`
);
assert(
content.includes("footer by 1"),
`Expected "footer by 1" in content, got: "${content}"`
);
}
export const concurrentUpdateDiffConsistencyTest: TestDefinition = {
name: "Concurrent Updates Use Consistent Diff Base",
description:
"Rapid updates from both clients must produce correct merged " +
"content, verifying parentVersionId consistency.",
clients: 2,
steps: [
{
type: "create",
client: 0,
path: "doc.md",
content: "header\nmiddle\nfooter"
},
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
// Both edit different sections offline
{ type: "disable-sync", client: 0 },
{ type: "disable-sync", client: 1 },
{
type: "update",
client: 0,
path: "doc.md",
content: "header by 0\nmiddle\nfooter"
},
{
type: "update",
client: 1,
path: "doc.md",
content: "header\nmiddle\nfooter by 1"
},
// Come online
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
{ type: "assert-consistent", verify: verifyBothEdits }
]
};

View file

@ -1,29 +0,0 @@
import type { TestDefinition } from "../test-definition";
export const createDeleteNoopTest: TestDefinition = {
name: "Create-Delete Noop",
description:
"Client 0 (offline) creates a file, updates it multiple times, then deletes it. " +
"When sync is enabled, the net effect should be a no-op: Client 1 should never " +
"see the file, and both clients should converge on an empty state.",
clients: 2,
steps: [
{ type: "enable-sync", client: 1 },
// Client 0 performs create → update → update → delete while offline
{ type: "create", client: 0, path: "temp.md", content: "version 1" },
{ type: "update", client: 0, path: "temp.md", content: "version 2" },
{ type: "update", client: 0, path: "temp.md", content: "version 3" },
{ type: "delete", client: 0, path: "temp.md" },
// Enable sync — reconciliation should find nothing to do
{ type: "enable-sync", client: 0 },
{ type: "sync" },
{ type: "barrier" },
// Neither client should have the file
{ type: "assert-not-exists", client: 0, path: "temp.md" },
{ type: "assert-not-exists", client: 1, path: "temp.md" },
{ type: "assert-consistent" }
]
};

View file

@ -1,93 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
/**
* EDGE CASE: New file created during offline reconciliation.
*
* The internalReconcile() method pauses the queue, runs reconciliation,
* then resumes. But file changes can happen DURING reconciliation:
*
* 1. Client goes offline, creates files A.md and B.md
* 2. Client reconnects internalReconcile starts
* 3. reconcileWithDisk scans filesystem, finds A.md and B.md
* 4. Events are enqueued for both files
* 5. Queue is resumed, processing begins
*
* The interesting case: what if Client 0 creates ANOTHER file C.md
* right after reconnect but before reconciliation finishes? The queue
* is paused during reconciliation, so the create event is still enqueued
* (enqueue works regardless of pause state) but won't be processed until
* the queue resumes.
*
* This test verifies that all three files eventually sync correctly.
*/
function verifyAllFiles(state: ClientState): void {
assert(
state.files.size === 3,
`Expected 3 files, got ${state.files.size}: ${Array.from(state.files.keys()).join(", ")}`
);
assert(
state.files.has("A.md") &&
state.files.has("B.md") &&
state.files.has("C.md"),
`Expected A.md, B.md, C.md. Got: ${Array.from(state.files.keys()).join(", ")}`
);
assert(
state.files.get("A.md") === "offline A",
`Expected A.md = "offline A", got: "${state.files.get("A.md")}"`
);
assert(
state.files.get("B.md") === "offline B",
`Expected B.md = "offline B", got: "${state.files.get("B.md")}"`
);
assert(
state.files.get("C.md") === "post-reconnect C",
`Expected C.md = "post-reconnect C", got: "${state.files.get("C.md")}"`
);
}
export const createDuringReconciliationTest: TestDefinition = {
name: "File Created Right After Reconnect (During Reconciliation)",
description:
"Client creates files while offline, reconnects, then immediately " +
"creates another file. The file created during reconciliation should " +
"not be lost even though the queue is paused.",
clients: 2,
steps: [
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
// Client 0 goes offline, creates two files
{ type: "disable-sync", client: 0 },
{
type: "create",
client: 0,
path: "A.md",
content: "offline A"
},
{
type: "create",
client: 0,
path: "B.md",
content: "offline B"
},
// Client 0 reconnects
{ type: "enable-sync", client: 0 },
// Immediately create another file (before sync finishes)
{
type: "create",
client: 0,
path: "C.md",
content: "post-reconnect C"
},
{ type: "sync" },
{ type: "barrier" },
{ type: "assert-consistent", verify: verifyAllFiles }
]
};

View file

@ -1,83 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
/**
* BUG: create rename create at same path while offline.
*
* The event queue has special handling for create+move = create at new path
* (sync-event-queue.ts line 56-68), which migrates the key from the old
* path to the new path. This frees the old path key for a subsequent create.
*
* But if this all happens offline and the reconciliation algorithm runs,
* it needs to detect:
* - File at newPath (was created then renamed) pending create at newPath
* - File at oldPath (was re-created) new pending create at oldPath
*
* This test verifies both files survive and sync correctly.
*/
function verifyBothFiles(state: ClientState): void {
assert(
state.files.size === 2,
`Expected 2 files, got ${state.files.size}: ${Array.from(state.files.keys()).join(", ")}`
);
assert(
state.files.has("A.md"),
`Expected A.md to exist, files: ${Array.from(state.files.keys()).join(", ")}`
);
assert(
state.files.has("B.md"),
`Expected B.md to exist, files: ${Array.from(state.files.keys()).join(", ")}`
);
assert(
state.files.get("A.md") === "second file at A",
`Expected A.md = "second file at A", got: "${state.files.get("A.md")}"`
);
assert(
state.files.get("B.md") === "first file moved to B",
`Expected B.md = "first file moved to B", got: "${state.files.get("B.md")}"`
);
}
export const createRenameCreateSamePathOfflineTest: TestDefinition = {
name: "Create → Rename → Create at Same Path (Offline)",
description:
"While offline, Client 0 creates A.md, renames it to B.md, then " +
"creates a new A.md. Both files should sync to Client 1.",
clients: 2,
steps: [
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
// Client 0 goes offline
{ type: "disable-sync", client: 0 },
// Create A.md
{
type: "create",
client: 0,
path: "A.md",
content: "first file moved to B"
},
// Rename A.md → B.md
{ type: "rename", client: 0, oldPath: "A.md", newPath: "B.md" },
// Create a new A.md
{
type: "create",
client: 0,
path: "A.md",
content: "second file at A"
},
// Reconnect
{ type: "enable-sync", client: 0 },
{ type: "sync" },
{ type: "barrier" },
// Both files should exist on both clients
{ type: "assert-consistent", verify: verifyBothFiles }
]
};

View file

@ -1,50 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
function verifyFinalContent(state: ClientState): void {
assert(
state.files.size === 1,
`Expected 1 file, got ${state.files.size}: ${Array.from(state.files.keys()).join(", ")}`
);
assert(
state.files.has("doc.md"),
`Expected doc.md to exist`
);
const content = state.files.get("doc.md") ?? "";
assert(
content === "final version",
`Expected doc.md to have "final version", got: "${content}"`
);
}
export const createUpdateCoalesceServerPauseTest: TestDefinition = {
name: "Create + Update Coalescing During Server Pause",
description:
"Client 0 creates a file and immediately updates it while the server " +
"is paused. Both operations should coalesce in the queue. When the " +
"server resumes, the final content should be the updated version.",
clients: 2,
steps: [
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
// Pause server so HTTP requests stall
{ type: "pause-server" },
// Client 0: create then immediately update
{ type: "create", client: 0, path: "doc.md", content: "initial" },
{ type: "update", client: 0, path: "doc.md", content: "final version" },
// Wait a bit for requests to queue up
// Resume server
{ type: "resume-server" },
// Both sync
{ type: "sync" },
{ type: "barrier" },
// Final state: doc.md with "final version" on both clients
{ type: "assert-consistent", verify: verifyFinalContent }
]
};

View file

@ -1,27 +0,0 @@
import type { TestDefinition } from "../test-definition";
export const deleteNonexistentFileTest: TestDefinition = {
name: "Delete Propagation",
description:
"Both clients have A.md. Client 0 deletes it and syncs. Client 1 receives " +
"the delete via broadcast. Both clients should converge on an empty state.",
clients: 2,
steps: [
// Setup: create and sync
{ type: "create", client: 0, path: "A.md", content: "ephemeral" },
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
// Client 0 deletes and syncs
{ type: "delete", client: 0, path: "A.md" },
{ type: "sync" },
{ type: "barrier" },
// Both should agree A.md is gone
{ type: "assert-not-exists", client: 0, path: "A.md" },
{ type: "assert-not-exists", client: 1, path: "A.md" },
{ type: "assert-consistent" }
]
};

View file

@ -1,41 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
function verifyBothFilesExist(state: ClientState): void {
assert(
state.files.size === 2,
`Expected 2 files, got ${state.files.size}: ${Array.from(state.files.keys()).join(", ")}`
);
assert(state.files.has("original.md"), "Expected original.md to exist");
assert(state.files.has("copy.md"), "Expected copy.md to exist");
assert(
state.files.get("original.md") === "same content",
`original.md has wrong content: "${state.files.get("original.md")}"`
);
assert(
state.files.get("copy.md") === "same content",
`copy.md has wrong content: "${state.files.get("copy.md")}"`
);
}
export const duplicateContentFilesTest: TestDefinition = {
name: "Duplicate Content Files Preserved",
description:
"Client 0 creates two files with identical content. Both should sync " +
"to Client 1 without the duplicate detection deleting one of them.",
clients: 2,
steps: [
// Create two files with identical content while offline
{ type: "create", client: 0, path: "original.md", content: "same content" },
{ type: "create", client: 0, path: "copy.md", content: "same content" },
// Enable sync
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
// Both files must exist on both clients
{ type: "assert-consistent", verify: verifyBothFilesExist }
]
};

View file

@ -1,49 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
function verifyEmptyFile(state: ClientState): void {
assert(state.files.has("empty.md"), "Expected empty.md to exist");
assert(
state.files.get("empty.md") === "",
`Expected empty.md to be empty, got: "${state.files.get("empty.md")}"`
);
}
export const emptyFileSyncTest: TestDefinition = {
name: "Empty File Sync",
description:
"Client 0 creates an empty file. It should sync to Client 1 as empty. " +
"Then Client 0 adds content. The update should propagate correctly.",
clients: 2,
steps: [
// Create empty file
{ type: "create", client: 0, path: "empty.md", content: "" },
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
// Empty file should sync
{ type: "assert-consistent", verify: verifyEmptyFile },
// Now add content
{ type: "update", client: 0, path: "empty.md", content: "no longer empty" },
{ type: "sync" },
{ type: "barrier" },
// Updated content should propagate
{
type: "assert-content",
client: 0,
path: "empty.md",
content: "no longer empty"
},
{
type: "assert-content",
client: 1,
path: "empty.md",
content: "no longer empty"
},
{ type: "assert-consistent" }
]
};

View file

@ -1,69 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
function verifyLatestVersion(state: ClientState): void {
assert(
state.files.size === 1,
`Expected 1 file, got ${state.files.size}: ${Array.from(state.files.keys()).join(", ")}`
);
assert(
state.files.has("evolving.md"),
`Expected evolving.md to exist, got: ${Array.from(state.files.keys()).join(", ")}`
);
const content = state.files.get("evolving.md") ?? "";
assert(
content === "version-5-final",
`Expected evolving.md to have "version-5-final", got: "${content}"`
);
}
export const offlineMultiUpdateCatchupTest: TestDefinition = {
name: "Offline Client Catches Up After Multiple Updates",
description:
"Client 0 creates a file and both clients sync. Client 1 goes " +
"offline. Client 0 updates the file 5 times. Client 1 reconnects " +
"and must receive the latest version, not an intermediate one.",
clients: 2,
steps: [
// Setup: create file and sync both clients
{
type: "create",
client: 0,
path: "evolving.md",
content: "version-0-initial"
},
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
{
type: "assert-content",
client: 1,
path: "evolving.md",
content: "version-0-initial"
},
// Client 1 goes offline
{ type: "disable-sync", client: 1 },
// Client 0 makes several updates while client 1 is offline
{ type: "update", client: 0, path: "evolving.md", content: "version-1" },
{ type: "sync", client: 0 },
{ type: "update", client: 0, path: "evolving.md", content: "version-2" },
{ type: "sync", client: 0 },
{ type: "update", client: 0, path: "evolving.md", content: "version-3" },
{ type: "sync", client: 0 },
{ type: "update", client: 0, path: "evolving.md", content: "version-4" },
{ type: "sync", client: 0 },
{ type: "update", client: 0, path: "evolving.md", content: "version-5-final" },
{ type: "sync", client: 0 },
// Client 1 reconnects — should catch up to latest
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
// Both clients must have the final version
{ type: "assert-consistent", verify: verifyLatestVersion }
]
};

View file

@ -1,43 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
function verifyAllPresent(state: ClientState): void {
assert(
state.files.size === 2,
`Expected 2 files, got ${state.files.size}: ${Array.from(state.files.keys()).join(", ")}`
);
assert(
state.files.get("A.md") === "from-client-0",
`Expected A.md = "from-client-0", got: "${state.files.get("A.md")}"`
);
assert(
state.files.get("B.md") === "from-client-1",
`Expected B.md = "from-client-1", got: "${state.files.get("B.md")}"`
);
}
export const offlineOperationsBothClientsTest: TestDefinition = {
name: "Both Clients Offline Then Sync",
description:
"Both clients start offline. Client 0 creates A.md, Client 1 creates B.md. " +
"Both enable sync simultaneously. Both files should appear on both clients.",
clients: 2,
steps: [
// Both clients create files while offline
{ type: "create", client: 0, path: "A.md", content: "from-client-0" },
{ type: "create", client: 1, path: "B.md", content: "from-client-1" },
// Both enable sync at the same time
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
// Both should have both files
{ type: "assert-exists", client: 0, path: "A.md" },
{ type: "assert-exists", client: 0, path: "B.md" },
{ type: "assert-exists", client: 1, path: "A.md" },
{ type: "assert-exists", client: 1, path: "B.md" },
{ type: "assert-consistent", verify: verifyAllPresent }
]
};

View file

@ -1,84 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
/**
* BUG/EDGE CASE: Both clients rename the same file to different targets.
*
* Client 0 renames XY, Client 1 renames XZ. Both happen offline.
* When they reconnect:
*
* - Client 0's rename (XY) goes through first server has doc at Y
* - Client 1's rename (XZ): Client 1 still has the old metadata
* pointing to X.md. But the server moved it to Y.md.
*
* The conflict: Client 1 will try to update with relativePath=Z.md
* and parentVersionId pointing to the old state. The server sees the
* path changed and processes it as a rename from YZ.
*
* Expected: The file ends up at one path (last rename wins), and both
* clients converge. Content should be preserved.
*/
function verifyFinalState(state: ClientState): void {
// X should not exist (renamed by both)
assert(
!state.files.has("X.md"),
`X.md should not exist, files: ${Array.from(state.files.keys()).join(", ")}`
);
// Exactly one file should exist (either Y.md or Z.md)
assert(
state.files.size === 1,
`Expected 1 file, got ${state.files.size}: ${Array.from(state.files.keys()).join(", ")}`
);
// Content should be preserved
const content = Array.from(state.files.values())[0];
assert(
content === "original content",
`Expected "original content", got: "${content}"`
);
}
export const offlineRenameBothClientsSameSourceTest: TestDefinition = {
name: "Both Clients Rename Same File to Different Targets (Offline)",
description:
"Client 0 renames X→Y, Client 1 renames X→Z, both offline. " +
"On reconnect, the conflicting renames should resolve and " +
"both clients should converge to the same final path.",
clients: 2,
steps: [
// Setup: create X.md
{
type: "create",
client: 0,
path: "X.md",
content: "original content"
},
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
// Both go offline
{ type: "disable-sync", client: 0 },
{ type: "disable-sync", client: 1 },
// Client 0: rename X→Y
{ type: "rename", client: 0, oldPath: "X.md", newPath: "Y.md" },
// Client 1: rename X→Z
{ type: "rename", client: 1, oldPath: "X.md", newPath: "Z.md" },
// Client 0 reconnects first
{ type: "enable-sync", client: 0 },
{ type: "sync", client: 0 },
// Client 1 reconnects
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
// Both clients should converge
{ type: "assert-consistent", verify: verifyFinalState }
]
};

View file

@ -1,68 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
function verifyRenamedFile(state: ClientState): void {
const files = Array.from(state.files.keys()).sort();
// original.md should not exist (it was renamed)
assert(
!state.files.has("original.md"),
`original.md should not exist. Files: ${files.join(", ")}`
);
// renamed.md should exist with the content
assert(
state.files.has("renamed.md"),
`Expected renamed.md to exist. Files: ${files.join(", ")}`
);
assert(
state.files.get("renamed.md") === "pending content",
`Expected "pending content", got: "${state.files.get("renamed.md")}"`
);
assert(
state.files.size === 1,
`Expected 1 file, got ${state.files.size}: ${files.join(", ")}`
);
}
export const offlineRenamePendingCreateTest: TestDefinition = {
name: "Offline Rename of Pending Create Before Key Resolution",
description:
"Client 0 creates a file (pending, not yet synced). Sync is disabled " +
"immediately. Client 0 renames the file locally. Sync is re-enabled. " +
"The idempotency key system must handle the pending create at the new " +
"path. The file should appear at the renamed path on both clients.",
clients: 2,
steps: [
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
// Create file, then immediately disable sync
{ type: "disable-sync", client: 0 },
{
type: "create",
client: 0,
path: "original.md",
content: "pending content"
},
// Rename while still offline (pending create not yet confirmed)
{
type: "rename",
client: 0,
oldPath: "original.md",
newPath: "renamed.md"
},
// Re-enable sync — triggers key resolution + offline reconciliation
{ type: "enable-sync", client: 0 },
{ type: "sync" },
{ type: "barrier" },
// Both clients should have renamed.md with the content
{ type: "assert-not-exists", client: 0, path: "original.md" },
{ type: "assert-not-exists", client: 1, path: "original.md" },
{ type: "assert-consistent", verify: verifyRenamedFile }
]
};

View file

@ -1,92 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
/**
* BUG: Smart create merge with empty parent can lose content.
*
* When the server merges a create with an existing document, it uses a
* 3-way merge with empty parent: reconcile("", existingContent, newContent).
*
* This is correct when both sides are independent additions. But when the
* existing content was an UPDATE (replacing previous content), the merge
* treats the update as an addition and produces garbled output.
*
* Specifically: if existingContent = "updated by client 1" (which replaced
* "original"), the merge sees it as an addition of "updated by client 1"
* from nothing. The new content "created by client 0" is also an addition
* from nothing. The merge concatenates both but the word fragments from
* "created" can bleed into "updated", producing garbage like
* "createdupdated by client 0 offline".
*
* This test verifies that the system produces a VALID merge where at least
* both clients' content fragments appear, even if the merge isn't perfect.
*
* Root cause: The empty parent in merge_with_stored_version (CLAUDE.md
* invariant #15) is necessary to prevent last-write-wins, but it can
* produce suboptimal merges when one side is a replacement of previous
* content (not a pure addition).
*/
function verifyMergedContent(state: ClientState): void {
assert(state.files.size === 1, `Expected 1 file, got ${state.files.size}`);
assert(state.files.has("notes.md"), "Expected notes.md to exist");
const content = state.files.get("notes.md") ?? "";
// Both pieces of content should appear in the merge
assert(
content.includes("client 1 update") && content.includes("client 0 offline"),
`Expected merged content to contain fragments from both clients, got: "${content}"`
);
}
export const reconcilePendingAtOccupiedPathTest: TestDefinition = {
name: "Offline Create at Path Updated by Other Client",
description:
"Client 1 creates and updates a file. Client 0 goes offline and " +
"creates a file at the same path. On reconnect, the server merges " +
"with empty parent. Both clients should converge.",
clients: 2,
steps: [
// Client 1 creates and updates
{
type: "create",
client: 1,
path: "notes.md",
content: "client 1 original"
},
{ type: "enable-sync", client: 1 },
{ type: "sync", client: 1 },
// Enable Client 0, sync, then go offline
{ type: "enable-sync", client: 0 },
{ type: "sync" },
{ type: "barrier" },
// Client 1 updates the file
{
type: "update",
client: 1,
path: "notes.md",
content: "client 1 update replaces everything"
},
{ type: "sync", client: 1 },
// Client 0 goes offline and creates at same path
{ type: "disable-sync", client: 0 },
// Delete the synced copy and create new content
{ type: "delete", client: 0, path: "notes.md" },
{
type: "create",
client: 0,
path: "notes.md",
content: "client 0 offline creates new content"
},
// Reconnect
{ type: "enable-sync", client: 0 },
{ type: "sync" },
{ type: "barrier" },
// Should converge (possibly with suboptimal merge)
{ type: "assert-consistent", verify: verifyMergedContent }
]
};

View file

@ -1,86 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
/**
* BUG: remote-delete + local-update = remote-delete silently discards user edit.
*
* In sync-events.ts coalesceFromRemoteDelete (line 295-297):
* case "local-update":
* return current; // remote-delete absorbs the local-update
*
* This means if a remote-delete broadcast arrives and then the user edits
* the file before the event is processed, the local edit is discarded at
* the coalescing level. The executor only sees "remote-delete" and deletes
* the file, permanently losing the user's work.
*
* Compare with coalesceFromUpdate (line 148-152) where:
* update + remote-delete = update (user edit takes precedence)
*
* The semantics should be the same: the user has unsaved local changes that
* should survive. But the ordering of events (remote-delete arrives FIRST)
* causes the user's intent to be silently discarded.
*
* This test verifies that when a remote-delete and a local-update race,
* both clients converge. The current behavior is that the file gets deleted
* (user's edit is lost). This test documents this data-loss scenario.
*/
function verifyState(state: ClientState): void {
// Current behavior: the file is deleted (remote-delete wins).
// Ideal behavior: the user's edit should survive.
// We test for convergence — both clients must agree.
//
// If the file exists, it should contain the user's edit.
// If it doesn't exist, both must agree on deletion.
if (state.files.size > 0) {
assert(
state.files.has("doc.md"),
`Unexpected files: ${Array.from(state.files.keys()).join(", ")}`
);
const content = state.files.get("doc.md")!;
assert(
content === "edited by local user",
`Expected local edit content, got: "${content}"`
);
}
// Either outcome is acceptable as long as both clients converge
}
export const remoteDeleteCoalesceLosesLocalUpdateTest: TestDefinition = {
name: "Remote Delete + Local Update Coalescing Race",
description:
"When a remote-delete broadcast arrives and the user then edits the " +
"same file, the coalescing (remote-delete + local-update = remote-delete) " +
"discards the user's edit. Both clients should converge.",
clients: 2,
steps: [
// Setup: both clients have doc.md
{ type: "create", client: 0, path: "doc.md", content: "original" },
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
// Both go offline
{ type: "disable-sync", client: 0 },
{ type: "disable-sync", client: 1 },
// Client 1 deletes the file
{ type: "delete", client: 1, path: "doc.md" },
// Client 0 edits the file
{ type: "update", client: 0, path: "doc.md", content: "edited by local user" },
// Client 1 comes online first — delete is sent to server
{ type: "enable-sync", client: 1 },
{ type: "sync", client: 1 },
// Client 0 comes online — receives remote-delete, then its
// local-update coalesces with it
{ type: "enable-sync", client: 0 },
{ type: "sync" },
{ type: "barrier" },
// Both must converge
{ type: "assert-consistent", verify: verifyState }
]
};

View file

@ -1,78 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
/**
* BUG: Renaming an empty file offline causes delete+create instead of move.
*
* In vfs.ts reconcileWithDisk (line 802-805):
* if (fileHash === undefined || fileHash === EMPTY_HASH) {
* remainingNew.push(path);
* continue;
* }
*
* Empty files (hash === EMPTY_HASH) are excluded from hash-based move
* detection. When an empty file is renamed offline, the reconciliation
* treats it as:
* - Old path: missing file delete
* - New path: new file create
*
* This loses the document's identity (gets a new documentId on the server).
* The observable consequence is that the file appears as deleted+created
* rather than renamed, and version history is lost.
*
* This test verifies that both clients converge after an empty file
* rename. The file should exist at the new path on both clients.
*/
function verifyRenamedFile(state: ClientState): void {
assert(
state.files.size === 1,
`Expected 1 file, got ${state.files.size}: ${Array.from(state.files.keys()).join(", ")}`
);
assert(
!state.files.has("empty.md"),
"empty.md should not exist (was renamed)"
);
assert(
state.files.has("renamed.md"),
"renamed.md should exist (renamed from empty.md)"
);
assert(
state.files.get("renamed.md") === "",
`Expected empty content, got: "${state.files.get("renamed.md")}"`
);
}
export const renameEmptyFileLosesIdentityTest: TestDefinition = {
name: "Rename Empty File Loses Document Identity",
description:
"When an empty file is renamed offline, the reconciliation cannot " +
"detect it as a move (empty files are excluded from hash-based " +
"move detection). This causes delete+create instead of move, " +
"losing the document's server-side identity/history.",
clients: 2,
steps: [
// Create and sync an empty file
{ type: "create", client: 0, path: "empty.md", content: "" },
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
{ type: "assert-exists", client: 1, path: "empty.md" },
// Client 0 goes offline and renames
{ type: "disable-sync", client: 0 },
{ type: "rename", client: 0, oldPath: "empty.md", newPath: "renamed.md" },
// Reconnect
{ type: "enable-sync", client: 0 },
{ type: "sync" },
{ type: "barrier" },
// Both should have only renamed.md
{ type: "assert-not-exists", client: 0, path: "empty.md" },
{ type: "assert-not-exists", client: 1, path: "empty.md" },
{ type: "assert-exists", client: 0, path: "renamed.md" },
{ type: "assert-exists", client: 1, path: "renamed.md" },
{ type: "assert-consistent", verify: verifyRenamedFile }
]
};

View file

@ -1,53 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
function verifyNestedPath(state: ClientState): void {
const files = Array.from(state.files.keys());
assert(
!files.includes("a.md"),
`a.md should not exist after rename to nested path, got: ${files.join(", ")}`
);
assert(
files.includes("folder/subfolder/a.md"),
`Expected folder/subfolder/a.md to exist, got: ${files.join(", ")}`
);
assert(
state.files.get("folder/subfolder/a.md") === "nested content",
`Expected nested file to have "nested content", got: "${state.files.get("folder/subfolder/a.md")}"`
);
}
export const renameNestedPathTest: TestDefinition = {
name: "Rename to Deeply Nested Path",
description:
"Client 0 creates a.md at the root, then renames it to folder/subfolder/a.md " +
"while offline. When Client 0 reconnects, the file should appear at the " +
"nested path on both clients. Tests that the system handles directory " +
"creation for deeply nested rename targets.",
clients: 2,
steps: [
// Setup: create file at root and sync
{ type: "create", client: 0, path: "a.md", content: "nested content" },
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
{ type: "assert-content", client: 1, path: "a.md", content: "nested content" },
// Client 0 goes offline and renames to nested path
{ type: "disable-sync", client: 0 },
{ type: "rename", client: 0, oldPath: "a.md", newPath: "folder/subfolder/a.md" },
// Client 0 reconnects
{ type: "enable-sync", client: 0 },
{ type: "sync" },
{ type: "barrier" },
// Original path gone, nested path exists
{ type: "assert-not-exists", client: 0, path: "a.md" },
{ type: "assert-not-exists", client: 1, path: "a.md" },
{ type: "assert-exists", client: 0, path: "folder/subfolder/a.md" },
{ type: "assert-exists", client: 1, path: "folder/subfolder/a.md" },
{ type: "assert-consistent", verify: verifyNestedPath }
]
};

View file

@ -1,91 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
function verifyResult(state: ClientState): void {
const files = Array.from(state.files.keys()).sort();
// The rename of B.md to A.md overwrites A.md on disk. The pending
// create's content ("first file at A") is lost because the user
// chose to overwrite it. VFS.move fails (A.md occupied by pending
// create), so the fallback enqueues an update for B.md which fails
// (FileNotFoundError — B.md no longer exists on disk).
//
// After reconciliation: A.md's pending create reads the overwritten
// content ("tracked file B") from disk, and B.md is deleted
// (missing from disk).
//
// Result: A.md with "tracked file B" content.
assert(
state.files.size === 1,
`Expected 1 file, got ${state.files.size}: ${files.join(", ")}`
);
assert(
state.files.has("A.md"),
`Expected A.md to exist. Files: ${files.join(", ")}`
);
const content = state.files.get("A.md") ?? "";
assert(
content === "tracked file B",
`Expected A.md to have "tracked file B", got: "${content}"`
);
}
/**
* BUG: Tests VFS.move failure when renaming a tracked file to a path
* occupied by a pending create. In syncer.ts, VFS.move is attempted
* but fails if the target path is occupied by a non-deleted-locally
* document. The move event falls back to an update at oldPath.
*
* When the user renames B.md to A.md, the filesystem overwrites A.md.
* The pending create's original content is lost from disk. After sync,
* only A.md survives with B.md's content.
*/
export const renameTrackedToOccupiedPendingPathTest: TestDefinition = {
name: "Rename Tracked File to Path Occupied by Pending Create",
description:
"Client creates A.md (pending, sync disabled) then renames B.md " +
"(tracked) to A.md. VFS.move should fail because A.md is occupied " +
"by the pending create. The rename overwrites A.md on disk, so " +
"only A.md survives with B.md's content.",
clients: 2,
steps: [
// Setup: create B.md and sync it (becomes tracked)
{
type: "create",
client: 0,
path: "B.md",
content: "tracked file B"
},
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
{
type: "assert-content",
client: 1,
path: "B.md",
content: "tracked file B"
},
// Disable sync on Client 0
{ type: "disable-sync", client: 0 },
// Create A.md (pending — sync disabled, not yet synced)
{
type: "create",
client: 0,
path: "A.md",
content: "first file at A"
},
// Try to rename tracked B.md to A.md (occupied by pending)
{ type: "rename", client: 0, oldPath: "B.md", newPath: "A.md" },
// Re-enable sync — after reconciliation, A.md survives
{ type: "enable-sync", client: 0 },
{ type: "sync" },
{ type: "barrier" },
// A.md exists with B.md's content (rename overwrite)
{ type: "assert-consistent", verify: verifyResult }
]
};

View file

@ -1,88 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
function verifyBothFilesPresent(state: ClientState): void {
const allContent = Array.from(state.files.values()).join("\n");
assert(
allContent.includes("offline-alpha"),
`Missing content "offline-alpha". Files: ${JSON.stringify(Object.fromEntries(state.files))}`
);
assert(
allContent.includes("offline-beta"),
`Missing content "offline-beta". Files: ${JSON.stringify(Object.fromEntries(state.files))}`
);
}
export const serverPauseConcurrentCreatesTest: TestDefinition = {
name: "Server Pause — Concurrent Creates From Both Clients",
description:
"The server is paused BEFORE either client creates anything. " +
"Client 0 creates fileA.md and Client 1 creates fileB.md — both HTTP " +
"requests stall because the server is frozen. After the server resumes, " +
"both creates should complete and both files should appear on both clients. " +
"This is a harder variant than the existing create-while-server-paused test " +
"because BOTH clients have stalled pending creates simultaneously, testing " +
"that the server correctly handles a burst of requests after SIGCONT and " +
"that idempotency keys prevent duplicate documents if retries occur.",
clients: 2,
steps: [
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
// Pause the server FIRST — no requests can succeed
{ type: "pause-server" },
// Both clients create different files while the server is frozen
{
type: "create",
client: 0,
path: "fileA.md",
content: "offline-alpha"
},
{
type: "create",
client: 1,
path: "fileB.md",
content: "offline-beta"
},
// Resume the server — both pending creates should complete
{ type: "resume-server" },
{ type: "sync" },
{ type: "barrier" },
// Both files must exist on both clients
{ type: "assert-exists", client: 0, path: "fileA.md" },
{ type: "assert-exists", client: 0, path: "fileB.md" },
{ type: "assert-exists", client: 1, path: "fileA.md" },
{ type: "assert-exists", client: 1, path: "fileB.md" },
{
type: "assert-content",
client: 0,
path: "fileA.md",
content: "offline-alpha"
},
{
type: "assert-content",
client: 1,
path: "fileA.md",
content: "offline-alpha"
},
{
type: "assert-content",
client: 0,
path: "fileB.md",
content: "offline-beta"
},
{
type: "assert-content",
client: 1,
path: "fileB.md",
content: "offline-beta"
},
{ type: "assert-consistent", verify: verifyBothFilesPresent }
]
};

View file

@ -1,73 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
function verifyRename(state: ClientState): void {
const files = Array.from(state.files.keys());
assert(
!state.files.has("original.md"),
`Expected original.md to NOT exist after rename, got files: ${files.join(", ")}`
);
assert(
state.files.has("renamed.md"),
`Expected renamed.md to exist after rename, got files: ${files.join(", ")}`
);
const content = state.files.get("renamed.md") ?? "";
assert(
content === "important data",
`Expected renamed.md content to be "important data", got: "${content}"`
);
}
export const serverPauseRenameTest: TestDefinition = {
name: "Server Pause Then Rename Propagation",
description:
"Client 0 creates original.md and both clients sync. The server is paused. " +
"Client 0 renames original.md to renamed.md while the server is frozen. " +
"After the server resumes, the rename should propagate to Client 1: " +
"original.md disappears and renamed.md appears with the same content. " +
"This tests that rename operations (which are update-with-oldPath on the " +
"HTTP layer) survive server outages and that Client 1 correctly applies " +
"the path change from the WebSocket broadcast.",
clients: 2,
steps: [
// Setup: create file and sync both clients
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{
type: "create",
client: 0,
path: "original.md",
content: "important data"
},
{ type: "sync" },
{ type: "barrier" },
{
type: "assert-content",
client: 1,
path: "original.md",
content: "important data"
},
// Pause the server, then rename on client 0
{ type: "pause-server" },
{
type: "rename",
client: 0,
oldPath: "original.md",
newPath: "renamed.md"
},
// Resume the server — the stalled rename request should complete
{ type: "resume-server" },
{ type: "sync" },
{ type: "barrier" },
// original.md should be gone, renamed.md should exist on both
{ type: "assert-not-exists", client: 0, path: "original.md" },
{ type: "assert-not-exists", client: 1, path: "original.md" },
{ type: "assert-exists", client: 0, path: "renamed.md" },
{ type: "assert-exists", client: 1, path: "renamed.md" },
{ type: "assert-consistent", verify: verifyRename }
]
};

View file

@ -1,39 +0,0 @@
import type { TestDefinition } from "../test-definition";
export const serverPauseResumeTest: TestDefinition = {
name: "Server Pause and Resume",
description:
"Client 0 creates a file and syncs it to the server. The server is then " +
"paused (SIGSTOP), which may stall WebSocket broadcasts to Client 1. " +
"After the server resumes, both clients should converge.",
clients: 2,
steps: [
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
// Create a file, then immediately pause the server
{ type: "create", client: 0, path: "resilient.md", content: "survives pause" },
{ type: "pause-server" },
{ type: "resume-server" },
// After resume, sync should eventually succeed
{ type: "sync" },
{ type: "barrier" },
{ type: "assert-exists", client: 0, path: "resilient.md" },
{ type: "assert-exists", client: 1, path: "resilient.md" },
{
type: "assert-content",
client: 0,
path: "resilient.md",
content: "survives pause"
},
{
type: "assert-content",
client: 1,
path: "resilient.md",
content: "survives pause"
},
{ type: "assert-consistent" }
]
};

View file

@ -1,122 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
/**
* BUG: Stale doc kept on disk creates duplicate content after create-merge.
*
* Found by: E2E test log analysis (log.log, process 672773)
*
* Root cause sequence:
* 1. Client 1 has document D1 tracked at path "target.md"
* 2. Client 0 renames D1 to "moved.md" on the server
* 3. Client 1 (offline) creates a new file at "moved.md"
* 4. Client 1 reconnects the create is sent to the server
* 5. Server merges the create with D1 (at "moved.md") MergingUpdate with D1
* 6. ensureUniqueDocumentId finds D1 at "target.md" stale doc
* 7. "target.md" was locally modified during the create's HTTP request
* hasLocalChanges = true file kept on disk, VFS record removed
* 8. On the next reconciliation, orphaned "target.md" is re-synced
* as a new document. Now BOTH "target.md" and "moved.md" contain
* the original content from D1 violating the content-uniqueness
* invariant.
*
* The server pause is used to keep the create HTTP request in-flight
* while the local file at D1's old path is modified (step 7).
*/
function verifyNoDuplicateContent(state: ClientState): void {
const entries = [...state.files.entries()];
// The word "original" was D1's initial content. After the create-merge,
// it should appear in at most ONE file. If the stale orphan was re-synced
// as a separate document, "original" will appear in multiple files.
const filesContainingOriginal = entries.filter(([, content]) =>
content.includes("original")
);
assert(
filesContainingOriginal.length <= 1,
`Content "original" found in ${filesContainingOriginal.length} files: ` +
`${filesContainingOriginal.map(([p]) => p).join(", ")}. ` +
`This means the stale doc orphan was re-synced, creating duplicate content.\n` +
`Files:\n${entries.map(([k, v]) => ` ${k}: "${v}"`).join("\n")}`
);
}
export const staleDocOrphanDuplicateContentTest: TestDefinition = {
name: "Stale Doc Orphan Creates Duplicate Content After Create-Merge",
description:
"When a create merges with an existing document, the stale VFS " +
"record is removed but the file is kept on disk (local changes). " +
"If the orphaned file is later re-synced as a new document, the " +
"original content appears in multiple files.",
clients: 2,
steps: [
// ── Setup: both clients share D1 at "target.md" ──
{ type: "create", client: 0, path: "target.md", content: "original" },
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
// ── Client 1 goes offline ──
{ type: "disable-sync", client: 1 },
// ── Client 0 renames the document to a new path ──
// Server now has D1 at "moved.md"
{
type: "rename",
client: 0,
oldPath: "target.md",
newPath: "moved.md"
},
{ type: "sync", client: 0 },
// ── Client 1 (offline) creates a file at D1's new server path ──
// Client 1 doesn't know D1 was renamed there.
{
type: "create",
client: 1,
path: "moved.md",
content: "unrelated-content"
},
// ── Pause server to stall the create HTTP request ──
{ type: "pause-server" },
// ── Enable sync on client 1 ──
// scheduleSyncForOfflineChanges runs:
// "target.md": D1, hash matches → no update
// "moved.md": no metadata → create scheduled
// The create HTTP request stalls (server frozen).
// enableSync waits up to 10 s for WebSocket then returns.
{ type: "enable-sync", client: 1 },
// ── Modify D1's old path while the create is in-flight ──
// This makes hasLocalChanges = true when ensureUniqueDocumentId
// checks the stale doc at "target.md".
{
type: "update",
client: 1,
path: "target.md",
content: "original extra-edit"
},
// ── Resume server ──
// Create completes: server merges with D1 → MergingUpdate
// ensureUniqueDocumentId: D1 at "target.md" → stale doc
// hasLocalChanges("target.md"): "original extra-edit" ≠ "original" → true
// File kept, VFS record removed.
//
// WebSocket connects → second reconciliation detects orphaned
// "target.md" → re-synced as new document → DUPLICATE CONTENT.
{ type: "resume-server" },
// ── Settle ──
{ type: "sync" },
{ type: "sync" },
{ type: "barrier" },
// ── Verify: "original" must not appear in multiple files ──
{ type: "assert-consistent", verify: verifyNoDuplicateContent }
]
};

View file

@ -1,53 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
function verifyAllContent(state: ClientState): void {
// All three creates at the same path should merge into a single file
assert(
state.files.size === 1,
`Expected 1 file after 3-way merge, got ${state.files.size}: ${Array.from(state.files.keys()).join(", ")}`
);
assert(
state.files.has("A.md"),
`Expected merged file at A.md, got: ${Array.from(state.files.keys()).join(", ")}`
);
const content = state.files.get("A.md") ?? "";
assert(
content.includes("from-zero"),
`Expected merged content to include "from-zero", got: "${content}"`
);
assert(
content.includes("from-one"),
`Expected merged content to include "from-one", got: "${content}"`
);
assert(
content.includes("from-two"),
`Expected merged content to include "from-two", got: "${content}"`
);
}
export const threeClientConvergenceTest: TestDefinition = {
name: "Three Client Convergence",
description:
"Three clients all create the same file offline with different content. " +
"When all three enable sync, the server must merge all three versions " +
"and all clients must converge to the same state with all content preserved.",
clients: 3,
steps: [
// All three create A.md offline with different content
{ type: "create", client: 0, path: "A.md", content: "from-zero" },
{ type: "create", client: 1, path: "A.md", content: "from-one" },
{ type: "create", client: 2, path: "A.md", content: "from-two" },
// Enable sync on all three
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "enable-sync", client: 2 },
{ type: "sync" },
{ type: "barrier" },
// All three must converge and all content must be preserved
{ type: "assert-consistent", verify: verifyAllContent }
]
};

View file

@ -1,85 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
/**
* BUG: update + remote-delete = update, but execution deletes the file.
*
* In sync-events.ts coalesceFromUpdate (line 148-152):
* case "remote-delete":
* return current; // comment: "user edit takes precedence"
*
* The coalescing INTENT is correct: the user's edit should survive.
* But the EXECUTION doesn't match:
*
* 1. The coalesced "update" action calls executeSyncUpdateSendChanges()
* 2. This sends putText/putBinary to the server
* 3. The server's update_document handler checks if latest_version.is_deleted
* 4. Since the doc IS deleted, server returns FastForwardUpdate(isDeleted=true)
* 5. applyServerResponse checks response.isDeleted at line 296
* 6. Calls applyRemoteDeleteLocally which DELETES the file!
*
* The user's edit is permanently lost despite the coalescing saying
* "user edit takes precedence."
*
* This test proves the data loss by having one client edit while another
* deletes, with the edit arriving at the event queue before the delete.
*/
function verifyUserEditPreserved(state: ClientState): void {
// The coalescing says "user edit takes precedence" so the file
// should ideally survive with the user's content.
// Current behavior: file is deleted (data loss).
// We test for convergence.
if (state.files.size > 0) {
assert(
state.files.has("doc.md"),
`Unexpected files: ${Array.from(state.files.keys()).join(", ")}`
);
const content = state.files.get("doc.md")!;
assert(
content.includes("user edit"),
`Expected user's edit content, got: "${content}"`
);
}
}
export const updateVsRemoteDeleteDataLossTest: TestDefinition = {
name: "Update + Remote Delete Coalescing Data Loss",
description:
"When a user edits a file and then a remote-delete arrives, the " +
"coalescing produces 'update' (user edit takes precedence). But " +
"the server returns isDeleted=true, causing the client to delete " +
"the file — contradicting the coalescing intent.",
clients: 2,
steps: [
// Setup: both clients have doc.md
{ type: "create", client: 0, path: "doc.md", content: "original" },
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
// Both go offline
{ type: "disable-sync", client: 0 },
{ type: "disable-sync", client: 1 },
// Client 0 edits the file (local-update queued first)
{ type: "update", client: 0, path: "doc.md", content: "user edit on client 0" },
// Client 1 deletes the file
{ type: "delete", client: 1, path: "doc.md" },
// Client 1 comes online first — delete sent to server
{ type: "enable-sync", client: 1 },
{ type: "sync", client: 1 },
// Client 0 comes online — local-update already queued,
// then remote-delete arrives and coalesces:
// update + remote-delete = update (per coalescing)
{ type: "enable-sync", client: 0 },
{ type: "sync" },
{ type: "barrier" },
// Both must converge to a consistent state
{ type: "assert-consistent", verify: verifyUserEditPreserved }
]
};

View file

@ -1,60 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
/**
* BUG FIX: User-created files with parenthesized names must not be deleted.
*
* The duplicate content detection in step 7 of reconciliation uses a regex
* that matches files like "Chapter (1).md". This should only delete files
* created by ensureClearPath, not user-intentionally-created files.
*
* Note: the two files MUST have different content, because the server
* merges deconflicted-path creates when the content is identical to the
* base-path document.
*/
function verifyBothFilesExist(state: ClientState): void {
assert(
state.files.size === 2,
`Expected 2 files, got ${state.files.size}: ${Array.from(state.files.keys()).join(", ")}`
);
assert(
state.files.has("Chapter.md"),
"Expected Chapter.md to exist"
);
assert(
state.files.has("Chapter (1).md"),
"Expected Chapter (1).md to exist"
);
}
export const userParenthesizedFileNotDeletedTest: TestDefinition = {
name: "User-Created Parenthesized Files Not Deleted",
description:
"A user-created file like 'Chapter (1).md' should not be silently " +
"deleted by the duplicate content detection heuristic. Uses " +
"different content to avoid server-side deconfliction merge.",
clients: 2,
steps: [
// Client 0 creates both files with DIFFERENT content
// (same content triggers server-side deconfliction merge)
{
type: "create",
client: 0,
path: "Chapter.md",
content: "chapter one"
},
{
type: "create",
client: 0,
path: "Chapter (1).md",
content: "chapter one notes"
},
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
// Both files should survive on both clients
{ type: "assert-consistent", verify: verifyBothFilesExist }
]
};

View file

@ -1,40 +0,0 @@
import type { ClientState, TestDefinition } from "../test-definition";
import { assert } from "../utils/assert";
function verifyMergedContent(state: ClientState): void {
assert(state.files.size === 1, `Expected 1 file, got ${state.files.size}`);
assert(state.files.has("A.md"), "Expected A.md to exist");
const content = state.files.get("A.md") ?? "";
assert(
content.includes("hello") && content.includes("world"),
`Expected A.md to contain both "hello" and "world", got: "${content}"`
);
// Verify no duplication — each word should appear exactly once
const helloCount = content.split("hello").length - 1;
const worldCount = content.split("world").length - 1;
assert(
helloCount === 1,
`Expected "hello" to appear once, appeared ${helloCount} times in: "${content}"`
);
assert(
worldCount === 1,
`Expected "world" to appear once, appeared ${worldCount} times in: "${content}"`
);
}
export const writeWriteConflictTest: TestDefinition = {
name: "Write/Write Conflict",
description:
"Two clients simultaneously create the same file with different content. " +
"The system should resolve the conflict and both clients should converge.",
clients: 2,
steps: [
{ type: "create", client: 0, path: "A.md", content: "hello" },
{ type: "create", client: 1, path: "A.md", content: "world" },
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "sync" },
{ type: "barrier" },
{ type: "assert-consistent", verify: verifyMergedContent }
]
};