1import { describe, expect, it } from "vitest"; 2 3import { 4 diffLinks, 5 applyCardinality, 6 planSetLinks, 7 linksForRecord, 8 relationCounts, 9} from "./index.js"; 10 11describe("diffLinks (absolute set semantics)", () => { 12 it("added/removed/unchanged", () => { 13 const d = diffLinks(["a", "b", "c"], ["b", "c", "d"]); 14 expect(d.added).toEqual(["d"]); 15 expect(d.removed).toEqual(["a"]); 16 expect(d.unchanged).toEqual(["b", "c"]); 17 }); 18 19 it("dedupes the desired set", () => { 20 expect(applyCardinality(["a"], ["a", "a"], "MANY_TO_MANY", true)).toEqual({ 21 ok: true, 22 replaced: [], 23 }); 24 }); 25}); 26 27describe("cardinality policy (1:1 replacement, 1:N one-side)", () => { 28 it("1:1 one side: second target replaces the first", () => { 29 const r = applyCardinality(["t1"], ["t2"], "ONE_TO_ONE", true); 30 expect(r.ok).toBe(true); 31 expect(r.replaced).toEqual(["t1"]); 32 }); 33 34 it("1:N one side rejects >1 target; many side unlimited", () => { 35 expect(applyCardinality([], ["t1", "t2"], "ONE_TO_MANY", true)).toMatchObject({ 36 ok: false, 37 error: "one_side_exceeds_one_target", 38 }); 39 expect(applyCardinality([], ["t1", "t2", "t3"], "ONE_TO_MANY", false).ok).toBe(true); 40 }); 41 42 it("M:N unlimited", () => { 43 expect(applyCardinality(["t1"], ["t2", "t3"], "MANY_TO_MANY", true).ok).toBe(true); 44 }); 45}); 46 47describe("planSetLinks (the transaction plan)", () => { 48 it("M:N add 2 remove 1", () => { 49 const plan = planSetLinks( 50 ["a", "b", "c"], 51 ["b", "c", "d", "e"], 52 "MANY_TO_MANY", 53 false, 54 new Map(), 55 ); 56 expect(plan.deleteIds).toEqual(["a"]); 57 expect(plan.insertIds).toEqual(["d", "e"]); 58 expect(plan.displacedPartners).toEqual([]); 59 }); 60 61 it("1:1 bidirectional: displaced partner on the target side", () => { 62 const partners = new Map([["t2", "other-record"]]); 63 const plan = planSetLinks(["t1"], ["t2"], "ONE_TO_ONE", true, partners); 64 expect(plan.insertIds).toEqual(["t2"]); 65 expect(plan.deleteIds).toEqual(["t1"]); 66 expect(plan.displacedPartners).toEqual([{ targetId: "t2", partnerRecordId: "other-record" }]); 67 expect(plan.replaced).toEqual(["t1"]); 68 }); 69 70 it("throws on a cardinality violation (API maps to 400)", () => { 71 expect(() => planSetLinks([], ["a", "b"], "ONE_TO_MANY", true, new Map())).toThrow( 72 "one_side_exceeds_one_target", 73 ); 74 }); 75}); 76 77describe("cascade + query integration helpers", () => { 78 it("linksForRecord (record trash cascade)", () => { 79 const links = [ 80 { databaseId: "db1", recordId: "r1", targetDatabaseId: "db2", targetRecordId: "t1" }, 81 { databaseId: "db1", recordId: "r1", targetDatabaseId: "db2", targetRecordId: "t2" }, 82 { databaseId: "db1", recordId: "r2", targetDatabaseId: "db2", targetRecordId: "t1" }, 83 ]; 84 expect(linksForRecord(links, "r1").sort()).toEqual(["t1", "t2"]); 85 }); 86 87 it("relationCounts for the query response (TASK-0081 R2)", () => { 88 const counts = relationCounts( 89 [ 90 { recordId: "r1", targetRecordId: "t1" }, 91 { recordId: "r1", targetRecordId: "t2" }, 92 { recordId: "r2", targetRecordId: "t1" }, 93 ], 94 ["r1", "r2", "r3"], 95 ); 96 expect(counts).toEqual({ r1: 2, r2: 1, r3: 0 }); 97 }); 98}); 99