From 0eb0b79396663c4901597becc6857a4cd795c58e Mon Sep 17 00:00:00 2001 From: Claudomator Agent Date: Fri, 5 Jun 2026 09:21:32 +0000 Subject: chore: remove stories/checker backend dead code Remove the dead stories/checker backend tracked as design debt in CLAUDE.md: - Delete internal/api/stories.go, stories_test.go, elaborate.go - Delete internal/task/story.go, story_test.go - Remove story/checker columns from storage schema and all CRUD methods - Remove checkStoryCompletion, spawnCheckerTask, triggerStoryDeploy, createValidationTask, checkValidationResult, ShipStory, CheckStoryCompletion from executor; simplify handleRunResult - Remove story/checker fields from task.Task (StoryID, AcceptanceCriteria, CheckerForTaskID, CheckerReport) - Remove story routes and geminiBinPath from API server - Move claudeJSONResult/extractJSON from elaborate.go to validate.go - Rename ensureStoryBranch -> ensureBranch in ContainerRunner - Fix git identity in bare-repo tests; fix initial-branch to use main Co-Authored-By: Claude Sonnet 4.6 --- internal/executor/executor_test.go | 548 +------------------------------------ 1 file changed, 2 insertions(+), 546 deletions(-) (limited to 'internal/executor/executor_test.go') diff --git a/internal/executor/executor_test.go b/internal/executor/executor_test.go index b737e22..cb6554c 100644 --- a/internal/executor/executor_test.go +++ b/internal/executor/executor_test.go @@ -5,7 +5,6 @@ import ( "fmt" "log/slog" "os" - "os/exec" "path/filepath" "strings" "sync" @@ -805,53 +804,6 @@ 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 still be IN_PROGRESS — READY tasks don't satisfy the completion check; - // the task must be accepted (READY → COMPLETED) before the story advances to SHIPPABLE. - s, err := store.GetStory(story.ID) - if err != nil { - t.Fatalf("GetStory: %v", err) - } - if s.Status != task.StoryInProgress { - t.Errorf("story status: want IN_PROGRESS, got %s", s.Status) - } -} - // TestPool_RecoverStaleBlocked_DoesNotPromoteQueuedLeafTask verifies that a top-level // QUEUED task with NO subtasks is not promoted to READY by RecoverStaleBlocked. // This guards against the bug where a task that failed to start (stuck in QUEUED due @@ -878,46 +830,6 @@ func TestPool_RecoverStaleBlocked_DoesNotPromoteQueuedLeafTask(t *testing.T) { } } -// TestPool_CheckStoryCompletion_ReadyTasksNotSufficient verifies that READY tasks -// alone do not advance a story to SHIPPABLE — tasks must be COMPLETED. -func TestPool_CheckStoryCompletion_ReadyTasksNotSufficient(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-ready-only", - Name: "Ready Only Story", - Status: task.StoryInProgress, - CreatedAt: now, - UpdatedAt: now, - } - store.CreateStory(story) - - // One task driven to READY (checker pending), one COMPLETED. - tk1 := makeTask("ro-task-1") - tk1.StoryID = story.ID - store.CreateTask(tk1) - for _, s := range []task.State{task.StateQueued, task.StateRunning, task.StateReady} { - store.UpdateTaskState(tk1.ID, s) - } - - tk2 := makeTask("ro-task-2") - tk2.StoryID = story.ID - store.CreateTask(tk2) - for _, s := range []task.State{task.StateQueued, task.StateRunning, task.StateReady, task.StateCompleted} { - store.UpdateTaskState(tk2.ID, s) - } - - pool.checkStoryCompletion(context.Background(), story.ID) - - got, _ := store.GetStory(story.ID) - if got.Status != task.StoryInProgress { - t.Errorf("story status: want IN_PROGRESS (tk1 still READY/checker pending), got %s", got.Status) - } -} - func TestPool_ActivePerAgent_DeletesZeroEntries(t *testing.T) { store := testStore(t) runner := &mockRunner{} @@ -1248,13 +1160,8 @@ func (m *minimalMockStore) UpdateExecutionChangestats(execID string, stats *task } func (m *minimalMockStore) RecordAgentEvent(_ storage.AgentEvent) error { return nil } func (m *minimalMockStore) GetProject(_ string) (*task.Project, error) { return nil, nil } -func (m *minimalMockStore) GetStory(_ string) (*task.Story, error) { return nil, nil } -func (m *minimalMockStore) ListTasksByStory(_ string) ([]*task.Task, error) { return nil, nil } -func (m *minimalMockStore) UpdateStoryStatus(_ string, _ task.StoryState) error { return nil } -func (m *minimalMockStore) CreateTask(_ *task.Task) error { return nil } -func (m *minimalMockStore) CreateEvent(_ *event.Event) error { return nil } -func (m *minimalMockStore) UpdateTaskCheckerReport(_ string, _ string) error { return nil } -func (m *minimalMockStore) GetCheckerTask(_ string) (*task.Task, error) { return nil, nil } +func (m *minimalMockStore) CreateTask(_ *task.Task) error { return nil } +func (m *minimalMockStore) CreateEvent(_ *event.Event) error { return nil } func (m *minimalMockStore) lastStateUpdate() (string, task.State, bool) { m.mu.Lock() @@ -1778,290 +1685,6 @@ func TestPool_ConsecutiveFailures_ResetOnSuccess(t *testing.T) { } } -func TestPool_CheckStoryCompletion_AllComplete(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) - - // Create a story in IN_PROGRESS state. - now := time.Now().UTC() - story := &task.Story{ - ID: "story-comp-1", - Name: "Completion Test", - Status: task.StoryInProgress, - CreatedAt: now, - UpdatedAt: now, - } - if err := store.CreateStory(story); err != nil { - t.Fatalf("CreateStory: %v", err) - } - - // Create two top-level story tasks and drive them through valid transitions to COMPLETED. - for i, id := range []string{"sctask-1", "sctask-2"} { - tk := makeTask(id) - tk.StoryID = "story-comp-1" - 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.StateReady, task.StateCompleted} { - if err := store.UpdateTaskState(id, s); err != nil { - t.Fatalf("UpdateTaskState %s → %s: %v", id, s, err) - } - } - } - - pool.checkStoryCompletion(context.Background(), "story-comp-1") - - got, err := store.GetStory("story-comp-1") - if err != nil { - t.Fatalf("GetStory: %v", err) - } - if got.Status != task.StoryShippable { - t.Errorf("story status: want SHIPPABLE, got %v", got.Status) - } -} - -func TestPool_CheckStoryCompletion_PartialComplete(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-partial-1", - Name: "Partial Test", - Status: task.StoryInProgress, - CreatedAt: now, - UpdatedAt: now, - } - if err := store.CreateStory(story); err != nil { - t.Fatalf("CreateStory: %v", err) - } - - // First top-level task driven to READY. - tk1 := makeTask("sptask-1") - tk1.StoryID = "story-partial-1" - store.CreateTask(tk1) - for _, s := range []task.State{task.StateQueued, task.StateRunning, task.StateReady} { - store.UpdateTaskState("sptask-1", s) - } - - // Second top-level task still in PENDING (not done). - tk2 := makeTask("sptask-2") - tk2.StoryID = "story-partial-1" - store.CreateTask(tk2) - - pool.checkStoryCompletion(context.Background(), "story-partial-1") - - got, err := store.GetStory("story-partial-1") - if err != nil { - t.Fatalf("GetStory: %v", err) - } - if got.Status != task.StoryInProgress { - t.Errorf("story status: want IN_PROGRESS (no transition), got %v", got.Status) - } -} - -func TestPool_StoryDeploy_RunsDeployScript(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) - - // Create a deploy script that writes a marker file. - tmpDir := t.TempDir() - markerFile := filepath.Join(tmpDir, "deployed.marker") - scriptPath := filepath.Join(tmpDir, "deploy.sh") - scriptContent := "#!/bin/sh\ntouch " + markerFile + "\n" - if err := os.WriteFile(scriptPath, []byte(scriptContent), 0755); err != nil { - t.Fatalf("write deploy script: %v", err) - } - - proj := &task.Project{ - ID: "proj-deploy-1", - Name: "Deploy Test Project", - DeployScript: scriptPath, - } - if err := store.CreateProject(proj); err != nil { - t.Fatalf("create project: %v", err) - } - - story := &task.Story{ - ID: "story-deploy-1", - Name: "Deploy Test Story", - ProjectID: proj.ID, - Status: task.StoryShippable, - } - if err := store.CreateStory(story); err != nil { - t.Fatalf("create story: %v", err) - } - - pool.triggerStoryDeploy(context.Background(), story.ID) - - if _, err := os.Stat(markerFile); os.IsNotExist(err) { - t.Error("deploy script did not run: marker file not found") - } - - got, err := store.GetStory(story.ID) - if err != nil { - t.Fatalf("get story: %v", err) - } - if got.Status != task.StoryDeployed { - t.Errorf("story status: want DEPLOYED, got %q", got.Status) - } -} - -func runGit(t *testing.T, dir string, args ...string) { - t.Helper() - cmd := exec.Command("git", args...) - if dir != "" { - cmd.Dir = dir - } - if out, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("git %v: %v\n%s", args, err, out) - } -} - -func TestPool_StoryDeploy_MergesStoryBranch(t *testing.T) { - tmpDir := t.TempDir() - - // Set up bare repo + working copy with a story branch. - bareDir := filepath.Join(tmpDir, "bare.git") - localDir := filepath.Join(tmpDir, "local") - runGit(t, "", "init", "--bare", bareDir) - runGit(t, "", "clone", bareDir, localDir) - runGit(t, localDir, "config", "user.email", "test@test.com") - runGit(t, localDir, "config", "user.name", "Test") - - // Initial commit on main. - runGit(t, localDir, "checkout", "-b", "main") - os.WriteFile(filepath.Join(localDir, "README.md"), []byte("initial"), 0644) - runGit(t, localDir, "add", ".") - runGit(t, localDir, "commit", "-m", "initial") - runGit(t, localDir, "push", "-u", "origin", "main") - - // Story branch with a feature commit. - runGit(t, localDir, "checkout", "-b", "story/test-feature") - os.WriteFile(filepath.Join(localDir, "feature.go"), []byte("package main"), 0644) - runGit(t, localDir, "add", ".") - runGit(t, localDir, "commit", "-m", "feature work") - runGit(t, localDir, "push", "origin", "story/test-feature") - runGit(t, localDir, "checkout", "main") - - 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) - - scriptPath := filepath.Join(tmpDir, "deploy.sh") - os.WriteFile(scriptPath, []byte("#!/bin/sh\nexit 0\n"), 0755) - - proj := &task.Project{ - ID: "proj-merge-1", Name: "Merge Test", - LocalPath: localDir, DeployScript: scriptPath, - } - if err := store.CreateProject(proj); err != nil { - t.Fatalf("create project: %v", err) - } - story := &task.Story{ - ID: "story-merge-1", Name: "Merge Test Story", - ProjectID: proj.ID, BranchName: "story/test-feature", - Status: task.StoryShippable, - } - if err := store.CreateStory(story); err != nil { - t.Fatalf("create story: %v", err) - } - - pool.triggerStoryDeploy(context.Background(), story.ID) - - // feature.go should now be on main in the working copy. - if _, err := os.Stat(filepath.Join(localDir, "feature.go")); os.IsNotExist(err) { - t.Error("story branch was not merged to main: feature.go missing") - } - got, _ := store.GetStory(story.ID) - if got.Status != task.StoryDeployed { - t.Errorf("story status: want DEPLOYED, got %q", got.Status) - } -} - -func TestPool_PostDeploy_CreatesValidationTask(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() - validationSpec := `{"type":"smoke","steps":["curl /health"],"success_criteria":"status 200"}` - story := &task.Story{ - ID: "story-postdeploy-1", - Name: "Post Deploy Test", - Status: task.StoryDeployed, - ValidationJSON: validationSpec, - CreatedAt: now, - UpdatedAt: now, - } - if err := store.CreateStory(story); err != nil { - t.Fatalf("CreateStory: %v", err) - } - - pool.createValidationTask(context.Background(), story.ID) - - // Story should now be VALIDATING. - got, err := store.GetStory(story.ID) - if err != nil { - t.Fatalf("GetStory: %v", err) - } - if got.Status != task.StoryValidating { - t.Errorf("story status: want VALIDATING, got %q", got.Status) - } - - // A validation task should have been created. - tasks, err := store.ListTasksByStory(story.ID) - if err != nil { - t.Fatalf("ListTasksByStory: %v", err) - } - if len(tasks) == 0 { - t.Fatal("expected a validation task to be created, got none") - } - vtask := tasks[0] - if !strings.Contains(strings.ToLower(vtask.Name), "validation") { - t.Errorf("task name %q does not contain 'validation'", vtask.Name) - } - if vtask.StoryID != story.ID { - t.Errorf("task story_id: want %q, got %q", story.ID, vtask.StoryID) - } - if !strings.Contains(vtask.Agent.Instructions, "smoke") { - t.Errorf("task instructions %q do not reference validation spec content", vtask.Agent.Instructions) - } -} - -func TestPool_ValidationTask_Pass_SetsReviewReady(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-val-pass-1", - Name: "Validation Pass", - Status: task.StoryValidating, - CreatedAt: now, - UpdatedAt: now, - } - if err := store.CreateStory(story); err != nil { - t.Fatalf("CreateStory: %v", err) - } - - pool.checkValidationResult(context.Background(), story.ID, task.StateCompleted, "") - - got, err := store.GetStory(story.ID) - if err != nil { - t.Fatalf("GetStory: %v", err) - } - if got.Status != task.StoryReviewReady { - t.Errorf("story status: want REVIEW_READY, got %q", got.Status) - } -} // TestPool_DependsOn_NoDeadlock verifies that a task waiting for a dependency // does NOT hold the per-agent slot, allowing the dependency to run first. @@ -2118,35 +1741,6 @@ func TestPool_DependsOn_NoDeadlock(t *testing.T) { } } -func TestPool_ValidationTask_Fail_SetsNeedsFix(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-val-fail-1", - Name: "Validation Fail", - Status: task.StoryValidating, - CreatedAt: now, - UpdatedAt: now, - } - if err := store.CreateStory(story); err != nil { - t.Fatalf("CreateStory: %v", err) - } - - execErr := "smoke test failed: /health returned 503" - pool.checkValidationResult(context.Background(), story.ID, task.StateFailed, execErr) - - got, err := store.GetStory(story.ID) - if err != nil { - t.Fatalf("GetStory: %v", err) - } - if got.Status != task.StoryNeedsFix { - t.Errorf("story status: want NEEDS_FIX, got %q", got.Status) - } -} - func TestPool_Shutdown_WaitsForWorkers(t *testing.T) { store := testStore(t) started := make(chan struct{}) @@ -2229,141 +1823,3 @@ func TestPool_Shutdown_TimesOut(t *testing.T) { close(unblock) // cleanup } -func TestPool_CheckerSpawned_OnReady(t *testing.T) { - store := testStore(t) - runner := &mockRunner{} // succeeds instantly - pool := NewPool(2, map[string]Runner{"claude": runner}, store, slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))) - - tk := makeTask("checker-spawn-1") - tk.RepositoryURL = "https://github.com/x/y" - store.CreateTask(tk) - pool.Submit(context.Background(), tk) - <-pool.Results() // wait for original task to finish - - // Poll until the async spawnCheckerTask goroutine has written the checker task. - var checker *task.Task - var err error - deadline := time.Now().Add(5 * time.Second) - for time.Now().Before(deadline) { - checker, err = store.GetCheckerTask("checker-spawn-1") - if err != nil { - t.Fatalf("GetCheckerTask: %v", err) - } - if checker != nil { - break - } - time.Sleep(50 * time.Millisecond) - } - if checker == nil { - t.Fatal("expected a checker task to be created, got nil") - } - if checker.CheckerForTaskID != "checker-spawn-1" { - t.Errorf("expected CheckerForTaskID=checker-spawn-1, got %q", checker.CheckerForTaskID) - } -} - -func TestPool_CheckerNotSpawned_ForSubtask(t *testing.T) { - store := testStore(t) - runner := &mockRunner{} - pool := NewPool(2, map[string]Runner{"claude": runner}, store, slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))) - - parent := makeTask("no-checker-parent") - parent.RepositoryURL = "https://github.com/x/y" - store.CreateTask(parent) - - sub := makeTask("no-checker-sub") - sub.ParentTaskID = "no-checker-parent" - sub.RepositoryURL = "https://github.com/x/y" - store.CreateTask(sub) - - pool.Submit(context.Background(), sub) - <-pool.Results() - - time.Sleep(100 * time.Millisecond) - - checker, err := store.GetCheckerTask("no-checker-sub") - if err != nil { - t.Fatalf("GetCheckerTask: %v", err) - } - if checker != nil { - t.Error("expected no checker for subtask, but one was created") - } -} - -func TestPool_CheckerPass_AutoAcceptsTask(t *testing.T) { - store := testStore(t) - // Two-phase: first runner succeeds (original task), second also succeeds (checker). - runner := &mockRunner{ - onRun: func(t *task.Task, e *storage.Execution) error { - return nil // both original and checker succeed - }, - } - pool := NewPool(2, map[string]Runner{"claude": runner}, store, slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))) - - tk := makeTask("autoaccept-1") - tk.RepositoryURL = "https://github.com/x/y" - store.CreateTask(tk) - pool.Submit(context.Background(), tk) - <-pool.Results() // original finishes → READY + checker spawned - - // Wait for checker to run and complete. - deadline := time.Now().Add(5 * time.Second) - for time.Now().Before(deadline) { - got, _ := store.GetTask("autoaccept-1") - if got != nil && got.State == task.StateCompleted { - break - } - <-pool.Results() - } - - got, err := store.GetTask("autoaccept-1") - if err != nil { - t.Fatalf("GetTask: %v", err) - } - if got.State != task.StateCompleted { - t.Errorf("expected COMPLETED after checker pass, got %s", got.State) - } -} - -func TestPool_CheckerFail_AttachesReport(t *testing.T) { - store := testStore(t) - runner := &mockRunner{ - onRun: func(t *task.Task, e *storage.Execution) error { - if t.CheckerForTaskID != "" { - return fmt.Errorf("test suite failed: 3 failures") - } - return nil // original task succeeds - }, - } - pool := NewPool(2, map[string]Runner{"claude": runner}, store, slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))) - - tk := makeTask("fail-checker-1") - tk.RepositoryURL = "https://github.com/x/y" - store.CreateTask(tk) - pool.Submit(context.Background(), tk) - <-pool.Results() // original → READY - - // Wait for checker to fail. - deadline := time.Now().Add(5 * time.Second) - for time.Now().Before(deadline) { - got, _ := store.GetTask("fail-checker-1") - if got != nil && got.CheckerReport != "" { - break - } - select { - case <-pool.Results(): - case <-time.After(100 * time.Millisecond): - } - } - - got, err := store.GetTask("fail-checker-1") - if err != nil { - t.Fatalf("GetTask: %v", err) - } - if got.State != task.StateReady { - t.Errorf("expected task to stay READY after checker fail, got %s", got.State) - } - if got.CheckerReport == "" { - t.Error("expected checker_report to be set after checker failure") - } -} -- cgit v1.2.3