Better det tests

This commit is contained in:
Andras Schmelczer 2026-09-27 21:51:01 +01:00
commit 77521c970b
62 changed files with 395 additions and 646 deletions

View file

@ -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"
);
}

View file

@ -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"
);
}
}
]

View file

@ -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"
);
}
}
]

View file

@ -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"
);
}
}
]

View file

@ -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"
);
}
}
}
]
};
]
};

View file

@ -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"
);
}
}
}
]
};
]
};

View file

@ -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");
}
}
]

View file

@ -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");
}
}
]

View file

@ -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"
);
}
},

View file

@ -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"
);
}
},

View file

@ -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");
}
}
]

View file

@ -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");
}
},

View file

@ -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" },
{

View file

@ -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");
}
}
]

View file

@ -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 },

View file

@ -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")
);
}
}
]

View file

@ -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" },

View file

@ -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" },
{

View file

@ -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"
);
}
}

View file

@ -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" }
]
}
]
};

Some files were not shown because too many files have changed in this diff Show more