This commit is contained in:
Andras Schmelczer 2026-05-04 13:07:18 +01:00
parent 39c5591d36
commit 35877b69da
94 changed files with 3157 additions and 1859 deletions

View file

@ -8,7 +8,9 @@ export function parseConcurrency(): number {
i + 1 < args.length
) {
const n = parseInt(args[i + 1], 10);
if (!isNaN(n) && n > 0) {return n;}
if (!isNaN(n) && n > 0) {
return n;
}
}
}
return os.cpus().length;

View file

@ -19,7 +19,9 @@ export class ServerManager {
}
public async stopAll(): Promise<void> {
if (this.isShuttingDown) {return;}
if (this.isShuttingDown) {
return;
}
this.isShuttingDown = true;
const servers = Array.from(this.activeServers);

View file

@ -92,14 +92,12 @@ import { displacedFileNotMarkedDeletedTest } from "./tests/displaced-file-not-ma
import { remoteUpdateResurrectsDeletedDocTest } from "./tests/remote-update-resurrects-deleted-doc.test";
import { localUpdateSurvivesRemoteRenameTest } from "./tests/local-update-survives-remote-rename.test";
import { mergingUpdateResponseSurvivesUserRenameTest } from "./tests/merging-update-response-survives-user-rename.test";
import { conflictUuidStashClearedAfterRenameDeconflictTest } from "./tests/conflict-uuid-stash-cleared-after-rename-deconflict.test";
import { catchupCreateAndUpdateNotSkippedTest } from "./tests/catchup-create-and-update-not-skipped.test";
import { localRenameSurvivesRemoteRenameTest } from "./tests/local-rename-survives-remote-rename.test";
import { renameChainDuringPendingCreateTest } from "./tests/rename-chain-during-pending-create.test";
import { remoteRenameCollidesWithPendingLocalCreateTest } from "./tests/remote-rename-collides-with-pending-local-create.test";
import { remoteUpdateSurvivesUserRenameTest } from "./tests/remote-update-survives-user-rename.test";
import { orphanStashOnCreateMergeTest } from "./tests/orphan-stash-on-create-merge.test";
import { orphanStashOnCreateDedupeMergeTest } from "./tests/orphan-stash-on-create-dedupe-merge.test";
import { sameDocIdCollapseOnLocalCreateAfterRemoteCreateTest } from "./tests/same-doc-id-collapse-on-local-create-after-remote-create.test";
export const TESTS: Partial<Record<string, TestDefinition>> = {
"rename-create-conflict": renameCreateConflictTest,
@ -210,25 +208,18 @@ export const TESTS: Partial<Record<string, TestDefinition>> = {
"online-create-update-while-other-creates-same-path":
onlineCreateUpdateWhileOtherCreatesSamePathTest,
"displaced-file-not-marked-deleted": displacedFileNotMarkedDeletedTest,
"remote-update-resurrects-deleted-doc": remoteUpdateResurrectsDeletedDocTest,
"local-update-survives-remote-rename":
localUpdateSurvivesRemoteRenameTest,
"remote-update-resurrects-deleted-doc":
remoteUpdateResurrectsDeletedDocTest,
"local-update-survives-remote-rename": localUpdateSurvivesRemoteRenameTest,
"merging-update-response-survives-user-rename":
mergingUpdateResponseSurvivesUserRenameTest,
"conflict-uuid-stash-cleared-after-rename-deconflict":
conflictUuidStashClearedAfterRenameDeconflictTest,
"catchup-create-and-update-not-skipped":
catchupCreateAndUpdateNotSkippedTest,
"local-rename-survives-remote-rename":
localRenameSurvivesRemoteRenameTest,
"rename-chain-during-pending-create":
renameChainDuringPendingCreateTest,
"local-rename-survives-remote-rename": localRenameSurvivesRemoteRenameTest,
"rename-chain-during-pending-create": renameChainDuringPendingCreateTest,
"remote-rename-collides-with-pending-local-create":
remoteRenameCollidesWithPendingLocalCreateTest,
"remote-update-survives-user-rename":
remoteUpdateSurvivesUserRenameTest,
"orphan-stash-on-create-merge":
orphanStashOnCreateMergeTest,
"orphan-stash-on-create-dedupe-merge":
orphanStashOnCreateDedupeMergeTest
"remote-update-survives-user-rename": remoteUpdateSurvivesUserRenameTest,
"same-doc-id-collapse-on-local-create-after-remote-create":
sameDocIdCollapseOnLocalCreateAfterRemoteCreateTest
};

View file

@ -2,7 +2,6 @@ import type { TestDefinition, TestResult, TestStep } from "./test-definition";
import { DeterministicAgent } from "./deterministic-agent";
import type { ServerControl } from "./server-control";
import type { SyncSettings, Logger } from "sync-client";
import { CONFLICT_PATH_REGEX } from "sync-client";
import { assert } from "./utils/assert";
import { AssertableState } from "./utils/assertable-state";
import { sleep } from "./utils/sleep";
@ -15,10 +14,6 @@ import {
} from "./consts";
import { randomUUID } from "node:crypto";
class ConflictFilesDetectedError extends Error {
public override readonly name = "ConflictFilesDetectedError";
}
export class TestRunner {
private agents: DeterministicAgent[] = [];
private readonly serverControl: ServerControl;
@ -236,9 +231,6 @@ export class TestRunner {
this.logger.info("Barrier complete: all clients converged");
return;
} catch (error) {
if (error instanceof ConflictFilesDetectedError) {
throw error;
}
lastError =
error instanceof Error ? error : new Error(String(error));
this.logger.info("Barrier: not yet converged, retrying...");
@ -304,25 +296,6 @@ export class TestRunner {
clientFiles.push(fileMap);
}
const conflictsByClient = clientFiles.map((files) =>
Array.from(files.keys()).filter((path) =>
CONFLICT_PATH_REGEX.test(path)
)
);
if (conflictsByClient.some((conflicts) => conflicts.length > 0)) {
const summary = conflictsByClient
.map((conflicts, i) =>
conflicts.length > 0
? `client ${i}: [${conflicts.join(", ")}]`
: null
)
.filter((s): s is string => s !== null)
.join("; ");
throw new ConflictFilesDetectedError(
`Found local conflict file(s): ${summary}`
);
}
const referenceFiles = Array.from(clientFiles[0].keys());
this.logger.info(

View file

@ -40,13 +40,12 @@ export const concurrentRenameAndCreateAtTargetTest: TestDefinition = {
verify: (state: AssertableState): void => {
state
.assertFileNotExists("X.md")
.assertFileExists(
"Y.md",
)
.assertFileExists(
"Y (1).md",
)
.assertAnyFileContains("original file X", "brand new Y content")
.assertFileExists("Y.md")
.assertFileExists("Y (1).md")
.assertAnyFileContains(
"original file X",
"brand new Y content"
);
}
}
]

View file

@ -51,8 +51,10 @@ 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");
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");
}
}
]

View file

@ -1,100 +0,0 @@
import type { AssertableState } from "../utils/assertable-state";
import type { TestDefinition } from "../test-definition";
export const conflictUuidStashClearedAfterRenameDeconflictTest: TestDefinition =
{
description:
"A `RemoteChange` for a brand-new doc D2 at `target.md` reaches " +
"Client 1's queue *before* Client 1's user-rename of D1 → " +
"`target.md`. The rename's `queue.enqueue` mutates " +
"`documents` synchronously, so by the time the drain processes " +
"the buffered broadcast, `target.md` is already tracked by D1 " +
"with a high `parentVersionId`. " +
"`processRemoteCreateForNewDocument`'s version comparison " +
"(`parentVersionId < remoteVaultUpdateId`) takes the " +
"`MoveOnConflict.NEW` branch and stashes D2 at " +
"`conflict-<uuid>-target.md`. The rename's `LocalUpdate` then " +
"drains, the server deconflicts D1 to `target (1).md`, freeing " +
"the `target.md` slot locally — but D2 is left orphaned at the " +
"`conflict-<uuid>-` path forever, diverging from Client 0 which " +
"has D2 at `target.md`.",
clients: 2,
steps: [
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
// Both clients have D1 at `original.md`.
{
type: "create",
client: 0,
path: "original.md",
content: "D1 v1\n"
},
{ type: "barrier" },
// Buffer Client 1's WebSocket so D2's broadcast doesn't land
// until we're ready to enqueue it ahead of the rename.
{ type: "pause-websocket", client: 1 },
// Client 0 creates D2 at target.md. Server stores it; broadcast
// is buffered at Client 1.
{
type: "create",
client: 0,
path: "target.md",
content: "D2 v1\n"
},
{ type: "sync", client: 0 },
// Pause the server. Now Client 1's next HTTP PUT will buffer in
// TCP and the drain will sit on `await sendUpdate`.
{ type: "pause-server" },
// Issue an update to D1. The drain pops the LocalUpdate and
// suspends on the HTTP PUT (server is SIGSTOPped). The drain is
// now busy and won't pop further events until resume-server.
{
type: "update",
client: 1,
path: "original.md",
content: "D1 v2\n"
},
// Replay the buffered D2 broadcast. It enqueues as a
// RemoteChange BEHIND the in-flight LocalUpdate but AHEAD of
// the rename event we're about to push.
{ type: "resume-websocket", client: 1 },
// User renames D1 onto target.md. `queue.enqueue` synchronously
// updates `documents` so target.md → D1. The rename's
// LocalUpdate is pushed to the END of the queue, *after* the
// buffered RemoteChange.
{
type: "rename",
client: 1,
oldPath: "original.md",
newPath: "target.md"
},
// Resume the server. Drain order: (1) finish the v2 update PUT
// → D1.parentVersionId bumps above D2's vaultUpdateId. (2)
// process the RemoteChange for D2 — sees `documents.get(target.md)
// = D1` with parentVersionId > vaultUpdateId → MoveOnConflict.NEW
// → stashes D2 at `conflict-<uuid>-target.md`. (3) process the
// rename's LocalUpdate — server deconflicts to target (1).md;
// local file moves there.
{ type: "resume-server" },
{ type: "barrier" },
{
type: "assert-consistent",
verify: (state: AssertableState): void => {
state.assertFileCount(2);
state.assertFileExists("target.md");
state.assertFileExists("target (1).md");
state.assertContent("target.md", "D2 v1\n");
state.assertContent("target (1).md", "D1 v2\n");
}
}
]
};

View file

@ -62,10 +62,7 @@ export const localUpdateSurvivesRemoteRenameTest: TestDefinition = {
verify: (state: AssertableState): void => {
state.assertFileCount(1);
state.assertFileExists("renamed.md");
state.assertContent(
"renamed.md",
"v1\nclient 0 edit\n"
);
state.assertContent("renamed.md", "v1\nclient 0 edit\n");
}
}
]

View file

@ -28,7 +28,10 @@ export const moveRemoteUpdateRevertsRenameTest: TestDefinition = {
{
type: "assert-consistent",
verify: (s: AssertableState): void => {
s.assertFileCount(1).assertContent("renamed.md", "updated by client 1");
s.assertFileCount(1).assertContent(
"renamed.md",
"updated by client 1"
);
}
}
]

View file

@ -29,8 +29,7 @@ export const offlineMoveThenRemoteDeleteTest: TestDefinition = {
{
type: "assert-consistent",
verify: (s: AssertableState): void => {
s
.assertFileCount(0);
s.assertFileCount(0);
}
}
]

View file

@ -1,97 +0,0 @@
import type { AssertableState } from "../utils/assertable-state";
import type { TestDefinition } from "../test-definition";
export const orphanStashOnCreateDedupeMergeTest: TestDefinition = {
description:
"When the server's create endpoint dedupe-merges a client's local " +
"create into an existing fresh remote doc that the client has " +
"already tracked at a `conflict-<uuid>-` stash (because the " +
"remote create's broadcast displaced its content there), " +
"`processCreate`'s response handler relocates the doc's record " +
"onto the canonical path via `setDocument` but the stash file on " +
"disk is left behind — outliving its tracking record and " +
"diverging from every other client. Reproducing the merge half " +
"of the dedupe is delicate: the server's merge gate requires the " +
"POST's `last_seen_vault_update_id` to be *strictly less than* " +
"the existing doc's `creation_vault_update_id`. A normal sync " +
"advances the watermark contiguously, so on the canonical " +
"create-vs-create race the watermark would already include the " +
"remote doc's create when the local POST ships, the merge gate " +
"fails, and the server deconflicts to `(1)`. This test pokes a " +
"permanent gap into the watermark via the catch-up replay's " +
"latest-only semantics: a tempdoc created and deleted while " +
"Client 0 is offline lives in catch-up only as the delete event, " +
"which the client processes (advancing the watermark to the " +
"delete) without ever filling the create's update id — so the " +
"watermark's contiguous-prefix min stays below the next doc's " +
"creation. With that gap in place, Client 0's post-displacement " +
"POST satisfies the server's merge gate, the server returns the " +
"existing docId, and `processCreate` walks straight into the " +
"orphan-stash bug. Pre-fix: `Files from agent-0 missing in " +
"agent-1` (the `conflict-<uuid>-` stash). Post-fix: cleaned up.",
clients: 2,
steps: [
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "barrier" },
// Client 0 goes offline. Its watermark is saved at v=0.
{ type: "disable-sync", client: 0 },
// Tempdoc that lives only as a delete in Client 0's catch-up
// (the create at v=1 is collapsed away by latest-only replay).
// The processed delete advances the watermark to v=2 but leaves
// v=1 unfilled, parking the contiguous-prefix min at 0.
{ type: "create", client: 1, path: "tempdoc.md", content: "x\n" },
{ type: "sync", client: 1 },
{ type: "delete", client: 1, path: "tempdoc.md" },
{ type: "sync", client: 1 },
// The doc whose dedupe-merge we want to trigger — fresh
// (creation == latest), mergeable text. Its creation v=3 is
// strictly greater than Client 0's stuck min of 0, so the
// server's merge gate will fire.
{ type: "create", client: 1, path: "file.md", content: "from-1\n" },
{ type: "sync", client: 1 },
// Re-arm the WS pause for the new socket Client 0 is about to
// create on enable-sync, so the catch-up broadcast is buffered
// until we explicitly release it. Without sticky pause across
// factory `constructorFn` calls this would silently miss the
// catch-up.
{ type: "pause-websocket", client: 0 },
{ type: "enable-sync", client: 0 },
// Server pause arrives before the buffered catch-up is released
// so the resume below parks Client 0's drain on the GET for
// file.md's content (the only fetching event in the catch-up;
// the tempdoc delete needs no fetch and runs through quickly,
// leaving the watermark gap intact).
{ type: "pause-server" },
{ type: "resume-websocket", client: 0 },
// Yield so the drain has time to traverse the WS handler →
// listener → enqueue → drain → processRemoteCreateForNewDocument
// → fetch hops before the local create runs.
{ type: "sleep", ms: 100 },
// Client 0 creates file.md locally while the GET is parked. The
// file occupies the canonical slot, so when the GET returns the
// remote create displaces D's bytes to `conflict-<uuid>-file.md`
// and tracks D there with `intendedPath=file.md`. The
// LocalCreate enqueues behind the in-flight RemoteChange.
{ type: "create", client: 0, path: "file.md", content: "from-0\n" },
{ type: "resume-server" },
{ type: "barrier" },
{
type: "assert-consistent",
verify: (s: AssertableState): void => {
s.assertFileCount(1);
s.assertFileExists("file.md");
}
}
]
};

View file

@ -1,59 +0,0 @@
import type { AssertableState } from "../utils/assertable-state";
import type { TestDefinition } from "../test-definition";
export const orphanStashOnCreateMergeTest: TestDefinition = {
description:
"Client 1 creates file.md (server doc D). Client 0's WebSocket is " +
"paused, so the broadcast is buffered. The server is paused, then " +
"the WebSocket released — Client 0 enters " +
"`processRemoteCreateForNewDocument` and parks on the GET for D's " +
"content. While parked, Client 0 creates file.md locally. The GET " +
"returns and the remote create displaces to " +
"`conflict-<uuid>-file.md` (slot occupied), tracking D there with " +
"`intendedPath=file.md`. Client 0's LocalCreate POST then drains " +
"and the server deconflicts (because Client 0's lastSeenVaultUpdateId " +
"now equals D's creation, so the merge condition fails) — creating " +
"a sibling doc D' at `file (1).md`. The convergence path then " +
"needs `unwindReadyStashes` to slide D off the conflict-uuid stash " +
"back to file.md once Client 0's local file moves to file (1).md, " +
"leaving both clients with [file.md, file (1).md]. Documents the " +
"displacement-then-deconflict-then-unwind path the fix to " +
"`processCreate`'s same-docId orphan cleanup must not regress.",
clients: 2,
steps: [
{ type: "enable-sync", client: 0 },
{ type: "enable-sync", client: 1 },
{ type: "barrier" },
{ type: "pause-websocket", client: 0 },
{ type: "create", client: 1, path: "file.md", content: "from-1\n" },
{ type: "sync", client: 1 },
{ type: "pause-server" },
{ type: "resume-websocket", client: 0 },
// Yield long enough for the drain to traverse all the microtask
// hops between the WS handler and the GET, so the request is
// queued at the (paused) server before the local create runs.
{ type: "sleep", ms: 50 },
{ type: "create", client: 0, path: "file.md", content: "from-0\n" },
{ type: "resume-server" },
{ type: "barrier" },
{
type: "assert-consistent",
verify: (s: AssertableState): void => {
s.assertFileCount(2);
s.assertFileExists("file.md");
s.assertFileExists("file (1).md");
s.assertAnyFileContains("from-0");
s.assertAnyFileContains("from-1");
}
}
]
};

View file

@ -2,23 +2,23 @@ import type { AssertableState } from "../utils/assertable-state";
import type { TestDefinition } from "../test-definition";
export const remoteRenameCollidesWithPendingLocalCreateTest: TestDefinition = {
// TODO(refactor): the failure mode described below is the
// pre-refactor "deflect-to-conflict-uuid" path that no longer
// exists. Under the new model the wire loop never moves files for
// path placement, so the remote rename can't deflect anywhere; the
// reconciler waits for the slot to free. Convergence assertion is
// still valid (no conflict-uuid stashes, both files present, the
// local create lands at a server-deconflicted sibling).
description:
"Client 0 has doc D tracked at `original.md`. Client 1 owns doc E " +
"and renames it to `target.md` server-side. Before client 0's " +
"drain processes the WS broadcast for E, the user creates a new " +
"local file `target.md` (a different doc, untracked). When the " +
"buffered RemoteChange for E drains, `processRemoteUpdate` " +
"tries to move client 0's tracked file from its old slot onto " +
"`target.md`. Pre-fix: `MoveOnConflict.NEW` deflects the remote " +
"rename to a `conflict-<uuid>-target.md` stash on client 0, " +
"leaving a permanent local-only divergence (client 1 has no " +
"such stash). Post-fix: when the slot is held by a non-tracked " +
"file (typically the agent's own pending LocalCreate), " +
"`processRemoteUpdate` uses `MoveOnConflict.EXISTING` to " +
"displace it; `updatePendingCreatePath` retargets the displaced " +
"create's `event.path`, so its drain reads the file from the " +
"new location and the server's deconflict on its create lands " +
"the new doc at a clean path.",
"buffered RemoteChange for E drains, the engine has to reconcile " +
"doc E onto `target.md` even though the slot is held by client " +
"0's pending LocalCreate. Convergence requires both clients end " +
"up with [target.md = E] and the local create lands at a " +
"server-deconflicted sibling (e.g. `target (1).md`).",
clients: 2,
steps: [
{ type: "enable-sync", client: 0 },
@ -34,7 +34,12 @@ export const remoteRenameCollidesWithPendingLocalCreateTest: TestDefinition = {
// Client 1 renames the doc. Server commits, broadcasts to
// client 0 (buffered).
{ type: "rename", client: 1, oldPath: "original.md", newPath: "target.md" },
{
type: "rename",
client: 1,
oldPath: "original.md",
newPath: "target.md"
},
{ type: "sync", client: 1 },
// Client 0 still believes the doc is at `original.md`. The user
@ -44,9 +49,8 @@ export const remoteRenameCollidesWithPendingLocalCreateTest: TestDefinition = {
{ type: "create", client: 0, path: "target.md", content: "extra\n" },
// Resume client 0's WS. The buffered RemoteChange drains.
// Pre-fix: `MoveOnConflict.NEW` deflects the rename of the
// tracked doc into `conflict-<uuid>-target.md`, with
// `intendedPath=target.md`.
// The reconciler must converge without ever leaving a
// conflict-uuid stash on disk.
{ type: "resume-websocket", client: 0 },
{ type: "barrier" },

View file

@ -30,8 +30,18 @@ export const renameChainDuringPendingCreateTest: TestDefinition = {
{ type: "pause-server" },
{ type: "create", client: 0, path: "first.md", content: "v1\n" },
{ type: "rename", client: 0, oldPath: "first.md", newPath: "second.md" },
{ type: "rename", client: 0, oldPath: "second.md", newPath: "third.md" },
{
type: "rename",
client: 0,
oldPath: "first.md",
newPath: "second.md"
},
{
type: "rename",
client: 0,
oldPath: "second.md",
newPath: "third.md"
},
// Resume — drain pops LocalCreate (now resolves), then the two
// queued LocalUpdates. Pre-fix: only the first rename's

View file

@ -25,7 +25,9 @@ export const renameCreateConflictTest: TestDefinition = {
{
type: "assert-consistent",
verify: (s: AssertableState): void => {
s.assertFileCount(2).assertContent("B.md", "hi").assertContent("B (1).md", "hi");
s.assertFileCount(2)
.assertContent("B.md", "hi")
.assertContent("B (1).md", "hi");
}
}
]

View file

@ -0,0 +1,121 @@
import type { AssertableState } from "../utils/assertable-state";
import type { TestDefinition } from "../test-definition";
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.",
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).
{
type: "create",
client: 1,
path: "X.md",
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 "
},
// 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: "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"
);
// 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}`
);
}
}
}
}
}
]
};