1
7import { describe, expect, it } from "vitest";
8import {
9 compareLwwTimestamps,
10 resolveFieldConflict,
11 type FieldVersion,
12} from "./lww.js";
13
14const local = (value: string, ts: number): FieldVersion => ({ value, ts });
15const remote = (value: string, ts: number, author = "Ana"): FieldVersion => ({ value, ts, author });
16
17describe("compareLwwTimestamps (the plain-LWW core)", () => {
18 it("the newer write wins (remote newer → remote; local newer → local)", () => {
19 expect(compareLwwTimestamps(1000, 2000)).toBe("remote");
20 expect(compareLwwTimestamps(2000, 1000)).toBe("local");
21 });
22 it("a tie resolves to LOCAL (the in-front-of-them intent — conservative)", () => {
23 expect(compareLwwTimestamps(1000, 1000)).toBe("local");
24 });
25});
26
27describe("resolveFieldConflict — title = LWW-with-consent (never auto-overwrite)", () => {
28 it("a 409 on the title → the consent outcome (refetch + toast + Apply anyway), even when the local ts is newer", () => {
29
30
31
32 const out = resolveFieldConflict("title", local("My title", 2000), remote("Their title", 1000));
33 expect(out.kind).toBe("consent");
34 if (out.kind === "consent") {
35 expect(out.applyAction).toBe("fresh-patch");
36 expect(out.localValue).toBe("My title");
37 expect(out.toast).toBe(
38 "Title was changed by Ana — your change 'My title' was not applied",
39 );
40 }
41 });
42
43 it("the toast names the remote author (and falls back to 'someone')", () => {
44 const out = resolveFieldConflict("title", local("X", 100), remote("Y", 200));
45 expect(out).toMatchObject({ kind: "consent", toast: expect.stringContaining("Ana") });
46 const noAuthor = resolveFieldConflict("title", local("X", 100), { value: "Y", ts: 200 });
47 expect(noAuthor).toMatchObject({
48 kind: "consent",
49 toast: expect.stringContaining("someone"),
50 });
51 });
52});
53
54describe("resolveFieldConflict — property = plain LWW (the newer timestamp wins)", () => {
55 it("the local write is newer → keep the local value (no toast)", () => {
56 const out = resolveFieldConflict("property", local("local-value", 2000), remote("remote-value", 1000));
57 expect(out).toEqual({ kind: "lww-local-wins", value: "local-value" });
58 });
59
60 it("the remote write is newer → take the remote value + the 'Updated to the latest value' toast", () => {
61 const out = resolveFieldConflict("property", local("local-value", 1000), remote("remote-value", 2000));
62 expect(out).toEqual({
63 kind: "lww-remote-wins",
64 value: "remote-value",
65 toast: "Updated to the latest value",
66 });
67 });
68
69 it("a tie → local wins (the conservative default, no toast)", () => {
70 const out = resolveFieldConflict("property", local("local", 1000), remote("remote", 1000));
71 expect(out).toEqual({ kind: "lww-local-wins", value: "local" });
72 });
73
74 it("the two-field split: the SAME 409 → consent for a title, LWW for a property", () => {
75 const a = resolveFieldConflict("title", local("t", 1000), remote("r", 2000));
76 const b = resolveFieldConflict("property", local("t", 1000), remote("r", 2000));
77 expect(a.kind).toBe("consent");
78 expect(b.kind).toBe("lww-remote-wins");
79 expect(b).toMatchObject({ toast: "Updated to the latest value" });
80 });
81});
82