From 5f3e9900358649f1356d0a242e643790e29e3701 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Thu, 26 Mar 2026 08:02:09 +0000 Subject: feat: cascade retry deps when running a task with failed dependencies When /run is called on a CANCELLED/FAILED task that has deps in a terminal failure state, automatically reset and resubmit those deps so the task isn't immediately re-cancelled by the pool's dep check. Also update reset-failed-tasks script to handle CANCELLED tasks and clean up preserved sandbox workspaces. Co-Authored-By: Claude Sonnet 4.6 --- internal/api/server_test.go | 67 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) (limited to 'internal/api/server_test.go') diff --git a/internal/api/server_test.go b/internal/api/server_test.go index 27fc645..4b45f25 100644 --- a/internal/api/server_test.go +++ b/internal/api/server_test.go @@ -2014,3 +2014,70 @@ func TestProjects_CRUD(t *testing.T) { t.Error("expected at least one project in list") } } + +func TestHandleRunTask_CascadesRetryToFailedDeps(t *testing.T) { + srv, store := testServer(t) + + now := time.Now().UTC() + + // Task A: the dependency, in FAILED state. + taskA := &task.Task{ + ID: "cascade-dep-a", + Name: "Dep A", + State: task.StateFailed, + DependsOn: []string{}, + Priority: task.PriorityNormal, + Tags: []string{}, + Agent: task.AgentConfig{Type: "claude", Instructions: "do A"}, + Retry: task.RetryConfig{MaxAttempts: 1, Backoff: "exponential"}, + CreatedAt: now, + UpdatedAt: now, + } + if err := store.CreateTask(taskA); err != nil { + t.Fatalf("CreateTask A: %v", err) + } + + // Task B: depends on A, in CANCELLED state (was cancelled because A failed). + taskB := &task.Task{ + ID: "cascade-task-b", + Name: "Task B", + State: task.StateCancelled, + DependsOn: []string{taskA.ID}, + Priority: task.PriorityNormal, + Tags: []string{}, + Agent: task.AgentConfig{Type: "claude", Instructions: "do B"}, + Retry: task.RetryConfig{MaxAttempts: 1, Backoff: "exponential"}, + CreatedAt: now, + UpdatedAt: now, + } + if err := store.CreateTask(taskB); err != nil { + t.Fatalf("CreateTask B: %v", err) + } + + // Run task B — should cascade-retry dep A. + req := httptest.NewRequest("POST", "/api/tasks/cascade-task-b/run", nil) + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusAccepted { + t.Fatalf("expected 202, got %d: %s", w.Code, w.Body.String()) + } + + // Dep A should now be QUEUED. + a, err := store.GetTask(taskA.ID) + if err != nil { + t.Fatalf("GetTask A: %v", err) + } + if a.State != task.StateQueued { + t.Errorf("dep A: want QUEUED after cascade, got %s", a.State) + } + + // Task B itself should be QUEUED. + b, err := store.GetTask(taskB.ID) + if err != nil { + t.Fatalf("GetTask B: %v", err) + } + if b.State != task.StateQueued { + t.Errorf("task B: want QUEUED, got %s", b.State) + } +} -- cgit v1.2.3