| import { describe, it, expect } from 'vitest'; |
| import { |
| resolveDependencies, |
| areDependenciesSatisfied, |
| getBlockingDependencies, |
| type DependencyResolutionResult, |
| } from '@automaker/dependency-resolver'; |
| import type { Feature } from '@automaker/types'; |
|
|
| |
| 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'); |
| 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'); |
| expect(result.orderedFeatures[2].id).toBe('f3'); |
| expect(result.orderedFeatures[3].id).toBe('f1'); |
| }); |
|
|
| it('should use default priority of 2 when not specified', () => { |
| const features = [ |
| createFeature('f1', { priority: 1 }), |
| createFeature('f2'), |
| 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); |
| }); |
|
|
| 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); |
| }); |
|
|
| 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'] }), |
| 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'] })]; |
|
|
| |
| 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'); |
| }); |
| }); |
| }); |
|
|