diff --git a/frontend/deterministic-tests/src/tests/binary-pending-create-not-displaced.test.ts b/frontend/deterministic-tests/src/tests/binary-pending-create-not-displaced.test.ts index 467c19f0..f2582964 100644 --- a/frontend/deterministic-tests/src/tests/binary-pending-create-not-displaced.test.ts +++ b/frontend/deterministic-tests/src/tests/binary-pending-create-not-displaced.test.ts @@ -7,6 +7,12 @@ export const binaryPendingCreateNotDisplacedTest: TestDefinition = { "After syncing, both files should exist on both clients at separate paths.", clients: 2, steps: [ + // Bootstrap both empty clients before making independent documents. + { type: "enable-sync", client: 0 }, + { type: "enable-sync", client: 1 }, + { type: "barrier" }, + { type: "disable-sync", client: 0 }, + { type: "disable-sync", client: 1 }, { type: "create", client: 0, @@ -29,7 +35,7 @@ export const binaryPendingCreateNotDisplacedTest: TestDefinition = { verify: (s: AssertableState): void => { s.assertFileCount(2) .assertFileExists("data.bin") - .assertFileExists("data (1).bin") + .assertFileExists(s.conflictPath("data.bin")) .assertAnyFileContains( "binary data from client 0", "binary data from client 1" diff --git a/frontend/deterministic-tests/src/tests/concurrent-rename-and-create-at-target-create-first.test.ts b/frontend/deterministic-tests/src/tests/concurrent-rename-and-create-at-target-create-first.test.ts index cd8046ce..d76d4053 100644 --- a/frontend/deterministic-tests/src/tests/concurrent-rename-and-create-at-target-create-first.test.ts +++ b/frontend/deterministic-tests/src/tests/concurrent-rename-and-create-at-target-create-first.test.ts @@ -41,7 +41,7 @@ export const concurrentRenameAndCreateAtTargetCreateFirstTest: TestDefinition = verify: (state: AssertableState): void => { state .assertFileCount(2) - .assertContains("Y (1).md", "original file X") + .assertContains(state.conflictPath("Y.md"), "original file X") .assertContains("Y.md", "brand new Y content"); } } diff --git a/frontend/deterministic-tests/src/tests/concurrent-rename-and-create-at-target-rename-first.test.ts b/frontend/deterministic-tests/src/tests/concurrent-rename-and-create-at-target-rename-first.test.ts index 0ac0b721..3ae30504 100644 --- a/frontend/deterministic-tests/src/tests/concurrent-rename-and-create-at-target-rename-first.test.ts +++ b/frontend/deterministic-tests/src/tests/concurrent-rename-and-create-at-target-rename-first.test.ts @@ -41,7 +41,7 @@ export const concurrentRenameAndCreateAtTargetRenameFirstTest: TestDefinition = state .assertFileNotExists("X.md") .assertFileExists("Y.md") - .assertFileExists("Y (1).md") + .assertFileExists(state.conflictPath("Y.md")) .assertAnyFileContains( "original file X", "brand new Y content" diff --git a/frontend/deterministic-tests/src/tests/concurrent-rename-first-wins.test.ts b/frontend/deterministic-tests/src/tests/concurrent-rename-first-wins.test.ts index 5337649d..44f3e0a1 100644 --- a/frontend/deterministic-tests/src/tests/concurrent-rename-first-wins.test.ts +++ b/frontend/deterministic-tests/src/tests/concurrent-rename-first-wins.test.ts @@ -52,9 +52,9 @@ export const concurrentRenameFirstWinsTest: TestDefinition = { type: "assert-consistent", verify: (s: AssertableState): void => { s.assertFileNotExists("A.md") - .assertFileCount(2) - .assertContent("B.md", "edit from 0\nline 2\nline 3") - .assertContent("C.md", "line 1\nline 2\nedit from 1"); + .assertFileCount(1) + .assertFileNotExists("C.md") + .assertContent("B.md", "edit from 0\nline 2\nedit from 1"); } } ] diff --git a/frontend/deterministic-tests/src/tests/concurrent-rename-same-target.test.ts b/frontend/deterministic-tests/src/tests/concurrent-rename-same-target.test.ts index 0b72c0f3..dcd8200d 100644 --- a/frontend/deterministic-tests/src/tests/concurrent-rename-same-target.test.ts +++ b/frontend/deterministic-tests/src/tests/concurrent-rename-same-target.test.ts @@ -31,7 +31,7 @@ export const concurrentRenameSameTargetTest: TestDefinition = { .assertFileNotExists("A.md") .assertFileNotExists("B.md") .assertFileExists("C.md") - .assertFileExists("C (1).md") + .assertFileExists(state.conflictPath("C.md")) .assertAnyFileContains("content-a", "content-b"); } } diff --git a/frontend/deterministic-tests/src/tests/delete-recreate-different-content.test.ts b/frontend/deterministic-tests/src/tests/delete-recreate-different-content.test.ts index 782c3cd5..f2ec68f5 100644 --- a/frontend/deterministic-tests/src/tests/delete-recreate-different-content.test.ts +++ b/frontend/deterministic-tests/src/tests/delete-recreate-different-content.test.ts @@ -4,7 +4,7 @@ import type { TestDefinition } from "../test-definition"; export const deleteRecreateDifferentContentTest: TestDefinition = { description: "Client 0 deletes and recreates A.md with new content offline while client 1 edits A.md offline. " + - "Both clients should converge with content from both sides merged.", + "Recreation gets a new UUID; an update to the deleted UUID must not edit the replacement.", clients: 2, steps: [ { @@ -43,11 +43,7 @@ export const deleteRecreateDifferentContentTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContains( - "A.md", - "brand new", - "client 1" - ); + s.assertFileCount(1).assertContent("A.md", "brand new content"); } } ] diff --git a/frontend/deterministic-tests/src/tests/offline-delete-remote-rename.test.ts b/frontend/deterministic-tests/src/tests/offline-delete-remote-rename.test.ts index 1e9ea8f7..7783daee 100644 --- a/frontend/deterministic-tests/src/tests/offline-delete-remote-rename.test.ts +++ b/frontend/deterministic-tests/src/tests/offline-delete-remote-rename.test.ts @@ -29,9 +29,8 @@ export const offlineDeleteRemoteRenameTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileNotExists("A.md").assertFileNotExists( - "A_renamed.md" - ); + // Both membership/path changed from the base: remote wins. + s.assertFileCount(1).assertFileNotExists("A.md").assertContent("A_renamed.md", "content-a"); } } ] diff --git a/frontend/deterministic-tests/src/tests/online-both-create-same-path-deconflict.test.ts b/frontend/deterministic-tests/src/tests/online-both-create-same-path-deconflict.test.ts index b951b0be..3e2ebf51 100644 --- a/frontend/deterministic-tests/src/tests/online-both-create-same-path-deconflict.test.ts +++ b/frontend/deterministic-tests/src/tests/online-both-create-same-path-deconflict.test.ts @@ -26,8 +26,8 @@ export const onlineBothCreateSamePathDeconflictTest: TestDefinition = { type: "assert-consistent", verify: (state: AssertableState): void => { state - .assertFileCount(1) - .assertContains("A.md", "updated-by-0", "from-client-1 "); + .assertFileCount(2) + .assertAnyFileContains("updated-by-0", "from-client-1 "); } } ] diff --git a/frontend/deterministic-tests/src/tests/rename-circular.test.ts b/frontend/deterministic-tests/src/tests/rename-circular.test.ts index 44a65149..c2168f59 100644 --- a/frontend/deterministic-tests/src/tests/rename-circular.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-circular.test.ts @@ -3,7 +3,7 @@ import type { TestDefinition } from "../test-definition"; export const renameCircularTest: TestDefinition = { description: - "Client 0 creates three files, syncs, then goes offline and performs a circular rename via a temp file (A->temp, C->A, B->C, temp->B). After reconnecting, all three contents should exist across three files but paths may be deconflicted.", + "Client 0 creates three files, syncs, then goes offline and performs a circular rename via a temp file (A->temp, C->A, B->C, temp->B). After reconnecting, all three UUIDs and exact contents must follow the requested cycle.", clients: 2, steps: [ { type: "create", client: 0, path: "A.md", content: "content-a" }, @@ -21,6 +21,9 @@ export const renameCircularTest: TestDefinition = { } }, + { type: "remember-identity", key: "A", path: "A.md" }, + { type: "remember-identity", key: "B", path: "B.md" }, + { type: "remember-identity", key: "C", path: "C.md" }, { type: "disable-sync", client: 0 }, { type: "rename", client: 0, oldPath: "A.md", newPath: "temp-a.md" }, { type: "rename", client: 0, oldPath: "C.md", newPath: "A.md" }, @@ -29,15 +32,18 @@ export const renameCircularTest: TestDefinition = { { type: "enable-sync", client: 0 }, { type: "barrier" }, + { type: "assert-identity", key: "A", path: "B.md" }, + { type: "assert-identity", key: "B", path: "C.md" }, + { type: "assert-identity", key: "C", path: "A.md" }, { type: "assert-consistent", verify: (s: AssertableState): void => { s.assertFileNotExists("temp-a.md") .assertFileCount(3) - .assertAnyFileContains("content-c") - .assertAnyFileContains("content-a") - .assertAnyFileContains("content-b"); + .assertContent("A.md", "content-c") + .assertContent("B.md", "content-a") + .assertContent("C.md", "content-b"); } } ] diff --git a/frontend/deterministic-tests/src/tests/rename-create-conflict.test.ts b/frontend/deterministic-tests/src/tests/rename-create-conflict.test.ts index fc6a00a7..cee99580 100644 --- a/frontend/deterministic-tests/src/tests/rename-create-conflict.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-create-conflict.test.ts @@ -27,7 +27,7 @@ export const renameCreateConflictTest: TestDefinition = { verify: (s: AssertableState): void => { s.assertFileCount(2) .assertContent("B.md", "hi") - .assertContent("B (1).md", "hi"); + .assertContent(s.conflictPath("B.md"), "hi"); } } ] diff --git a/frontend/deterministic-tests/src/tests/rename-swap.test.ts b/frontend/deterministic-tests/src/tests/rename-swap.test.ts index 9910e8ef..4298d2bc 100644 --- a/frontend/deterministic-tests/src/tests/rename-swap.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-swap.test.ts @@ -5,7 +5,7 @@ export const renameSwapTest: TestDefinition = { description: "Client 0 has A.md and B.md synced. Goes offline and swaps them using " + "a temp file: A.md -> temp.md, B.md -> A.md, temp.md -> B.md. " + - "When Client 0 reconnects, both contents should exist across two files.", + "When Client 0 reconnects, both UUIDs must follow their files to the swapped paths.", clients: 2, steps: [ { type: "create", client: 0, path: "A.md", content: "content-a" }, @@ -23,6 +23,8 @@ export const renameSwapTest: TestDefinition = { } }, + { type: "remember-identity", key: "A", path: "A.md" }, + { type: "remember-identity", key: "B", path: "B.md" }, { type: "disable-sync", client: 0 }, { type: "rename", client: 0, oldPath: "A.md", newPath: "temp.md" }, { type: "rename", client: 0, oldPath: "B.md", newPath: "A.md" }, @@ -30,14 +32,16 @@ export const renameSwapTest: TestDefinition = { { type: "enable-sync", client: 0 }, { type: "barrier" }, + { type: "assert-identity", key: "A", path: "B.md" }, + { type: "assert-identity", key: "B", path: "A.md" }, { type: "assert-consistent", verify: (s: AssertableState): void => { s.assertFileNotExists("temp.md") .assertFileCount(2) - .assertAnyFileContains("content-b") - .assertAnyFileContains("content-a"); + .assertContent("A.md", "content-b") + .assertContent("B.md", "content-a"); } } ] diff --git a/frontend/deterministic-tests/src/tests/same-doc-id-collapse-on-local-create-after-remote-create.test.ts b/frontend/deterministic-tests/src/tests/same-doc-id-collapse-on-local-create-after-remote-create.test.ts index dee3a9ad..cb49d530 100644 --- a/frontend/deterministic-tests/src/tests/same-doc-id-collapse-on-local-create-after-remote-create.test.ts +++ b/frontend/deterministic-tests/src/tests/same-doc-id-collapse-on-local-create-after-remote-create.test.ts @@ -1,121 +1,50 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; +// Historical registry name retained so old reproduction commands still work. export const sameDocIdCollapseOnLocalCreateAfterRemoteCreateTest: TestDefinition = { description: - "Client B creates X with content C2; the server commits and " + - "broadcasts. Client A's WS is paused so the RemoteCreate buffers. " + - "Server is then paused so A's about-to-POST LocalCreate will " + - "hang. A creates X with content C1: file lands on disk, " + - "LocalCreate enqueues, drain starts the POST, the POST stalls " + - "at the paused server. A's WS is resumed: the buffered " + - "RemoteCreate for doc-X is delivered to A and enqueues behind " + - "the in-flight LocalCreate. Per the lazy-paths model, when " + - "the RemoteCreate is processed it observes that path X is " + - "occupied locally by A's pending-create bytes, so it tracks " + - "doc-X with `localPath = undefined` / `remoteRelativePath = " + - "X` and does NOT fetch content. The server is then resumed: " + - "A's LocalCreate POST returns. The server, finding X already " + - "taken by doc-X, replies with doc-X's existing documentId " + - "(typically a MergingUpdate carrying the merged bytes). A's " + - "processCreate handler detects that response.documentId " + - "matches the no-localPath record built from the RemoteCreate " + - "and collapses the two: it sets localPath = X on that " + - "record, writes the merged bytes, and resolves the pending " + - "create promise. Final state: exactly one file at X on both " + - "clients, both pointing at doc-X's documentId, content " + - "carrying both contributions, and no conflict-- " + - "stash anywhere.", + "Two initialized clients create distinct UUIDs at X.md while remote observation is held. V4 must preserve both identities/content after catchup; the server never merges creates by path.", clients: 2, steps: [ { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, { type: "barrier" }, - - // Buffer broadcasts to client 0 (A) so client 1's create - // doesn't reach A's WS handler until we say so. - { type: "pause-websocket", client: 0 }, - - // Client 1 (B) commits doc-X at path X with content C2. - // The server commits, broadcasts (broadcast queued at A's - // paused WS). + // Freeze HTTP catchup as well as WS. Pausing only WS is insufficient. + { type: "pause-observation", client: 0 }, { type: "create", client: 1, path: "X.md", - content: "from-client-1 " + content: "from-client-1" }, { type: "sync", client: 1 }, - - // Pause the server so A's upcoming LocalCreate POST hangs. - // This holds A's drain on the in-flight POST while we - // release the WS so the RemoteCreate enqueues behind it. { type: "pause-server" }, - - // Client 0 (A) creates X locally with content C1. The - // file lands on A's disk; LocalCreate enqueues; drain - // starts the POST; POST stalls at the paused server. { type: "create", client: 0, path: "X.md", - content: "from-client-0 " + content: "from-client-0" }, - - // Release A's WS. The buffered RemoteCreate for doc-X is - // delivered to A and enqueues behind the in-flight - // LocalCreate. Whichever of (RemoteCreate processed first - // → no-localPath record, then LocalCreate POST returns - // with merging response that collapses) or (LocalCreate - // POST returns first with merging response that creates - // the canonical record, then RemoteCreate finds the doc - // already tracked by id and no-ops) actually plays out - // depends on the fine-grained interleaving the runtime - // produces, but both paths are required to converge to - // the same single-record same-docId state. - { type: "resume-websocket", client: 0 }, - - // Resume the server: A's LocalCreate POST completes. - // Server returns doc-X's existing documentId (MergingUpdate - // with merged content). processCreate runs the collapse - // path. + { type: "wait-for-observation", client: 0 }, + { type: "resume-observation", client: 0 }, { type: "resume-server" }, - { type: "barrier" }, - { type: "assert-consistent", - verify: (state: AssertableState): void => { - state.assertFileCount(1); - state.assertFileExists("X.md"); - // Server-side merge of the two text creates must - // carry both contributions through to the - // converged file. - state.assertContains( - "X.md", - "from-client-0", - "from-client-1" + verify: (state) => { + state + .assertFileCount(2) + .assertContent("X.md", "from-client-1"); + state.assertContent( + state.conflictPath("X.md"), + "from-client-0" ); - // The lazy-paths collapse path must not leave a - // conflict-- stash on either client. - for (const path of state.files.keys()) { - if (path.startsWith("conflict-")) { - throw new Error( - `Unexpected conflict-uuid stash on a converged client: ${path}` - ); - } - } - for (const perClient of state.clientFiles) { - for (const path of perClient.keys()) { - if (path.startsWith("conflict-")) { - throw new Error( - `Unexpected conflict-uuid stash on a per-client view: ${path}` - ); - } - } - } } + }, + { + type: "assert-markers", + markers: ["from-client-0", "from-client-1"] } ] }; diff --git a/frontend/deterministic-tests/src/tests/update-does-not-survive-remote-delete.test.ts b/frontend/deterministic-tests/src/tests/update-does-not-survive-remote-delete.test.ts index 70a2fc8c..e783cc4a 100644 --- a/frontend/deterministic-tests/src/tests/update-does-not-survive-remote-delete.test.ts +++ b/frontend/deterministic-tests/src/tests/update-does-not-survive-remote-delete.test.ts @@ -1,7 +1,7 @@ import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; -export const updateDoesNotSurvivesRemoteDeleteTest: TestDefinition = { +export const updateDoesNotSurviveRemoteDeleteTest: TestDefinition = { description: "Client 0 deletes a file while client 1 edits it offline. Client 0 syncs the delete first, then client 1 reconnects. Deletes always win.", clients: 2, diff --git a/frontend/deterministic-tests/src/tests/user-parenthesized-file-not-deleted.test.ts b/frontend/deterministic-tests/src/tests/user-parenthesized-file-not-deleted.test.ts index ef6cd771..58bebb44 100644 --- a/frontend/deterministic-tests/src/tests/user-parenthesized-file-not-deleted.test.ts +++ b/frontend/deterministic-tests/src/tests/user-parenthesized-file-not-deleted.test.ts @@ -8,6 +8,9 @@ export const userParenthesizedFileNotDeletedTest: TestDefinition = { clients: 2, steps: [ { type: "enable-sync", client: 0 }, + { type: "enable-sync", client: 1 }, + { type: "barrier" }, + { type: "disable-sync", client: 1 }, { type: "create", @@ -40,7 +43,8 @@ export const userParenthesizedFileNotDeletedTest: TestDefinition = { .assertFileCount(3) .assertFileExists("Chapter.bin") .assertFileExists("Chapter (1).bin") - .assertFileExists("Chapter (2).bin"); + .assertContent("Chapter (1).bin", "chapter one notes") + .assertContent(state.conflictPath("Chapter.bin"), "chapter one notes"); } } ] diff --git a/frontend/deterministic-tests/src/tests/watermark-advances-on-skip.test.ts b/frontend/deterministic-tests/src/tests/watermark-advances-on-skip.test.ts index 063faff4..9615b5aa 100644 --- a/frontend/deterministic-tests/src/tests/watermark-advances-on-skip.test.ts +++ b/frontend/deterministic-tests/src/tests/watermark-advances-on-skip.test.ts @@ -28,7 +28,7 @@ export const watermarkAdvancesOnSkipTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileCount(1).assertFileExists("doc.md"); + s.assertFileCount(2).assertFileExists("doc.md").assertAnyFileContains("from client 0", "from client 1"); } } ]