Improve tests
This commit is contained in:
parent
b4b4d1453b
commit
12d85f567e
15 changed files with 63 additions and 119 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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 ");
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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-<uuid>- " +
|
||||
"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-<uuid>- 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"]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in a new issue