import { describe, it, expect } from 'vitest'; import { resolveDependencies, areDependenciesSatisfied, getBlockingDependencies, } from '../src/resolver'; import type { Feature } from '@automaker/types'; // Helper to create test features function createFeature( id: string, options: { dependencies?: string[]; status?: string; priority?: number; } = {} ): Feature { return { id, category: 'test', description: `Feature ${id}`, dependencies: options.dependencies, status: options.status || 'pending', priority: options.priority, }; } describe('resolver.ts', () => { describe('resolveDependencies', () => { it('should handle features with no dependencies', () => { const features = [createFeature('A'), createFeature('B'), createFeature('C')]; const result = resolveDependencies(features); expect(result.orderedFeatures).toHaveLength(3); expect(result.circularDependencies).toEqual([]); expect(result.missingDependencies.size).toBe(0); expect(result.blockedFeatures.size).toBe(0); }); it('should order features with linear dependencies', () => { const features = [ createFeature('C', { dependencies: ['B'] }), createFeature('A'), createFeature('B', { dependencies: ['A'] }), ]; const result = resolveDependencies(features); const ids = result.orderedFeatures.map((f) => f.id); expect(ids.indexOf('A')).toBeLessThan(ids.indexOf('B')); expect(ids.indexOf('B')).toBeLessThan(ids.indexOf('C')); expect(result.circularDependencies).toEqual([]); }); it('should respect priority within same dependency level', () => { const features = [ createFeature('Low', { priority: 3 }), createFeature('High', { priority: 1 }), createFeature('Medium', { priority: 2 }), ]; const result = resolveDependencies(features); const ids = result.orderedFeatures.map((f) => f.id); expect(ids).toEqual(['High', 'Medium', 'Low']); }); it('should use default priority 2 when not specified', () => { const features = [ createFeature('NoPriority'), createFeature('HighPriority', { priority: 1 }), createFeature('LowPriority', { priority: 3 }), ]; const result = resolveDependencies(features); const ids = result.orderedFeatures.map((f) => f.id); expect(ids.indexOf('HighPriority')).toBeLessThan(ids.indexOf('NoPriority')); expect(ids.indexOf('NoPriority')).toBeLessThan(ids.indexOf('LowPriority')); }); it('should respect dependencies over priority', () => { const features = [ createFeature('B', { dependencies: ['A'], priority: 1 }), // High priority but depends on A createFeature('A', { priority: 3 }), // Low priority but no dependencies ]; const result = resolveDependencies(features); const ids = result.orderedFeatures.map((f) => f.id); expect(ids.indexOf('A')).toBeLessThan(ids.indexOf('B')); }); it('should detect circular dependencies (simple cycle)', () => { const features = [ createFeature('A', { dependencies: ['B'] }), createFeature('B', { dependencies: ['A'] }), ]; const result = resolveDependencies(features); expect(result.circularDependencies).toHaveLength(1); expect(result.circularDependencies[0]).toContain('A'); expect(result.circularDependencies[0]).toContain('B'); expect(result.orderedFeatures).toHaveLength(2); // All features still included }); it('should detect circular dependencies (3-way cycle)', () => { const features = [ createFeature('A', { dependencies: ['C'] }), createFeature('B', { dependencies: ['A'] }), createFeature('C', { dependencies: ['B'] }), ]; const result = resolveDependencies(features); expect(result.circularDependencies.length).toBeGreaterThan(0); const allCycleIds = result.circularDependencies.flat(); expect(allCycleIds).toContain('A'); expect(allCycleIds).toContain('B'); expect(allCycleIds).toContain('C'); }); it('should detect missing dependencies', () => { const features = [createFeature('A', { dependencies: ['NonExistent'] }), createFeature('B')]; const result = resolveDependencies(features); expect(result.missingDependencies.has('A')).toBe(true); expect(result.missingDependencies.get('A')).toContain('NonExistent'); }); it('should detect blocked features (incomplete dependencies)', () => { const features = [ createFeature('A', { status: 'pending' }), createFeature('B', { dependencies: ['A'], status: 'pending' }), ]; const result = resolveDependencies(features); expect(result.blockedFeatures.has('B')).toBe(true); expect(result.blockedFeatures.get('B')).toContain('A'); }); it('should not mark features as blocked if dependencies are completed', () => { const features = [ createFeature('A', { status: 'completed' }), createFeature('B', { dependencies: ['A'], status: 'pending' }), ]; const result = resolveDependencies(features); expect(result.blockedFeatures.has('B')).toBe(false); }); it('should not mark features as blocked if dependencies are verified', () => { const features = [ createFeature('A', { status: 'verified' }), createFeature('B', { dependencies: ['A'], status: 'pending' }), ]; const result = resolveDependencies(features); expect(result.blockedFeatures.has('B')).toBe(false); }); it('should handle complex dependency graph', () => { const features = [ createFeature('E', { dependencies: ['C', 'D'] }), createFeature('D', { dependencies: ['B'] }), createFeature('C', { dependencies: ['A', 'B'] }), createFeature('B'), createFeature('A'), ]; const result = resolveDependencies(features); const ids = result.orderedFeatures.map((f) => f.id); // A and B have no dependencies - can be first or second expect(ids.indexOf('A')).toBeLessThan(ids.indexOf('C')); expect(ids.indexOf('B')).toBeLessThan(ids.indexOf('C')); expect(ids.indexOf('B')).toBeLessThan(ids.indexOf('D')); // C depends on A and B expect(ids.indexOf('C')).toBeLessThan(ids.indexOf('E')); // D depends on B expect(ids.indexOf('D')).toBeLessThan(ids.indexOf('E')); expect(result.circularDependencies).toEqual([]); }); it('should handle multiple missing dependencies', () => { const features = [createFeature('A', { dependencies: ['X', 'Y', 'Z'] })]; const result = resolveDependencies(features); expect(result.missingDependencies.get('A')).toEqual(['X', 'Y', 'Z']); }); 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 both missing and existing dependencies', () => { const features = [ createFeature('A'), createFeature('B', { dependencies: ['A', 'NonExistent'] }), ]; const result = resolveDependencies(features); expect(result.missingDependencies.get('B')).toContain('NonExistent'); const ids = result.orderedFeatures.map((f) => f.id); expect(ids.indexOf('A')).toBeLessThan(ids.indexOf('B')); }); }); describe('areDependenciesSatisfied', () => { it('should return true for feature with no dependencies', () => { const feature = createFeature('A'); const allFeatures = [feature]; expect(areDependenciesSatisfied(feature, allFeatures)).toBe(true); }); it('should return true for feature with empty dependencies array', () => { const feature = createFeature('A', { dependencies: [] }); const allFeatures = [feature]; expect(areDependenciesSatisfied(feature, allFeatures)).toBe(true); }); it('should return true when all dependencies are completed', () => { const dep = createFeature('Dep', { status: 'completed' }); const feature = createFeature('A', { dependencies: ['Dep'] }); const allFeatures = [dep, feature]; expect(areDependenciesSatisfied(feature, allFeatures)).toBe(true); }); it('should return true when all dependencies are verified', () => { const dep = createFeature('Dep', { status: 'verified' }); const feature = createFeature('A', { dependencies: ['Dep'] }); const allFeatures = [dep, feature]; expect(areDependenciesSatisfied(feature, allFeatures)).toBe(true); }); it('should return false when any dependency is pending', () => { const dep = createFeature('Dep', { status: 'pending' }); const feature = createFeature('A', { dependencies: ['Dep'] }); const allFeatures = [dep, feature]; expect(areDependenciesSatisfied(feature, allFeatures)).toBe(false); }); it('should return false when any dependency is running', () => { const dep = createFeature('Dep', { status: 'running' }); const feature = createFeature('A', { dependencies: ['Dep'] }); const allFeatures = [dep, feature]; expect(areDependenciesSatisfied(feature, allFeatures)).toBe(false); }); it('should return false when dependency is missing', () => { const feature = createFeature('A', { dependencies: ['NonExistent'] }); const allFeatures = [feature]; expect(areDependenciesSatisfied(feature, allFeatures)).toBe(false); }); it('should check all dependencies', () => { const dep1 = createFeature('Dep1', { status: 'completed' }); const dep2 = createFeature('Dep2', { status: 'pending' }); const feature = createFeature('A', { dependencies: ['Dep1', 'Dep2'] }); const allFeatures = [dep1, dep2, feature]; expect(areDependenciesSatisfied(feature, allFeatures)).toBe(false); }); }); describe('getBlockingDependencies', () => { it('should return empty array for feature with no dependencies', () => { const feature = createFeature('A'); const allFeatures = [feature]; expect(getBlockingDependencies(feature, allFeatures)).toEqual([]); }); it('should return empty array when all dependencies are completed', () => { const dep = createFeature('Dep', { status: 'completed' }); const feature = createFeature('A', { dependencies: ['Dep'] }); const allFeatures = [dep, feature]; expect(getBlockingDependencies(feature, allFeatures)).toEqual([]); }); it('should return empty array when all dependencies are verified', () => { const dep = createFeature('Dep', { status: 'verified' }); const feature = createFeature('A', { dependencies: ['Dep'] }); const allFeatures = [dep, feature]; expect(getBlockingDependencies(feature, allFeatures)).toEqual([]); }); it('should return pending dependencies', () => { const dep = createFeature('Dep', { status: 'pending' }); const feature = createFeature('A', { dependencies: ['Dep'] }); const allFeatures = [dep, feature]; expect(getBlockingDependencies(feature, allFeatures)).toEqual(['Dep']); }); it('should return running dependencies', () => { const dep = createFeature('Dep', { status: 'running' }); const feature = createFeature('A', { dependencies: ['Dep'] }); const allFeatures = [dep, feature]; expect(getBlockingDependencies(feature, allFeatures)).toEqual(['Dep']); }); it('should return failed dependencies', () => { const dep = createFeature('Dep', { status: 'failed' }); const feature = createFeature('A', { dependencies: ['Dep'] }); const allFeatures = [dep, feature]; expect(getBlockingDependencies(feature, allFeatures)).toEqual(['Dep']); }); it('should return all incomplete dependencies', () => { const dep1 = createFeature('Dep1', { status: 'pending' }); const dep2 = createFeature('Dep2', { status: 'completed' }); const dep3 = createFeature('Dep3', { status: 'running' }); const feature = createFeature('A', { dependencies: ['Dep1', 'Dep2', 'Dep3'] }); const allFeatures = [dep1, dep2, dep3, feature]; const blocking = getBlockingDependencies(feature, allFeatures); expect(blocking).toContain('Dep1'); expect(blocking).toContain('Dep3'); expect(blocking).not.toContain('Dep2'); }); }); });