From 77521c970b4947af124fe65dd288301343550155 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 27 Sep 2026 21:51:01 +0100 Subject: [PATCH] Better det tests --- ...inary-pending-create-not-displaced.test.ts | 7 +- .../tests/binary-to-text-transition.test.ts | 11 +-- ...sce-update-remote-update-data-loss.test.ts | 14 ++- ...oncurrent-edit-exact-same-position.test.ts | 6 +- ...-and-create-at-target-create-first.test.ts | 81 ++++++++-------- ...-and-create-at-target-rename-first.test.ts | 83 ++++++++--------- .../concurrent-rename-same-target.test.ts | 12 +-- .../src/tests/create-delete-noop.test.ts | 2 +- .../src/tests/create-merge-delete.test.ts | 12 ++- ...ate-merge-preserves-renamed-update.test.ts | 10 +- .../create-rename-response-skips-file.test.ts | 3 +- ...lete-by-other-client-then-recreate.test.ts | 2 +- .../delete-during-pending-create.test.ts | 9 +- .../delete-recreate-concurrent-update.test.ts | 3 +- ...-create-with-stale-deleting-record.test.ts | 29 ++++-- .../src/tests/delete-rename-conflict.test.ts | 8 +- .../idempotency-after-server-pause.test.ts | 8 +- .../tests/interrupted-delete-retry.test.ts | 7 +- ...ocal-edit-lost-during-create-merge.test.ts | 8 +- ...ocal-rename-survives-remote-rename.test.ts | 72 ++++---------- ...ocal-update-survives-remote-rename.test.ts | 32 +------ ...mc-cross-create-rename-same-target.test.ts | 9 +- .../mc-delete-then-offline-rename.test.ts | 8 +- .../mc-multi-delete-offline-rename.test.ts | 12 +-- ...three-client-rename-offline-update.test.ts | 5 +- ...date-response-survives-user-rename.test.ts | 81 +++++----------- .../move-and-concurrent-remote-update.test.ts | 5 +- .../move-preserves-remote-update.test.ts | 11 +-- .../src/tests/multi-file-operations.test.ts | 9 +- .../tests/offline-concurrent-renames.test.ts | 11 +-- ...offline-create-same-path-mergeable.test.ts | 14 +-- .../tests/offline-edit-remote-rename.test.ts | 5 +- ...line-rename-remote-create-old-path.test.ts | 6 +- ...e-both-create-same-path-deconflict.test.ts | 14 +-- ...date-while-other-creates-same-path.test.ts | 19 ++-- .../overlapping-edits-same-section.test.ts | 8 +- ...e-reset-loses-coalesced-local-edit.test.ts | 8 +- ...delete-does-not-hijack-reused-path.test.ts | 6 +- .../rapid-create-update-delete-cycle.test.ts | 9 +- ...pid-edit-delete-online-convergence.test.ts | 19 ++-- .../tests/rapid-updates-after-merge.test.ts | 3 +- ...collides-with-pending-local-create.test.ts | 6 +- ...mote-update-resurrects-deleted-doc.test.ts | 39 +++++--- ...remote-update-survives-user-rename.test.ts | 84 ++++------------- ...rename-chain-during-pending-create.test.ts | 29 +----- ...rwrites-pending-create-then-delete.test.ts | 5 +- ...ame-pending-create-before-response.test.ts | 7 +- ...ng-create-onto-pending-delete-path.test.ts | 6 +- ...name-to-path-of-unconfirmed-delete.test.ts | 6 +- .../rename-to-pending-path-fallback.test.ts | 6 +- .../src/tests/rename-update-conflict.test.ts | 3 +- ...ing-create-reused-path-then-delete.test.ts | 12 +-- ...ears-recently-deleted-resurrection.test.ts | 2 +- ...ote-quick-write-and-pending-rename.test.ts | 20 ++-- ...nding-rename-aliases-second-create.test.ts | 93 ++----------------- .../server-pause-both-clients-create.test.ts | 7 +- .../server-pause-both-edit-same-file.test.ts | 7 +- .../text-pending-create-not-displaced.test.ts | 8 +- .../three-client-rename-create-delete.test.ts | 6 +- .../update-during-create-processing.test.ts | 7 +- ...ser-parenthesized-file-not-deleted.test.ts | 7 +- .../tests/watermark-advances-on-skip.test.ts | 4 +- 62 files changed, 397 insertions(+), 648 deletions(-) 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 f2582964..c5fed404 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 @@ -34,10 +34,9 @@ export const binaryPendingCreateNotDisplacedTest: TestDefinition = { type: "assert-consistent", verify: (s: AssertableState): void => { s.assertFileCount(2) - .assertFileExists("data.bin") - .assertFileExists(s.conflictPath("data.bin")) - .assertAnyFileContains( - "binary data from client 0", + .assertContent("data.bin", "binary data from client 0") + .assertContent( + s.conflictPath("data.bin"), "binary data from client 1" ); } diff --git a/frontend/deterministic-tests/src/tests/binary-to-text-transition.test.ts b/frontend/deterministic-tests/src/tests/binary-to-text-transition.test.ts index 8b934c1b..fc0cf3f9 100644 --- a/frontend/deterministic-tests/src/tests/binary-to-text-transition.test.ts +++ b/frontend/deterministic-tests/src/tests/binary-to-text-transition.test.ts @@ -38,11 +38,7 @@ export const binaryToTextTransitionTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContainsAny( - "data.bin", - "version A", - "version B" - ); + s.assertFileCount(1).assertContent("data.bin", "version A"); } }, @@ -90,7 +86,10 @@ export const binaryToTextTransitionTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContains("data.md", "alpha", "beta"); + s.assertFileCount(1).assertContent( + "data.md", + "alpha\nmiddle line\nbeta" + ); } } ] diff --git a/frontend/deterministic-tests/src/tests/coalesce-update-remote-update-data-loss.test.ts b/frontend/deterministic-tests/src/tests/coalesce-update-remote-update-data-loss.test.ts index 1972526a..5cc7e4df 100644 --- a/frontend/deterministic-tests/src/tests/coalesce-update-remote-update-data-loss.test.ts +++ b/frontend/deterministic-tests/src/tests/coalesce-update-remote-update-data-loss.test.ts @@ -45,14 +45,12 @@ export const coalesceUpdateRemoteUpdateDataLossTest: TestDefinition = { { type: "assert-consistent", - verify: (state: AssertableState): void => { - state - .assertFileCount(1) - .assertContains( - "doc.md", - "client 0 addition", - "client 1 addition" - ); + verify: (s: AssertableState): void => { + s.assertFileCount(1); + s.assertContent( + "doc.md", + "client 1 addition\nline 1\nline 2\nline 3\nclient 0 addition" + ); } } ] diff --git a/frontend/deterministic-tests/src/tests/concurrent-edit-exact-same-position.test.ts b/frontend/deterministic-tests/src/tests/concurrent-edit-exact-same-position.test.ts index 5c141a0e..39ca805f 100644 --- a/frontend/deterministic-tests/src/tests/concurrent-edit-exact-same-position.test.ts +++ b/frontend/deterministic-tests/src/tests/concurrent-edit-exact-same-position.test.ts @@ -42,7 +42,11 @@ export const concurrentEditExactSamePositionTest: TestDefinition = { verify: (state: AssertableState): void => { state .assertFileCount(1) - .assertContains("doc.md", "slow", "fast", "brown fox"); + .assertContentOneOf( + "doc.md", + "the fast slow brown fox", + "the slow fast brown fox" + ); } } ] 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 d76d4053..cc8d1163 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 @@ -1,49 +1,52 @@ import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; -export const concurrentRenameAndCreateAtTargetCreateFirstTest: TestDefinition = { - 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" }, +export const concurrentRenameAndCreateAtTargetCreateFirstTest: TestDefinition = + { + 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: "disable-sync", client: 0 }, + { type: "disable-sync", client: 1 }, - { type: "rename", client: 0, oldPath: "X.md", newPath: "Y.md" }, + { type: "rename", client: 0, oldPath: "X.md", newPath: "Y.md" }, - { - type: "create", - client: 1, - path: "Y.md", - content: "brand new Y content" - }, + { + 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: 1 }, + { type: "sync", client: 1 }, - { type: "enable-sync", client: 0 }, - { type: "barrier" }, + { type: "enable-sync", client: 0 }, + { type: "barrier" }, - { - type: "assert-consistent", - verify: (state: AssertableState): void => { - state - .assertFileCount(2) - .assertContains(state.conflictPath("Y.md"), "original file X") - .assertContains("Y.md", "brand new Y content"); + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertFileCount(2) + .assertContent("Y.md", "brand new Y content") + .assertContent( + s.conflictPath("Y.md"), + "original file X" + ); + } } - } - ] -}; + ] + }; 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 3ae30504..92180cfc 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 @@ -1,52 +1,51 @@ import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; -export const concurrentRenameAndCreateAtTargetRenameFirstTest: TestDefinition = { - description: - "One client renames X to Y while another creates a new file at Y, " + - "both offline. We can't merge the create because it would result in a cycle", - 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" }, +export const concurrentRenameAndCreateAtTargetRenameFirstTest: TestDefinition = + { + description: + "One client renames X to Y while another creates a new file at Y, " + + "both offline. We can't merge the create because it would result in a cycle", + 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: "disable-sync", client: 0 }, + { type: "disable-sync", client: 1 }, - { type: "rename", client: 0, oldPath: "X.md", newPath: "Y.md" }, + { type: "rename", client: 0, oldPath: "X.md", newPath: "Y.md" }, - { - type: "create", - client: 1, - path: "Y.md", - content: "brand new Y content" - }, + { + 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: 0 }, + { type: "sync", client: 0 }, - { type: "enable-sync", client: 1 }, - { type: "barrier" }, + { type: "enable-sync", client: 1 }, + { type: "barrier" }, - { - type: "assert-consistent", - verify: (state: AssertableState): void => { - state - .assertFileNotExists("X.md") - .assertFileExists("Y.md") - .assertFileExists(state.conflictPath("Y.md")) - .assertAnyFileContains( - "original file X", - "brand new Y content" - ); + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertFileCount(2) + .assertContent("Y.md", "original file X") + .assertContent( + s.conflictPath("Y.md"), + "brand new Y content" + ); + } } - } - ] -}; + ] + }; 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 dcd8200d..7b68dfd2 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 @@ -25,14 +25,10 @@ export const concurrentRenameSameTargetTest: TestDefinition = { { type: "assert-consistent", - verify: (state: AssertableState): void => { - state - .assertFileCount(2) - .assertFileNotExists("A.md") - .assertFileNotExists("B.md") - .assertFileExists("C.md") - .assertFileExists(state.conflictPath("C.md")) - .assertAnyFileContains("content-a", "content-b"); + verify: (s: AssertableState): void => { + s.assertFileCount(2) + .assertContent("C.md", "content-a") + .assertContent(s.conflictPath("C.md"), "content-b"); } } ] diff --git a/frontend/deterministic-tests/src/tests/create-delete-noop.test.ts b/frontend/deterministic-tests/src/tests/create-delete-noop.test.ts index 6c766001..a16969f9 100644 --- a/frontend/deterministic-tests/src/tests/create-delete-noop.test.ts +++ b/frontend/deterministic-tests/src/tests/create-delete-noop.test.ts @@ -20,7 +20,7 @@ export const createDeleteNoopTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileNotExists("temp.md"); + s.assertFileCount(0).assertFileNotExists("temp.md"); } } ] diff --git a/frontend/deterministic-tests/src/tests/create-merge-delete.test.ts b/frontend/deterministic-tests/src/tests/create-merge-delete.test.ts index ef7ea5c3..4553b7d6 100644 --- a/frontend/deterministic-tests/src/tests/create-merge-delete.test.ts +++ b/frontend/deterministic-tests/src/tests/create-merge-delete.test.ts @@ -17,10 +17,14 @@ export const createMergeDeleteTest: TestDefinition = { { type: "assert-consistent", - verify: (state: AssertableState): void => { - state - .assertFileCount(1) - .assertContains("A.md", "from-zero", "from-one"); + verify: (s: AssertableState): void => { + s.assertFileCount(1).assertContentOneOf( + "A.md", + "from-zerofrom-one", + "from-onefrom-zero", + "from-zero from-one", + "from-one from-zero" + ); } }, diff --git a/frontend/deterministic-tests/src/tests/create-merge-preserves-renamed-update.test.ts b/frontend/deterministic-tests/src/tests/create-merge-preserves-renamed-update.test.ts index a9bc37d4..3c54c6af 100644 --- a/frontend/deterministic-tests/src/tests/create-merge-preserves-renamed-update.test.ts +++ b/frontend/deterministic-tests/src/tests/create-merge-preserves-renamed-update.test.ts @@ -17,8 +17,14 @@ export const createMergePreservesRenamedUpdateTest: TestDefinition = { { type: "assert-consistent", - verify: (state: AssertableState): void => { - state.assertContains("doc.md", "alpha", "beta"); + verify: (s: AssertableState): void => { + s.assertFileCount(1).assertContentOneOf( + "doc.md", + "alphabeta", + "betaalpha", + "alpha beta", + "beta alpha" + ); } }, diff --git a/frontend/deterministic-tests/src/tests/create-rename-response-skips-file.test.ts b/frontend/deterministic-tests/src/tests/create-rename-response-skips-file.test.ts index aa24b110..ec90dd12 100644 --- a/frontend/deterministic-tests/src/tests/create-rename-response-skips-file.test.ts +++ b/frontend/deterministic-tests/src/tests/create-rename-response-skips-file.test.ts @@ -29,7 +29,8 @@ export const createRenameResponseSkipsFileTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileCount(1).assertAnyFileContains("the-content"); + s.assertFileCount(1); + s.assertContent("renamed.md", "the-content"); } } ] diff --git a/frontend/deterministic-tests/src/tests/delete-by-other-client-then-recreate.test.ts b/frontend/deterministic-tests/src/tests/delete-by-other-client-then-recreate.test.ts index dfef9961..0d4fa9ba 100644 --- a/frontend/deterministic-tests/src/tests/delete-by-other-client-then-recreate.test.ts +++ b/frontend/deterministic-tests/src/tests/delete-by-other-client-then-recreate.test.ts @@ -18,7 +18,7 @@ export const deleteByOtherClientThenRecreateTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileNotExists("A.md"); + s.assertFileCount(0).assertFileNotExists("A.md"); } }, diff --git a/frontend/deterministic-tests/src/tests/delete-during-pending-create.test.ts b/frontend/deterministic-tests/src/tests/delete-during-pending-create.test.ts index 3ba393b8..8768826c 100644 --- a/frontend/deterministic-tests/src/tests/delete-during-pending-create.test.ts +++ b/frontend/deterministic-tests/src/tests/delete-during-pending-create.test.ts @@ -3,15 +3,15 @@ import type { TestDefinition } from "../test-definition"; export const deleteDuringPendingCreateTest: TestDefinition = { description: - "Client 0 creates a file while the server is paused, then deletes it before the server resumes. " + - "After resume, the file should end up deleted on both clients.", + "Client 0 creates a file while its create request is held, then deletes it before releasing the request. " + + "After release, the file should end up deleted on both clients.", clients: 2, steps: [ { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, { type: "barrier" }, - { type: "pause-server" }, + { type: "hold-request", client: 0, kind: "create", point: "before" }, { type: "create", @@ -19,10 +19,11 @@ export const deleteDuringPendingCreateTest: TestDefinition = { path: "ephemeral.md", content: "this will be deleted" }, + { type: "wait-for-request", client: 0 }, { type: "delete", client: 0, path: "ephemeral.md" }, - { type: "resume-server" }, + { type: "release-request", client: 0 }, { type: "barrier" }, { diff --git a/frontend/deterministic-tests/src/tests/delete-recreate-concurrent-update.test.ts b/frontend/deterministic-tests/src/tests/delete-recreate-concurrent-update.test.ts index 6cb4cb98..57169798 100644 --- a/frontend/deterministic-tests/src/tests/delete-recreate-concurrent-update.test.ts +++ b/frontend/deterministic-tests/src/tests/delete-recreate-concurrent-update.test.ts @@ -35,7 +35,8 @@ export const deleteRecreateConcurrentUpdateTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileExists("A.md").assertContains("A.md", "recreated"); + s.assertFileCount(1); + s.assertContent("A.md", "recreated by client 0"); } } ] diff --git a/frontend/deterministic-tests/src/tests/delete-recreated-pending-create-with-stale-deleting-record.test.ts b/frontend/deterministic-tests/src/tests/delete-recreated-pending-create-with-stale-deleting-record.test.ts index 80e95f48..1a19edb2 100644 --- a/frontend/deterministic-tests/src/tests/delete-recreated-pending-create-with-stale-deleting-record.test.ts +++ b/frontend/deterministic-tests/src/tests/delete-recreated-pending-create-with-stale-deleting-record.test.ts @@ -4,10 +4,9 @@ import type { TestDefinition } from "../test-definition"; export const deleteRecreatedPendingCreateWithStaleDeletingRecordTest: TestDefinition = { description: - "A local delete for a recreated pending create must target the " + - "new pending create, not an older same-path record whose server " + - "delete has been acked but whose WebSocket delete receipt is " + - "still paused.", + "Delete two successive generations while each create request is " + + "held before send. Retrying and acknowledging the old generation " + + "must not resurrect either file, even with delayed WebSocket hints.", clients: 2, steps: [ { type: "enable-sync", client: 0 }, @@ -15,28 +14,38 @@ export const deleteRecreatedPendingCreateWithStaleDeletingRecordTest: TestDefini { type: "barrier" }, { type: "pause-websocket", client: 0 }, - { type: "pause-server" }, + { + type: "hold-request", + client: 0, + kind: "create", + point: "before" + }, { type: "create", client: 0, path: "binary-14.bin", content: "BINARY:first" }, - { type: "sleep", ms: 100 }, + { type: "wait-for-request", client: 0 }, { type: "delete", client: 0, path: "binary-14.bin" }, - { type: "resume-server" }, + { type: "release-request", client: 0 }, { type: "sync", client: 0 }, - { type: "pause-server" }, + { + type: "hold-request", + client: 0, + kind: "create", + point: "before" + }, { type: "create", client: 0, path: "binary-14.bin", content: "BINARY:second" }, - { type: "sleep", ms: 100 }, + { type: "wait-for-request", client: 0 }, { type: "delete", client: 0, path: "binary-14.bin" }, - { type: "resume-server" }, + { type: "release-request", client: 0 }, { type: "sync", client: 0 }, { type: "resume-websocket", client: 0 }, diff --git a/frontend/deterministic-tests/src/tests/delete-rename-conflict.test.ts b/frontend/deterministic-tests/src/tests/delete-rename-conflict.test.ts index 91e6289b..2b37989a 100644 --- a/frontend/deterministic-tests/src/tests/delete-rename-conflict.test.ts +++ b/frontend/deterministic-tests/src/tests/delete-rename-conflict.test.ts @@ -15,7 +15,8 @@ export const deleteRenameConflictTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileExists("A.md").assertFileExists("B.md"); + s.assertContent("A.md", "content-a"); + s.assertContent("B.md", "content-b"); } }, @@ -32,11 +33,8 @@ export const deleteRenameConflictTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { + s.assertFileCount(1); s.assertContent("B.md", "content-b"); - s.assertFileNotExists("A.md"); - s.ifFileExists("C.md", (inner) => - inner.assertContent("C.md", "content-a") - ); } } ] diff --git a/frontend/deterministic-tests/src/tests/idempotency-after-server-pause.test.ts b/frontend/deterministic-tests/src/tests/idempotency-after-server-pause.test.ts index 551c702d..325ad962 100644 --- a/frontend/deterministic-tests/src/tests/idempotency-after-server-pause.test.ts +++ b/frontend/deterministic-tests/src/tests/idempotency-after-server-pause.test.ts @@ -3,13 +3,13 @@ import type { TestDefinition } from "../test-definition"; export const idempotencyAfterServerPauseTest: TestDefinition = { description: - "Client 0 creates a file, then the server is paused mid-response. " + - "After the server resumes, both clients must converge to a single copy of the file with no duplicates.", + "Lose an accepted create response and require an identical retry, preserving one exact copy.", clients: 2, steps: [ { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, { type: "barrier" }, + { type: "drop-response", client: 0, kind: "create", point: "after" }, { type: "create", @@ -17,9 +17,7 @@ export const idempotencyAfterServerPauseTest: TestDefinition = { path: "doc.md", content: "important data" }, - { type: "pause-server" }, - - { type: "resume-server" }, + { type: "wait-for-response-drop", client: 0 }, { type: "barrier" }, diff --git a/frontend/deterministic-tests/src/tests/interrupted-delete-retry.test.ts b/frontend/deterministic-tests/src/tests/interrupted-delete-retry.test.ts index 3ae7eda5..4378c24b 100644 --- a/frontend/deterministic-tests/src/tests/interrupted-delete-retry.test.ts +++ b/frontend/deterministic-tests/src/tests/interrupted-delete-retry.test.ts @@ -3,20 +3,19 @@ import type { TestDefinition } from "../test-definition"; export const interruptedDeleteRetryTest: TestDefinition = { description: - "Client 0 deletes a file, then the server is paused. " + - "After the server resumes, both clients should have zero files.", + "Lose an accepted deletion manifest response and require an identical retry, with both clients empty.", clients: 2, steps: [ { type: "create", client: 0, path: "doc.md", content: "to be deleted" }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, { type: "barrier" }, + { type: "drop-response", client: 0, kind: "manifest", point: "after" }, { type: "delete", client: 0, path: "doc.md" }, - { type: "pause-server" }, + { type: "wait-for-response-drop", client: 0 }, - { type: "resume-server" }, { type: "barrier" }, { diff --git a/frontend/deterministic-tests/src/tests/local-edit-lost-during-create-merge.test.ts b/frontend/deterministic-tests/src/tests/local-edit-lost-during-create-merge.test.ts index 20925889..5d5e7588 100644 --- a/frontend/deterministic-tests/src/tests/local-edit-lost-during-create-merge.test.ts +++ b/frontend/deterministic-tests/src/tests/local-edit-lost-during-create-merge.test.ts @@ -30,10 +30,12 @@ export const localEditLostDuringCreateMergeTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContains( + s.assertFileCount(1).assertContentOneOf( "doc.md", - "from-client-1", - "local-edit-during-create" + "from-client-1local-edit-during-create", + "local-edit-during-createfrom-client-1", + "from-client-1 local-edit-during-create", + "local-edit-during-create from-client-1" ); } } diff --git a/frontend/deterministic-tests/src/tests/local-rename-survives-remote-rename.test.ts b/frontend/deterministic-tests/src/tests/local-rename-survives-remote-rename.test.ts index c2b80af3..7d1ca40b 100644 --- a/frontend/deterministic-tests/src/tests/local-rename-survives-remote-rename.test.ts +++ b/frontend/deterministic-tests/src/tests/local-rename-survives-remote-rename.test.ts @@ -1,21 +1,8 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const localRenameSurvivesRemoteRenameTest: TestDefinition = { description: - "Drain processes a RemoteChange (remote rename for doc D) while a " + - "LocalUpdate (user rename of D) is also queued behind it. " + - "`processRemoteUpdate` moves the disk file and, because there is a " + - "pending LocalUpdate, takes the else branch — but its setDocument " + - "uses the stale `record.path` (= the user-rename target) instead of " + - "the actualPath the file just moved to. The queued LocalUpdate then " + - "reads from `record.path`, throws FileNotFoundError, and is " + - "silently dropped. Setup pins the queue order: a sentinel " + - "LocalUpdate keeps drain busy on a SIGSTOPped HTTP roundtrip while " + - "we resume client 0's WebSocket (enqueues RemoteChange) and then " + - "user-rename D (enqueues LocalUpdate after the RemoteChange). On " + - "server resume the drain pops the sentinel, then RemoteChange, then " + - "LocalUpdate — exactly the order that triggers the bug.", + "A held remote manifest races with a local rename and a separate edit; the first committed rename wins and both identities survive.", clients: 2, steps: [ { type: "enable-sync", client: 0 }, @@ -23,58 +10,31 @@ export const localRenameSurvivesRemoteRenameTest: TestDefinition = { { type: "create", client: 0, path: "doc.md", content: "v1\n" }, { type: "create", client: 0, path: "sentinel.md", content: "s\n" }, { type: "barrier" }, - - // Pause client 0's WebSocket so the upcoming remote rename buffers. - { type: "pause-websocket", client: 0 }, - - // Server applies remote rename of doc.md -> remote.md. Broadcast - // is buffered on client 0's WebSocket. + { + type: "assert-documents", + expected: [ + { key: "note", path: "doc.md", content: "v1\n" }, + { key: "sentinel", path: "sentinel.md", content: "s\n" } + ] + }, + { type: "hold-request", client: 0, kind: "events", point: "after" }, { type: "rename", client: 1, oldPath: "doc.md", newPath: "remote.md" }, - { type: "sync", client: 1 }, - - // Pause the server BEFORE arming the sentinel, so the sentinel's - // HTTP request will buffer at the kernel and keep drain occupied. - { type: "pause-server" }, - - // Sentinel: a LocalUpdate on a *different* doc that drain pops - // first. Its HTTP roundtrip stalls on SIGSTOP, freezing drain - // until we resume the server. While drain is frozen we can grow - // the queue with additional events whose order we control. + { type: "wait-for-request", client: 0 }, { type: "update", client: 0, path: "sentinel.md", content: "s\nedit\n" }, - - // Resume the WebSocket — buffered remote rename enqueues as a - // RemoteChange. Drain is still stuck on the sentinel HTTP. - { type: "resume-websocket", client: 0 }, - - // User renames doc.md -> local.md on client 0. queue.enqueue - // mutates the doc's record.path to "local.md" and pushes a - // LocalUpdate(rename) onto the tail of the queue. Queue is now - // [sentinel-update (in-flight), RemoteChange, LocalUpdate-rename]. { type: "rename", client: 0, oldPath: "doc.md", newPath: "local.md" }, - - // Resume the server. Drain pops sentinel-update (succeeds), then - // RemoteChange. Pre-fix: processRemoteUpdate moves disk - // local.md -> remote.md, takes the else branch, and - // setDocument(record.path = "local.md", …) leaves record.path - // stale. Drain pops the LocalUpdate-rename and reads from the - // stale record.path, hits FileNotFoundError, silent skip. - // Post-fix: when a local event is pending, we re-queue the - // remote update without touching disk or record, so the local - // rename drains first and both ends converge. - { type: "resume-server" }, - + { type: "release-request", client: 0 }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (state: AssertableState): void => { - state.assertFileCount(2); - } + type: "assert-documents", + expected: [ + { key: "note", path: "remote.md", content: "v1\n" }, + { key: "sentinel", path: "sentinel.md", content: "s\nedit\n" } + ] } ] }; 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 index 0d8348c0..56afc341 100644 --- 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 @@ -3,17 +3,7 @@ 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.", + "Hold the event response carrying a remote rename, edit the old local path, then release catchup. Preserve the exact user edit at the renamed path.", clients: 2, steps: [ { type: "create", client: 0, path: "doc.md", content: "v1\n" }, @@ -21,11 +11,7 @@ export const localUpdateSurvivesRemoteRenameTest: TestDefinition = { { 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: "hold-request", client: 0, kind: "events", point: "after" }, { type: "rename", @@ -34,11 +20,8 @@ export const localUpdateSurvivesRemoteRenameTest: TestDefinition = { newPath: "renamed.md" }, { type: "sync", client: 1 }, + { type: "wait-for-request", client: 0 }, - // 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, @@ -46,14 +29,7 @@ export const localUpdateSurvivesRemoteRenameTest: TestDefinition = { 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: "release-request", client: 0 }, { type: "barrier" }, diff --git a/frontend/deterministic-tests/src/tests/mc-cross-create-rename-same-target.test.ts b/frontend/deterministic-tests/src/tests/mc-cross-create-rename-same-target.test.ts index d986a733..c6b56338 100644 --- a/frontend/deterministic-tests/src/tests/mc-cross-create-rename-same-target.test.ts +++ b/frontend/deterministic-tests/src/tests/mc-cross-create-rename-same-target.test.ts @@ -18,7 +18,8 @@ export const mcCrossCreateRenameSameTargetTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileExists("X.md").assertFileExists("Y.md"); + s.assertContent("X.md", "content-x"); + s.assertContent("Y.md", "content-y"); } }, @@ -36,10 +37,8 @@ export const mcCrossCreateRenameSameTargetTest: TestDefinition = { type: "assert-consistent", verify: (s: AssertableState): void => { s.assertFileCount(2) - .assertFileNotExists("X.md") - .assertFileNotExists("Y.md") - .assertFileExists("Z.md") - .assertAnyFileContains("content-x", "content-y"); + .assertContent("Z.md", "content-x") + .assertContent(s.conflictPath("Z.md"), "content-y"); } } ] diff --git a/frontend/deterministic-tests/src/tests/mc-delete-then-offline-rename.test.ts b/frontend/deterministic-tests/src/tests/mc-delete-then-offline-rename.test.ts index 6727e99d..e16b349d 100644 --- a/frontend/deterministic-tests/src/tests/mc-delete-then-offline-rename.test.ts +++ b/frontend/deterministic-tests/src/tests/mc-delete-then-offline-rename.test.ts @@ -27,12 +27,8 @@ export const mcDeleteThenOfflineRenameTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertContent("C.md", "unrelated").assertFileNotExists( - "A.md" - ); - s.ifFileExists("B.md", (inner) => - inner.assertContent("B.md", "original") - ); + s.assertFileCount(1); + s.assertContent("C.md", "unrelated"); } } ] diff --git a/frontend/deterministic-tests/src/tests/mc-multi-delete-offline-rename.test.ts b/frontend/deterministic-tests/src/tests/mc-multi-delete-offline-rename.test.ts index 8db90aab..1f33b91d 100644 --- a/frontend/deterministic-tests/src/tests/mc-multi-delete-offline-rename.test.ts +++ b/frontend/deterministic-tests/src/tests/mc-multi-delete-offline-rename.test.ts @@ -35,14 +35,10 @@ export const mcMultiDeleteOfflineRenameTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileExists("file-1.md") - .assertFileExists("file-3.md") - .assertFileExists("file-5.md") - .assertFileNotExists("file-2.md") - .assertFileNotExists("file-4.md"); - s.ifFileExists("renamed.md", (inner) => - inner.assertContent("renamed.md", "content-2") - ); + s.assertFileCount(3); + s.assertContent("file-1.md", "content-1"); + s.assertContent("file-3.md", "content-3"); + s.assertContent("file-5.md", "content-5"); } } ] diff --git a/frontend/deterministic-tests/src/tests/mc-three-client-rename-offline-update.test.ts b/frontend/deterministic-tests/src/tests/mc-three-client-rename-offline-update.test.ts index 4167b925..af1c85ba 100644 --- a/frontend/deterministic-tests/src/tests/mc-three-client-rename-offline-update.test.ts +++ b/frontend/deterministic-tests/src/tests/mc-three-client-rename-offline-update.test.ts @@ -32,9 +32,8 @@ export const mcThreeClientRenameOfflineUpdateTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileCount(1) - .assertFileNotExists("A.md") - .assertContains("B.md", "updated-by-client-2"); + s.assertFileCount(1); + s.assertContent("B.md", "updated-by-client-2"); } } ] diff --git a/frontend/deterministic-tests/src/tests/merging-update-response-survives-user-rename.test.ts b/frontend/deterministic-tests/src/tests/merging-update-response-survives-user-rename.test.ts index e93240f9..edfc2eea 100644 --- a/frontend/deterministic-tests/src/tests/merging-update-response-survives-user-rename.test.ts +++ b/frontend/deterministic-tests/src/tests/merging-update-response-survives-user-rename.test.ts @@ -1,77 +1,38 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const mergingUpdateResponseSurvivesUserRenameTest: TestDefinition = { description: - "Client 1 sends a content update with a stale `parent_version_id` " + - "(its WebSocket is paused, so it hasn't seen Client 0's intervening " + - "edit). The server merges and replies with `MergingUpdate` carrying " + - "the merged text. Before the response lands, the user renames the " + - "doc on Client 1, vacating the disk path the in-flight " + - "`processLocalUpdate` captured. Pre-fix: " + - "`handleMaybeMergingResponse`'s `operations.write(diskPath, …)` " + - "hits the `we wont recreate it` early-return inside `write`, " + - "silently dropping the server-merged content — Client 0's edit is " + - "lost on Client 1's disk, and Client 1's next local-update PUT " + - "(rebased on the now-untracked merged version) deletes Client 0's " + - "edit on the server too. Post-fix: the response is written to the " + - "doc's current tracked disk path, preserving both edits.", + "A proven stale content CAS is reconciled after the user renames its file.", clients: 2, steps: [ + { type: "create", client: 0, path: "doc.md", content: "0\n" }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, - { type: "create", client: 0, path: "doc.md", content: "0\n" }, { type: "barrier" }, - - // Stop Client 1 from seeing Client 0's next edit, so its next - // outbound PUT carries a stale `parent_version_id` and the server - // is forced to merge. - { type: "pause-websocket", client: 1 }, - - // Server now holds v_b = "0\nA\n". Client 1's tracked parent - // version stays at v_a = "0\n". + { + type: "assert-documents", + expected: [{ key: "note", path: "doc.md", content: "0\n" }] + }, + { type: "hold-request", client: 1, kind: "content", point: "before" }, + { type: "update", client: 1, path: "doc.md", content: "0\nB\n" }, + { type: "wait-for-request", client: 1 }, { type: "update", client: 0, path: "doc.md", content: "0\nA\n" }, { type: "sync", client: 0 }, - - // Pause the server. Subsequent HTTP PUTs from Client 1 buffer at - // the OS layer until resume. This guarantees the merge response - // for Client 1's update is still in flight when the rename below - // mutates `queue.documents`. - { type: "pause-server" }, - - // Client 1 edits doc.md with "B". The drain pops the LocalUpdate, - // captures `diskPath = "doc.md"`, reads the file, and sends the - // HTTP PUT — which buffers because the server is SIGSTOPped. - { type: "update", client: 1, path: "doc.md", content: "0\nB\n" }, - - // User renames the file while the previous PUT is still in flight. - // `queue.enqueue`'s rename branch updates `documents` to point at - // `renamed.md` synchronously, but `processLocalUpdate`'s captured - // `diskPath` ("doc.md") is a local — it can't be retargeted. { type: "rename", client: 1, oldPath: "doc.md", newPath: "renamed.md" }, - - // Resume the server. It reconciles parent=v_a, latest=v_b, - // new="0\nB\n" → v_c with both edits, replies `MergingUpdate`. - // Pre-fix: write("doc.md", …) sees no file at that path - // (renamed.md now holds the data) and bails out without ever - // writing the merged bytes. Post-fix: the merged bytes land at - // the tracked path (renamed.md). - { type: "resume-server" }, - { type: "resume-websocket", client: 1 }, - + { type: "release-request", client: 1 }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (state: AssertableState): void => { - state.assertFileCount(1); - state.assertFileExists("renamed.md"); - state.assertFileNotExists("doc.md"); - // Both edits survive: Client 0's "A" and Client 1's "B". - // The reconcile may interleave them either way; assert - // both tokens are present in the converged content. - state.assertContains("renamed.md", "A", "B"); - } + type: "assert-response", + client: 1, + kind: "content", + responseType: "StaleBase", + count: 1 + }, + { + type: "assert-documents", + expected: [ + { key: "note", path: "renamed.md", content: "0\nA\nB\n" } + ] } ] }; diff --git a/frontend/deterministic-tests/src/tests/move-and-concurrent-remote-update.test.ts b/frontend/deterministic-tests/src/tests/move-and-concurrent-remote-update.test.ts index 86657f0f..026f08f0 100644 --- a/frontend/deterministic-tests/src/tests/move-and-concurrent-remote-update.test.ts +++ b/frontend/deterministic-tests/src/tests/move-and-concurrent-remote-update.test.ts @@ -34,9 +34,8 @@ export const moveAndConcurrentRemoteUpdateTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileCount(1) - .assertFileNotExists("A.md") - .assertContains("B.md", "updated by client 1"); + s.assertFileCount(1); + s.assertContent("B.md", "updated by client 1"); } } ] diff --git a/frontend/deterministic-tests/src/tests/move-preserves-remote-update.test.ts b/frontend/deterministic-tests/src/tests/move-preserves-remote-update.test.ts index 13e27349..33df098b 100644 --- a/frontend/deterministic-tests/src/tests/move-preserves-remote-update.test.ts +++ b/frontend/deterministic-tests/src/tests/move-preserves-remote-update.test.ts @@ -35,13 +35,10 @@ export const movePreservesRemoteUpdateTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileCount(1); - const [content] = Array.from(s.files.values()); - if (!content.includes("client 1 edit")) { - throw new Error( - `Expected merged content to include "client 1 edit", got: "${content}"` - ); - } + s.assertFileCount(1).assertContent( + "renamed.md", + "line 1\nclient 1 edit\nline 2" + ); } } ] diff --git a/frontend/deterministic-tests/src/tests/multi-file-operations.test.ts b/frontend/deterministic-tests/src/tests/multi-file-operations.test.ts index a47f5a2a..c26d65d9 100644 --- a/frontend/deterministic-tests/src/tests/multi-file-operations.test.ts +++ b/frontend/deterministic-tests/src/tests/multi-file-operations.test.ts @@ -33,12 +33,9 @@ export const multiFileOperationsTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertContains("B.md", "updated") - .assertFileExists("C.md") - .assertFileNotExists("A.md"); - s.ifFileExists("D.md", (inner) => - inner.assertContent("D.md", "content-a") - ); + s.assertFileCount(2); + s.assertContent("B.md", "updated by client 1"); + s.assertContent("C.md", "content-c"); } } ] diff --git a/frontend/deterministic-tests/src/tests/offline-concurrent-renames.test.ts b/frontend/deterministic-tests/src/tests/offline-concurrent-renames.test.ts index 6c946b9c..1b2b18b4 100644 --- a/frontend/deterministic-tests/src/tests/offline-concurrent-renames.test.ts +++ b/frontend/deterministic-tests/src/tests/offline-concurrent-renames.test.ts @@ -44,15 +44,8 @@ export const offlineConcurrentRenamesTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileNotExists("A.md") - .assertFileCount(1) - .assertAnyFileContains("shared-content"); - s.ifFileExists("B.md", (inner) => - inner.assertContent("B.md", "shared-content") - ); - s.ifFileExists("C.md", (inner) => - inner.assertContent("C.md", "shared-content") - ); + s.assertFileCount(1); + s.assertContent("B.md", "shared-content"); } } ] diff --git a/frontend/deterministic-tests/src/tests/offline-create-same-path-mergeable.test.ts b/frontend/deterministic-tests/src/tests/offline-create-same-path-mergeable.test.ts index cbd59a4a..c8d28d02 100644 --- a/frontend/deterministic-tests/src/tests/offline-create-same-path-mergeable.test.ts +++ b/frontend/deterministic-tests/src/tests/offline-create-same-path-mergeable.test.ts @@ -28,13 +28,13 @@ export const offlineCreateSamePathMergeableTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileCount(1) - .assertFileExists("notes.md") - .assertContains( - "notes.md", - "alpha wrote this line", - "beta wrote this different line" - ); + s.assertFileCount(1).assertContentOneOf( + "notes.md", + "alpha wrote this linebeta wrote this different line", + "beta wrote this different linealpha wrote this line", + "alpha wrote this line beta wrote this different line", + "beta wrote this different line alpha wrote this line" + ); } } ] diff --git a/frontend/deterministic-tests/src/tests/offline-edit-remote-rename.test.ts b/frontend/deterministic-tests/src/tests/offline-edit-remote-rename.test.ts index ffc41b89..29d0fed3 100644 --- a/frontend/deterministic-tests/src/tests/offline-edit-remote-rename.test.ts +++ b/frontend/deterministic-tests/src/tests/offline-edit-remote-rename.test.ts @@ -40,9 +40,8 @@ export const offlineEditRemoteRenameTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileNotExists("A.md") - .assertFileCount(1) - .assertContains("B.md", "edited by client 0"); + s.assertFileCount(1); + s.assertContent("B.md", "edited by client 0"); } } ] diff --git a/frontend/deterministic-tests/src/tests/offline-rename-remote-create-old-path.test.ts b/frontend/deterministic-tests/src/tests/offline-rename-remote-create-old-path.test.ts index c1b2913a..f2273df5 100644 --- a/frontend/deterministic-tests/src/tests/offline-rename-remote-create-old-path.test.ts +++ b/frontend/deterministic-tests/src/tests/offline-rename-remote-create-old-path.test.ts @@ -41,10 +41,8 @@ export const offlineRenameRemoteCreateOldPathTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContains( - "Y.md", - "updated-by-client-1" - ); + s.assertFileCount(1); + s.assertContent("Y.md", "updated-by-client-1"); } } ] 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 3e2ebf51..9b29b778 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 @@ -12,22 +12,24 @@ export const onlineBothCreateSamePathDeconflictTest: TestDefinition = { { type: "enable-sync", client: 1 }, { type: "barrier" }, - { type: "pause-websocket", client: 1 }, + { type: "pause-observation", client: 1 }, { type: "create", client: 0, path: "A.md", content: " from-client-0 " }, { type: "update", client: 0, path: "A.md", content: " updated-by-0 " }, { type: "sync" }, { type: "create", client: 1, path: "A.md", content: " from-client-1 " }, - { type: "resume-websocket", client: 1 }, + { type: "wait-for-observation", client: 1 }, + { type: "sync", client: 0 }, + { type: "resume-observation", client: 1 }, { type: "barrier" }, { type: "assert-consistent", - verify: (state: AssertableState): void => { - state - .assertFileCount(2) - .assertAnyFileContains("updated-by-0", "from-client-1 "); + verify: (s: AssertableState): void => { + s.assertFileCount(2) + .assertContent("A.md", " updated-by-0 ") + .assertContent(s.conflictPath("A.md"), " from-client-1 "); } } ] diff --git a/frontend/deterministic-tests/src/tests/online-create-update-while-other-creates-same-path.test.ts b/frontend/deterministic-tests/src/tests/online-create-update-while-other-creates-same-path.test.ts index e0ddc21a..d315ba22 100644 --- a/frontend/deterministic-tests/src/tests/online-create-update-while-other-creates-same-path.test.ts +++ b/frontend/deterministic-tests/src/tests/online-create-update-while-other-creates-same-path.test.ts @@ -11,7 +11,7 @@ export const onlineCreateUpdateWhileOtherCreatesSamePathTest: TestDefinition = { { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, - { type: "pause-websocket", client: 1 }, + { type: "pause-observation", client: 1 }, { type: "create", client: 0, @@ -30,18 +30,21 @@ export const onlineCreateUpdateWhileOtherCreatesSamePathTest: TestDefinition = { path: "data.bin", content: "BINARY:other-content" }, - { type: "resume-websocket", client: 1 }, + { type: "wait-for-observation", client: 1 }, + { type: "sync", client: 0 }, + { type: "resume-observation", client: 1 }, { type: "barrier" }, { type: "assert-consistent", - verify: (state: AssertableState): void => { - state - .assertFileCount(2) - .assertNoFileContains("content-v1") - .assertAnyFileContains("content-v2") - .assertAnyFileContains("other-content"); + verify: (s: AssertableState): void => { + s.assertFileCount(2) + .assertContent("data.bin", "BINARY:content-v2") + .assertContent( + s.conflictPath("data.bin"), + "BINARY:other-content" + ); } } ] diff --git a/frontend/deterministic-tests/src/tests/overlapping-edits-same-section.test.ts b/frontend/deterministic-tests/src/tests/overlapping-edits-same-section.test.ts index a93a6f69..ed37240b 100644 --- a/frontend/deterministic-tests/src/tests/overlapping-edits-same-section.test.ts +++ b/frontend/deterministic-tests/src/tests/overlapping-edits-same-section.test.ts @@ -41,12 +41,10 @@ export const overlappingEditsSameSectionTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContains( + s.assertFileCount(1); + s.assertContent( "doc.md", - "# Title", - "alpha addition", - "beta addition", - "footer" + "# Title\nalpha addition\n\nbeta addition\nfooter" ); } } diff --git a/frontend/deterministic-tests/src/tests/queue-reset-loses-coalesced-local-edit.test.ts b/frontend/deterministic-tests/src/tests/queue-reset-loses-coalesced-local-edit.test.ts index 6d89acf4..b8608f30 100644 --- a/frontend/deterministic-tests/src/tests/queue-reset-loses-coalesced-local-edit.test.ts +++ b/frontend/deterministic-tests/src/tests/queue-reset-loses-coalesced-local-edit.test.ts @@ -25,10 +25,12 @@ export const queueResetLosesCoalescedLocalEditTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContains( + s.assertFileCount(1).assertContentOneOf( "doc.md", - "alpha", - "charlie" + "alpha bravocharlie delta", + "charlie deltaalpha bravo", + "alpha bravo charlie delta", + "charlie delta alpha bravo" ); } } diff --git a/frontend/deterministic-tests/src/tests/queued-create-delete-does-not-hijack-reused-path.test.ts b/frontend/deterministic-tests/src/tests/queued-create-delete-does-not-hijack-reused-path.test.ts index a29f8314..db0dcf80 100644 --- a/frontend/deterministic-tests/src/tests/queued-create-delete-does-not-hijack-reused-path.test.ts +++ b/frontend/deterministic-tests/src/tests/queued-create-delete-does-not-hijack-reused-path.test.ts @@ -12,14 +12,14 @@ export const queuedCreateDeleteDoesNotHijackReusedPathTest: TestDefinition = { { type: "enable-sync", client: 1 }, { type: "barrier" }, - { type: "pause-server" }, + { type: "hold-request", client: 1, kind: "create", point: "before" }, { type: "create", client: 1, path: "blocker.bin", content: "BINARY:blocker" }, - { type: "sleep", ms: 100 }, + { type: "wait-for-request", client: 1 }, { type: "create", client: 1, @@ -39,7 +39,7 @@ export const queuedCreateDeleteDoesNotHijackReusedPathTest: TestDefinition = { oldPath: "source.bin", newPath: "target.bin" }, - { type: "resume-server" }, + { type: "release-request", client: 1 }, { type: "barrier" }, { diff --git a/frontend/deterministic-tests/src/tests/rapid-create-update-delete-cycle.test.ts b/frontend/deterministic-tests/src/tests/rapid-create-update-delete-cycle.test.ts index f9c58753..814edb01 100644 --- a/frontend/deterministic-tests/src/tests/rapid-create-update-delete-cycle.test.ts +++ b/frontend/deterministic-tests/src/tests/rapid-create-update-delete-cycle.test.ts @@ -3,15 +3,15 @@ import type { TestDefinition } from "../test-definition"; export const rapidCreateUpdateDeleteCycleTest: TestDefinition = { description: - "Client 0 rapidly creates, updates, deletes, then re-creates a file while the server is paused. " + - "After the server resumes, client 1 must see only the final file.", + "Client 0 rapidly creates, updates, deletes, then re-creates a file while its create request is held. " + + "After releasing the request, client 1 must see only the final file.", clients: 2, steps: [ { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, { type: "barrier" }, - { type: "pause-server" }, + { type: "hold-request", client: 0, kind: "create", point: "before" }, { type: "create", @@ -19,6 +19,7 @@ export const rapidCreateUpdateDeleteCycleTest: TestDefinition = { path: "cycle.md", content: "version 1" }, + { type: "wait-for-request", client: 0 }, { type: "update", client: 0, @@ -27,7 +28,7 @@ export const rapidCreateUpdateDeleteCycleTest: TestDefinition = { }, { type: "delete", client: 0, path: "cycle.md" }, - { type: "resume-server" }, + { type: "release-request", client: 0 }, { type: "sync" }, { diff --git a/frontend/deterministic-tests/src/tests/rapid-edit-delete-online-convergence.test.ts b/frontend/deterministic-tests/src/tests/rapid-edit-delete-online-convergence.test.ts index 48c062e0..f2915cf0 100644 --- a/frontend/deterministic-tests/src/tests/rapid-edit-delete-online-convergence.test.ts +++ b/frontend/deterministic-tests/src/tests/rapid-edit-delete-online-convergence.test.ts @@ -16,7 +16,9 @@ export const rapidEditDeleteOnlineConvergenceTest: TestDefinition = { { type: "enable-sync", client: 1 }, { type: "barrier" }, + { type: "hold-request", client: 0, kind: "content", point: "before" }, { type: "update", client: 0, path: "A.md", content: "A edit 1" }, + { type: "wait-for-request", client: 0 }, { type: "update", client: 0, path: "B.md", content: "B edit 1" }, { type: "update", client: 0, path: "C.md", content: "C edit 1" }, { type: "delete", client: 1, path: "A.md" }, @@ -26,22 +28,15 @@ export const rapidEditDeleteOnlineConvergenceTest: TestDefinition = { { type: "update", client: 0, path: "B.md", content: "B edit 2" }, { type: "update", client: 0, path: "C.md", content: "C edit 2" }, + { type: "sync", client: 1 }, + { type: "release-request", client: 0 }, { type: "barrier" }, { type: "assert-consistent", verify: (s: AssertableState): void => { - for (const [path, content] of s.files) { - for (const clientFiles of s.clientFiles) { - if ( - clientFiles.has(path) && - clientFiles.get(path) !== content - ) { - throw new Error( - `Content mismatch for ${path}: "${clientFiles.get(path)}" vs "${content}"` - ); - } - } - } + s.assertFileCount(2) + .assertContent("B.md", "B edit 2") + .assertContent("D.md", "content D"); } } ] diff --git a/frontend/deterministic-tests/src/tests/rapid-updates-after-merge.test.ts b/frontend/deterministic-tests/src/tests/rapid-updates-after-merge.test.ts index 6f97ff05..624438ab 100644 --- a/frontend/deterministic-tests/src/tests/rapid-updates-after-merge.test.ts +++ b/frontend/deterministic-tests/src/tests/rapid-updates-after-merge.test.ts @@ -42,7 +42,8 @@ export const rapidUpdatesAfterMergeTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContains("doc.md", "update 3"); + s.assertFileCount(1); + s.assertContent("doc.md", "update 3"); } } ] diff --git a/frontend/deterministic-tests/src/tests/remote-rename-collides-with-pending-local-create.test.ts b/frontend/deterministic-tests/src/tests/remote-rename-collides-with-pending-local-create.test.ts index d30fdc67..b11290a9 100644 --- a/frontend/deterministic-tests/src/tests/remote-rename-collides-with-pending-local-create.test.ts +++ b/frontend/deterministic-tests/src/tests/remote-rename-collides-with-pending-local-create.test.ts @@ -30,7 +30,7 @@ export const remoteRenameCollidesWithPendingLocalCreateTest: TestDefinition = { // Pause client 0's WS so the upcoming remote rename buffers and // we can stage a colliding local create before the rename // drains on client 0. - { type: "pause-websocket", client: 0 }, + { type: "pause-observation", client: 0 }, // Client 1 renames the doc. Server commits, broadcasts to // client 0 (buffered). @@ -51,7 +51,8 @@ export const remoteRenameCollidesWithPendingLocalCreateTest: TestDefinition = { // Resume client 0's WS. The buffered RemoteChange drains. // The reconciler must converge without ever leaving a // conflict-uuid stash on disk. - { type: "resume-websocket", client: 0 }, + { type: "wait-for-observation", client: 0 }, + { type: "resume-observation", client: 0 }, { type: "barrier" }, @@ -68,6 +69,7 @@ export const remoteRenameCollidesWithPendingLocalCreateTest: TestDefinition = { } state.assertFileExists("target.md"); state.assertContent("target.md", "v1\n"); + state.assertContent(state.conflictPath("target.md"), "extra\n"); // The local create gets server-deconflicted to a // sibling path (e.g. `target (1).md`). } 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 index eb2ed86d..386e3644 100644 --- 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 @@ -1,14 +1,8 @@ -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.", + "Hold a remote content download while the peer deletes and recreates its path. A local edit to the old generation during this download must not resurrect it or overwrite the new identity.", clients: 2, steps: [ { type: "enable-sync", client: 0 }, @@ -17,7 +11,13 @@ export const remoteUpdateResurrectsDeletedDocTest: TestDefinition = { { type: "create", client: 1, path: "P.md", content: "v8 content\n" }, { type: "barrier" }, - { type: "pause-websocket", client: 0 }, + { type: "remember-identity", path: "P.md", key: "original" }, + { + type: "hold-request", + client: 0, + kind: "read-content", + point: "after" + }, { type: "update", @@ -26,6 +26,7 @@ export const remoteUpdateResurrectsDeletedDocTest: TestDefinition = { content: "v17 content from client 1\n" }, { type: "sync", client: 1 }, + { type: "wait-for-request", client: 0 }, { type: "delete", client: 1, path: "P.md" }, { type: "sync", client: 1 }, { @@ -36,7 +37,12 @@ export const remoteUpdateResurrectsDeletedDocTest: TestDefinition = { }, { type: "sync", client: 1 }, - { type: "resume-websocket", client: 0 }, + { + type: "remember-local-identity", + client: 1, + path: "P.md", + key: "replacement" + }, { type: "update", @@ -45,15 +51,18 @@ export const remoteUpdateResurrectsDeletedDocTest: TestDefinition = { content: "local edit by client 0\n" }, + { type: "release-request", client: 0 }, { type: "barrier" }, { - type: "assert-consistent", - verify: (state: AssertableState): void => { - state - .assertFileCount(1) - .assertContent("P.md", "v21 content (D2)\n"); - } + type: "assert-documents", + expected: [ + { + key: "replacement", + path: "P.md", + content: "v21 content (D2)\n" + } + ] } ] }; diff --git a/frontend/deterministic-tests/src/tests/remote-update-survives-user-rename.test.ts b/frontend/deterministic-tests/src/tests/remote-update-survives-user-rename.test.ts index b78ad143..86340f80 100644 --- a/frontend/deterministic-tests/src/tests/remote-update-survives-user-rename.test.ts +++ b/frontend/deterministic-tests/src/tests/remote-update-survives-user-rename.test.ts @@ -1,84 +1,32 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const remoteUpdateSurvivesUserRenameTest: TestDefinition = { description: - "Client 0 updates a tracked doc; while Client 1 is processing the " + - "broadcast and parked on the GET for the new version's content, the " + - "user renames the doc on Client 1. Pre-fix: `processRemoteUpdate` " + - "captures `actualPath` before the await and, after the GET returns, " + - "calls `write(actualPath, …)` (no-op — file was renamed away), " + - "`updateCache(actualPath, …)`, and `setDocument(actualPath, …)`. " + - "`setDocument` mutates the same record in place so its `path` is " + - "yanked from the user's renamed slot back to the pre-rename path, " + - "wiping the rename out of the queue's documents map. The queued " + - "`LocalUpdate` then reads from the now-stale `record.path`, hits " + - "`FileNotFoundError`, and is silently dropped — the user's rename " + - "never reaches the server. Post-fix: the handler defers when a " + - "local event landed mid-await, so the rename drains first and " + - "the deferred remote update is folded into the broadcast that " + - "follows the rename round-trip.", + "An observed remote content download is held while the user renames its destination.", 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" }, - - // Buffer Client 1's incoming broadcasts so it doesn't see - // Client 0's update until we've paused the server. - { type: "pause-websocket", client: 1 }, - - // Server now holds v=2 of doc.md. - { type: "update", client: 0, path: "doc.md", content: "v2\n" }, - { type: "sync", client: 0 }, - - // Pause the server. Client 1's upcoming GET for the new version - // content blocks at the OS layer until resume. - { type: "pause-server" }, - - // Release the buffered broadcast. Client 1's drain enters - // `processRemoteUpdate`, captures `actualPath`, fires the GET, - // and parks awaiting the response. - { type: "resume-websocket", client: 1 }, - - // Yield long enough for the drain to traverse all microtask - // hops between the WS handler and the GET, so the HTTP request - // is queued at the (paused) server before the rename runs. - // Without this yield the rename would be enqueued before - // `processRemoteUpdate`'s entry-time `hasPendingLocalEvents` - // check and the early-defer branch would mask the bug. - { type: "sleep", ms: 50 }, - - // While the GET is in flight the user renames the doc. The queue - // mutates `record.path` to "renamed.md" in place and pushes a - // LocalUpdate carrying the rename target. { - type: "rename", - client: 1, - oldPath: "doc.md", - newPath: "renamed.md" + type: "assert-documents", + expected: [{ key: "note", path: "doc.md", content: "v1\n" }] }, - - // Resume the server. The GET response unblocks - // `processRemoteUpdate`. With the fix in place it sees the - // queued LocalUpdate and defers; without the fix it walks past - // the rename and clobbers the documents map, dropping the - // pending LocalUpdate's read on the way back through. - { type: "resume-server" }, - - { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1); - s.assertFileExists("renamed.md"); - s.assertFileNotExists("doc.md"); - // Both edits survive: the user's rename and Client 0's - // content update at v=2. - s.assertContent("renamed.md", "v2\n"); - } + type: "hold-request", + client: 1, + kind: "read-content", + point: "after" + }, + { type: "update", client: 0, path: "doc.md", content: "v2\n" }, + { type: "wait-for-request", client: 1 }, + { type: "rename", client: 1, oldPath: "doc.md", newPath: "renamed.md" }, + { type: "release-request", client: 1 }, + { type: "barrier" }, + { + type: "assert-documents", + expected: [{ key: "note", path: "renamed.md", content: "v2\n" }] } ] }; diff --git a/frontend/deterministic-tests/src/tests/rename-chain-during-pending-create.test.ts b/frontend/deterministic-tests/src/tests/rename-chain-during-pending-create.test.ts index 822e83df..ffbbbed9 100644 --- a/frontend/deterministic-tests/src/tests/rename-chain-during-pending-create.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-chain-during-pending-create.test.ts @@ -3,33 +3,17 @@ import type { TestDefinition } from "../test-definition"; export const renameChainDuringPendingCreateTest: TestDefinition = { description: - "User creates a doc, then renames it twice while the LocalCreate's " + - "HTTP roundtrip is still in flight (server paused). Each rename " + - "pushes a LocalUpdate whose `documentId` is the create's Promise " + - "(see `pendingDocumentId` in `SyncEventQueue.enqueue`). After the " + - "create resolves, the first rename drains successfully and " + - "`setDocument` walks `events[]` to retarget queued LocalUpdates' " + - "`event.path` to the new disk location — but the comparison " + - "`e.documentId === record.documentId` mismatches the still-Promise " + - "references, so the second rename's `event.path` stays at the " + - "vacated previous slot. On the next drain step `skipIfOversized`'s " + - "`getFileSize(event.path)` throws FileNotFoundError, which " + - "`processEvent` swallows as 'Skipping sync event ... because the " + - "file no longer exists' — losing the user's final rename. " + - "Post-fix: `resolveCreate` (and the displacement-merge branch in " + - "`processCreate`) swap the Promise references for the resolved id " + - "before `setDocument` runs, so retarget works.", + "Hold a create before sending it, then rename its document twice. Both clients must retain the original bytes at the final path.", clients: 2, steps: [ { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, { type: "barrier" }, - // Pause the server so client 0's create stalls on the HTTP PUT - // while we queue rename events behind it. - { type: "pause-server" }, + { type: "hold-request", client: 0, kind: "create", point: "before" }, { type: "create", client: 0, path: "first.md", content: "v1\n" }, + { type: "wait-for-request", client: 0 }, { type: "rename", client: 0, @@ -43,12 +27,7 @@ export const renameChainDuringPendingCreateTest: TestDefinition = { newPath: "third.md" }, - // Resume — drain pops LocalCreate (now resolves), then the two - // queued LocalUpdates. Pre-fix: only the first rename's - // file-system effect lands; the second is silently dropped. - // The server ends up with the doc at second.md, leaving - // client 0's local third.md untracked / out-of-sync. - { type: "resume-server" }, + { type: "release-request", client: 0 }, { type: "barrier" }, diff --git a/frontend/deterministic-tests/src/tests/rename-overwrites-pending-create-then-delete.test.ts b/frontend/deterministic-tests/src/tests/rename-overwrites-pending-create-then-delete.test.ts index 0b47c781..3d7c7bda 100644 --- a/frontend/deterministic-tests/src/tests/rename-overwrites-pending-create-then-delete.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-overwrites-pending-create-then-delete.test.ts @@ -16,7 +16,7 @@ export const renameOverwritesPendingCreateThenDeleteTest: TestDefinition = { }, { type: "barrier" }, - { type: "pause-server" }, + { type: "hold-request", client: 0, kind: "create", point: "before" }, { type: "create", @@ -24,6 +24,7 @@ export const renameOverwritesPendingCreateThenDeleteTest: TestDefinition = { path: "pending.bin", content: "BINARY:pending" }, + { type: "wait-for-request", client: 0 }, { type: "rename", client: 0, @@ -38,7 +39,7 @@ export const renameOverwritesPendingCreateThenDeleteTest: TestDefinition = { }, { type: "delete", client: 0, path: "final.bin" }, - { type: "resume-server" }, + { type: "release-request", client: 0 }, { type: "barrier" }, { diff --git a/frontend/deterministic-tests/src/tests/rename-pending-create-before-response.test.ts b/frontend/deterministic-tests/src/tests/rename-pending-create-before-response.test.ts index 26623c43..fa532ac2 100644 --- a/frontend/deterministic-tests/src/tests/rename-pending-create-before-response.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-pending-create-before-response.test.ts @@ -3,13 +3,13 @@ import type { TestDefinition } from "../test-definition"; export const renamePendingCreateBeforeResponseTest: TestDefinition = { description: - "Client 0 creates a file while the server is paused, then renames it before the create completes. After the server resumes, both clients should converge with the file at the renamed path.", + "Client 0 creates a file while its create request is held, then renames it before the create completes. After releasing the request, both clients should converge with the file at the renamed path.", clients: 2, steps: [ { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, - { type: "pause-server" }, + { type: "hold-request", client: 0, kind: "create", point: "before" }, { type: "create", @@ -17,6 +17,7 @@ export const renamePendingCreateBeforeResponseTest: TestDefinition = { path: "doc.md", content: "original-content" }, + { type: "wait-for-request", client: 0 }, { type: "rename", @@ -25,7 +26,7 @@ export const renamePendingCreateBeforeResponseTest: TestDefinition = { newPath: "renamed.md" }, - { type: "resume-server" }, + { type: "release-request", client: 0 }, { type: "barrier" }, diff --git a/frontend/deterministic-tests/src/tests/rename-pending-create-onto-pending-delete-path.test.ts b/frontend/deterministic-tests/src/tests/rename-pending-create-onto-pending-delete-path.test.ts index 0906f209..251776fa 100644 --- a/frontend/deterministic-tests/src/tests/rename-pending-create-onto-pending-delete-path.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-pending-create-onto-pending-delete-path.test.ts @@ -21,14 +21,14 @@ export const renamePendingCreateOntoPendingDeletePathTest: TestDefinition = { }, { type: "barrier" }, - { type: "pause-server" }, + { type: "hold-request", client: 1, kind: "create", point: "before" }, { type: "create", client: 1, path: "blocker.md", content: "blocker\n" }, - { type: "sleep", ms: 100 }, + { type: "wait-for-request", client: 1 }, { type: "create", client: 1, @@ -42,7 +42,7 @@ export const renamePendingCreateOntoPendingDeletePathTest: TestDefinition = { oldPath: "file-23.md", newPath: "file-17.md" }, - { type: "resume-server" }, + { type: "release-request", client: 1 }, { type: "barrier" }, { diff --git a/frontend/deterministic-tests/src/tests/rename-to-path-of-unconfirmed-delete.test.ts b/frontend/deterministic-tests/src/tests/rename-to-path-of-unconfirmed-delete.test.ts index 34a3867c..c79af51b 100644 --- a/frontend/deterministic-tests/src/tests/rename-to-path-of-unconfirmed-delete.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-to-path-of-unconfirmed-delete.test.ts @@ -34,10 +34,8 @@ export const renameToPathOfUnconfirmedDeleteTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileNotExists("B.md").assertContains( - "A.md", - "content B" - ); + s.assertFileCount(1); + s.assertContent("A.md", "content B"); } } ] diff --git a/frontend/deterministic-tests/src/tests/rename-to-pending-path-fallback.test.ts b/frontend/deterministic-tests/src/tests/rename-to-pending-path-fallback.test.ts index 8747218a..7e90a07e 100644 --- a/frontend/deterministic-tests/src/tests/rename-to-pending-path-fallback.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-to-pending-path-fallback.test.ts @@ -33,10 +33,8 @@ export const renameToPendingPathFallbackTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileNotExists("B.md").assertContains( - "A.md", - "tracked B content" - ); + s.assertFileCount(1); + s.assertContent("A.md", "tracked B content"); } } ] diff --git a/frontend/deterministic-tests/src/tests/rename-update-conflict.test.ts b/frontend/deterministic-tests/src/tests/rename-update-conflict.test.ts index 18d4c101..db3f1c39 100644 --- a/frontend/deterministic-tests/src/tests/rename-update-conflict.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-update-conflict.test.ts @@ -35,7 +35,8 @@ export const renameUpdateConflictTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileNotExists("A.md").assertContains("B.md", "updated"); + s.assertFileCount(1); + s.assertContent("B.md", "updated by client 1"); } } ] diff --git a/frontend/deterministic-tests/src/tests/renamed-pending-create-reused-path-then-delete.test.ts b/frontend/deterministic-tests/src/tests/renamed-pending-create-reused-path-then-delete.test.ts index 3ffb376e..f92d5277 100644 --- a/frontend/deterministic-tests/src/tests/renamed-pending-create-reused-path-then-delete.test.ts +++ b/frontend/deterministic-tests/src/tests/renamed-pending-create-reused-path-then-delete.test.ts @@ -13,14 +13,14 @@ export const renamedPendingCreateReusedPathThenDeleteTest: TestDefinition = { { type: "enable-sync", client: 1 }, { type: "barrier" }, - { type: "pause-server" }, + { type: "hold-request", client: 1, kind: "create", point: "before" }, { type: "create", client: 1, path: "blocker.md", content: "blocker\n" }, - { type: "sleep", ms: 100 }, + { type: "wait-for-request", client: 1 }, { type: "create", @@ -41,14 +41,8 @@ export const renamedPendingCreateReusedPathThenDeleteTest: TestDefinition = { content: "new\n" }, - { - type: "resume-server-until-history-then-pause", - client: 1, - syncType: "CREATE", - path: "file-33.md" - }, { type: "delete", client: 1, path: "file-33.md" }, - { type: "resume-server" }, + { type: "release-request", client: 1 }, { type: "barrier" }, { diff --git a/frontend/deterministic-tests/src/tests/reset-clears-recently-deleted-resurrection.test.ts b/frontend/deterministic-tests/src/tests/reset-clears-recently-deleted-resurrection.test.ts index e0a1565c..b4da66f7 100644 --- a/frontend/deterministic-tests/src/tests/reset-clears-recently-deleted-resurrection.test.ts +++ b/frontend/deterministic-tests/src/tests/reset-clears-recently-deleted-resurrection.test.ts @@ -24,7 +24,7 @@ export const resetClearsRecentlyDeletedResurrectionTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileNotExists("ghost.md"); + s.assertFileCount(0).assertFileNotExists("ghost.md"); } }, diff --git a/frontend/deterministic-tests/src/tests/same-doc-id-collapse-after-remote-quick-write-and-pending-rename.test.ts b/frontend/deterministic-tests/src/tests/same-doc-id-collapse-after-remote-quick-write-and-pending-rename.test.ts index 2a3b5de4..930129ae 100644 --- a/frontend/deterministic-tests/src/tests/same-doc-id-collapse-after-remote-quick-write-and-pending-rename.test.ts +++ b/frontend/deterministic-tests/src/tests/same-doc-id-collapse-after-remote-quick-write-and-pending-rename.test.ts @@ -13,10 +13,6 @@ export const sameDocIdCollapseAfterRemoteQuickWriteAndPendingRenameTest: TestDef steps: [ { type: "enable-sync", client: 0 }, - // Create a deleted latest version before client 1 joins. - // Catch-up will advance MinCovered with a non-contiguous id, - // keeping client 1's create lastSeen low enough to exercise - // the server's same-doc merge path from the e2e failure. { type: "create", client: 0, @@ -37,7 +33,12 @@ export const sameDocIdCollapseAfterRemoteQuickWriteAndPendingRenameTest: TestDef { type: "enable-sync", client: 1 }, { type: "sync", client: 1 }, - { type: "pause-websocket", client: 1 }, + { + type: "hold-request", + client: 1, + kind: "read-content", + point: "after" + }, { type: "create", @@ -47,12 +48,7 @@ export const sameDocIdCollapseAfterRemoteQuickWriteAndPendingRenameTest: TestDef }, { type: "sync", client: 0 }, - // Let client 1's buffered RemoteCreate enter the quick-write - // path, but hold the content fetch until the local create has - // appeared and moved away from doc.md. - { type: "pause-server" }, - { type: "resume-websocket", client: 1 }, - { type: "sleep", ms: 100 }, + { type: "wait-for-request", client: 1 }, { type: "create", @@ -67,7 +63,7 @@ export const sameDocIdCollapseAfterRemoteQuickWriteAndPendingRenameTest: TestDef newPath: "renamed.md" }, - { type: "resume-server" }, + { type: "release-request", client: 1 }, { type: "barrier" }, { diff --git a/frontend/deterministic-tests/src/tests/self-merge-pending-rename-aliases-second-create.test.ts b/frontend/deterministic-tests/src/tests/self-merge-pending-rename-aliases-second-create.test.ts index ac8ed3ed..3dd9040e 100644 --- a/frontend/deterministic-tests/src/tests/self-merge-pending-rename-aliases-second-create.test.ts +++ b/frontend/deterministic-tests/src/tests/self-merge-pending-rename-aliases-second-create.test.ts @@ -3,47 +3,18 @@ import type { TestDefinition } from "../test-definition"; export const selfMergePendingRenameAliasesSecondCreateTest: TestDefinition = { description: - "Single client makes two distinct creates that briefly share a path. " + - "Client 0 POSTs the first create at primary.md while the server is " + - "paused. While that POST is in flight: a second create is queued at " + - "staging.md, primary.md is renamed to moved.md (rewriting the in- " + - "flight create's event.path to moved.md and pushing a rename " + - "LocalUpdate at the queue tail), and staging.md is renamed onto the " + - "now-vacated primary.md slot (rewriting the second create's " + - "event.path to primary.md and pushing another rename LocalUpdate). " + - "Client 0's WS is paused throughout, so its watermark stays at 0. " + - "On resume the first POST commits Doc-X at primary.md (creation_vuid " + - "= N). The drain then processes the second LocalCreate (POST " + - "relativePath=primary.md, last_seen=0); the server's path-based " + - "dedup sees N > 0 and merges the second create into Doc-X " + - "(MergingUpdate). The buggy behaviour: processCreate's resolveCreate " + - "calls upsertRecord with localPath=primary.md, but the existing " + - "record (from the first create) already holds localPath=moved.md, " + - "and upsertRecord's `existing.localPath !== undefined` guard " + - "silently drops the new claim. The file at primary.md is left " + - "orphaned: tracked by no record, never broadcast, never deleted. " + - "After the user's renames the expected user-visible state is two " + - "distinct files at moved.md and primary.md — both clients must " + - "converge to that.", + "Hold the first create before sending it, then create a second file " + + "and move both generations through primary.md. Releasing the request " + + "must preserve distinct documents at moved.md and primary.md, with " + + "each generation's exact content and the unrelated remote file intact.", clients: 2, steps: [ - // Both clients online so the WS connection is established before - // the test starts pausing things. { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, { type: "barrier" }, - // Pause client 0's WS so its MinCovered watermark stays at 0 - // through the whole bug sequence. The merge condition the - // server is going to fire is `creation_vuid > last_seen`; with - // a non-zero gap the same-device second create gets merged - // into the same-device first create. { type: "pause-websocket", client: 0 }, - // Client 1 commits a doc to push the server's vuid above 0. - // Without this filler, Doc-X's create vuid could be 1 and - // client 0's last_seen.add(1) would advance min to 1, killing - // the watermark gap that triggers the merge. { type: "create", client: 1, @@ -52,14 +23,8 @@ export const selfMergePendingRenameAliasesSecondCreateTest: TestDefinition = { }, { type: "sync", client: 1 }, - // Pause the server so client 0's first create POST hangs in - // flight, giving us a deterministic window in which to enqueue - // the second create and the renames. - { type: "pause-server" }, + { type: "hold-request", client: 0, kind: "create", point: "before" }, - // First create — Doc-X. The wire-loop drains it, captures - // requestPath = event.path = "primary.md", reads the bytes, - // sends the POST, and stalls on the response. { type: "create", client: 0, @@ -67,16 +32,8 @@ export const selfMergePendingRenameAliasesSecondCreateTest: TestDefinition = { content: "primary content " }, - // Make sure the POST is actually on the wire with - // relativePath="primary.md" before we rewrite event.path. - // Without this delay the rename can win the race, the POST - // goes out with relativePath="moved.md", and the server-side - // path-collision merge never fires. - { type: "sleep", ms: 100 }, + { type: "wait-for-request", client: 0 }, - // Second create at a staging path. The wire-loop is still - // blocked on Doc-X's POST, so this LocalCreate just queues at - // index 1. { type: "create", client: 0, @@ -84,15 +41,6 @@ export const selfMergePendingRenameAliasesSecondCreateTest: TestDefinition = { content: "secondary content " }, - // Rename Doc-X's path. enqueue's pending-create branch - // rewrites Doc-X's event.path in place (moved.md) and pushes - // a LocalUpdate(rename, originalPath=moved.md) at the END of - // the queue. Note the ordering: this LocalUpdate is enqueued - // AFTER the staging LocalCreate above. That ordering is - // load-bearing — it is what makes the second create's POST - // drain (and trigger the server-side merge) before Doc-X's - // rename PUT moves the doc away from primary.md on the - // server. { type: "rename", client: 0, @@ -100,12 +48,6 @@ export const selfMergePendingRenameAliasesSecondCreateTest: TestDefinition = { newPath: "moved.md" }, - // Rename the staging file onto Doc-X's now-vacated primary.md - // slot. enqueue rewrites the staging LocalCreate's event.path - // to primary.md and pushes a LocalUpdate(rename, - // originalPath=primary.md) at the queue tail. After this the - // disk has: moved.md = Doc-X's bytes, primary.md = Doc-Y's - // bytes. { type: "rename", client: 0, @@ -113,9 +55,7 @@ export const selfMergePendingRenameAliasesSecondCreateTest: TestDefinition = { newPath: "primary.md" }, - // Let everything fly: server processes the queued POSTs; - // client 0 catches up on broadcasts. - { type: "resume-server" }, + { type: "release-request", client: 0 }, { type: "resume-websocket", client: 0 }, { type: "barrier" }, @@ -123,27 +63,14 @@ export const selfMergePendingRenameAliasesSecondCreateTest: TestDefinition = { { type: "assert-consistent", verify: (state: AssertableState): void => { - // The user did two distinct creates (Doc-X and Doc-Y); - // both contents must survive on both clients. state.assertFileCount(3); - state.assertFileExists("filler.md"); + state.assertContent("filler.md", "filler-content "); state.assertFileExists("moved.md"); state.assertFileExists("primary.md"); - // After the renames the user expects: - // - moved.md = the file that was originally created - // at primary.md (Doc-X's content). - // - primary.md = the file that was originally created - // at staging.md (Doc-Y's content). - state.assertContains("moved.md", "primary content"); - state.assertContains("primary.md", "secondary content"); + state.assertContent("moved.md", "primary content "); + state.assertContent("primary.md", "secondary content "); - // No content cross-contamination: each contribution - // should land in exactly one of the user-visible - // files. Under the bug, the orphan at primary.md - // carries Doc-X's content (because Doc-Y's PUT was - // aliased onto Doc-X's record and read Doc-X's bytes - // from moved.md), so this catches the leak too. state.assertContentInAtMostOneFile("primary content"); state.assertContentInAtMostOneFile("secondary content"); } diff --git a/frontend/deterministic-tests/src/tests/server-pause-both-clients-create.test.ts b/frontend/deterministic-tests/src/tests/server-pause-both-clients-create.test.ts index f99cf92d..c048e194 100644 --- a/frontend/deterministic-tests/src/tests/server-pause-both-clients-create.test.ts +++ b/frontend/deterministic-tests/src/tests/server-pause-both-clients-create.test.ts @@ -32,10 +32,9 @@ export const serverPauseBothClientsCreateTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertContains("alpha.md", "from client 0").assertContains( - "beta.md", - "from client 1" - ); + s.assertFileCount(2); + s.assertContent("alpha.md", "from client 0"); + s.assertContent("beta.md", "from client 1"); } } ] diff --git a/frontend/deterministic-tests/src/tests/server-pause-both-edit-same-file.test.ts b/frontend/deterministic-tests/src/tests/server-pause-both-edit-same-file.test.ts index ff8cf194..475c6592 100644 --- a/frontend/deterministic-tests/src/tests/server-pause-both-edit-same-file.test.ts +++ b/frontend/deterministic-tests/src/tests/server-pause-both-edit-same-file.test.ts @@ -39,10 +39,9 @@ export const serverPauseBothEditSameFileTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContains( + s.assertFileCount(1).assertContent( "shared.md", - "edited by client 0", - "edited by client 1" + "line 1: edited by client 0\nline 2: original\nline 3: edited by client 1" ); } }, @@ -58,7 +57,7 @@ export const serverPauseBothEditSameFileTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContains( + s.assertFileCount(1).assertContent( "shared.md", "post-merge edit from client 0" ); diff --git a/frontend/deterministic-tests/src/tests/text-pending-create-not-displaced.test.ts b/frontend/deterministic-tests/src/tests/text-pending-create-not-displaced.test.ts index 28243525..4b636ecf 100644 --- a/frontend/deterministic-tests/src/tests/text-pending-create-not-displaced.test.ts +++ b/frontend/deterministic-tests/src/tests/text-pending-create-not-displaced.test.ts @@ -27,9 +27,11 @@ export const textPendingCreateNotDisplacedTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileCount(1) - .assertFileExists("data.txt") - .assertAnyFileContains("client-0", "client-1"); + s.assertFileCount(1).assertContentOneOf( + "data.txt", + "text data from client-0 client-1", + "text data from client-1 client-0" + ); } } ] diff --git a/frontend/deterministic-tests/src/tests/three-client-rename-create-delete.test.ts b/frontend/deterministic-tests/src/tests/three-client-rename-create-delete.test.ts index 80478adc..2a94de06 100644 --- a/frontend/deterministic-tests/src/tests/three-client-rename-create-delete.test.ts +++ b/frontend/deterministic-tests/src/tests/three-client-rename-create-delete.test.ts @@ -46,9 +46,9 @@ export const threeClientRenameCreateDeleteTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileNotExists("X.md").assertAnyFileContains( - "new from C" - ); + s.assertFileCount(2) + .assertContent("Y.md", "original from A") + .assertContent(s.conflictPath("Y.md"), "new from C"); } } ] diff --git a/frontend/deterministic-tests/src/tests/update-during-create-processing.test.ts b/frontend/deterministic-tests/src/tests/update-during-create-processing.test.ts index ca53244e..2dc8897f 100644 --- a/frontend/deterministic-tests/src/tests/update-during-create-processing.test.ts +++ b/frontend/deterministic-tests/src/tests/update-during-create-processing.test.ts @@ -3,14 +3,14 @@ import type { TestDefinition } from "../test-definition"; export const updateDuringCreateProcessingTest: TestDefinition = { description: - "Client 0 creates a file while the server is paused, then immediately updates it. After the server resumes, both clients should converge with the updated content.", + "Client 0 creates a file while its create request is held, then immediately updates it. After releasing the request, both clients should converge with the updated content.", clients: 2, steps: [ { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, { type: "barrier" }, - { type: "pause-server" }, + { type: "hold-request", client: 0, kind: "create", point: "before" }, { type: "create", @@ -18,6 +18,7 @@ export const updateDuringCreateProcessingTest: TestDefinition = { path: "file.md", content: "initial" }, + { type: "wait-for-request", client: 0 }, { type: "update", @@ -26,7 +27,7 @@ export const updateDuringCreateProcessingTest: TestDefinition = { content: "updated during create" }, - { type: "resume-server" }, + { type: "release-request", client: 0 }, { type: "barrier" }, { 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 58bebb44..16d473c9 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 @@ -41,10 +41,13 @@ export const userParenthesizedFileNotDeletedTest: TestDefinition = { verify: (state: AssertableState): void => { state .assertFileCount(3) - .assertFileExists("Chapter.bin") + .assertContent("Chapter.bin", "chapter one") .assertFileExists("Chapter (1).bin") .assertContent("Chapter (1).bin", "chapter one notes") - .assertContent(state.conflictPath("Chapter.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 9615b5aa..43ba6a05 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,9 @@ export const watermarkAdvancesOnSkipTest: TestDefinition = { { type: "assert-consistent", verify: (s: AssertableState): void => { - s.assertFileCount(2).assertFileExists("doc.md").assertAnyFileContains("from client 0", "from client 1"); + s.assertFileCount(2) + .assertContent("doc.md", "from client 0") + .assertContent(s.conflictPath("doc.md"), "from client 1"); } } ]