Skip to content

Commit bb23a8d

Browse files
committed
test: add algorithm module unit tests
Add unit tests for: - decomposition: biconnected, core-periphery, k-core - extraction: validators - graph: graph-adapter - hierarchical: clustering - pathfinding: mutual-information, path-ranking - utils: csr, type-guards, validators
1 parent fc17446 commit bb23a8d

File tree

11 files changed

+3771
-0
lines changed

11 files changed

+3771
-0
lines changed
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { Graph } from "../graph/graph";
4+
import { type Edge, type Node } from "../types/graph";
5+
import { biconnectedComponents } from "./biconnected";
6+
7+
// Test node and edge types
8+
interface TestNode extends Node {
9+
id: string;
10+
type: string;
11+
}
12+
13+
interface TestEdge extends Edge {
14+
id: string;
15+
source: string;
16+
target: string;
17+
type: string;
18+
}
19+
20+
// Helper to create a test node
21+
const createNode = (id: string): TestNode => ({ id, type: "test" });
22+
23+
// Helper to create a test edge
24+
const createEdge = (id: string, source: string, target: string): TestEdge => ({
25+
id,
26+
source,
27+
target,
28+
type: "test",
29+
});
30+
31+
describe("biconnectedComponents", () => {
32+
describe("validation errors", () => {
33+
it("should return error for empty graph", () => {
34+
const graph = new Graph<TestNode, TestEdge>(false);
35+
const result = biconnectedComponents(graph);
36+
37+
expect(result.ok).toBe(false);
38+
if (!result.ok) {
39+
expect(result.error.type).toBe("EmptyGraph");
40+
}
41+
});
42+
43+
it("should return error for single node graph", () => {
44+
const graph = new Graph<TestNode, TestEdge>(false);
45+
graph.addNode(createNode("A"));
46+
47+
const result = biconnectedComponents(graph);
48+
49+
expect(result.ok).toBe(false);
50+
if (!result.ok) {
51+
expect(result.error.type).toBe("InsufficientNodes");
52+
}
53+
});
54+
55+
it("should return error for directed graph", () => {
56+
const graph = new Graph<TestNode, TestEdge>(true);
57+
graph.addNode(createNode("A"));
58+
graph.addNode(createNode("B"));
59+
graph.addEdge(createEdge("E1", "A", "B"));
60+
61+
const result = biconnectedComponents(graph);
62+
63+
expect(result.ok).toBe(false);
64+
if (!result.ok) {
65+
expect(result.error.type).toBe("InvalidInput");
66+
}
67+
});
68+
});
69+
70+
describe("simple graphs", () => {
71+
it("should handle two nodes with no edges", () => {
72+
const graph = new Graph<TestNode, TestEdge>(false);
73+
graph.addNode(createNode("A"));
74+
graph.addNode(createNode("B"));
75+
76+
const result = biconnectedComponents(graph);
77+
78+
expect(result.ok).toBe(true);
79+
if (result.ok) {
80+
// Each node is its own trivial component
81+
expect(result.value.components.length).toBe(2);
82+
expect(result.value.articulationPoints.size).toBe(0);
83+
}
84+
});
85+
86+
it("should handle two connected nodes (bridge)", () => {
87+
const graph = new Graph<TestNode, TestEdge>(false);
88+
graph.addNode(createNode("A"));
89+
graph.addNode(createNode("B"));
90+
graph.addEdge(createEdge("E1", "A", "B"));
91+
92+
const result = biconnectedComponents(graph);
93+
94+
expect(result.ok).toBe(true);
95+
if (result.ok) {
96+
expect(result.value.components.length).toBe(1);
97+
expect(result.value.components[0].isBridge).toBe(true);
98+
expect(result.value.components[0].size).toBe(2);
99+
}
100+
});
101+
102+
it("should identify triangle as single biconnected component", () => {
103+
const graph = new Graph<TestNode, TestEdge>(false);
104+
graph.addNode(createNode("A"));
105+
graph.addNode(createNode("B"));
106+
graph.addNode(createNode("C"));
107+
graph.addEdge(createEdge("E1", "A", "B"));
108+
graph.addEdge(createEdge("E2", "B", "C"));
109+
graph.addEdge(createEdge("E3", "C", "A"));
110+
111+
const result = biconnectedComponents(graph);
112+
113+
expect(result.ok).toBe(true);
114+
if (result.ok) {
115+
expect(result.value.components.length).toBe(1);
116+
expect(result.value.components[0].size).toBe(3);
117+
expect(result.value.components[0].isBridge).toBe(false);
118+
expect(result.value.articulationPoints.size).toBe(0);
119+
}
120+
});
121+
});
122+
123+
describe("articulation points", () => {
124+
it("should identify articulation point in linear graph", () => {
125+
// A - B - C (B is articulation point)
126+
const graph = new Graph<TestNode, TestEdge>(false);
127+
graph.addNode(createNode("A"));
128+
graph.addNode(createNode("B"));
129+
graph.addNode(createNode("C"));
130+
graph.addEdge(createEdge("E1", "A", "B"));
131+
graph.addEdge(createEdge("E2", "B", "C"));
132+
133+
const result = biconnectedComponents(graph);
134+
135+
expect(result.ok).toBe(true);
136+
if (result.ok) {
137+
expect(result.value.articulationPoints.has("B")).toBe(true);
138+
expect(result.value.components.length).toBe(2);
139+
}
140+
});
141+
142+
it("should identify articulation point connecting two triangles", () => {
143+
// Triangle (A-B-C) connected to triangle (C-D-E) via C
144+
const graph = new Graph<TestNode, TestEdge>(false);
145+
graph.addNode(createNode("A"));
146+
graph.addNode(createNode("B"));
147+
graph.addNode(createNode("C"));
148+
graph.addNode(createNode("D"));
149+
graph.addNode(createNode("E"));
150+
151+
// First triangle
152+
graph.addEdge(createEdge("E1", "A", "B"));
153+
graph.addEdge(createEdge("E2", "B", "C"));
154+
graph.addEdge(createEdge("E3", "C", "A"));
155+
156+
// Second triangle
157+
graph.addEdge(createEdge("E4", "C", "D"));
158+
graph.addEdge(createEdge("E5", "D", "E"));
159+
graph.addEdge(createEdge("E6", "E", "C"));
160+
161+
const result = biconnectedComponents(graph);
162+
163+
expect(result.ok).toBe(true);
164+
if (result.ok) {
165+
expect(result.value.articulationPoints.has("C")).toBe(true);
166+
expect(result.value.components.length).toBe(2);
167+
}
168+
});
169+
170+
it("should identify root articulation point with multiple children", () => {
171+
// Star graph: A at center connected to B, C, D
172+
// A has multiple DFS children, so it's an articulation point
173+
const graph = new Graph<TestNode, TestEdge>(false);
174+
graph.addNode(createNode("A"));
175+
graph.addNode(createNode("B"));
176+
graph.addNode(createNode("C"));
177+
graph.addNode(createNode("D"));
178+
graph.addEdge(createEdge("E1", "A", "B"));
179+
graph.addEdge(createEdge("E2", "A", "C"));
180+
graph.addEdge(createEdge("E3", "A", "D"));
181+
182+
const result = biconnectedComponents(graph);
183+
184+
expect(result.ok).toBe(true);
185+
if (result.ok) {
186+
expect(result.value.articulationPoints.has("A")).toBe(true);
187+
// 3 bridge components
188+
expect(result.value.components.length).toBe(3);
189+
}
190+
});
191+
});
192+
193+
describe("complex graphs", () => {
194+
it("should handle disconnected components", () => {
195+
const graph = new Graph<TestNode, TestEdge>(false);
196+
// Component 1: A - B
197+
graph.addNode(createNode("A"));
198+
graph.addNode(createNode("B"));
199+
graph.addEdge(createEdge("E1", "A", "B"));
200+
201+
// Component 2: C - D
202+
graph.addNode(createNode("C"));
203+
graph.addNode(createNode("D"));
204+
graph.addEdge(createEdge("E2", "C", "D"));
205+
206+
const result = biconnectedComponents(graph);
207+
208+
expect(result.ok).toBe(true);
209+
if (result.ok) {
210+
expect(result.value.components.length).toBe(2);
211+
expect(result.value.articulationPoints.size).toBe(0);
212+
}
213+
});
214+
215+
it("should handle graph with multiple biconnected components and articulation points", () => {
216+
// Graph: A-B-C-D-E with B-C forming a triangle
217+
const graph = new Graph<TestNode, TestEdge>(false);
218+
graph.addNode(createNode("A"));
219+
graph.addNode(createNode("B"));
220+
graph.addNode(createNode("C"));
221+
graph.addNode(createNode("D"));
222+
graph.addNode(createNode("E"));
223+
224+
graph.addEdge(createEdge("E1", "A", "B"));
225+
graph.addEdge(createEdge("E2", "B", "C"));
226+
graph.addEdge(createEdge("E3", "B", "D")); // Creates back edge
227+
graph.addEdge(createEdge("E4", "C", "D"));
228+
graph.addEdge(createEdge("E5", "D", "E"));
229+
230+
const result = biconnectedComponents(graph);
231+
232+
expect(result.ok).toBe(true);
233+
if (result.ok) {
234+
// B is articulation point (connects A to rest)
235+
// D is articulation point (connects E to rest)
236+
expect(result.value.articulationPoints.size).toBeGreaterThanOrEqual(1);
237+
}
238+
});
239+
});
240+
241+
describe("metadata", () => {
242+
it("should include runtime in metadata", () => {
243+
const graph = new Graph<TestNode, TestEdge>(false);
244+
graph.addNode(createNode("A"));
245+
graph.addNode(createNode("B"));
246+
graph.addEdge(createEdge("E1", "A", "B"));
247+
248+
const result = biconnectedComponents(graph);
249+
250+
expect(result.ok).toBe(true);
251+
if (result.ok) {
252+
expect(result.value.metadata.algorithm).toBe("biconnected");
253+
expect(typeof result.value.metadata.runtime).toBe("number");
254+
expect(result.value.metadata.runtime).toBeGreaterThanOrEqual(0);
255+
}
256+
});
257+
});
258+
});

0 commit comments

Comments
 (0)