From 5dc08d8becc5f206e1e6d41496e629c472e4235b Mon Sep 17 00:00:00 2001 From: Vasist10 <155972527+Vasist10@users.noreply.github.com> Date: Fri, 26 Dec 2025 23:36:34 +0530 Subject: [PATCH] fix: add circular dependency detection for tasks --- backend/controllers/add_task.go | 25 ++++++++++-- backend/controllers/edit_task.go | 25 ++++++++++-- backend/controllers/modify_task.go | 25 ++++++++++-- backend/utils/utils_test.go | 40 +++++++++++++++---- backend/utils/validation.go | 64 ++++++++++++++++++++++++++++++ 5 files changed, 163 insertions(+), 16 deletions(-) diff --git a/backend/controllers/add_task.go b/backend/controllers/add_task.go index e0510162..63891aac 100644 --- a/backend/controllers/add_task.go +++ b/backend/controllers/add_task.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "net/http" + "os" ) var GlobalJobQueue *JobQueue @@ -62,9 +63,27 @@ func AddTaskHandler(w http.ResponseWriter, r *http.Request) { } // Validate dependencies - if err := utils.ValidateDependencies(depends, ""); err != nil { - http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest) - return + origin := os.Getenv("CONTAINER_ORIGIN") + existingTasks, err := tw.FetchTasksFromTaskwarrior(email, encryptionSecret, origin, uuid) + if err != nil { + if err := utils.ValidateDependencies(depends, ""); err != nil { + http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest) + return + } + } else { + taskDeps := make([]utils.TaskDependency, len(existingTasks)) + for i, task := range existingTasks { + taskDeps[i] = utils.TaskDependency{ + UUID: task.UUID, + Depends: task.Depends, + Status: task.Status, + } + } + + if err := utils.ValidateCircularDependencies(depends, "", taskDeps); err != nil { + http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest) + return + } } var dueDateStr string if dueDate != nil && *dueDate != "" { diff --git a/backend/controllers/edit_task.go b/backend/controllers/edit_task.go index 40a6fa59..ce4bac79 100644 --- a/backend/controllers/edit_task.go +++ b/backend/controllers/edit_task.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "net/http" + "os" ) // EditTaskHandler godoc @@ -62,9 +63,27 @@ func EditTaskHandler(w http.ResponseWriter, r *http.Request) { } // Validate dependencies - if err := utils.ValidateDependencies(depends, uuid); err != nil { - http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest) - return + origin := os.Getenv("CONTAINER_ORIGIN") + existingTasks, err := tw.FetchTasksFromTaskwarrior(email, encryptionSecret, origin, uuid) + if err != nil { + if err := utils.ValidateDependencies(depends, uuid); err != nil { + http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest) + return + } + } else { + taskDeps := make([]utils.TaskDependency, len(existingTasks)) + for i, task := range existingTasks { + taskDeps[i] = utils.TaskDependency{ + UUID: task.UUID, + Depends: task.Depends, + Status: task.Status, + } + } + + if err := utils.ValidateCircularDependencies(depends, uuid, taskDeps); err != nil { + http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest) + return + } } logStore := models.GetLogStore() diff --git a/backend/controllers/modify_task.go b/backend/controllers/modify_task.go index e4eb2acd..58be5ac5 100644 --- a/backend/controllers/modify_task.go +++ b/backend/controllers/modify_task.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "net/http" + "os" ) // ModifyTaskHandler godoc @@ -61,9 +62,27 @@ func ModifyTaskHandler(w http.ResponseWriter, r *http.Request) { } // Validate dependencies - if err := utils.ValidateDependencies(depends, uuid); err != nil { - http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest) - return + origin := os.Getenv("CONTAINER_ORIGIN") + existingTasks, err := tw.FetchTasksFromTaskwarrior(email, encryptionSecret, origin, uuid) + if err != nil { + if err := utils.ValidateDependencies(depends, uuid); err != nil { + http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest) + return + } + } else { + taskDeps := make([]utils.TaskDependency, len(existingTasks)) + for i, task := range existingTasks { + taskDeps[i] = utils.TaskDependency{ + UUID: task.UUID, + Depends: task.Depends, + Status: task.Status, + } + } + + if err := utils.ValidateCircularDependencies(depends, uuid, taskDeps); err != nil { + http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest) + return + } } // if err := tw.ModifyTaskInTaskwarrior(uuid, description, project, priority, status, due, email, encryptionSecret, taskID); err != nil { diff --git a/backend/utils/utils_test.go b/backend/utils/utils_test.go index b8fe26f3..bd403a8f 100644 --- a/backend/utils/utils_test.go +++ b/backend/utils/utils_test.go @@ -85,16 +85,42 @@ func Test_ExecCommandForOutputInDir(t *testing.T) { } } -func Test_ValidateDependencies_ValidDependencies(t *testing.T) { - depends := []string{"task-uuid-1", "task-uuid-2"} - currentTaskUUID := "current-task-uuid" - err := ValidateDependencies(depends, currentTaskUUID) - assert.NoError(t, err) -} - func Test_ValidateDependencies_EmptyList(t *testing.T) { depends := []string{} currentTaskUUID := "current-task-uuid" err := ValidateDependencies(depends, currentTaskUUID) assert.NoError(t, err) } + +// Circular Dependency Detection Tests +func Test_detectCycle_NoCycle(t *testing.T) { //A -> B -> C + graph := map[string][]string{ + "A": {"B"}, + "B": {"C"}, + "C": {}, + } + + hasCycle := detectCycle(graph, "A") + assert.False(t, hasCycle, "Should not detect cycle in linear dependency") +} + +func Test_detectCycle_SimpleCycle(t *testing.T) { // A -> B -> A + graph := map[string][]string{ + "A": {"B"}, + "B": {"A"}, + } + + hasCycle := detectCycle(graph, "A") + assert.True(t, hasCycle, "Should detect simple cycle A -> B -> A") +} + +func Test_detectCycle_ComplexCycle(t *testing.T) { // A -> B -> C -> A + graph := map[string][]string{ + "A": {"B"}, + "B": {"C"}, + "C": {"A"}, + } + + hasCycle := detectCycle(graph, "A") + assert.True(t, hasCycle, "Should detect complex cycle A -> B -> C -> A") +} diff --git a/backend/utils/validation.go b/backend/utils/validation.go index a439e055..2a570db0 100644 --- a/backend/utils/validation.go +++ b/backend/utils/validation.go @@ -19,3 +19,67 @@ func ValidateDependencies(depends []string, currentTaskUUID string) error { return nil } + +type TaskDependency struct { + UUID string `json:"uuid"` + Depends []string `json:"depends"` + Status string `json:"status"` +} + +func ValidateCircularDependencies(depends []string, currentTaskUUID string, existingTasks []TaskDependency) error { + if len(depends) == 0 { + return nil + } + + dependencyGraph := make(map[string][]string) + for _, task := range existingTasks { + if task.Status == "pending" { + dependencyGraph[task.UUID] = task.Depends + } + } + + dependencyGraph[currentTaskUUID] = depends + + if hasCycle := detectCycle(dependencyGraph, currentTaskUUID); hasCycle { + return fmt.Errorf("circular dependency detected: adding these dependencies would create a cycle") + } + + return nil +} + +// (0): unvisited, (1): visiting,(2): visited +func detectCycle(graph map[string][]string, startNode string) bool { + color := make(map[string]int) + + for node := range graph { + color[node] = 0 + } + + for _, deps := range graph { + for _, dep := range deps { + if _, exists := color[dep]; !exists { + color[dep] = 0 + } + } + } + + return dfsHasCycle(graph, startNode, color) +} + +func dfsHasCycle(graph map[string][]string, node string, color map[string]int) bool { + color[node] = 1 + + for _, dep := range graph[node] { + if color[dep] == 1 { + return true + } + if color[dep] == 0 { + if dfsHasCycle(graph, dep, color) { + return true + } + } + } + + color[node] = 2 + return false +}