Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205import { describe, expect, it } from "vitest";import { identityPolicy, isContentAddressed, mintOpId,} from "../src/identity.js";
/** * Conformance tests for operation identity. * * These are the contract two independent clients must agree on: the same logical * edit must mint the same `opId` (so the sort-boundary dedup collapses it), and * distinct edits must not. They double as executable documentation of the policy * table in `identity.ts`. * * The `opId` field on the inputs is deliberately junk: it is never read by minting * (the id is *derived* from content), so two clients can carry different garbage * there and still converge. */describe("identity", () => { describe("policy", () => { it("classifies each family", () => { const p = (op: any, opts?: any) => identityPolicy(op, opts); expect(p({ $type: "local.crdt.ops#insert" })).toEqual({ strategy: "content", includesDeps: true, }); expect(p({ $type: "local.crdt.ops#delete" })).toEqual({ strategy: "content", includesDeps: false, }); expect(p({ $type: "local.crdt.ops#set" }).includesDeps).toBe(true); expect(p({ $type: "local.crdt.ops#assign" }).includesDeps).toBe(true); expect(p({ $type: "local.crdt.ops#remove" }).includesDeps).toBe(true); // ref add -> content; inline add -> nonce by default, content under "set". expect(p({ $type: "local.crdt.ops#add", valueId: "v1" }).strategy).toBe("content"); expect(p({ $type: "local.crdt.ops#add" }).strategy).toBe("nonce"); expect(p({ $type: "local.crdt.ops#add" }, { inlineAdd: "set" }).strategy).toBe("content"); expect(p({ $type: "local.crdt.ops#incr" }).strategy).toBe("nonce"); }); });
describe("inserts merge by content (typo case)", () => { it("two clients, same edit -> same opId (deps reordered, side omitted vs explicit)", async () => { // Both fix "a typ[a]" -> "a typo": insert "o" after element c2.4, deleting // the bad char is a separate (idempotent) delete. The inserts are what // would otherwise double. const clientA = { opId: "garbage-A", $type: "local.crdt.ops#insert" as const, previousOpIds: ["c2", "d-bad"], target: "seq", anchor: "c2.4", value: "o", // side omitted -> defaults to "after" }; const clientB = { opId: "garbage-B", $type: "local.crdt.ops#insert" as const, previousOpIds: ["d-bad", "c2"], // different order, same set target: "seq", anchor: "c2.4", side: "after" as const, // explicit value: "o", }; expect(await mintOpId(clientA)).toBe(await mintOpId(clientB)); });
it("different value -> different opId", async () => { const base = { $type: "local.crdt.ops#insert" as const, previousOpIds: [], target: "seq", anchor: "BEGIN", }; expect(await mintOpId({ ...base, value: "o" })).not.toBe( await mintOpId({ ...base, value: "x" }), ); });
it("different anchor -> different opId", async () => { const base = { $type: "local.crdt.ops#insert" as const, previousOpIds: [], target: "seq", value: "o", }; expect(await mintOpId({ ...base, anchor: "c2.4" })).not.toBe( await mintOpId({ ...base, anchor: "c2.5" }), ); });
it("same content but different deps -> different opId (deps are identity)", async () => { // Re-insert after a delete must stay distinct so it can re-appear. const base = { $type: "local.crdt.ops#insert" as const, target: "seq", anchor: "BEGIN", value: "o", }; expect(await mintOpId({ ...base, previousOpIds: [] })).not.toBe( await mintOpId({ ...base, previousOpIds: ["del-1"] }), ); }); });
describe("deletes are position-independent (deps excluded)", () => { it("same range, different deps -> SAME opId", async () => { const base = { $type: "local.crdt.ops#delete" as const, target: "seq", id: "c2", startIdx: 4, endIdx: 4, }; expect(await mintOpId({ ...base, previousOpIds: ["c2"] })).toBe( await mintOpId({ ...base, previousOpIds: ["c2", "x"] }), ); });
it("different range -> different opId", async () => { const base = { $type: "local.crdt.ops#delete" as const, previousOpIds: ["c2"], target: "seq", id: "c2", }; expect(await mintOpId({ ...base, startIdx: 0, endIdx: 2 })).not.toBe( await mintOpId({ ...base, startIdx: 0, endIdx: 4 }), ); });
it("open bound omitted matches open bound omitted", async () => { // NOTE: omitted (-> null) is a *different* identity from an explicit 0. // Clients must canonically represent open ranges (omit the bound). const whole = { $type: "local.crdt.ops#delete" as const, previousOpIds: ["c2"], target: "seq", id: "c2", }; expect(await mintOpId(whole)).toBe(await mintOpId({ ...whole })); expect(await mintOpId(whole)).not.toBe( await mintOpId({ ...whole, startIdx: 0 }), ); }); });
describe("registers", () => { it("set: identical value+deps merge; re-assertion (new deps) stays distinct", async () => { const five = { $type: "local.crdt.ops#set" as const, target: "reg", value: 5 }; expect(await mintOpId({ ...five, previousOpIds: [] })).toBe( await mintOpId({ ...five, previousOpIds: [] }), ); expect(await mintOpId({ ...five, previousOpIds: [] })).not.toBe( await mintOpId({ ...five, previousOpIds: ["set-7"] }), ); }); });
describe("or-set", () => { it("ref add: same valueId+deps -> same opId", async () => { const add = { $type: "local.crdt.ops#add" as const, previousOpIds: [], target: "set", valueId: "v1", value: { $ref: "v1" }, }; // opId is ignored by minting, so two clients' different garbage ids agree. expect(await mintOpId({ ...add, opId: "a" })).toBe( await mintOpId({ ...add, opId: "b" }), ); });
it("inline add: multiset (default) refuses content id; set collapses identical data", async () => { const add = { $type: "local.crdt.ops#add" as const, previousOpIds: [], target: "set", value: "hello", }; expect(isContentAddressed(add)).toBe(false); await expect(mintOpId(add)).rejects.toThrow(/occurrence-bearing/); // Opt into set semantics: two identical inline adds collapse. expect(await mintOpId(add, { inlineAdd: "set" })).toBe( await mintOpId({ ...add }, { inlineAdd: "set" }), ); }); });
describe("counters are not content-addressed", () => { it("incr refuses a content id (two +1s must stay distinct -> +2)", async () => { const incr = { $type: "local.crdt.ops#incr" as const, previousOpIds: [], target: "ctr", delta: 1, }; expect(isContentAddressed(incr)).toBe(false); await expect(mintOpId(incr)).rejects.toThrow(/occurrence-bearing/); }); });});