77 lines
3.8 KiB
TypeScript
77 lines
3.8 KiB
TypeScript
import type { AssertableState } from "../utils/assertable-state";
|
|
import type { TestDefinition } from "../test-definition";
|
|
|
|
export const mergingUpdateResponseSurvivesUserRenameTest: TestDefinition = {
|
|
description:
|
|
"Client 1 sends a content update with a stale `parent_version_id` " +
|
|
"(its WebSocket is paused, so it hasn't seen Client 0's intervening " +
|
|
"edit). The server merges and replies with `MergingUpdate` carrying " +
|
|
"the merged text. Before the response lands, the user renames the " +
|
|
"doc on Client 1, vacating the disk path the in-flight " +
|
|
"`processLocalUpdate` captured. Pre-fix: " +
|
|
"`handleMaybeMergingResponse`'s `operations.write(diskPath, …)` " +
|
|
"hits the `we wont recreate it` early-return inside `write`, " +
|
|
"silently dropping the server-merged content — Client 0's edit is " +
|
|
"lost on Client 1's disk, and Client 1's next local-update PUT " +
|
|
"(rebased on the now-untracked merged version) deletes Client 0's " +
|
|
"edit on the server too. Post-fix: the response is written to the " +
|
|
"doc's current tracked disk path, preserving both edits.",
|
|
clients: 2,
|
|
steps: [
|
|
{ type: "enable-sync", client: 0 },
|
|
{ type: "enable-sync", client: 1 },
|
|
{ type: "create", client: 0, path: "doc.md", content: "0\n" },
|
|
{ type: "barrier" },
|
|
|
|
// Stop Client 1 from seeing Client 0's next edit, so its next
|
|
// outbound PUT carries a stale `parent_version_id` and the server
|
|
// is forced to merge.
|
|
{ type: "pause-websocket", client: 1 },
|
|
|
|
// Server now holds v_b = "0\nA\n". Client 1's tracked parent
|
|
// version stays at v_a = "0\n".
|
|
{ type: "update", client: 0, path: "doc.md", content: "0\nA\n" },
|
|
{ type: "sync", client: 0 },
|
|
|
|
// Pause the server. Subsequent HTTP PUTs from Client 1 buffer at
|
|
// the OS layer until resume. This guarantees the merge response
|
|
// for Client 1's update is still in flight when the rename below
|
|
// mutates `queue.documents`.
|
|
{ type: "pause-server" },
|
|
|
|
// Client 1 edits doc.md with "B". The drain pops the LocalUpdate,
|
|
// captures `diskPath = "doc.md"`, reads the file, and sends the
|
|
// HTTP PUT — which buffers because the server is SIGSTOPped.
|
|
{ type: "update", client: 1, path: "doc.md", content: "0\nB\n" },
|
|
|
|
// User renames the file while the previous PUT is still in flight.
|
|
// `queue.enqueue`'s rename branch updates `documents` to point at
|
|
// `renamed.md` synchronously, but `processLocalUpdate`'s captured
|
|
// `diskPath` ("doc.md") is a local — it can't be retargeted.
|
|
{ type: "rename", client: 1, oldPath: "doc.md", newPath: "renamed.md" },
|
|
|
|
// Resume the server. It reconciles parent=v_a, latest=v_b,
|
|
// new="0\nB\n" → v_c with both edits, replies `MergingUpdate`.
|
|
// Pre-fix: write("doc.md", …) sees no file at that path
|
|
// (renamed.md now holds the data) and bails out without ever
|
|
// writing the merged bytes. Post-fix: the merged bytes land at
|
|
// the tracked path (renamed.md).
|
|
{ type: "resume-server" },
|
|
{ type: "resume-websocket", client: 1 },
|
|
|
|
{ type: "barrier" },
|
|
|
|
{
|
|
type: "assert-consistent",
|
|
verify: (state: AssertableState): void => {
|
|
state.assertFileCount(1);
|
|
state.assertFileExists("renamed.md");
|
|
state.assertFileNotExists("doc.md");
|
|
// Both edits survive: Client 0's "A" and Client 1's "B".
|
|
// The reconcile may interleave them either way; assert
|
|
// both tokens are present in the converged content.
|
|
state.assertContains("renamed.md", "A", "B");
|
|
}
|
|
}
|
|
]
|
|
};
|