import { describe, it, expect } from 'vitest'; import { topologicalSortPhases, type PhaseForSort, type DependencyEdge } from '@codewalk-district/shared'; function mkPhase(id: string, createdAt: string | Date): PhaseForSort { return { id, createdAt }; } describe('topologicalSortPhases', () => { it('should return empty array for empty input', () => { expect(topologicalSortPhases([], [])).toEqual([]); }); it('should return phases in createdAt order when no edges', () => { const phases = [ mkPhase('c', '2026-01-03'), mkPhase('a', '2026-01-01'), mkPhase('b', '2026-01-02'), ]; const result = topologicalSortPhases(phases, []); expect(result.map((p) => p.id)).toEqual(['a', 'b', 'c']); }); it('should sort linear chain correctly', () => { // A -> B -> C (B depends on A, C depends on B) const phases = [ mkPhase('a', '2026-01-01'), mkPhase('b', '2026-01-02'), mkPhase('c', '2026-01-03'), ]; const edges: DependencyEdge[] = [ { phaseId: 'b', dependsOnPhaseId: 'a' }, { phaseId: 'c', dependsOnPhaseId: 'b' }, ]; const result = topologicalSortPhases(phases, edges); expect(result.map((p) => p.id)).toEqual(['a', 'b', 'c']); }); it('should handle diamond dependency', () => { // A // / \ // B C // \ / // D const phases = [ mkPhase('a', '2026-01-01'), mkPhase('b', '2026-01-02'), mkPhase('c', '2026-01-03'), mkPhase('d', '2026-01-04'), ]; const edges: DependencyEdge[] = [ { phaseId: 'b', dependsOnPhaseId: 'a' }, { phaseId: 'c', dependsOnPhaseId: 'a' }, { phaseId: 'd', dependsOnPhaseId: 'b' }, { phaseId: 'd', dependsOnPhaseId: 'c' }, ]; const result = topologicalSortPhases(phases, edges); // A must come first, D must come last, B before C by createdAt expect(result[0].id).toBe('a'); expect(result[3].id).toBe('d'); expect(result.map((p) => p.id)).toEqual(['a', 'b', 'c', 'd']); }); it('should use createdAt as deterministic tiebreaker', () => { // Three independent phases — should sort by createdAt const phases = [ mkPhase('z', '2026-01-03'), mkPhase('y', '2026-01-01'), mkPhase('x', '2026-01-02'), ]; const result = topologicalSortPhases(phases, []); expect(result.map((p) => p.id)).toEqual(['y', 'x', 'z']); }); it('should handle cycle gracefully by appending cycled nodes', () => { // A -> B -> A (cycle), C is independent const phases = [ mkPhase('a', '2026-01-01'), mkPhase('b', '2026-01-02'), mkPhase('c', '2026-01-03'), ]; const edges: DependencyEdge[] = [ { phaseId: 'b', dependsOnPhaseId: 'a' }, { phaseId: 'a', dependsOnPhaseId: 'b' }, ]; const result = topologicalSortPhases(phases, edges); // C has no deps so it comes first, then A and B appended (cycle) expect(result[0].id).toBe('c'); expect(result.length).toBe(3); // A and B are appended in createdAt order expect(result[1].id).toBe('a'); expect(result[2].id).toBe('b'); }); it('should ignore edges referencing non-existent phases', () => { const phases = [ mkPhase('a', '2026-01-01'), mkPhase('b', '2026-01-02'), ]; const edges: DependencyEdge[] = [ { phaseId: 'b', dependsOnPhaseId: 'nonexistent' }, ]; const result = topologicalSortPhases(phases, edges); // Edge is ignored, both treated as independent expect(result.map((p) => p.id)).toEqual(['a', 'b']); }); it('should handle single phase with no edges', () => { const phases = [mkPhase('only', '2026-01-01')]; const result = topologicalSortPhases(phases, []); expect(result.map((p) => p.id)).toEqual(['only']); }); it('should work with Date objects', () => { const phases = [ mkPhase('b', new Date('2026-01-02')), mkPhase('a', new Date('2026-01-01')), ]; const edges: DependencyEdge[] = [ { phaseId: 'b', dependsOnPhaseId: 'a' }, ]; const result = topologicalSortPhases(phases, edges); expect(result.map((p) => p.id)).toEqual(['a', 'b']); }); it('should preserve extra properties on phase objects', () => { const phases = [ { id: 'a', createdAt: '2026-01-01', name: 'Alpha', status: 'pending' }, { id: 'b', createdAt: '2026-01-02', name: 'Beta', status: 'active' }, ]; const result = topologicalSortPhases(phases, []); expect(result[0].name).toBe('Alpha'); expect(result[1].name).toBe('Beta'); }); });