diff options
| author | Claudomator Agent <agent@claudomator.local> | 2026-07-09 04:39:05 +0000 |
|---|---|---|
| committer | Claudomator Agent <agent@claudomator.local> | 2026-07-09 04:39:05 +0000 |
| commit | 4ba335c8eacb3f9e20bccce1643b2e5320d5239d (patch) | |
| tree | 397433d1db8564b3818f6b73744567662c15c9d0 /internal/executor | |
| parent | 884da0ba74f622dc5c41cde25710718762321ae8 (diff) | |
feat(task,executor): add CurrentAttempt resolution, wire maybeUnblockParent to resolve through it
Diffstat (limited to 'internal/executor')
| -rw-r--r-- | internal/executor/executor.go | 7 | ||||
| -rw-r--r-- | internal/executor/executor_test.go | 51 |
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) { |
