import { describe, it, expect } from "vitest"; import { resolveDependencies, areDependenciesSatisfied, getBlockingDependencies, type DependencyResolutionResult, } from "@/lib/dependency-resolver.js"; import type { Feature } from "@/services/feature-loader.js"; // Helper to create test features function createFeature( id: string, options: { status?: string; priority?: number; dependencies?: string[]; category?: string; description?: string; } = {} ): Feature { return { id, category: options.category || "test", description: options.description || `Feature ${id}`, status: options.status || "backlog", priority: options.priority, dependencies: options.dependencies, }; } describe("dependency-resolver.ts", () => { describe("resolveDependencies", () => { it("should handle empty feature list", () => { const result = resolveDependencies([]); expect(result.orderedFeatures).toEqual([]); expect(result.circularDependencies).toEqual([]); expect(result.missingDependencies.size).toBe(0); expect(result.blockedFeatures.size).toBe(0); }); it("should handle features with no dependencies", () => { const features = [ createFeature("f1", { priority: 1 }), createFeature("f2", { priority: 2 }), createFeature("f3", { priority: 3 }), ]; const result = resolveDependencies(features); expect(result.orderedFeatures).toHaveLength(3); expect(result.orderedFeatures[0].id).toBe("f1"); // Highest priority first expect(result.orderedFeatures[1].id).toBe("f2"); expect(result.orderedFeatures[2].id).toBe("f3"); expect(result.circularDependencies).toEqual([]); expect(result.missingDependencies.size).toBe(0); expect(result.blockedFeatures.size).toBe(0); }); it("should order features by dependencies (simple chain)", () => { const features = [ createFeature("f3", { dependencies: ["f2"] }), createFeature("f1"), createFeature("f2", { dependencies: ["f1"] }), ]; const result = resolveDependencies(features); expect(result.orderedFeatures).toHaveLength(3); expect(result.orderedFeatures[0].id).toBe("f1"); expect(result.orderedFeatures[1].id).toBe("f2"); expect(result.orderedFeatures[2].id).toBe("f3"); expect(result.circularDependencies).toEqual([]); }); it("should respect priority within same dependency level", () => { const features = [ createFeature("f1", { priority: 3, dependencies: ["base"] }), createFeature("f2", { priority: 1, dependencies: ["base"] }), createFeature("f3", { priority: 2, dependencies: ["base"] }), createFeature("base"), ]; const result = resolveDependencies(features); expect(result.orderedFeatures[0].id).toBe("base"); expect(result.orderedFeatures[1].id).toBe("f2"); // Priority 1 expect(result.orderedFeatures[2].id).toBe("f3"); // Priority 2 expect(result.orderedFeatures[3].id).toBe("f1"); // Priority 3 }); it("should use default priority of 2 when not specified", () => { const features = [ createFeature("f1", { priority: 1 }), createFeature("f2"), // No priority = default 2 createFeature("f3", { priority: 3 }), ]; const result = resolveDependencies(features); expect(result.orderedFeatures[0].id).toBe("f1"); expect(result.orderedFeatures[1].id).toBe("f2"); expect(result.orderedFeatures[2].id).toBe("f3"); }); it("should detect missing dependencies", () => { const features = [ createFeature("f1", { dependencies: ["missing1", "missing2"] }), createFeature("f2", { dependencies: ["f1", "missing3"] }), ]; const result = resolveDependencies(features); expect(result.missingDependencies.size).toBe(2); expect(result.missingDependencies.get("f1")).toEqual(["missing1", "missing2"]); expect(result.missingDependencies.get("f2")).toEqual(["missing3"]); expect(result.orderedFeatures).toHaveLength(2); }); it("should detect blocked features (incomplete dependencies)", () => { const features = [ createFeature("f1", { status: "in_progress" }), createFeature("f2", { status: "backlog", dependencies: ["f1"] }), createFeature("f3", { status: "completed" }), createFeature("f4", { status: "backlog", dependencies: ["f3"] }), ]; const result = resolveDependencies(features); expect(result.blockedFeatures.size).toBe(1); expect(result.blockedFeatures.get("f2")).toEqual(["f1"]); expect(result.blockedFeatures.has("f4")).toBe(false); // f3 is completed }); it("should not block features whose dependencies are verified", () => { const features = [ createFeature("f1", { status: "verified" }), createFeature("f2", { status: "backlog", dependencies: ["f1"] }), ]; const result = resolveDependencies(features); expect(result.blockedFeatures.size).toBe(0); }); it("should detect circular dependencies (simple cycle)", () => { const features = [ createFeature("f1", { dependencies: ["f2"] }), createFeature("f2", { dependencies: ["f1"] }), ]; const result = resolveDependencies(features); expect(result.circularDependencies).toHaveLength(1); expect(result.circularDependencies[0]).toContain("f1"); expect(result.circularDependencies[0]).toContain("f2"); expect(result.orderedFeatures).toHaveLength(2); // Features still included }); it("should detect circular dependencies (multi-node cycle)", () => { const features = [ createFeature("f1", { dependencies: ["f3"] }), createFeature("f2", { dependencies: ["f1"] }), createFeature("f3", { dependencies: ["f2"] }), ]; const result = resolveDependencies(features); expect(result.circularDependencies.length).toBeGreaterThan(0); expect(result.orderedFeatures).toHaveLength(3); }); it("should handle mixed valid and circular dependencies", () => { const features = [ createFeature("base"), createFeature("f1", { dependencies: ["base", "f2"] }), createFeature("f2", { dependencies: ["f1"] }), // Circular with f1 createFeature("f3", { dependencies: ["base"] }), ]; const result = resolveDependencies(features); expect(result.circularDependencies.length).toBeGreaterThan(0); expect(result.orderedFeatures[0].id).toBe("base"); expect(result.orderedFeatures).toHaveLength(4); }); it("should handle complex dependency graph", () => { const features = [ createFeature("ui", { dependencies: ["api", "auth"], priority: 1 }), createFeature("api", { dependencies: ["db"], priority: 2 }), createFeature("auth", { dependencies: ["db"], priority: 1 }), createFeature("db", { priority: 1 }), createFeature("tests", { dependencies: ["ui"], priority: 3 }), ]; const result = resolveDependencies(features); const order = result.orderedFeatures.map(f => f.id); expect(order[0]).toBe("db"); expect(order.indexOf("db")).toBeLessThan(order.indexOf("api")); expect(order.indexOf("db")).toBeLessThan(order.indexOf("auth")); expect(order.indexOf("api")).toBeLessThan(order.indexOf("ui")); expect(order.indexOf("auth")).toBeLessThan(order.indexOf("ui")); expect(order.indexOf("ui")).toBeLessThan(order.indexOf("tests")); expect(result.circularDependencies).toEqual([]); }); it("should handle features with empty dependencies array", () => { const features = [ createFeature("f1", { dependencies: [] }), createFeature("f2", { dependencies: [] }), ]; const result = resolveDependencies(features); expect(result.orderedFeatures).toHaveLength(2); expect(result.circularDependencies).toEqual([]); expect(result.blockedFeatures.size).toBe(0); }); it("should track multiple blocking dependencies", () => { const features = [ createFeature("f1", { status: "in_progress" }), createFeature("f2", { status: "backlog" }), createFeature("f3", { status: "backlog", dependencies: ["f1", "f2"] }), ]; const result = resolveDependencies(features); expect(result.blockedFeatures.get("f3")).toEqual(["f1", "f2"]); }); it("should handle self-referencing dependency", () => { const features = [ createFeature("f1", { dependencies: ["f1"] }), ]; const result = resolveDependencies(features); expect(result.circularDependencies.length).toBeGreaterThan(0); expect(result.orderedFeatures).toHaveLength(1); }); }); describe("areDependenciesSatisfied", () => { it("should return true for feature with no dependencies", () => { const feature = createFeature("f1"); const allFeatures = [feature]; expect(areDependenciesSatisfied(feature, allFeatures)).toBe(true); }); it("should return true for feature with empty dependencies array", () => { const feature = createFeature("f1", { dependencies: [] }); const allFeatures = [feature]; expect(areDependenciesSatisfied(feature, allFeatures)).toBe(true); }); it("should return true when all dependencies are completed", () => { const allFeatures = [ createFeature("f1", { status: "completed" }), createFeature("f2", { status: "completed" }), createFeature("f3", { status: "backlog", dependencies: ["f1", "f2"] }), ]; expect(areDependenciesSatisfied(allFeatures[2], allFeatures)).toBe(true); }); it("should return true when all dependencies are verified", () => { const allFeatures = [ createFeature("f1", { status: "verified" }), createFeature("f2", { status: "verified" }), createFeature("f3", { status: "backlog", dependencies: ["f1", "f2"] }), ]; expect(areDependenciesSatisfied(allFeatures[2], allFeatures)).toBe(true); }); it("should return true when dependencies are mix of completed and verified", () => { const allFeatures = [ createFeature("f1", { status: "completed" }), createFeature("f2", { status: "verified" }), createFeature("f3", { status: "backlog", dependencies: ["f1", "f2"] }), ]; expect(areDependenciesSatisfied(allFeatures[2], allFeatures)).toBe(true); }); it("should return false when any dependency is in_progress", () => { const allFeatures = [ createFeature("f1", { status: "completed" }), createFeature("f2", { status: "in_progress" }), createFeature("f3", { status: "backlog", dependencies: ["f1", "f2"] }), ]; expect(areDependenciesSatisfied(allFeatures[2], allFeatures)).toBe(false); }); it("should return false when any dependency is in backlog", () => { const allFeatures = [ createFeature("f1", { status: "completed" }), createFeature("f2", { status: "backlog" }), createFeature("f3", { status: "backlog", dependencies: ["f1", "f2"] }), ]; expect(areDependenciesSatisfied(allFeatures[2], allFeatures)).toBe(false); }); it("should return false when dependency is missing", () => { const allFeatures = [ createFeature("f1", { status: "backlog", dependencies: ["missing"] }), ]; expect(areDependenciesSatisfied(allFeatures[0], allFeatures)).toBe(false); }); it("should return false when multiple dependencies are incomplete", () => { const allFeatures = [ createFeature("f1", { status: "backlog" }), createFeature("f2", { status: "in_progress" }), createFeature("f3", { status: "waiting_approval" }), createFeature("f4", { status: "backlog", dependencies: ["f1", "f2", "f3"] }), ]; expect(areDependenciesSatisfied(allFeatures[3], allFeatures)).toBe(false); }); }); describe("getBlockingDependencies", () => { it("should return empty array for feature with no dependencies", () => { const feature = createFeature("f1"); const allFeatures = [feature]; expect(getBlockingDependencies(feature, allFeatures)).toEqual([]); }); it("should return empty array for feature with empty dependencies array", () => { const feature = createFeature("f1", { dependencies: [] }); const allFeatures = [feature]; expect(getBlockingDependencies(feature, allFeatures)).toEqual([]); }); it("should return empty array when all dependencies are completed", () => { const allFeatures = [ createFeature("f1", { status: "completed" }), createFeature("f2", { status: "completed" }), createFeature("f3", { status: "backlog", dependencies: ["f1", "f2"] }), ]; expect(getBlockingDependencies(allFeatures[2], allFeatures)).toEqual([]); }); it("should return empty array when all dependencies are verified", () => { const allFeatures = [ createFeature("f1", { status: "verified" }), createFeature("f2", { status: "verified" }), createFeature("f3", { status: "backlog", dependencies: ["f1", "f2"] }), ]; expect(getBlockingDependencies(allFeatures[2], allFeatures)).toEqual([]); }); it("should return blocking dependencies in backlog status", () => { const allFeatures = [ createFeature("f1", { status: "backlog" }), createFeature("f2", { status: "completed" }), createFeature("f3", { status: "backlog", dependencies: ["f1", "f2"] }), ]; expect(getBlockingDependencies(allFeatures[2], allFeatures)).toEqual(["f1"]); }); it("should return blocking dependencies in in_progress status", () => { const allFeatures = [ createFeature("f1", { status: "in_progress" }), createFeature("f2", { status: "verified" }), createFeature("f3", { status: "backlog", dependencies: ["f1", "f2"] }), ]; expect(getBlockingDependencies(allFeatures[2], allFeatures)).toEqual(["f1"]); }); it("should return blocking dependencies in waiting_approval status", () => { const allFeatures = [ createFeature("f1", { status: "waiting_approval" }), createFeature("f2", { status: "completed" }), createFeature("f3", { status: "backlog", dependencies: ["f1", "f2"] }), ]; expect(getBlockingDependencies(allFeatures[2], allFeatures)).toEqual(["f1"]); }); it("should return all blocking dependencies", () => { const allFeatures = [ createFeature("f1", { status: "backlog" }), createFeature("f2", { status: "in_progress" }), createFeature("f3", { status: "waiting_approval" }), createFeature("f4", { status: "completed" }), createFeature("f5", { status: "backlog", dependencies: ["f1", "f2", "f3", "f4"] }), ]; const blocking = getBlockingDependencies(allFeatures[4], allFeatures); expect(blocking).toHaveLength(3); expect(blocking).toContain("f1"); expect(blocking).toContain("f2"); expect(blocking).toContain("f3"); expect(blocking).not.toContain("f4"); }); it("should handle missing dependencies", () => { const allFeatures = [ createFeature("f1", { status: "backlog", dependencies: ["missing"] }), ]; // Missing dependencies won't be in the blocking list since they don't exist expect(getBlockingDependencies(allFeatures[0], allFeatures)).toEqual([]); }); it("should handle mix of completed, verified, and incomplete dependencies", () => { const allFeatures = [ createFeature("f1", { status: "completed" }), createFeature("f2", { status: "verified" }), createFeature("f3", { status: "in_progress" }), createFeature("f4", { status: "backlog" }), createFeature("f5", { status: "backlog", dependencies: ["f1", "f2", "f3", "f4"] }), ]; const blocking = getBlockingDependencies(allFeatures[4], allFeatures); expect(blocking).toHaveLength(2); expect(blocking).toContain("f3"); expect(blocking).toContain("f4"); }); }); });