diff options
| author | Claudomator Agent <agent@claudomator> | 2026-03-09 08:04:07 +0000 |
|---|---|---|
| committer | Claudomator Agent <agent@claudomator> | 2026-03-09 08:04:07 +0000 |
| commit | c8e3b467afdfcee9c5047902662d49d33c862764 (patch) | |
| tree | e39df5351d6624edc531a76ada0c0c15fd9b862a /internal/executor/executor_test.go | |
| parent | 441ed9eef3d9691cd9269772857307b84a7f5700 (diff) | |
executor: unblock parent task when all subtasks complete
Add maybeUnblockParent helper that transitions a BLOCKED parent task to
READY once every subtask is in the COMPLETED state. Called in both
execute() and executeResume() immediately after a subtask is marked
COMPLETED. Any non-COMPLETED sibling (RUNNING, FAILED, etc.) keeps the
parent BLOCKED.
Tests added:
- TestPool_Submit_LastSubtask_UnblocksParent
- TestPool_Submit_NotLastSubtask_ParentStaysBlocked
- TestPool_Submit_ParentNotBlocked_NoTransition
Diffstat (limited to 'internal/executor/executor_test.go')
| -rw-r--r-- | internal/executor/executor_test.go | 121 |
1 files changed, 121 insertions, 0 deletions
diff --git a/internal/executor/executor_test.go b/internal/executor/executor_test.go index 91f7636..9896ba1 100644 --- a/internal/executor/executor_test.go +++ b/internal/executor/executor_test.go @@ -684,6 +684,127 @@ func TestPool_Submit_TopLevel_WithSubtasks_GoesBlocked(t *testing.T) { } } +// TestPool_Submit_LastSubtask_UnblocksParent verifies that when the last +// remaining subtask completes, the parent task transitions from BLOCKED to READY. +func TestPool_Submit_LastSubtask_UnblocksParent(t *testing.T) { + store := testStore(t) + runner := &mockRunner{} + runners := map[string]Runner{"claude": runner} + logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})) + pool := NewPool(2, runners, store, logger) + + // Parent starts BLOCKED (waiting for subtasks). + parent := makeTask("unblock-parent-1") + parent.State = task.StateBlocked + store.CreateTask(parent) + + // First subtask already completed. + sub1 := makeTask("unblock-sub-1a") + sub1.ParentTaskID = parent.ID + sub1.State = task.StateCompleted + store.CreateTask(sub1) + + // Second (last) subtask — the one we submit. + sub2 := makeTask("unblock-sub-1b") + sub2.ParentTaskID = parent.ID + store.CreateTask(sub2) + + if err := pool.Submit(context.Background(), sub2); err != nil { + t.Fatalf("submit: %v", err) + } + + result := <-pool.Results() + if result.Err != nil { + t.Errorf("expected no error, got: %v", result.Err) + } + if result.Execution.Status != "COMPLETED" { + t.Errorf("subtask status: want COMPLETED, got %q", result.Execution.Status) + } + + // Parent must now be READY. + 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, got %v", got.State) + } +} + +// TestPool_Submit_NotLastSubtask_ParentStaysBlocked verifies that when a subtask +// completes but another sibling subtask is still running, the parent stays BLOCKED. +func TestPool_Submit_NotLastSubtask_ParentStaysBlocked(t *testing.T) { + store := testStore(t) + runner := &mockRunner{} + runners := map[string]Runner{"claude": runner} + logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})) + pool := NewPool(2, runners, store, logger) + + parent := makeTask("blocked-parent-2") + parent.State = task.StateBlocked + store.CreateTask(parent) + + // First subtask still RUNNING — not done yet. + sub1 := makeTask("blocked-sub-2a") + sub1.ParentTaskID = parent.ID + sub1.State = task.StateRunning + store.CreateTask(sub1) + + // Second subtask — the one we submit. + sub2 := makeTask("blocked-sub-2b") + sub2.ParentTaskID = parent.ID + store.CreateTask(sub2) + + if err := pool.Submit(context.Background(), sub2); err != nil { + t.Fatalf("submit: %v", err) + } + + <-pool.Results() + + // Parent must remain BLOCKED because sub1 is still RUNNING. + 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 BLOCKED, 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) { + store := testStore(t) + runner := &mockRunner{} + runners := map[string]Runner{"claude": runner} + logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})) + pool := NewPool(2, runners, store, logger) + + // Parent is already READY (not BLOCKED). + parent := makeTask("ready-parent-3") + parent.State = task.StateReady + store.CreateTask(parent) + + sub1 := makeTask("ready-sub-3a") + sub1.ParentTaskID = parent.ID + store.CreateTask(sub1) + + if err := pool.Submit(context.Background(), sub1); err != nil { + t.Fatalf("submit: %v", err) + } + + <-pool.Results() + + // Parent must remain READY — no spurious state transition. + 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, got %v", got.State) + } +} + func TestPool_UnsupportedAgent(t *testing.T) { store := testStore(t) runners := map[string]Runner{"claude": &mockRunner{}} |
