1import { describe, expect, it } from "vitest";
2
3import {
4 aggregateRollup,
5 collectLinkedValues,
6 inferredType,
7 aggregateAllowed,
8 ROLLUP_AGGREGATES,
9} from "./index.js";
10import type { PropertyValue } from "../values/types.js";
11
12const cfg = (aggregate: string, target = "number", display?: "list" | "count") => ({
13 aggregate: aggregate as never,
14 targetPropertyType: target,
15 ...(display ? { display } : {}),
16});
17
18describe("aggregateRollup — the empty-set rules (recorded)", () => {
19 it("COUNT over zero matching records = 0", () => {
20 expect(aggregateRollup([], cfg("COUNT"))).toBe(0);
21 });
22
23 it("SUM/AVERAGE over zero numerics = null (not NaN / not 0)", () => {
24 expect(aggregateRollup([], cfg("SUM"))).toBeNull();
25 expect(aggregateRollup([null, null], cfg("SUM"))).toBeNull();
26 expect(aggregateRollup([], cfg("AVERAGE"))).toBeNull();
27 });
28
29 it("MIN/MAX over zero values = null", () => {
30 expect(aggregateRollup([], cfg("MIN"))).toBeNull();
31 expect(aggregateRollup([], cfg("MAX"))).toBeNull();
32 });
33
34 it("the spec example: 4 tasks, 3 Done, estimates 5/10/15/null", () => {
35 const estimates = [5, 10, 15, null];
36 expect(aggregateRollup([5, 10, 15, null], cfg("COUNT"))).toBe(4);
37
38 expect(aggregateRollup([5, 10, 15], cfg("COUNT"))).toBe(3);
39 expect(aggregateRollup(estimates, cfg("SUM"))).toBe(30);
40 expect(aggregateRollup(estimates, cfg("AVERAGE"))).toBe(10);
41 expect(aggregateRollup(estimates, cfg("MIN"))).toBe(5);
42 expect(aggregateRollup(estimates, cfg("MAX"))).toBe(15);
43
44 expect(aggregateRollup([5, 10, 15], cfg("PERCENT"), 4)).toBe(0.75);
45
46 expect(aggregateRollup(estimates, cfg("PERCENT"))).toBe(1);
47 });
48
49 it("UNIQUE: deduped list + count display mode", () => {
50 expect(aggregateRollup(["done", "open", "done"], cfg("UNIQUE", "select"))).toEqual([
51 "done",
52 "open",
53 ]);
54 expect(aggregateRollup(["done", "open", "done"], cfg("UNIQUE", "select", "count"))).toBe(2);
55 expect(aggregateRollup([], cfg("UNIQUE", "select"))).toEqual([]);
56 });
57
58 it("MIN/MAX over dates (ISO strings, UTC-day math)", () => {
59 const dates = ["2025-06-10", "2025-06-01", "2025-06-20"];
60 expect(aggregateRollup(dates, cfg("MIN", "date"))).toBe("2025-06-01");
61 expect(aggregateRollup(dates, cfg("MAX", "date"))).toBe("2025-06-20");
62 });
63});
64
65describe("inferredType + aggregateAllowed (config-time 400s)", () => {
66 it("type inference per aggregate", () => {
67 expect(inferredType("SUM", "number")).toBe("number");
68 expect(inferredType("AVERAGE", "number")).toBe("number");
69 expect(inferredType("COUNT", "select")).toBe("number");
70 expect(inferredType("PERCENT", "checkbox")).toBe("number");
71 expect(inferredType("MIN", "date")).toBe("date");
72 expect(inferredType("MAX", "number")).toBe("number");
73 expect(inferredType("UNIQUE", "select")).toBe("array");
74 });
75
76 it("allowed target types per aggregate", () => {
77 expect(ROLLUP_AGGREGATES).toHaveLength(7);
78 expect(aggregateAllowed("SUM", "number")).toBe(true);
79 expect(aggregateAllowed("SUM", "date")).toBe(false);
80 expect(aggregateAllowed("AVERAGE", "text")).toBe(false);
81 expect(aggregateAllowed("MIN", "date")).toBe(true);
82 expect(aggregateAllowed("MIN", "text")).toBe(false);
83 expect(aggregateAllowed("COUNT", "relation")).toBe(true);
84 expect(aggregateAllowed("UNIQUE", "multi_select")).toBe(true);
85 });
86});
87
88describe("collectLinkedValues", () => {
89 it("joins links → target values; respects matchIds (the rollup filter)", () => {
90 const values = new Map<string, PropertyValue | undefined>([
91 ["t1", 5],
92 ["t2", 10],
93 ["t3", 15],
94 ]);
95 expect(collectLinkedValues(["t1", "t2", "t3", "t4"], values)).toEqual([5, 10, 15, undefined]);
96
97 expect(collectLinkedValues(["t1", "t2", "t3"], values, new Set(["t1", "t2"]))).toEqual([5, 10]);
98 });
99});
100