Add deterministic-tests (#190)
Reviewed-on: https://home.schmelczer.dev/git/git/andras/vault-link/pulls/190 Co-authored-by: Andras Schmelczer <andras@schmelczer.dev> Co-committed-by: Andras Schmelczer <andras@schmelczer.dev>
This commit is contained in:
parent
4482e0155f
commit
0e3132f96c
105 changed files with 7722 additions and 1 deletions
|
|
@ -0,0 +1,40 @@
|
|||
import type { AssertableState } from "../utils/assertable-state";
|
||||
import type { TestDefinition } from "../test-definition";
|
||||
|
||||
export const binaryPendingCreateNotDisplacedTest: TestDefinition = {
|
||||
description:
|
||||
"Two clients each create a binary file at the same path while offline. " +
|
||||
"After syncing, both files should exist on both clients at separate paths.",
|
||||
clients: 2,
|
||||
steps: [
|
||||
{
|
||||
type: "create",
|
||||
client: 0,
|
||||
path: "data.bin",
|
||||
content: "binary data from client 0"
|
||||
},
|
||||
{
|
||||
type: "create",
|
||||
client: 1,
|
||||
path: "data.bin",
|
||||
content: "binary data from client 1"
|
||||
},
|
||||
|
||||
{ type: "enable-sync", client: 0 },
|
||||
{ type: "enable-sync", client: 1 },
|
||||
{ type: "barrier" },
|
||||
|
||||
{
|
||||
type: "assert-consistent",
|
||||
verify: (s: AssertableState): void => {
|
||||
s.assertFileCount(2)
|
||||
.assertFileExists("data.bin")
|
||||
.assertFileExists("data (1).bin")
|
||||
.assertAnyFileContains(
|
||||
"binary data from client 0",
|
||||
"binary data from client 1"
|
||||
);
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
import type { AssertableState } from "../utils/assertable-state";
|
||||
import type { TestDefinition } from "../test-definition";
|
||||
|
||||
export const binaryToTextTransitionTest: TestDefinition = {
|
||||
description:
|
||||
"A .bin file is created and synced. Both clients edit it offline " +
|
||||
"(binary last-write-wins), then client 0 renames it to .md and " +
|
||||
"writes a clean text baseline. Both clients edit different sections " +
|
||||
"offline. The text merge should preserve both edits.",
|
||||
clients: 2,
|
||||
steps: [
|
||||
{
|
||||
type: "create",
|
||||
client: 0,
|
||||
path: "data.bin",
|
||||
content: "original content"
|
||||
},
|
||||
{ type: "enable-sync", client: 0 },
|
||||
{ type: "enable-sync", client: 1 },
|
||||
{ type: "barrier" },
|
||||
{
|
||||
type: "assert-consistent",
|
||||
verify: (s: AssertableState): void => {
|
||||
s.assertContent("data.bin", "original content");
|
||||
}
|
||||
},
|
||||
|
||||
{ type: "disable-sync", client: 0 },
|
||||
{ type: "disable-sync", client: 1 },
|
||||
|
||||
{ type: "update", client: 0, path: "data.bin", content: "version A" },
|
||||
{ type: "update", client: 1, path: "data.bin", content: "version B" },
|
||||
|
||||
{ type: "enable-sync", client: 0 },
|
||||
{ type: "enable-sync", client: 1 },
|
||||
{ type: "barrier" },
|
||||
|
||||
{
|
||||
type: "assert-consistent",
|
||||
verify: (s: AssertableState): void => {
|
||||
s.assertFileCount(1).assertContainsAny(
|
||||
"data.bin",
|
||||
"version A",
|
||||
"version B"
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
{ type: "disable-sync", client: 1 },
|
||||
{ type: "rename", client: 0, oldPath: "data.bin", newPath: "data.md" },
|
||||
{
|
||||
type: "update",
|
||||
client: 0,
|
||||
path: "data.md",
|
||||
content: "top line\nmiddle line\nbottom line"
|
||||
},
|
||||
{ type: "sync", client: 0 },
|
||||
{ type: "enable-sync", client: 1 },
|
||||
{ type: "barrier" },
|
||||
{
|
||||
type: "assert-consistent",
|
||||
verify: (s: AssertableState): void => {
|
||||
s.assertContent(
|
||||
"data.md",
|
||||
"top line\nmiddle line\nbottom line"
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
{ type: "disable-sync", client: 0 },
|
||||
{ type: "disable-sync", client: 1 },
|
||||
|
||||
{
|
||||
type: "update",
|
||||
client: 0,
|
||||
path: "data.md",
|
||||
content: "alpha\nmiddle line\nbottom line"
|
||||
},
|
||||
{
|
||||
type: "update",
|
||||
client: 1,
|
||||
path: "data.md",
|
||||
content: "top line\nmiddle line\nbeta"
|
||||
},
|
||||
|
||||
{ type: "enable-sync", client: 0 },
|
||||
{ type: "enable-sync", client: 1 },
|
||||
{ type: "barrier" },
|
||||
|
||||
{
|
||||
type: "assert-consistent",
|
||||
verify: (s: AssertableState): void => {
|
||||
s.assertFileCount(1).assertContains("data.md", "alpha", "beta");
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import type { AssertableState } from "../utils/assertable-state";
|
||||
import type { TestDefinition } from "../test-definition";
|
||||
|
||||
export const catchupCreateAndUpdateNotSkippedTest: TestDefinition = {
|
||||
description:
|
||||
"Client 1 disconnects (sync disabled). Client 0 creates a doc and " +
|
||||
"then updates it. When Client 1 reconnects, the server's catch-up " +
|
||||
"stream sends only the doc's *latest* version (the update), not the " +
|
||||
"full history. Pre-fix the wire's `is_new_file` was set to " +
|
||||
"`creation == latest_version`, so the catch-up flagged the doc as " +
|
||||
"non-new even though Client 1 had never seen its creation. Client " +
|
||||
"1's `processRemoteChange` then dropped it as a 'stale RemoteChange " +
|
||||
"for untracked, non-new document' and the doc was silently lost. " +
|
||||
"Post-fix `is_new_file` in the catch-up stream means 'new relative " +
|
||||
"to the recipient's watermark' (`creation > last_seen_vault_update_id`).",
|
||||
clients: 2,
|
||||
steps: [
|
||||
{ type: "enable-sync", client: 0 },
|
||||
{ type: "enable-sync", client: 1 },
|
||||
// Establish a baseline so Client 1's last_seen is non-zero before
|
||||
// we take it offline. This makes the bug genuinely about catch-up
|
||||
// missing the create rather than just an empty-vault first sync.
|
||||
{ type: "create", client: 0, path: "warmup.md", content: "w\n" },
|
||||
{ type: "barrier" },
|
||||
|
||||
// Client 1 goes offline.
|
||||
{ type: "disable-sync", client: 1 },
|
||||
|
||||
// Client 0 creates the doc (vault_update_id v_C, after Client 1's
|
||||
// watermark). Client 1 doesn't see this because it's offline.
|
||||
{ type: "create", client: 0, path: "doc.md", content: "v1\n" },
|
||||
// Wait for the create's HTTP to land before the update; otherwise
|
||||
// both writes are coalesced into a single POST and the server
|
||||
// never sees the doc as "create followed by update".
|
||||
{ type: "sync", client: 0 },
|
||||
|
||||
// Client 0 updates the doc (vault_update_id v_X > v_C). The
|
||||
// server's `latest_document_versions` view now returns the
|
||||
// *update* row — its `creation_vault_update_id != vault_update_id`.
|
||||
{
|
||||
type: "update",
|
||||
client: 0,
|
||||
path: "doc.md",
|
||||
content: "v1\nupdate\n"
|
||||
},
|
||||
{ type: "sync", client: 0 },
|
||||
|
||||
// Client 1 reconnects. Server's catch-up replays docs with
|
||||
// `vault_update_id > last_seen`. For doc.md it sends v_X with
|
||||
// `is_new_file` derived from `creation_vault_update_id >
|
||||
// last_seen_vault_update_id` (post-fix) — so Client 1 treats it
|
||||
// as a fresh create and downloads the latest content.
|
||||
{ type: "enable-sync", client: 1 },
|
||||
{ type: "barrier" },
|
||||
|
||||
{
|
||||
type: "assert-consistent",
|
||||
verify: (state: AssertableState): void => {
|
||||
state.assertFileCount(2);
|
||||
state.assertFileExists("doc.md");
|
||||
state.assertContent("doc.md", "v1\nupdate\n");
|
||||
state.assertContent("warmup.md", "w\n");
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import type { AssertableState } from "../utils/assertable-state";
|
||||
import type { TestDefinition } from "../test-definition";
|
||||
|
||||
export const coalesceUpdateRemoteUpdateDataLossTest: TestDefinition = {
|
||||
description:
|
||||
"Divergent offline edits with text-merge expectation. Client 0's " +
|
||||
"remote update fully lands before Client 1 reconnects (`sync`-after " +
|
||||
"the c0 update enforces this), so Client 1's offline edit merges " +
|
||||
"against a server-known version, not a coalesced batch. Both " +
|
||||
"additions must survive in the final merged content. (Filename's " +
|
||||
"'coalesce' framing is aspirational — a true update-coalesce test " +
|
||||
"would skip the c0 sync and queue overlapping local + remote " +
|
||||
"updates against the same parent version.)",
|
||||
clients: 2,
|
||||
steps: [
|
||||
{
|
||||
type: "create",
|
||||
client: 0,
|
||||
path: "doc.md",
|
||||
content: "line 1\nline 2\nline 3"
|
||||
},
|
||||
{ type: "enable-sync", client: 0 },
|
||||
{ type: "enable-sync", client: 1 },
|
||||
{ type: "barrier" },
|
||||
|
||||
{ type: "disable-sync", client: 1 },
|
||||
|
||||
{
|
||||
type: "update",
|
||||
client: 0,
|
||||
path: "doc.md",
|
||||
content: "line 1\nline 2\nline 3\nclient 0 addition"
|
||||
},
|
||||
{ type: "sync", client: 0 },
|
||||
|
||||
{
|
||||
type: "update",
|
||||
client: 1,
|
||||
path: "doc.md",
|
||||
content: "client 1 addition\nline 1\nline 2\nline 3"
|
||||
},
|
||||
|
||||
{ type: "enable-sync", client: 1 },
|
||||
{ type: "barrier" },
|
||||
|
||||
{
|
||||
type: "assert-consistent",
|
||||
verify: (state: AssertableState): void => {
|
||||
state
|
||||
.assertFileCount(1)
|
||||
.assertContains(
|
||||
"doc.md",
|
||||
"client 0 addition",
|
||||
"client 1 addition"
|
||||
);
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import type { AssertableState } from "../utils/assertable-state";
|
||||
import type { TestDefinition } from "../test-definition";
|
||||
|
||||
export const coalescedRemoteUpdateWatermarkLossTest: TestDefinition = {
|
||||
description:
|
||||
"Client 0 sends three rapid updates. After syncing, both clients " +
|
||||
"disconnect and reconnect twice. Content should remain correct " +
|
||||
"after each reconnect.",
|
||||
clients: 2,
|
||||
steps: [
|
||||
{ type: "create", client: 0, path: "doc.md", content: "original" },
|
||||
{ type: "enable-sync", client: 0 },
|
||||
{ type: "enable-sync", client: 1 },
|
||||
{ type: "barrier" },
|
||||
|
||||
{ type: "update", client: 0, path: "doc.md", content: "update 1" },
|
||||
{ type: "update", client: 0, path: "doc.md", content: "update 2" },
|
||||
{ type: "update", client: 0, path: "doc.md", content: "final update" },
|
||||
|
||||
{ type: "barrier" },
|
||||
{
|
||||
type: "assert-consistent",
|
||||
verify: (s: AssertableState): void => {
|
||||
s.assertFileCount(1).assertContent("doc.md", "final update");
|
||||
}
|
||||
},
|
||||
|
||||
{ type: "disable-sync", client: 0 },
|
||||
{ type: "disable-sync", client: 1 },
|
||||
{ type: "enable-sync", client: 0 },
|
||||
{ type: "enable-sync", client: 1 },
|
||||
{ type: "barrier" },
|
||||
|
||||
{
|
||||
type: "assert-consistent",
|
||||
verify: (s: AssertableState): void => {
|
||||
s.assertFileCount(1).assertContent("doc.md", "final update");
|
||||
}
|
||||
},
|
||||
|
||||
{ type: "disable-sync", client: 0 },
|
||||
{ type: "disable-sync", client: 1 },
|
||||
{ type: "enable-sync", client: 0 },
|
||||
{ type: "enable-sync", client: 1 },
|
||||
{ type: "barrier" },
|
||||
{
|
||||
type: "assert-consistent",
|
||||
verify: (s: AssertableState): void => {
|
||||
s.assertFileCount(1).assertContent("doc.md", "final update");
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import type { AssertableState } from "../utils/assertable-state";
|
||||
import type { TestDefinition } from "../test-definition";
|
||||
|
||||
export const concurrentDeleteDuringRemoteUpdateTest: TestDefinition = {
|
||||
description:
|
||||
"One client updates a file while the other deletes it at the same " +
|
||||
"time. Both clients should converge without errors.",
|
||||
clients: 2,
|
||||
steps: [
|
||||
{ type: "create", client: 0, path: "doc.md", content: "original" },
|
||||
{ type: "enable-sync", client: 0 },
|
||||
{ type: "enable-sync", client: 1 },
|
||||
{ type: "barrier" },
|
||||
|
||||
{ type: "disable-sync", client: 0 },
|
||||
{ type: "disable-sync", client: 1 },
|
||||
|
||||
{ type: "update", client: 0, path: "doc.md", content: "updated by 0" },
|
||||
{ type: "delete", client: 1, path: "doc.md" },
|
||||
|
||||
{ type: "enable-sync", client: 0 },
|
||||
{ type: "enable-sync", client: 1 },
|
||||
{ type: "barrier" },
|
||||
|
||||
{
|
||||
type: "assert-consistent",
|
||||
verify: (state: AssertableState): void => {
|
||||
state.assertFileCount(0);
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import type { AssertableState } from "../utils/assertable-state";
|
||||
import type { TestDefinition } from "../test-definition";
|
||||
|
||||
export const concurrentEditExactSamePositionTest: TestDefinition = {
|
||||
description:
|
||||
"Both clients replace the same word in a file with different text " +
|
||||
"while offline. After syncing, the merged result should contain " +
|
||||
"both replacements.",
|
||||
clients: 2,
|
||||
steps: [
|
||||
{
|
||||
type: "create",
|
||||
client: 0,
|
||||
path: "doc.md",
|
||||
content: "the quick brown fox"
|
||||
},
|
||||
{ type: "enable-sync", client: 0 },
|
||||
{ type: "enable-sync", client: 1 },
|
||||
{ type: "barrier" },
|
||||
|
||||
{ type: "disable-sync", client: 0 },
|
||||
{ type: "disable-sync", client: 1 },
|
||||
{
|
||||
type: "update",
|
||||
client: 0,
|
||||
path: "doc.md",
|
||||
content: "the slow brown fox"
|
||||
},
|
||||
{
|
||||
type: "update",
|
||||
client: 1,
|
||||
path: "doc.md",
|
||||
content: "the fast brown fox"
|
||||
},
|
||||
|
||||
{ type: "enable-sync", client: 0 },
|
||||
{ type: "enable-sync", client: 1 },
|
||||
{ type: "barrier" },
|
||||
|
||||
{
|
||||
type: "assert-consistent",
|
||||
verify: (state: AssertableState): void => {
|
||||
state
|
||||
.assertFileCount(1)
|
||||
.assertContains("doc.md", "slow", "fast", "brown fox");
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
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" },
|
||||
|
||||
{ type: "disable-sync", client: 0 },
|
||||
{ type: "disable-sync", client: 1 },
|
||||
|
||||
{ type: "rename", client: 0, oldPath: "X.md", newPath: "Y.md" },
|
||||
|
||||
{
|
||||
type: "create",
|
||||
client: 1,
|
||||
path: "Y.md",
|
||||
content: "brand new Y content"
|
||||
},
|
||||
|
||||
{ type: "enable-sync", client: 1 },
|
||||
{ type: "sync", client: 1 },
|
||||
|
||||
{ type: "enable-sync", client: 0 },
|
||||
{ type: "barrier" },
|
||||
|
||||
{
|
||||
type: "assert-consistent",
|
||||
verify: (state: AssertableState): void => {
|
||||
state
|
||||
.assertFileCount(2)
|
||||
.assertContains("Y (1).md", "original file X")
|
||||
.assertContains("Y.md", "brand new Y content");
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
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" },
|
||||
|
||||
{ type: "disable-sync", client: 0 },
|
||||
{ type: "disable-sync", client: 1 },
|
||||
|
||||
{ type: "rename", client: 0, oldPath: "X.md", newPath: "Y.md" },
|
||||
|
||||
{
|
||||
type: "create",
|
||||
client: 1,
|
||||
path: "Y.md",
|
||||
content: "brand new Y content"
|
||||
},
|
||||
|
||||
{ type: "enable-sync", client: 0 },
|
||||
{ type: "sync", client: 0 },
|
||||
|
||||
{ type: "enable-sync", client: 1 },
|
||||
{ type: "barrier" },
|
||||
|
||||
{
|
||||
type: "assert-consistent",
|
||||
verify: (state: AssertableState): void => {
|
||||
state
|
||||
.assertFileNotExists("X.md")
|
||||
.assertFileExists("Y.md")
|
||||
.assertFileExists("Y (1).md")
|
||||
.assertAnyFileContains(
|
||||
"original file X",
|
||||
"brand new Y content"
|
||||
);
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import type { AssertableState } from "../utils/assertable-state";
|
||||
import type { TestDefinition } from "../test-definition";
|
||||
|
||||
export const concurrentRenameFirstWinsTest: TestDefinition = {
|
||||
description:
|
||||
"Both clients start online with the same file. Both go offline, " +
|
||||
"rename the file to different paths, and edit it. When they reconnect, " +
|
||||
"the first rename to reach the server wins the path and both content " +
|
||||
"edits are merged.",
|
||||
clients: 2,
|
||||
steps: [
|
||||
{
|
||||
type: "create",
|
||||
client: 0,
|
||||
path: "A.md",
|
||||
content: "line 1\nline 2\nline 3"
|
||||
},
|
||||
{ type: "enable-sync", client: 0 },
|
||||
{ type: "enable-sync", client: 1 },
|
||||
{ type: "barrier" },
|
||||
{
|
||||
type: "assert-consistent",
|
||||
verify: (s: AssertableState): void => {
|
||||
s.assertContent("A.md", "line 1\nline 2\nline 3");
|
||||
}
|
||||
},
|
||||
|
||||
{ type: "disable-sync", client: 0 },
|
||||
{ type: "disable-sync", client: 1 },
|
||||
|
||||
{ type: "rename", client: 0, oldPath: "A.md", newPath: "B.md" },
|
||||
{
|
||||
type: "update",
|
||||
client: 0,
|
||||
path: "B.md",
|
||||
content: "edit from 0\nline 2\nline 3"
|
||||
},
|
||||
|
||||
{ type: "rename", client: 1, oldPath: "A.md", newPath: "C.md" },
|
||||
{
|
||||
type: "update",
|
||||
client: 1,
|
||||
path: "C.md",
|
||||
content: "line 1\nline 2\nedit from 1"
|
||||
},
|
||||
|
||||
{ type: "enable-sync", client: 0 },
|
||||
{ type: "enable-sync", client: 1 },
|
||||
{ type: "barrier" },
|
||||
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import type { AssertableState } from "../utils/assertable-state";
|
||||
import type { TestDefinition } from "../test-definition";
|
||||
|
||||
export const concurrentRenameSameTargetTest: TestDefinition = {
|
||||
description:
|
||||
"One client renames A to C while the other renames B to C, both offline. " +
|
||||
"After syncing, both file contents should be preserved via path deconfliction.",
|
||||
clients: 2,
|
||||
steps: [
|
||||
{ type: "create", client: 0, path: "A.md", content: "content-a" },
|
||||
{ type: "create", client: 0, path: "B.md", content: "content-b" },
|
||||
{ type: "enable-sync", client: 0 },
|
||||
{ type: "enable-sync", client: 1 },
|
||||
{ type: "barrier" },
|
||||
|
||||
{ type: "disable-sync", client: 1 },
|
||||
|
||||
{ type: "rename", client: 0, oldPath: "A.md", newPath: "C.md" },
|
||||
{ type: "sync", client: 0 },
|
||||
|
||||
{ type: "rename", client: 1, oldPath: "B.md", newPath: "C.md" },
|
||||
|
||||
{ type: "enable-sync", client: 1 },
|
||||
{ type: "barrier" },
|
||||
|
||||
{
|
||||
type: "assert-consistent",
|
||||
verify: (state: AssertableState): void => {
|
||||
state
|
||||
.assertFileCount(2)
|
||||
.assertFileNotExists("A.md")
|
||||
.assertFileNotExists("B.md")
|
||||
.assertFileExists("C.md")
|
||||
.assertFileExists("C (1).md")
|
||||
.assertAnyFileContains("content-a", "content-b");
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import type { AssertableState } from "../utils/assertable-state";
|
||||
import type { TestDefinition } from "../test-definition";
|
||||
|
||||
export const concurrentUpdateDiffConsistencyTest: TestDefinition = {
|
||||
description:
|
||||
"Both clients edit different sections of the same file while offline. " +
|
||||
"After syncing, the merged file should contain both edits.",
|
||||
clients: 2,
|
||||
steps: [
|
||||
{
|
||||
type: "create",
|
||||
client: 0,
|
||||
path: "doc.md",
|
||||
content: "header\nmiddle\nfooter"
|
||||
},
|
||||
{ type: "enable-sync", client: 0 },
|
||||
{ type: "enable-sync", client: 1 },
|
||||
{ type: "barrier" },
|
||||
|
||||
{ type: "disable-sync", client: 0 },
|
||||
{ type: "disable-sync", client: 1 },
|
||||
{
|
||||
type: "update",
|
||||
client: 0,
|
||||
path: "doc.md",
|
||||
content: "header by 0\nmiddle\nfooter"
|
||||
},
|
||||
{
|
||||
type: "update",
|
||||
client: 1,
|
||||
path: "doc.md",
|
||||
content: "header\nmiddle\nfooter by 1"
|
||||
},
|
||||
|
||||
{ type: "enable-sync", client: 0 },
|
||||
{ type: "enable-sync", client: 1 },
|
||||
{ type: "barrier" },
|
||||
|
||||
{
|
||||
type: "assert-consistent",
|
||||
verify: (state: AssertableState): void => {
|
||||
state
|
||||
.assertFileCount(1)
|
||||
.assertContent(
|
||||
"doc.md",
|
||||
"header by 0\nmiddle\nfooter by 1"
|
||||
);
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import type { AssertableState } from "../utils/assertable-state";
|
||||
import type { TestDefinition } from "../test-definition";
|
||||
|
||||
export const createDeleteNoopTest: TestDefinition = {
|
||||
description:
|
||||
"A client creates a file, updates it multiple times, then deletes it, all while " +
|
||||
"offline. After syncing, neither client should have the file.",
|
||||
clients: 2,
|
||||
steps: [
|
||||
{ type: "enable-sync", client: 1 },
|
||||
|
||||
{ type: "create", client: 0, path: "temp.md", content: "version 1" },
|
||||
{ type: "update", client: 0, path: "temp.md", content: "version 2" },
|
||||
{ type: "update", client: 0, path: "temp.md", content: "version 3" },
|
||||
{ type: "delete", client: 0, path: "temp.md" },
|
||||
|
||||
{ type: "enable-sync", client: 0 },
|
||||
{ type: "barrier" },
|
||||
|
||||
{
|
||||
type: "assert-consistent",
|
||||
verify: (s: AssertableState): void => {
|
||||
s.assertFileNotExists("temp.md");
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import type { AssertableState } from "../utils/assertable-state";
|
||||
import type { TestDefinition } from "../test-definition";
|
||||
|
||||
export const createDuringReconciliationTest: TestDefinition = {
|
||||
description:
|
||||
"Client creates two files while offline, reconnects, then immediately " +
|
||||
"creates a third file. All three files should sync to the other client.",
|
||||
clients: 2,
|
||||
steps: [
|
||||
{ type: "enable-sync", client: 0 },
|
||||
{ type: "enable-sync", client: 1 },
|
||||
{ type: "barrier" },
|
||||
|
||||
{ type: "disable-sync", client: 0 },
|
||||
{
|
||||
type: "create",
|
||||
client: 0,
|
||||
path: "A.md",
|
||||
content: "offline A"
|
||||
},
|
||||
{
|
||||
type: "create",
|
||||
client: 0,
|
||||
path: "B.md",
|
||||
content: "offline B"
|
||||
},
|
||||
|
||||
{ type: "enable-sync", client: 0 },
|
||||
|
||||
{
|
||||
type: "create",
|
||||
client: 0,
|
||||
path: "C.md",
|
||||
content: "post-reconnect C"
|
||||
},
|
||||
|
||||
{ type: "barrier" },
|
||||
|
||||
{
|
||||
type: "assert-consistent",
|
||||
verify: (state: AssertableState): void => {
|
||||
state
|
||||
.assertFileCount(3)
|
||||
.assertContent("A.md", "offline A")
|
||||
.assertContent("B.md", "offline B")
|
||||
.assertContent("C.md", "post-reconnect C");
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import type { AssertableState } from "../utils/assertable-state";
|
||||
import type { TestDefinition } from "../test-definition";
|
||||
|
||||
export const createMergeDeleteTest: TestDefinition = {
|
||||
description:
|
||||
"Two clients create A.md offline with different content. Both come online and " +
|
||||
"the content is merged. Then one client deletes A.md. Both clients should " +
|
||||
"converge on an empty state.",
|
||||
clients: 2,
|
||||
steps: [
|
||||
{ type: "create", client: 0, path: "A.md", content: "from-zero" },
|
||||
{ type: "create", client: 1, path: "A.md", content: "from-one" },
|
||||
|
||||
{ type: "enable-sync", client: 0 },
|
||||
{ type: "enable-sync", client: 1 },
|
||||
{ type: "barrier" },
|
||||
|
||||
{
|
||||
type: "assert-consistent",
|
||||
verify: (state: AssertableState): void => {
|
||||
state
|
||||
.assertFileCount(1)
|
||||
.assertContains("A.md", "from-zero", "from-one");
|
||||
}
|
||||
},
|
||||
|
||||
{ type: "delete", client: 0, path: "A.md" },
|
||||
{ type: "barrier" },
|
||||
|
||||
{
|
||||
type: "assert-consistent",
|
||||
verify: (s: AssertableState): void => {
|
||||
s.assertFileCount(0).assertFileNotExists("A.md");
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import type { AssertableState } from "../utils/assertable-state";
|
||||
import type { TestDefinition } from "../test-definition";
|
||||
|
||||
export const createMergePreservesRenamedUpdateTest: TestDefinition = {
|
||||
description:
|
||||
"Both clients create the same file, which gets merged. One client goes " +
|
||||
"offline, renames the file, updates it, and creates a new file at the " +
|
||||
"original path. After reconnecting, the updated content must be preserved.",
|
||||
clients: 2,
|
||||
steps: [
|
||||
{ type: "create", client: 0, path: "doc.md", content: "alpha" },
|
||||
{ type: "create", client: 1, path: "doc.md", content: "beta" },
|
||||
|
||||
{ type: "enable-sync", client: 0 },
|
||||
{ type: "enable-sync", client: 1 },
|
||||
{ type: "barrier" },
|
||||
|
||||
{
|
||||
type: "assert-consistent",
|
||||
verify: (state: AssertableState): void => {
|
||||
state.assertContains("doc.md", "alpha", "beta");
|
||||
}
|
||||
},
|
||||
|
||||
{ type: "disable-sync", client: 1 },
|
||||
|
||||
{
|
||||
type: "rename",
|
||||
client: 1,
|
||||
oldPath: "doc.md",
|
||||
newPath: "moved.md"
|
||||
},
|
||||
{
|
||||
type: "update",
|
||||
client: 1,
|
||||
path: "moved.md",
|
||||
content: "alpha beta extra-update"
|
||||
},
|
||||
|
||||
{
|
||||
type: "create",
|
||||
client: 1,
|
||||
path: "doc.md",
|
||||
content: "new-content"
|
||||
},
|
||||
|
||||
{ type: "enable-sync", client: 1 },
|
||||
{ type: "barrier" },
|
||||
|
||||
{
|
||||
type: "assert-consistent",
|
||||
verify: (state: AssertableState): void => {
|
||||
state
|
||||
.assertContent("moved.md", "alpha beta extra-update")
|
||||
.assertContent("doc.md", "new-content");
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import type { AssertableState } from "../utils/assertable-state";
|
||||
import type { TestDefinition } from "../test-definition";
|
||||
|
||||
export const createRenameCreateSamePathTest: TestDefinition = {
|
||||
description:
|
||||
"Client creates A.md, renames to B.md, creates new A.md, renames " +
|
||||
"to C.md, creates yet another A.md. All three files should exist " +
|
||||
"as separate documents on both clients.",
|
||||
clients: 2,
|
||||
steps: [
|
||||
{ type: "create", client: 0, path: "A.md", content: "first file" },
|
||||
{ type: "rename", client: 0, oldPath: "A.md", newPath: "B.md" },
|
||||
|
||||
{ type: "create", client: 0, path: "A.md", content: "second file" },
|
||||
{ type: "rename", client: 0, oldPath: "A.md", newPath: "C.md" },
|
||||
|
||||
{ type: "create", client: 0, path: "A.md", content: "third file" },
|
||||
|
||||
{ type: "enable-sync", client: 0 },
|
||||
{ type: "enable-sync", client: 1 },
|
||||
{ type: "barrier" },
|
||||
|
||||
{
|
||||
type: "assert-consistent",
|
||||
verify: (state: AssertableState): void => {
|
||||
state
|
||||
.assertFileCount(3)
|
||||
.assertContent("B.md", "first file")
|
||||
.assertContent("C.md", "second file")
|
||||
.assertContent("A.md", "third file");
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import type { AssertableState } from "../utils/assertable-state";
|
||||
import type { TestDefinition } from "../test-definition";
|
||||
|
||||
export const createRenameResponseSkipsFileTest: TestDefinition = {
|
||||
description:
|
||||
"Client 0 creates a file online then immediately renames it. " +
|
||||
"Client 1 must receive the file content at the renamed path.",
|
||||
clients: 2,
|
||||
steps: [
|
||||
{ type: "enable-sync", client: 0 },
|
||||
{ type: "enable-sync", client: 1 },
|
||||
|
||||
{
|
||||
type: "create",
|
||||
client: 0,
|
||||
path: "doc.md",
|
||||
content: "the-content"
|
||||
},
|
||||
|
||||
{
|
||||
type: "rename",
|
||||
client: 0,
|
||||
oldPath: "doc.md",
|
||||
newPath: "renamed.md"
|
||||
},
|
||||
|
||||
{ type: "barrier" },
|
||||
|
||||
{
|
||||
type: "assert-consistent",
|
||||
verify: (s: AssertableState): void => {
|
||||
s.assertFileCount(1).assertAnyFileContains("the-content");
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import type { AssertableState } from "../utils/assertable-state";
|
||||
import type { TestDefinition } from "../test-definition";
|
||||
|
||||
export const createUpdateCoalesceServerPauseTest: TestDefinition = {
|
||||
description:
|
||||
"Client creates a file and immediately updates it while the server is " +
|
||||
"paused. When the server resumes, both clients should have the final " +
|
||||
"updated content.",
|
||||
clients: 2,
|
||||
steps: [
|
||||
{ type: "enable-sync", client: 0 },
|
||||
{ type: "enable-sync", client: 1 },
|
||||
|
||||
{ type: "pause-server" },
|
||||
|
||||
{ type: "create", client: 0, path: "doc.md", content: "initial" },
|
||||
{ type: "update", client: 0, path: "doc.md", content: "final version" },
|
||||
|
||||
{ type: "resume-server" },
|
||||
|
||||
{ type: "barrier" },
|
||||
|
||||
{
|
||||
type: "assert-consistent",
|
||||
verify: (state: AssertableState): void => {
|
||||
state
|
||||
.assertFileCount(1)
|
||||
.assertContent("doc.md", "final version");
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import type { AssertableState } from "../utils/assertable-state";
|
||||
import type { TestDefinition } from "../test-definition";
|
||||
|
||||
export const deleteByOtherClientThenRecreateTest: TestDefinition = {
|
||||
description:
|
||||
"Client 1 deletes a file and the delete propagates. Then client 0 " +
|
||||
"creates a new file at the same path. Both clients must have the file.",
|
||||
clients: 2,
|
||||
steps: [
|
||||
{ type: "create", client: 0, path: "A.md", content: "original" },
|
||||
{ type: "enable-sync", client: 0 },
|
||||
{ type: "enable-sync", client: 1 },
|
||||
{ type: "barrier" },
|
||||
|
||||
{ type: "delete", client: 1, path: "A.md" },
|
||||
{ type: "barrier" },
|
||||
|
||||
{
|
||||
type: "assert-consistent",
|
||||
verify: (s: AssertableState): void => {
|
||||
s.assertFileNotExists("A.md");
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
type: "create",
|
||||
client: 0,
|
||||
path: "A.md",
|
||||
content: "recreated by client 0"
|
||||
},
|
||||
{ type: "barrier" },
|
||||
|
||||
{
|
||||
type: "assert-consistent",
|
||||
verify: (s: AssertableState): void => {
|
||||
s.assertContent("A.md", "recreated by client 0");
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue