Spaces:
Paused
Paused
| 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'); | |
| }); | |
| }); | |
| }); | |