Better det tests
This commit is contained in:
parent
6a19587d7d
commit
77521c970b
62 changed files with 395 additions and 646 deletions
|
|
@ -34,10 +34,9 @@ export const binaryPendingCreateNotDisplacedTest: TestDefinition = {
|
||||||
type: "assert-consistent",
|
type: "assert-consistent",
|
||||||
verify: (s: AssertableState): void => {
|
verify: (s: AssertableState): void => {
|
||||||
s.assertFileCount(2)
|
s.assertFileCount(2)
|
||||||
.assertFileExists("data.bin")
|
.assertContent("data.bin", "binary data from client 0")
|
||||||
.assertFileExists(s.conflictPath("data.bin"))
|
.assertContent(
|
||||||
.assertAnyFileContains(
|
s.conflictPath("data.bin"),
|
||||||
"binary data from client 0",
|
|
||||||
"binary data from client 1"
|
"binary data from client 1"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,11 +38,7 @@ export const binaryToTextTransitionTest: TestDefinition = {
|
||||||
{
|
{
|
||||||
type: "assert-consistent",
|
type: "assert-consistent",
|
||||||
verify: (s: AssertableState): void => {
|
verify: (s: AssertableState): void => {
|
||||||
s.assertFileCount(1).assertContainsAny(
|
s.assertFileCount(1).assertContent("data.bin", "version A");
|
||||||
"data.bin",
|
|
||||||
"version A",
|
|
||||||
"version B"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -90,7 +86,10 @@ export const binaryToTextTransitionTest: TestDefinition = {
|
||||||
{
|
{
|
||||||
type: "assert-consistent",
|
type: "assert-consistent",
|
||||||
verify: (s: AssertableState): void => {
|
verify: (s: AssertableState): void => {
|
||||||
s.assertFileCount(1).assertContains("data.md", "alpha", "beta");
|
s.assertFileCount(1).assertContent(
|
||||||
|
"data.md",
|
||||||
|
"alpha\nmiddle line\nbeta"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -45,13 +45,11 @@ export const coalesceUpdateRemoteUpdateDataLossTest: TestDefinition = {
|
||||||
|
|
||||||
{
|
{
|
||||||
type: "assert-consistent",
|
type: "assert-consistent",
|
||||||
verify: (state: AssertableState): void => {
|
verify: (s: AssertableState): void => {
|
||||||
state
|
s.assertFileCount(1);
|
||||||
.assertFileCount(1)
|
s.assertContent(
|
||||||
.assertContains(
|
|
||||||
"doc.md",
|
"doc.md",
|
||||||
"client 0 addition",
|
"client 1 addition\nline 1\nline 2\nline 3\nclient 0 addition"
|
||||||
"client 1 addition"
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,11 @@ export const concurrentEditExactSamePositionTest: TestDefinition = {
|
||||||
verify: (state: AssertableState): void => {
|
verify: (state: AssertableState): void => {
|
||||||
state
|
state
|
||||||
.assertFileCount(1)
|
.assertFileCount(1)
|
||||||
.assertContains("doc.md", "slow", "fast", "brown fox");
|
.assertContentOneOf(
|
||||||
|
"doc.md",
|
||||||
|
"the fast slow brown fox",
|
||||||
|
"the slow fast brown fox"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import type { AssertableState } from "../utils/assertable-state";
|
import type { AssertableState } from "../utils/assertable-state";
|
||||||
import type { TestDefinition } from "../test-definition";
|
import type { TestDefinition } from "../test-definition";
|
||||||
|
|
||||||
export const concurrentRenameAndCreateAtTargetCreateFirstTest: TestDefinition = {
|
export const concurrentRenameAndCreateAtTargetCreateFirstTest: TestDefinition =
|
||||||
|
{
|
||||||
description:
|
description:
|
||||||
"One client renames X to Y while another creates a new file at Y, " +
|
"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 offline. After syncing, Y should contain merged content from " +
|
||||||
|
|
@ -38,11 +39,13 @@ export const concurrentRenameAndCreateAtTargetCreateFirstTest: TestDefinition =
|
||||||
|
|
||||||
{
|
{
|
||||||
type: "assert-consistent",
|
type: "assert-consistent",
|
||||||
verify: (state: AssertableState): void => {
|
verify: (s: AssertableState): void => {
|
||||||
state
|
s.assertFileCount(2)
|
||||||
.assertFileCount(2)
|
.assertContent("Y.md", "brand new Y content")
|
||||||
.assertContains(state.conflictPath("Y.md"), "original file X")
|
.assertContent(
|
||||||
.assertContains("Y.md", "brand new Y content");
|
s.conflictPath("Y.md"),
|
||||||
|
"original file X"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import type { AssertableState } from "../utils/assertable-state";
|
import type { AssertableState } from "../utils/assertable-state";
|
||||||
import type { TestDefinition } from "../test-definition";
|
import type { TestDefinition } from "../test-definition";
|
||||||
|
|
||||||
export const concurrentRenameAndCreateAtTargetRenameFirstTest: TestDefinition = {
|
export const concurrentRenameAndCreateAtTargetRenameFirstTest: TestDefinition =
|
||||||
|
{
|
||||||
description:
|
description:
|
||||||
"One client renames X to Y while another creates a new file at Y, " +
|
"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",
|
"both offline. We can't merge the create because it would result in a cycle",
|
||||||
|
|
@ -37,13 +38,11 @@ export const concurrentRenameAndCreateAtTargetRenameFirstTest: TestDefinition =
|
||||||
|
|
||||||
{
|
{
|
||||||
type: "assert-consistent",
|
type: "assert-consistent",
|
||||||
verify: (state: AssertableState): void => {
|
verify: (s: AssertableState): void => {
|
||||||
state
|
s.assertFileCount(2)
|
||||||
.assertFileNotExists("X.md")
|
.assertContent("Y.md", "original file X")
|
||||||
.assertFileExists("Y.md")
|
.assertContent(
|
||||||
.assertFileExists(state.conflictPath("Y.md"))
|
s.conflictPath("Y.md"),
|
||||||
.assertAnyFileContains(
|
|
||||||
"original file X",
|
|
||||||
"brand new Y content"
|
"brand new Y content"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,14 +25,10 @@ export const concurrentRenameSameTargetTest: TestDefinition = {
|
||||||
|
|
||||||
{
|
{
|
||||||
type: "assert-consistent",
|
type: "assert-consistent",
|
||||||
verify: (state: AssertableState): void => {
|
verify: (s: AssertableState): void => {
|
||||||
state
|
s.assertFileCount(2)
|
||||||
.assertFileCount(2)
|
.assertContent("C.md", "content-a")
|
||||||
.assertFileNotExists("A.md")
|
.assertContent(s.conflictPath("C.md"), "content-b");
|
||||||
.assertFileNotExists("B.md")
|
|
||||||
.assertFileExists("C.md")
|
|
||||||
.assertFileExists(state.conflictPath("C.md"))
|
|
||||||
.assertAnyFileContains("content-a", "content-b");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ export const createDeleteNoopTest: TestDefinition = {
|
||||||
{
|
{
|
||||||
type: "assert-consistent",
|
type: "assert-consistent",
|
||||||
verify: (s: AssertableState): void => {
|
verify: (s: AssertableState): void => {
|
||||||
s.assertFileNotExists("temp.md");
|
s.assertFileCount(0).assertFileNotExists("temp.md");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,14 @@ export const createMergeDeleteTest: TestDefinition = {
|
||||||
|
|
||||||
{
|
{
|
||||||
type: "assert-consistent",
|
type: "assert-consistent",
|
||||||
verify: (state: AssertableState): void => {
|
verify: (s: AssertableState): void => {
|
||||||
state
|
s.assertFileCount(1).assertContentOneOf(
|
||||||
.assertFileCount(1)
|
"A.md",
|
||||||
.assertContains("A.md", "from-zero", "from-one");
|
"from-zerofrom-one",
|
||||||
|
"from-onefrom-zero",
|
||||||
|
"from-zero from-one",
|
||||||
|
"from-one from-zero"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,14 @@ export const createMergePreservesRenamedUpdateTest: TestDefinition = {
|
||||||
|
|
||||||
{
|
{
|
||||||
type: "assert-consistent",
|
type: "assert-consistent",
|
||||||
verify: (state: AssertableState): void => {
|
verify: (s: AssertableState): void => {
|
||||||
state.assertContains("doc.md", "alpha", "beta");
|
s.assertFileCount(1).assertContentOneOf(
|
||||||
|
"doc.md",
|
||||||
|
"alphabeta",
|
||||||
|
"betaalpha",
|
||||||
|
"alpha beta",
|
||||||
|
"beta alpha"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,8 @@ export const createRenameResponseSkipsFileTest: TestDefinition = {
|
||||||
{
|
{
|
||||||
type: "assert-consistent",
|
type: "assert-consistent",
|
||||||
verify: (s: AssertableState): void => {
|
verify: (s: AssertableState): void => {
|
||||||
s.assertFileCount(1).assertAnyFileContains("the-content");
|
s.assertFileCount(1);
|
||||||
|
s.assertContent("renamed.md", "the-content");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ export const deleteByOtherClientThenRecreateTest: TestDefinition = {
|
||||||
{
|
{
|
||||||
type: "assert-consistent",
|
type: "assert-consistent",
|
||||||
verify: (s: AssertableState): void => {
|
verify: (s: AssertableState): void => {
|
||||||
s.assertFileNotExists("A.md");
|
s.assertFileCount(0).assertFileNotExists("A.md");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,15 @@ import type { TestDefinition } from "../test-definition";
|
||||||
|
|
||||||
export const deleteDuringPendingCreateTest: TestDefinition = {
|
export const deleteDuringPendingCreateTest: TestDefinition = {
|
||||||
description:
|
description:
|
||||||
"Client 0 creates a file while the server is paused, then deletes it before the server resumes. " +
|
"Client 0 creates a file while its create request is held, then deletes it before releasing the request. " +
|
||||||
"After resume, the file should end up deleted on both clients.",
|
"After release, the file should end up deleted on both clients.",
|
||||||
clients: 2,
|
clients: 2,
|
||||||
steps: [
|
steps: [
|
||||||
{ type: "enable-sync", client: 0 },
|
{ type: "enable-sync", client: 0 },
|
||||||
{ type: "enable-sync", client: 1 },
|
{ type: "enable-sync", client: 1 },
|
||||||
{ type: "barrier" },
|
{ type: "barrier" },
|
||||||
|
|
||||||
{ type: "pause-server" },
|
{ type: "hold-request", client: 0, kind: "create", point: "before" },
|
||||||
|
|
||||||
{
|
{
|
||||||
type: "create",
|
type: "create",
|
||||||
|
|
@ -19,10 +19,11 @@ export const deleteDuringPendingCreateTest: TestDefinition = {
|
||||||
path: "ephemeral.md",
|
path: "ephemeral.md",
|
||||||
content: "this will be deleted"
|
content: "this will be deleted"
|
||||||
},
|
},
|
||||||
|
{ type: "wait-for-request", client: 0 },
|
||||||
|
|
||||||
{ type: "delete", client: 0, path: "ephemeral.md" },
|
{ type: "delete", client: 0, path: "ephemeral.md" },
|
||||||
|
|
||||||
{ type: "resume-server" },
|
{ type: "release-request", client: 0 },
|
||||||
{ type: "barrier" },
|
{ type: "barrier" },
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,8 @@ export const deleteRecreateConcurrentUpdateTest: TestDefinition = {
|
||||||
{
|
{
|
||||||
type: "assert-consistent",
|
type: "assert-consistent",
|
||||||
verify: (s: AssertableState): void => {
|
verify: (s: AssertableState): void => {
|
||||||
s.assertFileExists("A.md").assertContains("A.md", "recreated");
|
s.assertFileCount(1);
|
||||||
|
s.assertContent("A.md", "recreated by client 0");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,9 @@ import type { TestDefinition } from "../test-definition";
|
||||||
export const deleteRecreatedPendingCreateWithStaleDeletingRecordTest: TestDefinition =
|
export const deleteRecreatedPendingCreateWithStaleDeletingRecordTest: TestDefinition =
|
||||||
{
|
{
|
||||||
description:
|
description:
|
||||||
"A local delete for a recreated pending create must target the " +
|
"Delete two successive generations while each create request is " +
|
||||||
"new pending create, not an older same-path record whose server " +
|
"held before send. Retrying and acknowledging the old generation " +
|
||||||
"delete has been acked but whose WebSocket delete receipt is " +
|
"must not resurrect either file, even with delayed WebSocket hints.",
|
||||||
"still paused.",
|
|
||||||
clients: 2,
|
clients: 2,
|
||||||
steps: [
|
steps: [
|
||||||
{ type: "enable-sync", client: 0 },
|
{ type: "enable-sync", client: 0 },
|
||||||
|
|
@ -15,28 +14,38 @@ export const deleteRecreatedPendingCreateWithStaleDeletingRecordTest: TestDefini
|
||||||
{ type: "barrier" },
|
{ type: "barrier" },
|
||||||
|
|
||||||
{ type: "pause-websocket", client: 0 },
|
{ type: "pause-websocket", client: 0 },
|
||||||
{ type: "pause-server" },
|
{
|
||||||
|
type: "hold-request",
|
||||||
|
client: 0,
|
||||||
|
kind: "create",
|
||||||
|
point: "before"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: "create",
|
type: "create",
|
||||||
client: 0,
|
client: 0,
|
||||||
path: "binary-14.bin",
|
path: "binary-14.bin",
|
||||||
content: "BINARY:first"
|
content: "BINARY:first"
|
||||||
},
|
},
|
||||||
{ type: "sleep", ms: 100 },
|
{ type: "wait-for-request", client: 0 },
|
||||||
{ type: "delete", client: 0, path: "binary-14.bin" },
|
{ type: "delete", client: 0, path: "binary-14.bin" },
|
||||||
{ type: "resume-server" },
|
{ type: "release-request", client: 0 },
|
||||||
{ type: "sync", client: 0 },
|
{ type: "sync", client: 0 },
|
||||||
|
|
||||||
{ type: "pause-server" },
|
{
|
||||||
|
type: "hold-request",
|
||||||
|
client: 0,
|
||||||
|
kind: "create",
|
||||||
|
point: "before"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: "create",
|
type: "create",
|
||||||
client: 0,
|
client: 0,
|
||||||
path: "binary-14.bin",
|
path: "binary-14.bin",
|
||||||
content: "BINARY:second"
|
content: "BINARY:second"
|
||||||
},
|
},
|
||||||
{ type: "sleep", ms: 100 },
|
{ type: "wait-for-request", client: 0 },
|
||||||
{ type: "delete", client: 0, path: "binary-14.bin" },
|
{ type: "delete", client: 0, path: "binary-14.bin" },
|
||||||
{ type: "resume-server" },
|
{ type: "release-request", client: 0 },
|
||||||
{ type: "sync", client: 0 },
|
{ type: "sync", client: 0 },
|
||||||
|
|
||||||
{ type: "resume-websocket", client: 0 },
|
{ type: "resume-websocket", client: 0 },
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ export const deleteRenameConflictTest: TestDefinition = {
|
||||||
{
|
{
|
||||||
type: "assert-consistent",
|
type: "assert-consistent",
|
||||||
verify: (s: AssertableState): void => {
|
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",
|
type: "assert-consistent",
|
||||||
verify: (s: AssertableState): void => {
|
verify: (s: AssertableState): void => {
|
||||||
|
s.assertFileCount(1);
|
||||||
s.assertContent("B.md", "content-b");
|
s.assertContent("B.md", "content-b");
|
||||||
s.assertFileNotExists("A.md");
|
|
||||||
s.ifFileExists("C.md", (inner) =>
|
|
||||||
inner.assertContent("C.md", "content-a")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,13 @@ import type { TestDefinition } from "../test-definition";
|
||||||
|
|
||||||
export const idempotencyAfterServerPauseTest: TestDefinition = {
|
export const idempotencyAfterServerPauseTest: TestDefinition = {
|
||||||
description:
|
description:
|
||||||
"Client 0 creates a file, then the server is paused mid-response. " +
|
"Lose an accepted create response and require an identical retry, preserving one exact copy.",
|
||||||
"After the server resumes, both clients must converge to a single copy of the file with no duplicates.",
|
|
||||||
clients: 2,
|
clients: 2,
|
||||||
steps: [
|
steps: [
|
||||||
{ type: "enable-sync", client: 0 },
|
{ type: "enable-sync", client: 0 },
|
||||||
{ type: "enable-sync", client: 1 },
|
{ type: "enable-sync", client: 1 },
|
||||||
{ type: "barrier" },
|
{ type: "barrier" },
|
||||||
|
{ type: "drop-response", client: 0, kind: "create", point: "after" },
|
||||||
|
|
||||||
{
|
{
|
||||||
type: "create",
|
type: "create",
|
||||||
|
|
@ -17,9 +17,7 @@ export const idempotencyAfterServerPauseTest: TestDefinition = {
|
||||||
path: "doc.md",
|
path: "doc.md",
|
||||||
content: "important data"
|
content: "important data"
|
||||||
},
|
},
|
||||||
{ type: "pause-server" },
|
{ type: "wait-for-response-drop", client: 0 },
|
||||||
|
|
||||||
{ type: "resume-server" },
|
|
||||||
|
|
||||||
{ type: "barrier" },
|
{ type: "barrier" },
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,19 @@ import type { TestDefinition } from "../test-definition";
|
||||||
|
|
||||||
export const interruptedDeleteRetryTest: TestDefinition = {
|
export const interruptedDeleteRetryTest: TestDefinition = {
|
||||||
description:
|
description:
|
||||||
"Client 0 deletes a file, then the server is paused. " +
|
"Lose an accepted deletion manifest response and require an identical retry, with both clients empty.",
|
||||||
"After the server resumes, both clients should have zero files.",
|
|
||||||
clients: 2,
|
clients: 2,
|
||||||
steps: [
|
steps: [
|
||||||
{ type: "create", client: 0, path: "doc.md", content: "to be deleted" },
|
{ type: "create", client: 0, path: "doc.md", content: "to be deleted" },
|
||||||
{ type: "enable-sync", client: 0 },
|
{ type: "enable-sync", client: 0 },
|
||||||
{ type: "enable-sync", client: 1 },
|
{ type: "enable-sync", client: 1 },
|
||||||
{ type: "barrier" },
|
{ type: "barrier" },
|
||||||
|
{ type: "drop-response", client: 0, kind: "manifest", point: "after" },
|
||||||
|
|
||||||
{ type: "delete", client: 0, path: "doc.md" },
|
{ type: "delete", client: 0, path: "doc.md" },
|
||||||
|
|
||||||
{ type: "pause-server" },
|
{ type: "wait-for-response-drop", client: 0 },
|
||||||
|
|
||||||
{ type: "resume-server" },
|
|
||||||
{ type: "barrier" },
|
{ type: "barrier" },
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -30,10 +30,12 @@ export const localEditLostDuringCreateMergeTest: TestDefinition = {
|
||||||
{
|
{
|
||||||
type: "assert-consistent",
|
type: "assert-consistent",
|
||||||
verify: (s: AssertableState): void => {
|
verify: (s: AssertableState): void => {
|
||||||
s.assertFileCount(1).assertContains(
|
s.assertFileCount(1).assertContentOneOf(
|
||||||
"doc.md",
|
"doc.md",
|
||||||
"from-client-1",
|
"from-client-1local-edit-during-create",
|
||||||
"local-edit-during-create"
|
"local-edit-during-createfrom-client-1",
|
||||||
|
"from-client-1 local-edit-during-create",
|
||||||
|
"local-edit-during-create from-client-1"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,8 @@
|
||||||
import type { AssertableState } from "../utils/assertable-state";
|
|
||||||
import type { TestDefinition } from "../test-definition";
|
import type { TestDefinition } from "../test-definition";
|
||||||
|
|
||||||
export const localRenameSurvivesRemoteRenameTest: TestDefinition = {
|
export const localRenameSurvivesRemoteRenameTest: TestDefinition = {
|
||||||
description:
|
description:
|
||||||
"Drain processes a RemoteChange (remote rename for doc D) while a " +
|
"A held remote manifest races with a local rename and a separate edit; the first committed rename wins and both identities survive.",
|
||||||
"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.",
|
|
||||||
clients: 2,
|
clients: 2,
|
||||||
steps: [
|
steps: [
|
||||||
{ type: "enable-sync", client: 0 },
|
{ 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: "doc.md", content: "v1\n" },
|
||||||
{ type: "create", client: 0, path: "sentinel.md", content: "s\n" },
|
{ type: "create", client: 0, path: "sentinel.md", content: "s\n" },
|
||||||
{ type: "barrier" },
|
{ type: "barrier" },
|
||||||
|
{
|
||||||
// Pause client 0's WebSocket so the upcoming remote rename buffers.
|
type: "assert-documents",
|
||||||
{ type: "pause-websocket", client: 0 },
|
expected: [
|
||||||
|
{ key: "note", path: "doc.md", content: "v1\n" },
|
||||||
// Server applies remote rename of doc.md -> remote.md. Broadcast
|
{ key: "sentinel", path: "sentinel.md", content: "s\n" }
|
||||||
// is buffered on client 0's WebSocket.
|
]
|
||||||
|
},
|
||||||
|
{ type: "hold-request", client: 0, kind: "events", point: "after" },
|
||||||
{ type: "rename", client: 1, oldPath: "doc.md", newPath: "remote.md" },
|
{ type: "rename", client: 1, oldPath: "doc.md", newPath: "remote.md" },
|
||||||
{ type: "sync", client: 1 },
|
{ type: "wait-for-request", client: 0 },
|
||||||
|
|
||||||
// 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: "update",
|
type: "update",
|
||||||
client: 0,
|
client: 0,
|
||||||
path: "sentinel.md",
|
path: "sentinel.md",
|
||||||
content: "s\nedit\n"
|
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" },
|
{ type: "rename", client: 0, oldPath: "doc.md", newPath: "local.md" },
|
||||||
|
{ type: "release-request", client: 0 },
|
||||||
// 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: "barrier" },
|
{ type: "barrier" },
|
||||||
|
|
||||||
{
|
{
|
||||||
type: "assert-consistent",
|
type: "assert-documents",
|
||||||
verify: (state: AssertableState): void => {
|
expected: [
|
||||||
state.assertFileCount(2);
|
{ 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
Loading…
Reference in a new issue