import { describe, it, expect } from "vitest"; import { CookieStore } from "../src/sync/conflict.js"; import type { CookieEntry, CookieSyncPayload } from "../src/protocol/spec.js"; function makeCookie(overrides: Partial = {}): CookieEntry { return { domain: "example.com", name: "session", value: "abc123", path: "/", secure: true, httpOnly: true, sameSite: "lax", expiresAt: null, ...overrides, }; } describe("CookieStore — conflict resolution", () => { it("applies first write", () => { const store = new CookieStore(); const payload: CookieSyncPayload = { action: "set", cookies: [makeCookie({ value: "v1" })], lamportTs: 1, }; const applied = store.applyRemote(payload, "device-a"); expect(applied).toHaveLength(1); expect(store.getAll()).toHaveLength(1); expect(store.getAll()[0].value).toBe("v1"); }); it("last-writer-wins: higher timestamp wins", () => { const store = new CookieStore(); store.applyRemote( { action: "set", cookies: [makeCookie({ value: "old" })], lamportTs: 1 }, "device-a", ); store.applyRemote( { action: "set", cookies: [makeCookie({ value: "new" })], lamportTs: 5 }, "device-b", ); expect(store.getAll()[0].value).toBe("new"); }); it("rejects stale update", () => { const store = new CookieStore(); store.applyRemote( { action: "set", cookies: [makeCookie({ value: "newer" })], lamportTs: 10 }, "device-a", ); const applied = store.applyRemote( { action: "set", cookies: [makeCookie({ value: "older" })], lamportTs: 3 }, "device-b", ); expect(applied).toHaveLength(0); expect(store.getAll()[0].value).toBe("newer"); }); it("breaks ties by deviceId (lexicographic)", () => { const store = new CookieStore(); store.applyRemote( { action: "set", cookies: [makeCookie({ value: "from-a" })], lamportTs: 5 }, "aaa", ); // Same timestamp, higher deviceId wins const applied = store.applyRemote( { action: "set", cookies: [makeCookie({ value: "from-b" })], lamportTs: 5 }, "bbb", ); expect(applied).toHaveLength(1); expect(store.getAll()[0].value).toBe("from-b"); }); it("tie-break: lower deviceId loses", () => { const store = new CookieStore(); store.applyRemote( { action: "set", cookies: [makeCookie({ value: "from-z" })], lamportTs: 5 }, "zzz", ); const applied = store.applyRemote( { action: "set", cookies: [makeCookie({ value: "from-a" })], lamportTs: 5 }, "aaa", ); expect(applied).toHaveLength(0); expect(store.getAll()[0].value).toBe("from-z"); }); it("handles delete action", () => { const store = new CookieStore(); store.applyRemote( { action: "set", cookies: [makeCookie()], lamportTs: 1 }, "device-a", ); expect(store.getAll()).toHaveLength(1); store.applyRemote( { action: "delete", cookies: [makeCookie()], lamportTs: 5 }, "device-a", ); expect(store.getAll()).toHaveLength(0); }); it("tracks local changes with Lamport clock", () => { const store = new CookieStore(); const cookie = makeCookie({ value: "local" }); const ts = store.setLocal(cookie, "my-device"); expect(ts).toBe(1); expect(store.getAll()).toHaveLength(1); expect(store.currentTs).toBe(1); const ts2 = store.setLocal(makeCookie({ name: "other", value: "x" }), "my-device"); expect(ts2).toBe(2); }); it("advances Lamport clock on remote apply", () => { const store = new CookieStore(); store.applyRemote( { action: "set", cookies: [makeCookie()], lamportTs: 100 }, "device-x", ); // Clock should be max(0, 100) + 1 = 101 expect(store.currentTs).toBe(101); }); it("tracks different cookies by domain|name|path", () => { const store = new CookieStore(); store.applyRemote( { action: "set", cookies: [makeCookie({ domain: "a.com", name: "s1" })], lamportTs: 1 }, "d1", ); store.applyRemote( { action: "set", cookies: [makeCookie({ domain: "b.com", name: "s1" })], lamportTs: 1 }, "d1", ); expect(store.getAll()).toHaveLength(2); }); });