Path: blob/master/src/packages/backend/conat/test/sync/dkv-merge.test.ts
1451 views
/*1Testing merge conflicts with dkv23DEVELOPMENT:45pnpm test ./dkv-merge.test.ts67*/89import { dkv as createDkv } from "@cocalc/backend/conat/sync";10import { once } from "@cocalc/util/async-utils";11import { diff_match_patch } from "@cocalc/util/dmp";12import { before, after } from "@cocalc/backend/conat/test/setup";1314beforeAll(before);1516async function getKvs(opts?) {17const name = `test${Math.round(1000 * Math.random())}`;18// We disable autosave so that we have more precise control of how conflicts19// get resolved, etc. for testing purposes.20const kv1 = await createDkv({21name,22noAutosave: true,23...opts,24noCache: true,25});26const kv2 = await createDkv({27name,28noAutosave: true,29...opts,30noCache: true,31});32// @ts-ignore -- a little double check33if (kv1.kv === kv2.kv) {34throw Error("must not being using same underlying kv");35}36return { kv1, kv2 };37}3839describe("test the default 'local first' merge conflict resolution function", () => {40it("sets up and resolves a merge conflict", async () => {41const { kv1, kv2 } = await getKvs();42kv1.set("x", 5);43kv2.set("x", 10);44expect(kv1["x"]).toEqual(5);45expect(kv2["x"]).toEqual(10);4647// now make kv2 save, which makes kv1 detect a conflict:48await kv2.save();49// kv1 just resolves it in its own favor.50expect(kv1["x"]).toEqual(5);51await kv1.save();5253// wait until kv2 gets a change, which will be learning about54// how the merge conflict got resolved.55if (kv2.get("x") != 5) {56// might have to wait57await once(kv2, "change");58}59expect(kv2["x"]).toEqual(5);60});61});6263describe("test the default 'local first' merge conflict resolution function, but where we do the sets in the opposite order", () => {64it("sets up and resolves a merge conflict", async () => {65const { kv1, kv2 } = await getKvs();66kv2.set("x", 10);67kv1.set("x", 5);68expect(kv1["x"]).toEqual(5);69expect(kv2["x"]).toEqual(10);7071// now make kv2 save, which makes kv1 detect a conflict:72await kv2.save();73// kv1 just resolves it in its own favor.74expect(kv1["x"]).toEqual(5);75await kv1.save();7677// wait until kv2 gets a change, which will be learning about78// how the merge conflict got resolved.79if (kv2.get("x") != 5) {80// might have to wait81await once(kv2, "change");82}83expect(kv2["x"]).toEqual(5);84});85});8687describe("test a trivial merge conflict resolution function", () => {88it("sets up and resolves a merge conflict", async () => {89const { kv1, kv2 } = await getKvs({90merge: () => {91// our merge strategy is to replace the value by 'conflict'92return "conflict";93},94});95kv1.set("x", 5);96kv2.set("x", 10);97expect(kv1["x"]).toEqual(5);98expect(kv2["x"]).toEqual(10);99100// now make kv2 save, which makes kv1 detect a conflict:101await kv2.save();102if (kv1["x"] != "conflict") {103// might have to wait104await once(kv1, "change");105}106expect(kv1["x"]).toEqual("conflict");107108await kv1.save();109// wait until kv2 gets a change, which will be learning about110// how the merge conflict got resolved.111if (kv2["x"] != "conflict") {112// might have to wait113await once(kv2, "change");114}115expect(kv2["x"]).toEqual("conflict");116});117});118119describe("test a 3-way merge of strings conflict resolution function", () => {120const dmp = new diff_match_patch();121const threeWayMerge = (opts: {122prev: string;123local: string;124remote: string;125}) => {126return dmp.patch_apply(127dmp.patch_make(opts.prev, opts.local),128opts.remote,129)[0];130};131it("sets up and resolves a merge conflict", async () => {132const { kv1, kv2 } = await getKvs({133merge: ({ local, remote, prev = "" }) => {134// our merge strategy is to replace the value by 'conflict'135return threeWayMerge({ local, remote, prev });136},137});138kv1.set("x", "cocalc");139await kv1.save();140if (kv2["x"] != "cocalc") {141// might have to wait142await once(kv2, "change");143}144expect(kv2["x"]).toEqual("cocalc");145await kv2.save();146147kv2.set("x", "cocalc!");148kv1.set("x", "LOVE cocalc");149await kv2.save();150if (kv1.get("x") != "LOVE cocalc!") {151await once(kv1, "change");152}153expect(kv1.get("x")).toEqual("LOVE cocalc!");154await kv1.save();155if (kv2.get("x") != "LOVE cocalc!") {156await once(kv2, "change");157}158expect(kv2.get("x")).toEqual("LOVE cocalc!");159});160});161162describe("test a 3-way merge of that merges objects", () => {163it("sets up and resolves a merge conflict", async () => {164const { kv1, kv2 } = await getKvs({165merge: ({ local, remote }) => {166return { ...remote, ...local };167},168});169kv1.set("x", { a: 5 });170await kv1.save();171if (kv2["x"] == null) {172await once(kv2, "change");173}174expect(kv2["x"]).toEqual({ a: 5 });175176kv1.set("x", { a: 5, b: 15, c: 12 });177kv2.set("x", { a: 5, b: 7, d: 3 });178await kv2.save();179if (kv1.get("x").d != 3) {180await once(kv1, "change");181}182expect(kv1.get("x")).toEqual({ a: 5, b: 15, c: 12, d: 3 });183await kv1.save();184if (kv2.get("x").b != 15) {185await once(kv2, "change");186}187expect(kv2.get("x")).toEqual({ a: 5, b: 15, c: 12, d: 3 });188});189});190191afterAll(after);192193194