84 lines
2.8 KiB
TypeScript
84 lines
2.8 KiB
TypeScript
import type { ClientState, TestDefinition } from "../test-definition";
|
|
import { assert } from "../utils/assert";
|
|
|
|
/**
|
|
* BUG/EDGE CASE: Both clients rename the same file to different targets.
|
|
*
|
|
* Client 0 renames X→Y, Client 1 renames X→Z. Both happen offline.
|
|
* When they reconnect:
|
|
*
|
|
* - Client 0's rename (X→Y) goes through first → server has doc at Y
|
|
* - Client 1's rename (X→Z): Client 1 still has the old metadata
|
|
* pointing to X.md. But the server moved it to Y.md.
|
|
*
|
|
* The conflict: Client 1 will try to update with relativePath=Z.md
|
|
* and parentVersionId pointing to the old state. The server sees the
|
|
* path changed and processes it as a rename from Y→Z.
|
|
*
|
|
* Expected: The file ends up at one path (last rename wins), and both
|
|
* clients converge. Content should be preserved.
|
|
*/
|
|
function verifyFinalState(state: ClientState): void {
|
|
// X should not exist (renamed by both)
|
|
assert(
|
|
!state.files.has("X.md"),
|
|
`X.md should not exist, files: ${Array.from(state.files.keys()).join(", ")}`
|
|
);
|
|
|
|
// Exactly one file should exist (either Y.md or Z.md)
|
|
assert(
|
|
state.files.size === 1,
|
|
`Expected 1 file, got ${state.files.size}: ${Array.from(state.files.keys()).join(", ")}`
|
|
);
|
|
|
|
// Content should be preserved
|
|
const content = Array.from(state.files.values())[0];
|
|
assert(
|
|
content === "original content",
|
|
`Expected "original content", got: "${content}"`
|
|
);
|
|
}
|
|
|
|
export const offlineRenameBothClientsSameSourceTest: TestDefinition = {
|
|
name: "Both Clients Rename Same File to Different Targets (Offline)",
|
|
description:
|
|
"Client 0 renames X→Y, Client 1 renames X→Z, both offline. " +
|
|
"On reconnect, the conflicting renames should resolve and " +
|
|
"both clients should converge to the same final path.",
|
|
clients: 2,
|
|
steps: [
|
|
// Setup: create X.md
|
|
{
|
|
type: "create",
|
|
client: 0,
|
|
path: "X.md",
|
|
content: "original content"
|
|
},
|
|
{ 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: rename X→Z
|
|
{ type: "rename", client: 1, oldPath: "X.md", newPath: "Z.md" },
|
|
|
|
// Client 0 reconnects first
|
|
{ type: "enable-sync", client: 0 },
|
|
{ type: "sync", client: 0 },
|
|
|
|
// Client 1 reconnects
|
|
{ type: "enable-sync", client: 1 },
|
|
{ type: "sync" },
|
|
{ type: "barrier" },
|
|
|
|
// Both clients should converge
|
|
{ type: "assert-consistent", verify: verifyFinalState }
|
|
]
|
|
};
|