From cc44b66fcd7c1527ef14abf493c86e3db5c5f2bc Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Mon, 27 Apr 2026 22:46:14 +0100 Subject: [PATCH] More tests --- .../deterministic-tests/src/test-registry.ts | 7 +- ...ocal-update-survives-remote-rename.test.ts | 72 +++++++++++++++++++ .../move-remote-update-reverts-rename.test.ts | 8 +-- ...mote-update-resurrects-deleted-doc.test.ts | 59 +++++++++++++++ 4 files changed, 138 insertions(+), 8 deletions(-) create mode 100644 frontend/deterministic-tests/src/tests/local-update-survives-remote-rename.test.ts create mode 100644 frontend/deterministic-tests/src/tests/remote-update-resurrects-deleted-doc.test.ts diff --git a/frontend/deterministic-tests/src/test-registry.ts b/frontend/deterministic-tests/src/test-registry.ts index 1d004cda..f8bfd220 100644 --- a/frontend/deterministic-tests/src/test-registry.ts +++ b/frontend/deterministic-tests/src/test-registry.ts @@ -89,6 +89,8 @@ import { serverPauseDeleteRecreateTest } from "./tests/server-pause-delete-recre import { onlineBothCreateSamePathDeconflictTest } from "./tests/online-both-create-same-path-deconflict.test"; import { onlineCreateUpdateWhileOtherCreatesSamePathTest } from "./tests/online-create-update-while-other-creates-same-path.test"; import { displacedFileNotMarkedDeletedTest } from "./tests/displaced-file-not-marked-deleted.test"; +import { remoteUpdateResurrectsDeletedDocTest } from "./tests/remote-update-resurrects-deleted-doc.test"; +import { localUpdateSurvivesRemoteRenameTest } from "./tests/local-update-survives-remote-rename.test"; export const TESTS: Partial> = { "rename-create-conflict": renameCreateConflictTest, @@ -198,5 +200,8 @@ export const TESTS: Partial> = { onlineBothCreateSamePathDeconflictTest, "online-create-update-while-other-creates-same-path": onlineCreateUpdateWhileOtherCreatesSamePathTest, - "displaced-file-not-marked-deleted": displacedFileNotMarkedDeletedTest + "displaced-file-not-marked-deleted": displacedFileNotMarkedDeletedTest, + "remote-update-resurrects-deleted-doc": remoteUpdateResurrectsDeletedDocTest, + "local-update-survives-remote-rename": + localUpdateSurvivesRemoteRenameTest }; diff --git a/frontend/deterministic-tests/src/tests/local-update-survives-remote-rename.test.ts b/frontend/deterministic-tests/src/tests/local-update-survives-remote-rename.test.ts new file mode 100644 index 00000000..6448a1b9 --- /dev/null +++ b/frontend/deterministic-tests/src/tests/local-update-survives-remote-rename.test.ts @@ -0,0 +1,72 @@ +import type { AssertableState } from "../utils/assertable-state"; +import type { TestDefinition } from "../test-definition"; + +export const localUpdateSurvivesRemoteRenameTest: TestDefinition = { + description: + "Client 0 has a local content edit pending while a remote rename for " + + "the same doc arrives over the WebSocket. The remote rename's internal " + + "move relocates the disk file from the old path (where the user wrote) " + + "to the new server path. Previously, the queued LocalUpdate's " + + "`event.path` was left pointing at the now-vacated old path, so " + + "`skipIfOversized`'s `getFileSize(event.path)` threw " + + "`FileNotFoundError`, which `processEvent`'s catch silently swallowed " + + "as 'Skipping sync event 'local-update' because the file no longer " + + "exists' — and the user's edit was lost. The fix routes the size " + + "check through `tracked.path` (the doc's current disk path), " + + "matching the path `processLocalUpdate` itself reads from.", + clients: 2, + steps: [ + { type: "create", client: 0, path: "doc.md", content: "v1\n" }, + { type: "enable-sync", client: 0 }, + { type: "enable-sync", client: 1 }, + { type: "barrier" }, + + // Pause client 0's WebSocket so the upcoming remote rename buffers + // there until we've already enqueued client 0's local content + // edit. This guarantees the LocalUpdate sits in client 0's queue + // when the rename's RemoteChange drains. + { type: "pause-websocket", client: 0 }, + + { + type: "rename", + client: 1, + oldPath: "doc.md", + newPath: "renamed.md" + }, + { type: "sync", client: 1 }, + + // Client 0 still believes the file is at `doc.md` (its WebSocket is + // paused, so the rename hasn't reached it). The user edits content + // at `doc.md`. This pushes a LocalUpdate(D, path=doc.md, + // originalPath=doc.md, isUserRename=false) into client 0's queue. + { + type: "update", + client: 0, + path: "doc.md", + content: "v1\nclient 0 edit\n" + }, + + // Resume the WebSocket. The buffered remote rename (server-broadcast) + // drains. `processRemoteUpdate` does an internal `move(doc.md, + // renamed.md)` and, because there's a pending LocalUpdate for D, + // takes the else branch (re-enqueue v_K, setDocument(renamed.md, …)). + // Then drain reaches the LocalUpdate. Pre-fix: skipped silently. + // Post-fix: PUTs the user's content to the doc (at its new path, + // since this is a content-only edit, not a user rename). + { type: "resume-websocket", client: 0 }, + + { type: "barrier" }, + + { + type: "assert-consistent", + verify: (state: AssertableState): void => { + state.assertFileCount(1); + state.assertFileExists("renamed.md"); + state.assertContent( + "renamed.md", + "v1\nclient 0 edit\n" + ); + } + } + ] +}; diff --git a/frontend/deterministic-tests/src/tests/move-remote-update-reverts-rename.test.ts b/frontend/deterministic-tests/src/tests/move-remote-update-reverts-rename.test.ts index 4fe6f9cb..9fc3a9bf 100644 --- a/frontend/deterministic-tests/src/tests/move-remote-update-reverts-rename.test.ts +++ b/frontend/deterministic-tests/src/tests/move-remote-update-reverts-rename.test.ts @@ -28,13 +28,7 @@ export const moveRemoteUpdateRevertsRenameTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileCount(1); - const [content] = Array.from(s.files.values()); - if (content !== "updated by client 1") { - throw new Error( - `Expected "updated by client 1", got: "${content}"` - ); - } + s.assertFileCount(1).assertContent("renamed.md", "updated by client 1"); } } ] diff --git a/frontend/deterministic-tests/src/tests/remote-update-resurrects-deleted-doc.test.ts b/frontend/deterministic-tests/src/tests/remote-update-resurrects-deleted-doc.test.ts new file mode 100644 index 00000000..eb2ed86d --- /dev/null +++ b/frontend/deterministic-tests/src/tests/remote-update-resurrects-deleted-doc.test.ts @@ -0,0 +1,59 @@ +import type { AssertableState } from "../utils/assertable-state"; +import type { TestDefinition } from "../test-definition"; + +export const remoteUpdateResurrectsDeletedDocTest: TestDefinition = { + description: + "Client 1 updates, deletes, and recreates P (with a new docId D2). " + + "While the buffered remote events are being processed by client 0, " + + "client 0 also makes a local edit to P. The local edit lands in the " + + "queue while v17 is mid-process, sending v17 down processRemoteUpdate's " + + "re-enqueue branch. The deferred v17 must NOT later resurrect D1 as a " + + "conflict-… file at P after the delete and the D2 create have drained.", + clients: 2, + steps: [ + { type: "enable-sync", client: 0 }, + { type: "enable-sync", client: 1 }, + + { type: "create", client: 1, path: "P.md", content: "v8 content\n" }, + { type: "barrier" }, + + { type: "pause-websocket", client: 0 }, + + { + type: "update", + client: 1, + path: "P.md", + content: "v17 content from client 1\n" + }, + { type: "sync", client: 1 }, + { type: "delete", client: 1, path: "P.md" }, + { type: "sync", client: 1 }, + { + type: "create", + client: 1, + path: "P.md", + content: "v21 content (D2)\n" + }, + { type: "sync", client: 1 }, + + { type: "resume-websocket", client: 0 }, + + { + type: "update", + client: 0, + path: "P.md", + content: "local edit by client 0\n" + }, + + { type: "barrier" }, + + { + type: "assert-consistent", + verify: (state: AssertableState): void => { + state + .assertFileCount(1) + .assertContent("P.md", "v21 content (D2)\n"); + } + } + ] +};