Files
Test e00762bb0b feat(T3.3): implement task dependency graph
- Add internal/graph package for dependency management
- Implement DependencyGraph for task ordering
- Support task dependencies and prerequisite tracking
- Validate graph for cycles (no circular dependencies)
- Topological sort for execution order (Kahn's algorithm)
- Track task status (pending, completed, failed)
- Get ready-to-execute tasks based on dependencies
- Get tasks that depend on a given task
- Check if task can execute (all deps complete)
- Calculate critical path through graph
- Task metadata support
- 23 graph tests, all passing

Features:
- AddTask() - add task to graph
- AddDependency(dependent, prerequisite) - specify ordering
- ValidateGraph() - check for cycles
- GetTopologicalOrder() - execution order
- GetReadyTasks() - tasks ready to run
- MarkCompleted(taskID) - mark as done
- MarkFailed(taskID) - mark as failed
- GetDependencies(taskID) - what task depends on
- GetDependents(taskID) - what depends on task
- CanExecuteTask(taskID) - check if ready
- GetCriticalPath() - longest path in graph

Graph Properties:
- Directed acyclic graph (DAG)
- Cycle detection (prevents deadlocks)
- Multi-dependency support (diamond dependencies)
- Status tracking (pending/completed/failed)
- Thread-safe (RWMutex)
- Kahn's algorithm for topological sort
- O(V+E) for validation and sorting

Example Usage:
- T0.1 Analyze (no deps)
- T0.2 Implement (depends on T0.1)
- T0.3 Test (depends on T0.2)
- T0.4 Review (depends on T0.2, T0.3)

Ready Detection:
- T0.1 ready (no dependencies)
- After T0.1 complete: T0.2 ready
- After T0.2 complete: T0.3 ready
- After T0.2, T0.3 complete: T0.4 ready

Test Coverage:
- 23 dependency graph tests
- Cycle detection verified
- Topological sort tested
- Multiple dependency chains
- Diamond dependency patterns
- Ready task calculation
- Status tracking
- Critical path calculation
- Complex graphs (10+ tasks)
- Metadata handling
- Performance benchmarks

Performance:
- Cycle detection: O(V+E) DFS
- Topological sort: O(V+E) Kahn's algorithm
- Ready tasks: O(V) scan
- Add task: O(1)
- Add dependency: O(1) amortized

Use Cases:
- Workflow orchestration (T0.1 -> T0.2 -> T0.3 -> ...)
- CI/CD pipelines (build -> test -> deploy)
- Milestone hierarchies (T0 milestone with sub-tasks)
- Parallel tasks with merge points (diamond deps)

Next: T3.4 (Human-in-the-loop gates)
2026-08-23 17:32:55 -07:00

373 lines
8.3 KiB
Go

package graph
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewDependencyGraph(t *testing.T) {
graph := NewDependencyGraph()
assert.NotNil(t, graph)
assert.Equal(t, 0, len(graph.GetAllTasks()))
}
func TestAddTask(t *testing.T) {
graph := NewDependencyGraph()
task := &Task{ID: "T1", Title: "Task 1"}
err := graph.AddTask(task)
assert.NoError(t, err)
retrieved, exists := graph.GetTask("T1")
assert.True(t, exists)
assert.Equal(t, "T1", retrieved.ID)
}
func TestAddTaskNil(t *testing.T) {
graph := NewDependencyGraph()
err := graph.AddTask(nil)
assert.Error(t, err)
}
func TestAddTaskDuplicate(t *testing.T) {
graph := NewDependencyGraph()
task := &Task{ID: "T1", Title: "Task 1"}
graph.AddTask(task)
err := graph.AddTask(task)
assert.Error(t, err)
}
func TestAddDependency(t *testing.T) {
graph := NewDependencyGraph()
graph.AddTask(&Task{ID: "T1", Title: "Task 1"})
graph.AddTask(&Task{ID: "T2", Title: "Task 2"})
err := graph.AddDependency("T2", "T1")
assert.NoError(t, err)
deps, _ := graph.GetDependencies("T2")
assert.Equal(t, 1, len(deps))
assert.Equal(t, "T1", deps[0])
}
func TestAddDependencyNotFound(t *testing.T) {
graph := NewDependencyGraph()
graph.AddTask(&Task{ID: "T1", Title: "Task 1"})
err := graph.AddDependency("T2", "T1")
assert.Error(t, err)
}
func TestValidateGraphNoCycles(t *testing.T) {
graph := NewDependencyGraph()
graph.AddTask(&Task{ID: "T1", Title: "Task 1"})
graph.AddTask(&Task{ID: "T2", Title: "Task 2"})
graph.AddTask(&Task{ID: "T3", Title: "Task 3"})
graph.AddDependency("T2", "T1")
graph.AddDependency("T3", "T2")
err := graph.ValidateGraph()
assert.NoError(t, err)
}
func TestValidateGraphWithCycle(t *testing.T) {
graph := NewDependencyGraph()
graph.AddTask(&Task{ID: "T1", Title: "Task 1"})
graph.AddTask(&Task{ID: "T2", Title: "Task 2"})
graph.AddTask(&Task{ID: "T3", Title: "Task 3"})
graph.AddDependency("T2", "T1")
graph.AddDependency("T3", "T2")
graph.AddDependency("T1", "T3") // Creates cycle
err := graph.ValidateGraph()
assert.Error(t, err)
}
func TestGetTopologicalOrder(t *testing.T) {
graph := NewDependencyGraph()
graph.AddTask(&Task{ID: "T1", Title: "Task 1"})
graph.AddTask(&Task{ID: "T2", Title: "Task 2"})
graph.AddTask(&Task{ID: "T3", Title: "Task 3"})
graph.AddDependency("T2", "T1")
graph.AddDependency("T3", "T2")
graph.ValidateGraph()
order, err := graph.GetTopologicalOrder()
assert.NoError(t, err)
assert.Equal(t, 3, len(order))
assert.Equal(t, "T1", order[0])
assert.Equal(t, "T2", order[1])
assert.Equal(t, "T3", order[2])
}
func TestGetReadyTasks(t *testing.T) {
graph := NewDependencyGraph()
graph.AddTask(&Task{ID: "T1", Title: "Task 1"})
graph.AddTask(&Task{ID: "T2", Title: "Task 2"})
graph.AddTask(&Task{ID: "T3", Title: "Task 3"})
graph.AddDependency("T2", "T1")
graph.AddDependency("T3", "T1")
ready := graph.GetReadyTasks()
assert.Equal(t, 1, len(ready))
assert.Equal(t, "T1", ready[0])
graph.MarkCompleted("T1")
ready = graph.GetReadyTasks()
assert.Equal(t, 2, len(ready))
}
func TestMarkCompleted(t *testing.T) {
graph := NewDependencyGraph()
graph.AddTask(&Task{ID: "T1", Title: "Task 1"})
err := graph.MarkCompleted("T1")
assert.NoError(t, err)
status, _ := graph.GetTaskStatus("T1")
assert.Equal(t, "completed", status)
}
func TestMarkFailed(t *testing.T) {
graph := NewDependencyGraph()
graph.AddTask(&Task{ID: "T1", Title: "Task 1"})
err := graph.MarkFailed("T1")
assert.NoError(t, err)
status, _ := graph.GetTaskStatus("T1")
assert.Equal(t, "failed", status)
}
func TestGetDependencies(t *testing.T) {
graph := NewDependencyGraph()
graph.AddTask(&Task{ID: "T1", Title: "Task 1"})
graph.AddTask(&Task{ID: "T2", Title: "Task 2"})
graph.AddTask(&Task{ID: "T3", Title: "Task 3"})
graph.AddDependency("T3", "T1")
graph.AddDependency("T3", "T2")
deps, _ := graph.GetDependencies("T3")
assert.Equal(t, 2, len(deps))
}
func TestGetDependents(t *testing.T) {
graph := NewDependencyGraph()
graph.AddTask(&Task{ID: "T1", Title: "Task 1"})
graph.AddTask(&Task{ID: "T2", Title: "Task 2"})
graph.AddTask(&Task{ID: "T3", Title: "Task 3"})
graph.AddDependency("T2", "T1")
graph.AddDependency("T3", "T1")
dependents, _ := graph.GetDependents("T1")
assert.Equal(t, 2, len(dependents))
}
func TestCanExecuteTask(t *testing.T) {
graph := NewDependencyGraph()
graph.AddTask(&Task{ID: "T1", Title: "Task 1"})
graph.AddTask(&Task{ID: "T2", Title: "Task 2"})
graph.AddDependency("T2", "T1")
assert.False(t, graph.CanExecuteTask("T2"))
graph.MarkCompleted("T1")
assert.True(t, graph.CanExecuteTask("T2"))
}
func TestGetGraphStats(t *testing.T) {
graph := NewDependencyGraph()
graph.AddTask(&Task{ID: "T1", Title: "Task 1"})
graph.AddTask(&Task{ID: "T2", Title: "Task 2"})
graph.MarkCompleted("T1")
stats := graph.GetGraphStats()
assert.Equal(t, 2, stats["total_tasks"])
assert.Equal(t, 1, stats["completed_tasks"])
assert.Equal(t, 1, stats["pending_tasks"])
}
func TestClear(t *testing.T) {
graph := NewDependencyGraph()
graph.AddTask(&Task{ID: "T1", Title: "Task 1"})
assert.Equal(t, 1, len(graph.GetAllTasks()))
graph.Clear()
assert.Equal(t, 0, len(graph.GetAllTasks()))
}
func TestMultipleDependencies(t *testing.T) {
graph := NewDependencyGraph()
for i := 1; i <= 5; i++ {
id := string(rune(48 + i))
graph.AddTask(&Task{ID: "T" + id, Title: "Task " + id})
}
// Chain: T1 -> T2 -> T3 -> T4 -> T5
for i := 2; i <= 5; i++ {
graph.AddDependency("T"+string(rune(48+i)), "T"+string(rune(48+i-1)))
}
ready := graph.GetReadyTasks()
assert.Equal(t, 1, len(ready))
assert.Equal(t, "T1", ready[0])
}
func TestDiamondDependency(t *testing.T) {
graph := NewDependencyGraph()
// Diamond: T1 -> (T2, T3) -> T4
graph.AddTask(&Task{ID: "T1"})
graph.AddTask(&Task{ID: "T2"})
graph.AddTask(&Task{ID: "T3"})
graph.AddTask(&Task{ID: "T4"})
graph.AddDependency("T2", "T1")
graph.AddDependency("T3", "T1")
graph.AddDependency("T4", "T2")
graph.AddDependency("T4", "T3")
graph.ValidateGraph()
order, _ := graph.GetTopologicalOrder()
assert.Equal(t, 4, len(order))
assert.Equal(t, "T1", order[0])
assert.Equal(t, "T4", order[3])
}
func TestGetCriticalPath(t *testing.T) {
graph := NewDependencyGraph()
graph.AddTask(&Task{ID: "T1"})
graph.AddTask(&Task{ID: "T2"})
graph.AddTask(&Task{ID: "T3"})
graph.AddTask(&Task{ID: "T4"})
graph.AddDependency("T2", "T1")
graph.AddDependency("T3", "T2")
graph.AddDependency("T4", "T3")
graph.ValidateGraph()
graph.GetTopologicalOrder()
path := graph.GetCriticalPath()
assert.Greater(t, len(path), 0)
assert.Equal(t, "T1", path[0])
}
func TestComplexGraph(t *testing.T) {
graph := NewDependencyGraph()
// Create 10 tasks with complex dependencies
for i := 1; i <= 10; i++ {
id := string(rune(48 + i%10))
if i >= 10 {
id = "T" + id
} else {
id = "T0" + id
}
graph.AddTask(&Task{ID: id})
}
// Add various dependencies
graph.AddDependency("T02", "T01")
graph.AddDependency("T03", "T01")
graph.AddDependency("T04", "T02")
graph.AddDependency("T04", "T03")
graph.ValidateGraph()
order, _ := graph.GetTopologicalOrder()
assert.Equal(t, 10, len(order))
}
func TestTaskWithMetadata(t *testing.T) {
graph := NewDependencyGraph()
task := &Task{
ID: "T1",
Title: "Task 1",
Metadata: map[string]interface{}{
"priority": "high",
"owner": "team-a",
},
}
graph.AddTask(task)
retrieved, _ := graph.GetTask("T1")
assert.Equal(t, "high", retrieved.Metadata["priority"])
}
func TestGetAllTasks(t *testing.T) {
graph := NewDependencyGraph()
for i := 1; i <= 5; i++ {
id := string(rune(48 + i))
graph.AddTask(&Task{ID: "T" + id})
}
all := graph.GetAllTasks()
assert.Equal(t, 5, len(all))
}
func BenchmarkAddTask(b *testing.B) {
graph := NewDependencyGraph()
for i := 0; i < b.N; i++ {
id := string(rune(48 + i%100))
graph.AddTask(&Task{ID: "T" + id})
}
}
func BenchmarkAddDependency(b *testing.B) {
graph := NewDependencyGraph()
for i := 0; i < 1000; i++ {
graph.AddTask(&Task{ID: "T" + string(rune(48+i%100))})
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
from := "T" + string(rune(48+i%100))
to := "T" + string(rune(48+(i+1)%100))
graph.AddDependency(from, to)
}
}
func BenchmarkGetReadyTasks(b *testing.B) {
graph := NewDependencyGraph()
for i := 1; i <= 100; i++ {
id := "T" + string(rune(48+i%100))
graph.AddTask(&Task{ID: id})
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
graph.GetReadyTasks()
}
}