import { describe, it, expect } from 'vitest'; import { resolveDependencies, areDependenciesSatisfied, getBlockingDependencies, type DependencyResolutionResult, } from '@automaker/dependency-resolver'; import type { Feature } from '@automaker/types'; // 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'); }); }); });