diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-03-26 09:36:30 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-03-26 09:36:30 +0000 |
| commit | 759396855a967a3d509498cc55faa3b4d8cadfba (patch) | |
| tree | 339845073da2bb6b1dc5765932570d5716773f61 /internal/executor/executor_test.go | |
| parent | 3f9843b34d7ae9df2dd9c69427ecab45744b97e9 (diff) | |
When a story is approved with pre-created subtasks, parent tasks are
QUEUED but never run. Their subtasks complete, but:
- maybeUnblockParent only handled BLOCKED parents, not QUEUED ones
- checkStoryCompletion required ALL tasks (incl. subtasks) to be done
Fixes:
- maybeUnblockParent now also promotes QUEUED parents to READY when all
subtasks are COMPLETED
- checkStoryCompletion only checks top-level tasks (parent_task_id="")
- RecoverStaleBlocked now also scans QUEUED parents on startup and
triggers checkStoryCompletion if it promotes them
- Add QUEUED→READY to valid state transitions (subtask delegation path)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/executor/executor_test.go')
| -rw-r--r-- | internal/executor/executor_test.go | 59 |
1 files changed, 51 insertions, 8 deletions
diff --git a/internal/executor/executor_test.go b/internal/executor/executor_test.go index e16185d..64e3ecb 100644 --- a/internal/executor/executor_test.go +++ b/internal/executor/executor_test.go @@ -747,6 +747,52 @@ func TestPool_RecoverStaleBlocked_KeepsBlockedWhenSubtaskIncomplete(t *testing.T } } +func TestPool_RecoverStaleBlocked_PromotesQueuedParentWithAllSubtasksDone(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) + + now := time.Now().UTC() + story := &task.Story{ + ID: "story-queued-parent", Name: "Queued Parent Story", + Status: task.StoryInProgress, CreatedAt: now, UpdatedAt: now, + } + store.CreateStory(story) + + // Parent task stuck QUEUED (approved with pre-created subtasks, never run). + parent := makeTask("queued-parent-1") + parent.State = task.StateQueued + parent.StoryID = story.ID + store.CreateTask(parent) + + for i := 0; i < 2; i++ { + sub := makeTask(fmt.Sprintf("queued-sub-%d", i)) + sub.ParentTaskID = parent.ID + sub.StoryID = story.ID + sub.State = task.StateCompleted + store.CreateTask(sub) + } + + pool.RecoverStaleBlocked() + + got, err := store.GetTask(parent.ID) + if err != nil { + t.Fatalf("GetTask: %v", err) + } + if got.State != task.StateReady { + t.Errorf("parent state: want READY, got %s", got.State) + } + + // Story should have been advanced to SHIPPABLE. + s, err := store.GetStory(story.ID) + if err != nil { + t.Fatalf("GetStory: %v", err) + } + if s.Status != task.StoryShippable { + t.Errorf("story status: want SHIPPABLE, got %s", s.Status) + } +} + func TestPool_ActivePerAgent_DeletesZeroEntries(t *testing.T) { store := testStore(t) runner := &mockRunner{} @@ -1659,16 +1705,15 @@ func TestPool_CheckStoryCompletion_AllComplete(t *testing.T) { t.Fatalf("CreateStory: %v", err) } - // Create two story tasks and drive them through valid transitions to COMPLETED. + // Create two top-level story tasks and drive them through valid transitions to READY. for i, id := range []string{"sctask-1", "sctask-2"} { tk := makeTask(id) tk.StoryID = "story-comp-1" - tk.ParentTaskID = "fake-parent" // so it goes to COMPLETED tk.State = task.StatePending if err := store.CreateTask(tk); err != nil { t.Fatalf("CreateTask %d: %v", i, err) } - for _, s := range []task.State{task.StateQueued, task.StateRunning, task.StateCompleted} { + for _, s := range []task.State{task.StateQueued, task.StateRunning, task.StateReady} { if err := store.UpdateTaskState(id, s); err != nil { t.Fatalf("UpdateTaskState %s → %s: %v", id, s, err) } @@ -1703,19 +1748,17 @@ func TestPool_CheckStoryCompletion_PartialComplete(t *testing.T) { t.Fatalf("CreateStory: %v", err) } - // First task driven to COMPLETED. + // First top-level task driven to READY. tk1 := makeTask("sptask-1") tk1.StoryID = "story-partial-1" - tk1.ParentTaskID = "fake-parent" store.CreateTask(tk1) - for _, s := range []task.State{task.StateQueued, task.StateRunning, task.StateCompleted} { + for _, s := range []task.State{task.StateQueued, task.StateRunning, task.StateReady} { store.UpdateTaskState("sptask-1", s) } - // Second task still in PENDING (not done). + // Second top-level task still in PENDING (not done). tk2 := makeTask("sptask-2") tk2.StoryID = "story-partial-1" - tk2.ParentTaskID = "fake-parent" store.CreateTask(tk2) pool.checkStoryCompletion(context.Background(), "story-partial-1") |
