From 67d410b5200d37789d0ed1d02cec52a1f116889f Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Thu, 26 Mar 2026 21:13:44 +0000 Subject: [PATCH] Add a few good deterministic tests --- ...-text-pending-create-not-displaced.test.ts | 40 +++++++++++++ ...concurrent-update-diff-consistency.test.ts | 41 +++++++++++++ .../src/tests/11-create-delete-noop.test.ts | 24 ++++++++ .../src/tests/12-create-merge-delete.test.ts | 30 ++++++++++ ...3-move-identical-content-ambiguity.test.ts | 57 +++++++++++++++++++ ...inary-pending-create-not-displaced.test.ts | 41 +++++++++++++ ...sce-update-remote-update-data-loss.test.ts | 49 ++++++++++++++++ ...esced-remote-update-watermark-loss.test.ts | 48 ++++++++++++++++ ...urrent-delete-during-remote-update.test.ts | 29 ++++++++++ ...oncurrent-edit-exact-same-position.test.ts | 55 ++++++++++++++++++ ...urrent-rename-and-create-at-target.test.ts | 48 ++++++++++++++++ ...urrent-rename-and-create-at-target.test.ts | 49 ++++++++++++++++ .../9-concurrent-rename-same-target.test.ts | 40 +++++++++++++ 13 files changed, 551 insertions(+) create mode 100644 frontend/deterministic-tests/src/tests/1-text-pending-create-not-displaced.test.ts create mode 100644 frontend/deterministic-tests/src/tests/10-concurrent-update-diff-consistency.test.ts create mode 100644 frontend/deterministic-tests/src/tests/11-create-delete-noop.test.ts create mode 100644 frontend/deterministic-tests/src/tests/12-create-merge-delete.test.ts create mode 100644 frontend/deterministic-tests/src/tests/13-move-identical-content-ambiguity.test.ts create mode 100644 frontend/deterministic-tests/src/tests/2-binary-pending-create-not-displaced.test.ts create mode 100644 frontend/deterministic-tests/src/tests/3-coalesce-update-remote-update-data-loss.test.ts create mode 100644 frontend/deterministic-tests/src/tests/4-coalesced-remote-update-watermark-loss.test.ts create mode 100644 frontend/deterministic-tests/src/tests/5-concurrent-delete-during-remote-update.test.ts create mode 100644 frontend/deterministic-tests/src/tests/6-concurrent-edit-exact-same-position.test.ts create mode 100644 frontend/deterministic-tests/src/tests/7-concurrent-rename-and-create-at-target.test.ts create mode 100644 frontend/deterministic-tests/src/tests/8-concurrent-rename-and-create-at-target.test.ts create mode 100644 frontend/deterministic-tests/src/tests/9-concurrent-rename-same-target.test.ts diff --git a/frontend/deterministic-tests/src/tests/1-text-pending-create-not-displaced.test.ts b/frontend/deterministic-tests/src/tests/1-text-pending-create-not-displaced.test.ts new file mode 100644 index 00000000..506e2b59 --- /dev/null +++ b/frontend/deterministic-tests/src/tests/1-text-pending-create-not-displaced.test.ts @@ -0,0 +1,40 @@ +import type { TestDefinition } from "../test-definition"; +import type { AssertableState } from "../utils/assertable-state"; + +export const textPendingCreateNotDisplacedTest: TestDefinition = { + name: "Both offline binary creates at same path survive sync", + description: + "Two clients each create a binary file at the same path while offline. " + + "After syncing, both files should exist on both clients at separate paths.", + clients: 2, + steps: [ + { + type: "create", + client: 0, + path: "data.txt", + content: "text data from client 0" + }, + { + type: "create", + client: 1, + path: "data.txt", + content: "text data from client 1" + }, + + { type: "enable-sync", client: 0 }, + { type: "enable-sync", client: 1 }, + { type: "barrier" }, + + { type: "assert-consistent", verify: verifyBothFilesExist } + ] +}; + +function verifyBothFilesExist(state: AssertableState): void { + state + .assertFileCount(1) + .assertFileExists("data.txt") + .assertAnyFileContains( + "data from client 0", + "data from client 1" + ); +} diff --git a/frontend/deterministic-tests/src/tests/10-concurrent-update-diff-consistency.test.ts b/frontend/deterministic-tests/src/tests/10-concurrent-update-diff-consistency.test.ts new file mode 100644 index 00000000..baa8bc52 --- /dev/null +++ b/frontend/deterministic-tests/src/tests/10-concurrent-update-diff-consistency.test.ts @@ -0,0 +1,41 @@ +import type { TestDefinition } from "../test-definition"; + +export const concurrentUpdateDiffConsistencyTest: TestDefinition = { + name: "Concurrent edits to different sections merge correctly", + description: + "Both clients edit different sections of the same file while offline. " + + "After syncing, the merged file should contain both edits.", + 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: "barrier" }, + + { 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" + }, + + { type: "enable-sync", client: 0 }, + { type: "enable-sync", client: 1 }, + { type: "barrier" }, + + { type: "assert-consistent", verify: (state) => state.assertFileCount(1).assertContent("doc.md", "header by 0\nmiddle\nfooter by 1") } + ] +}; diff --git a/frontend/deterministic-tests/src/tests/11-create-delete-noop.test.ts b/frontend/deterministic-tests/src/tests/11-create-delete-noop.test.ts new file mode 100644 index 00000000..f575fc79 --- /dev/null +++ b/frontend/deterministic-tests/src/tests/11-create-delete-noop.test.ts @@ -0,0 +1,24 @@ +import type { TestDefinition } from "../test-definition"; + +export const createDeleteNoopTest: TestDefinition = { + name: "Offline create then delete results in no file", + description: + "A client creates a file, updates it multiple times, then deletes it, all while " + + "offline. After syncing, neither client should have the file.", + clients: 2, + steps: [ + { type: "enable-sync", client: 1 }, + + { 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" }, + + { type: "enable-sync", client: 0 }, + { type: "barrier" }, + + { type: "assert-not-exists", client: 0, path: "temp.md" }, + { type: "assert-not-exists", client: 1, path: "temp.md" }, + { type: "assert-consistent" } + ] +}; diff --git a/frontend/deterministic-tests/src/tests/12-create-merge-delete.test.ts b/frontend/deterministic-tests/src/tests/12-create-merge-delete.test.ts new file mode 100644 index 00000000..4a40b59f --- /dev/null +++ b/frontend/deterministic-tests/src/tests/12-create-merge-delete.test.ts @@ -0,0 +1,30 @@ +import type { TestDefinition } from "../test-definition"; + +export const createMergeDeleteTest: TestDefinition = { + name: "Concurrent Create, Merge, Then Delete", + description: + "Two clients create A.md offline with different content. Both come online and " + + "the content is merged. Then one client deletes A.md. Both clients should " + + "converge on an empty state.", + clients: 2, + steps: [ + { type: "create", client: 0, path: "A.md", content: "from-zero" }, + { type: "create", client: 1, path: "A.md", content: "from-one" }, + + { type: "enable-sync", client: 0 }, + { type: "enable-sync", client: 1 }, + { type: "barrier" }, + + { + type: "assert-consistent", + verify: (state) => state.assertFileCount(1).assertContains("A.md", "from-zero", "from-one") + }, + + { type: "delete", client: 0, path: "A.md" }, + { type: "barrier" }, + + { type: "assert-not-exists", client: 0, path: "A.md" }, + { type: "assert-not-exists", client: 1, path: "A.md" }, + { type: "assert-consistent", verify: (state) => state.assertFileCount(0) } + ] +}; diff --git a/frontend/deterministic-tests/src/tests/13-move-identical-content-ambiguity.test.ts b/frontend/deterministic-tests/src/tests/13-move-identical-content-ambiguity.test.ts new file mode 100644 index 00000000..91a52496 --- /dev/null +++ b/frontend/deterministic-tests/src/tests/13-move-identical-content-ambiguity.test.ts @@ -0,0 +1,57 @@ +import type { TestDefinition } from "../test-definition"; + +export const moveIdenticalContentAmbiguityTest: TestDefinition = { + name: "Move Detection Ambiguity With Identical Content", + description: + "Two files with identical content exist. One is deleted and the other renamed " + + "while offline. The system should still converge correctly despite the ambiguity.", + clients: 2, + steps: [ + { + type: "create", + client: 0, + path: "A.md", + content: "identical content" + }, + { + type: "create", + client: 0, + path: "B.md", + content: "identical content" + }, + { type: "enable-sync", client: 0 }, + { type: "enable-sync", client: 1 }, + { type: "barrier" }, + + { + type: "assert-content", + client: 1, + path: "A.md", + content: "identical content" + }, + { + type: "assert-content", + client: 1, + path: "B.md", + content: "identical content" + }, + + { type: "disable-sync", client: 1 }, + { type: "delete", client: 1, path: "A.md" }, + { type: "rename", client: 1, oldPath: "B.md", newPath: "C.md" }, + + { type: "enable-sync", client: 1 }, + { type: "barrier" }, + + { + type: "assert-consistent", + verify: (state) => { + state + .assertFileCount(1) + .assertFileNotExists("A.md") + .assertFileNotExists("B.md") + .assertContent("C.md", "identical content"); + } + } + ] +}; diff --git a/frontend/deterministic-tests/src/tests/2-binary-pending-create-not-displaced.test.ts b/frontend/deterministic-tests/src/tests/2-binary-pending-create-not-displaced.test.ts new file mode 100644 index 00000000..5ad89cbe --- /dev/null +++ b/frontend/deterministic-tests/src/tests/2-binary-pending-create-not-displaced.test.ts @@ -0,0 +1,41 @@ +import type { TestDefinition } from "../test-definition"; +import type { AssertableState } from "../utils/assertable-state"; + +export const binaryPendingCreateNotDisplacedTest: TestDefinition = { + name: "Both offline binary creates at same path survive sync", + description: + "Two clients each create a binary file at the same path while offline. " + + "After syncing, both files should exist on both clients at separate paths.", + clients: 2, + steps: [ + { + 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" + }, + + { type: "enable-sync", client: 0 }, + { type: "enable-sync", client: 1 }, + { type: "barrier" }, + + { type: "assert-consistent", verify: verifyBothFilesExist } + ] +}; + +function verifyBothFilesExist(state: AssertableState): void { + state + .assertFileCount(2) + .assertFileExists("data.bin") + .assertFileExists("data (1).bin") + .assertAnyFileContains( + "binary data from client 0", + "binary data from client 1" + ); +} diff --git a/frontend/deterministic-tests/src/tests/3-coalesce-update-remote-update-data-loss.test.ts b/frontend/deterministic-tests/src/tests/3-coalesce-update-remote-update-data-loss.test.ts new file mode 100644 index 00000000..d66a2cf3 --- /dev/null +++ b/frontend/deterministic-tests/src/tests/3-coalesce-update-remote-update-data-loss.test.ts @@ -0,0 +1,49 @@ +import type { TestDefinition } from "../test-definition"; + +export const coalesceUpdateRemoteUpdateDataLossTest: TestDefinition = { + name: "Local and remote edits to the same file are both preserved", + description: + "Client 0 edits a file while client 1 is offline. Client 1 reconnects " + + "and immediately edits the same file. Both edits should be preserved.", + clients: 2, + steps: [ + { + 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: "barrier" }, + + { type: "disable-sync", client: 1 }, + + { + type: "update", + client: 0, + path: "doc.md", + content: "line 1\nline 2\nline 3\nclient 0 addition" + }, + { type: "sync", client: 0 }, + + { + type: "update", + client: 1, + path: "doc.md", + content: "client 1 addition\nline 1\nline 2\nline 3" + }, + + { type: "enable-sync", client: 1 }, + { type: "barrier" }, + + { + type: "assert-consistent", + verify: (state) => { + state + .assertFileCount(1) + .assertContains("doc.md", "client 0 addition", "client 1 addition"); + } + } + ] +}; diff --git a/frontend/deterministic-tests/src/tests/4-coalesced-remote-update-watermark-loss.test.ts b/frontend/deterministic-tests/src/tests/4-coalesced-remote-update-watermark-loss.test.ts new file mode 100644 index 00000000..2d8fd4b6 --- /dev/null +++ b/frontend/deterministic-tests/src/tests/4-coalesced-remote-update-watermark-loss.test.ts @@ -0,0 +1,48 @@ +import type { TestDefinition } from "../test-definition"; +import type { AssertableState } from "../utils/assertable-state"; + +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: "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 }, + + { type: "barrier" }, + { type: "assert-consistent", verify: verifyContent }, + + { type: "disable-sync", client: 0 }, + { type: "disable-sync", client: 1 }, + { type: "enable-sync", client: 0 }, + { type: "enable-sync", client: 1 }, + { type: "barrier" }, + + { type: "assert-consistent", verify: verifyContent }, + + { type: "disable-sync", client: 0 }, + { type: "disable-sync", client: 1 }, + { type: "enable-sync", client: 0 }, + { type: "enable-sync", client: 1 }, + { type: "barrier" }, + { type: "assert-consistent", verify: verifyContent } + ] +}; + + +function verifyContent(state: AssertableState): void { + state.assertFileCount(1).assertContent("doc.md", "final update"); +} diff --git a/frontend/deterministic-tests/src/tests/5-concurrent-delete-during-remote-update.test.ts b/frontend/deterministic-tests/src/tests/5-concurrent-delete-during-remote-update.test.ts new file mode 100644 index 00000000..1a4014ac --- /dev/null +++ b/frontend/deterministic-tests/src/tests/5-concurrent-delete-during-remote-update.test.ts @@ -0,0 +1,29 @@ +import { AssertableState } from "src/utils/assertable-state"; +import type { TestDefinition } from "../test-definition"; + +export const concurrentDeleteDuringRemoteUpdateTest: TestDefinition = { + name: "Delete and remote update of same file do not crash", + description: + "One client updates a file while the other deletes it at the same " + + "time. Both clients should converge without errors.", + clients: 2, + steps: [ + { type: "create", client: 0, path: "doc.md", content: "original" }, + { type: "enable-sync", client: 0 }, + { type: "enable-sync", client: 1 }, + { type: "barrier" }, + + { type: "disable-sync", client: 0 }, + { type: "disable-sync", client: 1 }, + + { type: "update", client: 0, path: "doc.md", content: "updated by 0" }, + { type: "delete", client: 1, path: "doc.md" }, + + { type: "enable-sync", client: 0 }, + { type: "enable-sync", client: 1 }, + { type: "barrier" }, + + { type: "assert-consistent", verify: (state) => state.assertFileCount(0) } + ] +}; + diff --git a/frontend/deterministic-tests/src/tests/6-concurrent-edit-exact-same-position.test.ts b/frontend/deterministic-tests/src/tests/6-concurrent-edit-exact-same-position.test.ts new file mode 100644 index 00000000..93cc6fc3 --- /dev/null +++ b/frontend/deterministic-tests/src/tests/6-concurrent-edit-exact-same-position.test.ts @@ -0,0 +1,55 @@ +import type { TestDefinition } from "../test-definition"; + +export const concurrentEditExactSamePositionTest: TestDefinition = { + name: "Concurrent edits to the exact same word are both preserved", + description: + "Both clients replace the same word in a file with different text " + + "while offline. After syncing, the merged result should contain " + + "both replacements.", + clients: 2, + steps: [ + { + type: "create", + client: 0, + path: "doc.md", + content: "the quick brown fox" + }, + { type: "enable-sync", client: 0 }, + { type: "enable-sync", client: 1 }, + { type: "barrier" }, + { + type: "assert-content", + client: 1, + path: "doc.md", + content: "the quick brown fox" + }, + + { 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" + }, + + { type: "enable-sync", client: 0 }, + { type: "enable-sync", client: 1 }, + { type: "barrier" }, + + { + type: "assert-consistent", + verify: (state) => { + state + .assertFileCount(1) + .assertContains("doc.md", "slow", "fast", "brown fox"); + } + } + ] +}; diff --git a/frontend/deterministic-tests/src/tests/7-concurrent-rename-and-create-at-target.test.ts b/frontend/deterministic-tests/src/tests/7-concurrent-rename-and-create-at-target.test.ts new file mode 100644 index 00000000..7c08b392 --- /dev/null +++ b/frontend/deterministic-tests/src/tests/7-concurrent-rename-and-create-at-target.test.ts @@ -0,0 +1,48 @@ +import type { TestDefinition } from "../test-definition"; + +export const concurrentRenameAndCreateAtTargetTest: TestDefinition = { + name: "Rename to path where another client creates a file", + description: + "One client renames X to Y while another creates a new file at Y, " + + "both offline. After syncing, Y should contain merged content from " + + "both the renamed file and the newly created file.", + clients: 2, + steps: [ + { + type: "create", + client: 0, + path: "X.md", + content: "original file X" + }, + { type: "enable-sync", client: 0 }, + { type: "enable-sync", client: 1 }, + { type: "barrier" }, + + { type: "disable-sync", client: 0 }, + { type: "disable-sync", client: 1 }, + + { type: "rename", client: 0, oldPath: "X.md", newPath: "Y.md" }, + + { + type: "create", + client: 1, + path: "Y.md", + content: "brand new Y content" + }, + + { type: "enable-sync", client: 0 }, + { type: "sync", client: 0 }, + + { type: "enable-sync", client: 1 }, + { type: "barrier" }, + + { + type: "assert-consistent", + verify: (state) => { + state + .assertFileNotExists("X.md") + .assertContains("Y.md", "original file X", "brand new Y content"); + } + } + ] +}; diff --git a/frontend/deterministic-tests/src/tests/8-concurrent-rename-and-create-at-target.test.ts b/frontend/deterministic-tests/src/tests/8-concurrent-rename-and-create-at-target.test.ts new file mode 100644 index 00000000..4cd7c1d9 --- /dev/null +++ b/frontend/deterministic-tests/src/tests/8-concurrent-rename-and-create-at-target.test.ts @@ -0,0 +1,49 @@ +import type { TestDefinition } from "../test-definition"; + +export const concurrentRenameAndCreateAtTargetTest: TestDefinition = { + name: "Rename to path where another client creates a file", + description: + "One client renames X to Y while another creates a new file at Y, " + + "both offline. After syncing, Y should contain merged content from " + + "both the renamed file and the newly created file.", + clients: 2, + steps: [ + { + type: "create", + client: 0, + path: "X.md", + content: "original file X" + }, + { type: "enable-sync", client: 0 }, + { type: "enable-sync", client: 1 }, + { type: "barrier" }, + + { type: "disable-sync", client: 0 }, + { type: "disable-sync", client: 1 }, + + { type: "rename", client: 0, oldPath: "X.md", newPath: "Y.md" }, + + { + type: "create", + client: 1, + path: "Y.md", + content: "brand new Y content" + }, + + { type: "enable-sync", client: 1 }, + { type: "sync", client: 1 }, + + { type: "enable-sync", client: 0 }, + { type: "barrier" }, + + { + type: "assert-consistent", + verify: (state) => { + state + .assertFileCount(2) + .assertContains("Y (1).md", "original file X") + .assertContains("Y.md", "brand new Y content"); + } + } + ] +}; diff --git a/frontend/deterministic-tests/src/tests/9-concurrent-rename-same-target.test.ts b/frontend/deterministic-tests/src/tests/9-concurrent-rename-same-target.test.ts new file mode 100644 index 00000000..e0419a47 --- /dev/null +++ b/frontend/deterministic-tests/src/tests/9-concurrent-rename-same-target.test.ts @@ -0,0 +1,40 @@ +import type { TestDefinition } from "../test-definition"; + +export const concurrentRenameSameTargetTest: TestDefinition = { + name: "Two clients rename different files to the same target path", + description: + "One client renames A to C while the other renames B to C, both offline. " + + "After syncing, both file contents should be preserved via path deconfliction.", + clients: 2, + steps: [ + { 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: "barrier" }, + + { type: "disable-sync", client: 1 }, + + { type: "rename", client: 0, oldPath: "A.md", newPath: "C.md" }, + { type: "sync", client: 0 }, + + { type: "rename", client: 1, oldPath: "B.md", newPath: "C.md" }, + + { type: "enable-sync", client: 1 }, + { type: "sync", client: 1 }, + { type: "barrier" }, + + { + type: "assert-consistent", + verify: (state) => { + state + .assertFileCount(2) + .assertFileNotExists("A.md") + .assertFileNotExists("B.md") + .assertFileExists("C.md") + .assertFileExists("C (1).md") + .assertAnyFileContains("content-a", "content-b"); + } + } + ] +};