import { describe, expect, it } from "vitest"; import { diffLinks, applyCardinality, planSetLinks, linksForRecord, relationCounts, } from "./index.js"; describe("diffLinks (absolute set semantics)", () => { it("added/removed/unchanged", () => { const d = diffLinks(["a", "b", "c"], ["b", "c", "d"]); expect(d.added).toEqual(["d"]); expect(d.removed).toEqual(["a"]); expect(d.unchanged).toEqual(["b", "c"]); }); it("dedupes the desired set", () => { expect(applyCardinality(["a"], ["a", "a"], "MANY_TO_MANY", true)).toEqual({ ok: true, replaced: [], }); }); }); describe("cardinality policy (1:1 replacement, 1:N one-side)", () => { it("1:1 one side: second target replaces the first", () => { const r = applyCardinality(["t1"], ["t2"], "ONE_TO_ONE", true); expect(r.ok).toBe(true); expect(r.replaced).toEqual(["t1"]); }); it("1:N one side rejects >1 target; many side unlimited", () => { expect(applyCardinality([], ["t1", "t2"], "ONE_TO_MANY", true)).toMatchObject({ ok: false, error: "one_side_exceeds_one_target", }); expect(applyCardinality([], ["t1", "t2", "t3"], "ONE_TO_MANY", false).ok).toBe(true); }); it("M:N unlimited", () => { expect(applyCardinality(["t1"], ["t2", "t3"], "MANY_TO_MANY", true).ok).toBe(true); }); }); describe("planSetLinks (the transaction plan)", () => { it("M:N add 2 remove 1", () => { const plan = planSetLinks( ["a", "b", "c"], ["b", "c", "d", "e"], "MANY_TO_MANY", false, new Map(), ); expect(plan.deleteIds).toEqual(["a"]); expect(plan.insertIds).toEqual(["d", "e"]); expect(plan.displacedPartners).toEqual([]); }); it("1:1 bidirectional: displaced partner on the target side", () => { const partners = new Map([["t2", "other-record"]]); const plan = planSetLinks(["t1"], ["t2"], "ONE_TO_ONE", true, partners); expect(plan.insertIds).toEqual(["t2"]); expect(plan.deleteIds).toEqual(["t1"]); expect(plan.displacedPartners).toEqual([{ targetId: "t2", partnerRecordId: "other-record" }]); expect(plan.replaced).toEqual(["t1"]); }); it("throws on a cardinality violation (API maps to 400)", () => { expect(() => planSetLinks([], ["a", "b"], "ONE_TO_MANY", true, new Map())).toThrow( "one_side_exceeds_one_target", ); }); }); describe("cascade + query integration helpers", () => { it("linksForRecord (record trash cascade)", () => { const links = [ { databaseId: "db1", recordId: "r1", targetDatabaseId: "db2", targetRecordId: "t1" }, { databaseId: "db1", recordId: "r1", targetDatabaseId: "db2", targetRecordId: "t2" }, { databaseId: "db1", recordId: "r2", targetDatabaseId: "db2", targetRecordId: "t1" }, ]; expect(linksForRecord(links, "r1").sort()).toEqual(["t1", "t2"]); }); it("relationCounts for the query response (TASK-0081 R2)", () => { const counts = relationCounts( [ { recordId: "r1", targetRecordId: "t1" }, { recordId: "r1", targetRecordId: "t2" }, { recordId: "r2", targetRecordId: "t1" }, ], ["r1", "r2", "r3"], ); expect(counts).toEqual({ r1: 2, r2: 1, r3: 0 }); }); });