import { describe, expect, it } from "vitest"; import { aggregateRollup, collectLinkedValues, inferredType, aggregateAllowed, ROLLUP_AGGREGATES, } from "./index.js"; import type { PropertyValue } from "../values/types.js"; const cfg = (aggregate: string, target = "number", display?: "list" | "count") => ({ aggregate: aggregate as never, targetPropertyType: target, ...(display ? { display } : {}), }); describe("aggregateRollup — the empty-set rules (recorded)", () => { it("COUNT over zero matching records = 0", () => { expect(aggregateRollup([], cfg("COUNT"))).toBe(0); }); it("SUM/AVERAGE over zero numerics = null (not NaN / not 0)", () => { expect(aggregateRollup([], cfg("SUM"))).toBeNull(); expect(aggregateRollup([null, null], cfg("SUM"))).toBeNull(); expect(aggregateRollup([], cfg("AVERAGE"))).toBeNull(); }); it("MIN/MAX over zero values = null", () => { expect(aggregateRollup([], cfg("MIN"))).toBeNull(); expect(aggregateRollup([], cfg("MAX"))).toBeNull(); }); it("the spec example: 4 tasks, 3 Done, estimates 5/10/15/null", () => { const estimates = [5, 10, 15, null]; expect(aggregateRollup([5, 10, 15, null], cfg("COUNT"))).toBe(4); // all links // filtered to Done (3 records: estimates 5, 10, 15) expect(aggregateRollup([5, 10, 15], cfg("COUNT"))).toBe(3); expect(aggregateRollup(estimates, cfg("SUM"))).toBe(30); expect(aggregateRollup(estimates, cfg("AVERAGE"))).toBe(10); expect(aggregateRollup(estimates, cfg("MIN"))).toBe(5); expect(aggregateRollup(estimates, cfg("MAX"))).toBe(15); // PERCENT Done: 3 matching / 4 linked = 0.75 expect(aggregateRollup([5, 10, 15], cfg("PERCENT"), 4)).toBe(0.75); // PERCENT without filter = 1.0 (degenerate, allowed) expect(aggregateRollup(estimates, cfg("PERCENT"))).toBe(1); }); it("UNIQUE: deduped list + count display mode", () => { expect(aggregateRollup(["done", "open", "done"], cfg("UNIQUE", "select"))).toEqual([ "done", "open", ]); expect(aggregateRollup(["done", "open", "done"], cfg("UNIQUE", "select", "count"))).toBe(2); expect(aggregateRollup([], cfg("UNIQUE", "select"))).toEqual([]); }); it("MIN/MAX over dates (ISO strings, UTC-day math)", () => { const dates = ["2025-06-10", "2025-06-01", "2025-06-20"]; expect(aggregateRollup(dates, cfg("MIN", "date"))).toBe("2025-06-01"); expect(aggregateRollup(dates, cfg("MAX", "date"))).toBe("2025-06-20"); }); }); describe("inferredType + aggregateAllowed (config-time 400s)", () => { it("type inference per aggregate", () => { expect(inferredType("SUM", "number")).toBe("number"); expect(inferredType("AVERAGE", "number")).toBe("number"); expect(inferredType("COUNT", "select")).toBe("number"); expect(inferredType("PERCENT", "checkbox")).toBe("number"); expect(inferredType("MIN", "date")).toBe("date"); expect(inferredType("MAX", "number")).toBe("number"); expect(inferredType("UNIQUE", "select")).toBe("array"); }); it("allowed target types per aggregate", () => { expect(ROLLUP_AGGREGATES).toHaveLength(7); expect(aggregateAllowed("SUM", "number")).toBe(true); expect(aggregateAllowed("SUM", "date")).toBe(false); expect(aggregateAllowed("AVERAGE", "text")).toBe(false); expect(aggregateAllowed("MIN", "date")).toBe(true); expect(aggregateAllowed("MIN", "text")).toBe(false); expect(aggregateAllowed("COUNT", "relation")).toBe(true); expect(aggregateAllowed("UNIQUE", "multi_select")).toBe(true); }); }); describe("collectLinkedValues", () => { it("joins links → target values; respects matchIds (the rollup filter)", () => { const values = new Map([ ["t1", 5], ["t2", 10], ["t3", 15], ]); expect(collectLinkedValues(["t1", "t2", "t3", "t4"], values)).toEqual([5, 10, 15, undefined]); // the "status = Done" pre-filter: only t1, t2 match expect(collectLinkedValues(["t1", "t2", "t3"], values, new Set(["t1", "t2"]))).toEqual([5, 10]); }); });