/** * TASK-0095 R1 — the LWW timestamp compare (the S12 gate's named unit test: * "the two-field split: title consent vs property LWW"). * * Pure (no UI/fetch/store) — the outbox worker + the web UI call this. */ import { describe, expect, it } from "vitest"; import { compareLwwTimestamps, resolveFieldConflict, type FieldVersion, } from "./lww.js"; const local = (value: string, ts: number): FieldVersion => ({ value, ts }); const remote = (value: string, ts: number, author = "Ana"): FieldVersion => ({ value, ts, author }); describe("compareLwwTimestamps (the plain-LWW core)", () => { it("the newer write wins (remote newer → remote; local newer → local)", () => { expect(compareLwwTimestamps(1000, 2000)).toBe("remote"); expect(compareLwwTimestamps(2000, 1000)).toBe("local"); }); it("a tie resolves to LOCAL (the in-front-of-them intent — conservative)", () => { expect(compareLwwTimestamps(1000, 1000)).toBe("local"); }); }); describe("resolveFieldConflict — title = LWW-with-consent (never auto-overwrite)", () => { it("a 409 on the title → the consent outcome (refetch + toast + Apply anyway), even when the local ts is newer", () => { // The local write is NEWER, but a title 409 still asks the user (the CAS // says the remote write already landed — silently overwriting a title is // the unsafe direction). const out = resolveFieldConflict("title", local("My title", 2000), remote("Their title", 1000)); expect(out.kind).toBe("consent"); if (out.kind === "consent") { expect(out.applyAction).toBe("fresh-patch"); expect(out.localValue).toBe("My title"); expect(out.toast).toBe( "Title was changed by Ana — your change 'My title' was not applied", ); } }); it("the toast names the remote author (and falls back to 'someone')", () => { const out = resolveFieldConflict("title", local("X", 100), remote("Y", 200)); expect(out).toMatchObject({ kind: "consent", toast: expect.stringContaining("Ana") }); const noAuthor = resolveFieldConflict("title", local("X", 100), { value: "Y", ts: 200 }); expect(noAuthor).toMatchObject({ kind: "consent", toast: expect.stringContaining("someone"), }); }); }); describe("resolveFieldConflict — property = plain LWW (the newer timestamp wins)", () => { it("the local write is newer → keep the local value (no toast)", () => { const out = resolveFieldConflict("property", local("local-value", 2000), remote("remote-value", 1000)); expect(out).toEqual({ kind: "lww-local-wins", value: "local-value" }); }); it("the remote write is newer → take the remote value + the 'Updated to the latest value' toast", () => { const out = resolveFieldConflict("property", local("local-value", 1000), remote("remote-value", 2000)); expect(out).toEqual({ kind: "lww-remote-wins", value: "remote-value", toast: "Updated to the latest value", }); }); it("a tie → local wins (the conservative default, no toast)", () => { const out = resolveFieldConflict("property", local("local", 1000), remote("remote", 1000)); expect(out).toEqual({ kind: "lww-local-wins", value: "local" }); }); it("the two-field split: the SAME 409 → consent for a title, LWW for a property", () => { const a = resolveFieldConflict("title", local("t", 1000), remote("r", 2000)); const b = resolveFieldConflict("property", local("t", 1000), remote("r", 2000)); expect(a.kind).toBe("consent"); // the title asks expect(b.kind).toBe("lww-remote-wins"); // the property applies (newer wins) expect(b).toMatchObject({ toast: "Updated to the latest value" }); }); });