import { describe, it, expect } from "vitest"; import { FlowGraph } from "../../src/graph/construction.js"; import { topologicalOrder, parallelGroups, criticalPath, reachableFrom, ancestors, descendants, } from "../../src/analysis/ordering.js"; import { CycleError, NodeNotFoundError } from "../../src/error/index.js"; function buildDiamondDag(): FlowGraph { const fg = new FlowGraph(); fg.addNode("top", { name: "top" }); fg.addNode("left", { name: "left" }); fg.addNode("right", { name: "right" }); fg.addNode("bottom", { name: "bottom" }); fg.addEdge("top", "left"); fg.addEdge("top", "right"); fg.addEdge("left", "bottom"); fg.addEdge("right", "bottom"); return fg; } function buildChainDag(): FlowGraph { const fg = new FlowGraph(); fg.addNode("a", { name: "a" }); fg.addNode("b", { name: "b" }); fg.addNode("c", { name: "c" }); fg.addNode("d", { name: "d" }); fg.addEdge("a", "b"); fg.addEdge("b", "c"); fg.addEdge("c", "d"); return fg; } function buildWideDag(): FlowGraph { const fg = new FlowGraph(); fg.addNode("root", { name: "root" }); fg.addNode("x", { name: "x" }); fg.addNode("y", { name: "y" }); fg.addNode("z", { name: "z" }); fg.addEdge("root", "x"); fg.addEdge("root", "y"); fg.addEdge("root", "z"); return fg; } function buildDisconnectedDag(): FlowGraph { const fg = new FlowGraph(); fg.addNode("a", { name: "a" }); fg.addNode("b", { name: "b" }); fg.addNode("c", { name: "c" }); fg.addEdge("a", "b"); return fg; } describe("topologicalOrder (analysis)", () => { it("returns topological order for a chain", () => { const fg = buildChainDag(); expect(topologicalOrder(fg)).toEqual(["a", "b", "c", "d"]); }); it("returns topological order for a diamond DAG", () => { const fg = buildDiamondDag(); const order = topologicalOrder(fg); expect(order[0]).toBe("top"); expect(order[3]).toBe("bottom"); expect(order).toContain("left"); expect(order).toContain("right"); }); it("returns empty array for empty graph", () => { const fg = new FlowGraph(); expect(topologicalOrder(fg)).toEqual([]); }); it("returns single node for graph with one node", () => { const fg = new FlowGraph(); fg.addNode("only", { name: "only" }); expect(topologicalOrder(fg)).toEqual(["only"]); }); it("throws CycleError when graph has cycles", () => { const fg = new FlowGraph(); fg.graph.addNode("a"); fg.graph.addNode("b"); fg.graph.addNode("c"); fg.graph.addEdgeWithKey("a->b", "a", "b"); fg.graph.addEdgeWithKey("b->c", "b", "c"); fg.graph.addEdgeWithKey("c->a", "c", "a"); expect(() => topologicalOrder(fg)).toThrow(CycleError); }); }); describe("parallelGroups (analysis)", () => { it("groups chain nodes into sequential groups", () => { const fg = buildChainDag(); const groups = parallelGroups(fg); expect(groups).toEqual([["a"], ["b"], ["c"], ["d"]]); }); it("groups diamond DAG correctly", () => { const fg = buildDiamondDag(); const groups = parallelGroups(fg); expect(groups.length).toBe(3); expect(groups[0]).toEqual(["top"]); expect(groups[1]!.sort()).toEqual(["left", "right"]); expect(groups[2]).toEqual(["bottom"]); }); it("groups wide DAG with root and leaves", () => { const fg = buildWideDag(); const groups = parallelGroups(fg); expect(groups.length).toBe(2); expect(groups[0]).toEqual(["root"]); expect(groups[1]!.sort()).toEqual(["x", "y", "z"]); }); it("groups disconnected DAG correctly", () => { const fg = buildDisconnectedDag(); const groups = parallelGroups(fg); expect(groups.length).toBe(2); expect(groups[0]!.sort()).toEqual(["a", "c"]); expect(groups[1]).toEqual(["b"]); }); it("returns empty array for empty graph", () => { const fg = new FlowGraph(); expect(parallelGroups(fg)).toEqual([]); }); it("returns single group for single node with no edges", () => { const fg = new FlowGraph(); fg.addNode("only", { name: "only" }); expect(parallelGroups(fg)).toEqual([["only"]]); }); it("throws CycleError when graph has cycles", () => { const fg = new FlowGraph(); fg.graph.addNode("a"); fg.graph.addNode("b"); fg.graph.addEdgeWithKey("a->b", "a", "b"); fg.graph.addEdgeWithKey("b->a", "b", "a"); expect(() => parallelGroups(fg)).toThrow(CycleError); }); }); describe("criticalPath (analysis)", () => { it("returns full chain for a chain DAG", () => { const fg = buildChainDag(); expect(criticalPath(fg)).toEqual(["a", "b", "c", "d"]); }); it("returns a longest path for diamond DAG", () => { const fg = buildDiamondDag(); const path = criticalPath(fg); expect(path.length).toBe(3); expect(path[0]).toBe("top"); expect(path[2]).toBe("bottom"); expect(path[1] === "left" || path[1] === "right").toBe(true); }); it("returns single node for graph with one node", () => { const fg = new FlowGraph(); fg.addNode("only", { name: "only" }); expect(criticalPath(fg)).toEqual(["only"]); }); it("returns empty array for empty graph", () => { const fg = new FlowGraph(); expect(criticalPath(fg)).toEqual([]); }); it("returns correct path for wide DAG", () => { const fg = buildWideDag(); const path = criticalPath(fg); expect(path.length).toBe(2); expect(path[0]).toBe("root"); }); it("throws CycleError when graph has cycles", () => { const fg = new FlowGraph(); fg.graph.addNode("a"); fg.graph.addNode("b"); fg.graph.addEdgeWithKey("a->b", "a", "b"); fg.graph.addEdgeWithKey("b->a", "b", "a"); expect(() => criticalPath(fg)).toThrow(CycleError); }); }); describe("reachableFrom (analysis)", () => { it("returns all reachable nodes from a single start node", () => { const fg = buildChainDag(); expect(reachableFrom(fg, ["a"])).toEqual(new Set(["a", "b", "c", "d"])); }); it("returns only the start node if it is a leaf", () => { const fg = buildChainDag(); expect(reachableFrom(fg, ["d"])).toEqual(new Set(["d"])); }); it("returns union of reachable nodes from multiple start nodes", () => { const fg = buildDiamondDag(); expect(reachableFrom(fg, ["left", "right"])).toEqual(new Set(["left", "right", "bottom"])); }); it("returns empty set for empty input", () => { const fg = buildChainDag(); expect(reachableFrom(fg, [])).toEqual(new Set()); }); it("returns full reachable set from root in diamond", () => { const fg = buildDiamondDag(); expect(reachableFrom(fg, ["top"])).toEqual(new Set(["top", "left", "right", "bottom"])); }); }); describe("ancestors (analysis)", () => { it("returns all ancestors for a node in a chain", () => { const fg = buildChainDag(); expect(ancestors(fg, "d")).toEqual(["c", "b", "a"]); }); it("returns all ancestors for a node in a diamond DAG", () => { const fg = buildDiamondDag(); const anc = ancestors(fg, "bottom"); expect(anc).toContain("left"); expect(anc).toContain("right"); expect(anc).toContain("top"); expect(anc.length).toBe(3); }); it("returns empty array for a root node", () => { const fg = buildChainDag(); expect(ancestors(fg, "a")).toEqual([]); }); it("throws NodeNotFoundError for missing node", () => { const fg = new FlowGraph(); expect(() => ancestors(fg, "missing")).toThrow(NodeNotFoundError); }); }); describe("descendants (analysis)", () => { it("returns all descendants for a node in a chain", () => { const fg = buildChainDag(); expect(descendants(fg, "a")).toEqual(["b", "c", "d"]); }); it("returns all descendants for a node in a diamond DAG", () => { const fg = buildDiamondDag(); const desc = descendants(fg, "top"); expect(desc).toContain("left"); expect(desc).toContain("right"); expect(desc).toContain("bottom"); expect(desc.length).toBe(3); }); it("returns empty array for a leaf node", () => { const fg = buildChainDag(); expect(descendants(fg, "d")).toEqual([]); }); it("throws NodeNotFoundError for missing node", () => { const fg = new FlowGraph(); expect(() => descendants(fg, "missing")).toThrow(NodeNotFoundError); }); });