90 lines
2.9 KiB
TypeScript
90 lines
2.9 KiB
TypeScript
import type { ClientState, TestDefinition } from "../test-definition";
|
|
import { assert } from "../utils/assert";
|
|
|
|
/**
|
|
* EDGE CASE: Client A renames X→Y while Client B creates at Y.
|
|
*
|
|
* This tests a tricky scenario where:
|
|
* 1. Both clients know about X.md
|
|
* 2. Client A renames X→Y (offline)
|
|
* 3. Client B creates a NEW file at Y (offline)
|
|
* 4. Both reconnect
|
|
*
|
|
* The server should handle this by:
|
|
* - Client A's rename succeeds (X→Y)
|
|
* - Client B's create at Y triggers a smart merge with A's renamed document
|
|
* - Both documents' content should be preserved
|
|
*/
|
|
function verifyFinalState(state: ClientState): void {
|
|
// X should not exist (renamed by A)
|
|
assert(
|
|
!state.files.has("X.md"),
|
|
`X.md should not exist, files: ${Array.from(state.files.keys()).join(", ")}`
|
|
);
|
|
|
|
// Y should exist with merged content
|
|
assert(
|
|
state.files.has("Y.md"),
|
|
`Y.md should exist, files: ${Array.from(state.files.keys()).join(", ")}`
|
|
);
|
|
|
|
const content = state.files.get("Y.md") ?? "";
|
|
// Both pieces of content should be preserved through merge
|
|
assert(
|
|
content.includes("original file X"),
|
|
`Expected content to include "original file X", got: "${content}"`
|
|
);
|
|
assert(
|
|
content.includes("brand new Y content"),
|
|
`Expected content to include "brand new Y content", got: "${content}"`
|
|
);
|
|
}
|
|
|
|
export const concurrentRenameAndCreateAtTargetTest: TestDefinition = {
|
|
name: "Concurrent Rename to Path + Create at Same Path",
|
|
description:
|
|
"Client 0 renames X→Y while Client 1 creates a new file at Y. " +
|
|
"Both operations happen offline. On reconnect, the server should " +
|
|
"merge the renamed document with the created document.",
|
|
clients: 2,
|
|
steps: [
|
|
// Setup: create X.md on Client 0
|
|
{
|
|
type: "create",
|
|
client: 0,
|
|
path: "X.md",
|
|
content: "original file X"
|
|
},
|
|
{ type: "enable-sync", client: 0 },
|
|
{ type: "enable-sync", client: 1 },
|
|
{ type: "sync" },
|
|
{ type: "barrier" },
|
|
|
|
// Both go offline
|
|
{ type: "disable-sync", client: 0 },
|
|
{ type: "disable-sync", client: 1 },
|
|
|
|
// Client 0: rename X→Y
|
|
{ type: "rename", client: 0, oldPath: "X.md", newPath: "Y.md" },
|
|
|
|
// Client 1: create Y with different content
|
|
// (Client 1 still has X.md locally)
|
|
{
|
|
type: "create",
|
|
client: 1,
|
|
path: "Y.md",
|
|
content: "brand new Y content"
|
|
},
|
|
|
|
// Client 0 reconnects first (rename goes through)
|
|
{ type: "enable-sync", client: 0 },
|
|
{ type: "sync", client: 0 },
|
|
|
|
// Client 1 reconnects (create at Y triggers smart merge)
|
|
{ type: "enable-sync", client: 1 },
|
|
{ type: "sync" },
|
|
{ type: "barrier" },
|
|
|
|
{ type: "assert-consistent", verify: verifyFinalState }
|
|
]
|
|
};
|