| import { describe, it, expect } from 'vitest'; |
| import { |
| resolveDependencies, |
| areDependenciesSatisfied, |
| getBlockingDependencies, |
| createFeatureMap, |
| getBlockingDependenciesFromMap, |
| wouldCreateCircularDependency, |
| dependencyExists, |
| } from '../src/resolver'; |
| import type { Feature } from '@automaker/types'; |
|
|
| |
| 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 }), |
| createFeature('A', { priority: 3 }), |
| ]; |
|
|
| 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); |
| }); |
|
|
| 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); |
|
|
| |
| expect(ids.indexOf('A')).toBeLessThan(ids.indexOf('C')); |
| expect(ids.indexOf('B')).toBeLessThan(ids.indexOf('C')); |
| expect(ids.indexOf('B')).toBeLessThan(ids.indexOf('D')); |
|
|
| |
| expect(ids.indexOf('C')).toBeLessThan(ids.indexOf('E')); |
|
|
| |
| 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'); |
| }); |
| }); |
|
|
| describe('getBlockingDependenciesFromMap', () => { |
| it('should match getBlockingDependencies when using a feature map', () => { |
| 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 featureMap = createFeatureMap(allFeatures); |
|
|
| expect(getBlockingDependenciesFromMap(feature, featureMap)).toEqual( |
| getBlockingDependencies(feature, allFeatures) |
| ); |
| }); |
| }); |
|
|
| describe('wouldCreateCircularDependency', () => { |
| it('should return false for features with no existing dependencies', () => { |
| const features = [createFeature('A'), createFeature('B')]; |
|
|
| |
| expect(wouldCreateCircularDependency(features, 'A', 'B')).toBe(false); |
| }); |
|
|
| it('should return false for valid linear dependency chain', () => { |
| |
| const features = [ |
| createFeature('A'), |
| createFeature('B', { dependencies: ['A'] }), |
| createFeature('C', { dependencies: ['B'] }), |
| ]; |
|
|
| |
| const featuresWithD = [...features, createFeature('D')]; |
| expect(wouldCreateCircularDependency(featuresWithD, 'C', 'D')).toBe(false); |
| }); |
|
|
| it('should detect direct circular dependency (A -> B -> A)', () => { |
| |
| const features = [createFeature('A'), createFeature('B', { dependencies: ['A'] })]; |
|
|
| |
| |
| |
| expect(wouldCreateCircularDependency(features, 'B', 'A')).toBe(true); |
| }); |
|
|
| it('should detect transitive circular dependency (A -> B -> C -> A)', () => { |
| |
| const features = [ |
| createFeature('A'), |
| createFeature('B', { dependencies: ['A'] }), |
| createFeature('C', { dependencies: ['B'] }), |
| ]; |
|
|
| |
| |
| expect(wouldCreateCircularDependency(features, 'C', 'A')).toBe(true); |
| }); |
|
|
| it('should detect cycle in complex graph', () => { |
| |
| const features = [ |
| createFeature('A'), |
| createFeature('B', { dependencies: ['A'] }), |
| createFeature('C', { dependencies: ['A', 'B'] }), |
| ]; |
|
|
| |
| expect(wouldCreateCircularDependency(features, 'C', 'A')).toBe(true); |
|
|
| |
| expect(wouldCreateCircularDependency(features, 'C', 'B')).toBe(true); |
| }); |
|
|
| it('should return false for parallel branches', () => { |
| |
| const features = [ |
| createFeature('A'), |
| createFeature('B', { dependencies: ['A'] }), |
| createFeature('C', { dependencies: ['A'] }), |
| ]; |
|
|
| |
| expect(wouldCreateCircularDependency(features, 'C', 'B')).toBe(false); |
|
|
| |
| expect(wouldCreateCircularDependency(features, 'B', 'C')).toBe(false); |
| }); |
|
|
| it('should handle self-dependency check', () => { |
| const features = [createFeature('A')]; |
|
|
| |
| expect(wouldCreateCircularDependency(features, 'A', 'A')).toBe(true); |
| }); |
|
|
| it('should handle feature not in list', () => { |
| const features = [createFeature('A')]; |
|
|
| |
| expect(wouldCreateCircularDependency(features, 'NonExistent', 'A')).toBe(false); |
|
|
| |
| expect(wouldCreateCircularDependency(features, 'A', 'NonExistent')).toBe(false); |
| }); |
|
|
| it('should handle empty features list', () => { |
| const features: Feature[] = []; |
|
|
| expect(wouldCreateCircularDependency(features, 'A', 'B')).toBe(false); |
| }); |
|
|
| it('should handle longer transitive chains', () => { |
| |
| const features = [ |
| createFeature('A'), |
| createFeature('B', { dependencies: ['A'] }), |
| createFeature('C', { dependencies: ['B'] }), |
| createFeature('D', { dependencies: ['C'] }), |
| createFeature('E', { dependencies: ['D'] }), |
| ]; |
|
|
| |
| expect(wouldCreateCircularDependency(features, 'E', 'A')).toBe(true); |
|
|
| |
| expect(wouldCreateCircularDependency(features, 'E', 'B')).toBe(true); |
|
|
| |
| |
| const featuresWithF = [...features, createFeature('F')]; |
| expect(wouldCreateCircularDependency(featuresWithF, 'E', 'F')).toBe(false); |
| }); |
|
|
| it('should handle diamond dependency pattern', () => { |
| |
| |
| |
| |
| |
| const features = [ |
| createFeature('A'), |
| createFeature('B', { dependencies: ['A'] }), |
| createFeature('C', { dependencies: ['A'] }), |
| createFeature('D', { dependencies: ['B', 'C'] }), |
| ]; |
|
|
| |
| expect(wouldCreateCircularDependency(features, 'D', 'A')).toBe(true); |
|
|
| |
| expect(wouldCreateCircularDependency(features, 'D', 'B')).toBe(true); |
|
|
| |
| const featuresWithE = [...features, createFeature('E')]; |
| expect(wouldCreateCircularDependency(featuresWithE, 'D', 'E')).toBe(false); |
| }); |
| }); |
|
|
| describe('dependencyExists', () => { |
| it('should return false when target has no dependencies', () => { |
| const features = [createFeature('A'), createFeature('B')]; |
|
|
| expect(dependencyExists(features, 'A', 'B')).toBe(false); |
| }); |
|
|
| it('should return true when direct dependency exists', () => { |
| const features = [createFeature('A'), createFeature('B', { dependencies: ['A'] })]; |
|
|
| expect(dependencyExists(features, 'A', 'B')).toBe(true); |
| }); |
|
|
| it('should return false for reverse direction', () => { |
| const features = [createFeature('A'), createFeature('B', { dependencies: ['A'] })]; |
|
|
| |
| expect(dependencyExists(features, 'B', 'A')).toBe(false); |
| }); |
|
|
| it('should return false for transitive dependencies', () => { |
| |
| const features = [ |
| createFeature('A'), |
| createFeature('B', { dependencies: ['A'] }), |
| createFeature('C', { dependencies: ['B'] }), |
| ]; |
|
|
| |
| expect(dependencyExists(features, 'A', 'C')).toBe(false); |
| }); |
|
|
| it('should return true for one of multiple dependencies', () => { |
| const features = [ |
| createFeature('A'), |
| createFeature('B'), |
| createFeature('C', { dependencies: ['A', 'B'] }), |
| ]; |
|
|
| expect(dependencyExists(features, 'A', 'C')).toBe(true); |
| expect(dependencyExists(features, 'B', 'C')).toBe(true); |
| }); |
|
|
| it('should return false when target feature does not exist', () => { |
| const features = [createFeature('A')]; |
|
|
| expect(dependencyExists(features, 'A', 'NonExistent')).toBe(false); |
| }); |
|
|
| it('should return false for empty dependencies array', () => { |
| const features = [createFeature('A'), createFeature('B', { dependencies: [] })]; |
|
|
| expect(dependencyExists(features, 'A', 'B')).toBe(false); |
| }); |
| }); |
| }); |
|
|