summaryrefslogtreecommitdiff
path: root/internal/executor
diff options
context:
space:
mode:
Diffstat (limited to 'internal/executor')
-rw-r--r--internal/executor/executor.go7
-rw-r--r--internal/executor/executor_test.go51
2 files changed, 57 insertions, 1 deletions
diff --git a/internal/executor/executor.go b/internal/executor/executor.go
index 88a5783..03b368e 100644
--- a/internal/executor/executor.go
+++ b/internal/executor/executor.go
@@ -1259,7 +1259,12 @@ func (p *Pool) maybeUnblockParent(parentID string) {
return
}
for _, sub := range subtasks {
- if sub.State != task.StateCompleted {
+ current, err := task.CurrentAttempt(p.store, sub.ID)
+ if err != nil {
+ p.logger.Error("maybeUnblockParent: resolve current attempt", "parentID", parentID, "subtaskID", sub.ID, "error", err)
+ return
+ }
+ if current.State != task.StateCompleted {
return
}
}
diff --git a/internal/executor/executor_test.go b/internal/executor/executor_test.go
index e2b9b44..d505a3a 100644
--- a/internal/executor/executor_test.go
+++ b/internal/executor/executor_test.go
@@ -1169,6 +1169,57 @@ func TestPool_RecoverStaleBlocked_PromotesNestedSubtaskParent(t *testing.T) {
}
}
+// TestPool_MaybeUnblockParent_ResolvesSubtaskThroughCurrentAttempt proves
+// the parent's "are all subtasks done" gate resolves each subtask through
+// task.CurrentAttempt first -- so a subtask that was superseded by a
+// fix-attempt sibling (same ParentTaskID, DependsOn=[original]) correctly
+// waits for the FIX ATTEMPT to complete, not just the stale original that
+// already reached COMPLETED before being superseded.
+func TestPool_MaybeUnblockParent_ResolvesSubtaskThroughCurrentAttempt(t *testing.T) {
+ store := testStore(t)
+ logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
+ pool := NewPool(2, map[string]Runner{"claude": &mockRunner{}}, store, logger)
+
+ parent := makeTask("resolve-parent")
+ parent.State = task.StateBlocked
+ store.CreateTask(parent)
+
+ original := makeTask("resolve-original")
+ original.ParentTaskID = parent.ID
+ original.State = task.StateCompleted // rejected, but its own execution finished
+ store.CreateTask(original)
+
+ fixAttempt := makeTask("resolve-fix-attempt")
+ fixAttempt.ParentTaskID = parent.ID
+ fixAttempt.DependsOn = []string{original.ID}
+ fixAttempt.State = task.StateRunning // still in progress
+ store.CreateTask(fixAttempt)
+
+ pool.maybeUnblockParent(parent.ID)
+
+ got, err := store.GetTask(parent.ID)
+ if err != nil {
+ t.Fatalf("get parent: %v", err)
+ }
+ if got.State != task.StateBlocked {
+ t.Errorf("parent state: want still BLOCKED (fix attempt not done yet), got %v", got.State)
+ }
+
+ // Now the fix attempt finishes.
+ if err := store.UpdateTaskState(fixAttempt.ID, task.StateCompleted); err != nil {
+ t.Fatalf("complete fix attempt: %v", err)
+ }
+ pool.maybeUnblockParent(parent.ID)
+
+ got, err = store.GetTask(parent.ID)
+ if err != nil {
+ t.Fatalf("get parent: %v", err)
+ }
+ if got.State != task.StateReady {
+ t.Errorf("parent state: want READY (fix attempt now done), got %v", got.State)
+ }
+}
+
// TestPool_Submit_ParentNotBlocked_NoTransition verifies that completing a subtask
// does not change the parent's state when the parent is not BLOCKED.
func TestPool_Submit_ParentNotBlocked_NoTransition(t *testing.T) {