package supervisor import ( "context" "os" "os/exec" "path/filepath" "testing" "time" "github.com/dal/kaos/internal/config" ) // setupE2E creates a temp directory with TASKS structure and a passing Go project. func setupE2E(t *testing.T) (*Supervisor, string) { t.Helper() dir := t.TempDir() // Create TASKS structure tasksDir := filepath.Join(dir, "TASKS") for _, folder := range []string{"backlog", "ready", "active", "review", "done", "reports"} { os.MkdirAll(filepath.Join(tasksDir, folder), 0755) } // Create a passing Go project codeDir := filepath.Join(dir, "code") os.MkdirAll(codeDir, 0755) os.WriteFile(filepath.Join(codeDir, "go.mod"), []byte("module testproj\n\ngo 1.22\n"), 0644) os.WriteFile(filepath.Join(codeDir, "main.go"), []byte("package main\n\nfunc main() {}\n"), 0644) os.WriteFile(filepath.Join(codeDir, "main_test.go"), []byte("package main\n\nimport \"testing\"\n\nfunc TestMain(t *testing.T) {}\n"), 0644) cfg := &config.Config{ Timeout: 30 * time.Second, ProjectPath: codeDir, TasksDir: tasksDir, } sv := NewSupervisor(cfg) // Use mock command builder so we don't need real Claude CLI sv.CmdBuilder = func(ctx context.Context, task Task, projectPath string) *exec.Cmd { return exec.CommandContext(ctx, "echo", "mock agent output for "+task.ID) } return sv, dir } func writeTaskFile(t *testing.T, tasksDir, folder, filename, content string) { t.Helper() path := filepath.Join(tasksDir, folder, filename) if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatalf("write task file: %v", err) } } const e2eTask1 = `# T01: Bazni task **Agent:** coder **Model:** Sonnet **Zavisi od:** — --- ## Opis Bazni task koji nema zavisnosti. --- ` const e2eTask2 = `# T02: Zavisni task **Agent:** coder **Model:** Sonnet **Zavisi od:** T01 --- ## Opis Zavisi od T01. --- ` func TestSupervisor_Run_EndToEnd(t *testing.T) { sv, _ := setupE2E(t) // Put task in ready writeTaskFile(t, sv.TasksDir, "ready", "T01.md", e2eTask1) err := sv.Run("T01") if err != nil { t.Fatalf("Run failed: %v", err) } // Verify task moved to review if _, err := os.Stat(filepath.Join(sv.TasksDir, "review", "T01.md")); err != nil { t.Error("expected T01.md in review/") } if _, err := os.Stat(filepath.Join(sv.TasksDir, "ready", "T01.md")); !os.IsNotExist(err) { t.Error("expected T01.md removed from ready/") } if _, err := os.Stat(filepath.Join(sv.TasksDir, "active", "T01.md")); !os.IsNotExist(err) { t.Error("expected T01.md removed from active/") } // Verify report written if _, err := os.Stat(filepath.Join(sv.ReportsDir, "T01-report.md")); err != nil { t.Error("expected T01-report.md in reports/") } } func TestSupervisor_RunNext_PicksCorrectTask(t *testing.T) { sv, _ := setupE2E(t) // T01 is done, T02 depends on T01 and is in ready writeTaskFile(t, sv.TasksDir, "done", "T01.md", e2eTask1) writeTaskFile(t, sv.TasksDir, "ready", "T02.md", e2eTask2) err := sv.RunNext() if err != nil { t.Fatalf("RunNext failed: %v", err) } // T02 should be in review now if _, err := os.Stat(filepath.Join(sv.TasksDir, "review", "T02.md")); err != nil { t.Error("expected T02.md in review/") } } func TestSupervisor_RunNext_BlockedTask(t *testing.T) { sv, _ := setupE2E(t) // T02 depends on T01, but T01 is NOT done (it's in backlog) writeTaskFile(t, sv.TasksDir, "backlog", "T01.md", e2eTask1) writeTaskFile(t, sv.TasksDir, "ready", "T02.md", e2eTask2) err := sv.RunNext() if err == nil { t.Fatal("expected error for blocked task") } } func TestSupervisor_RunNext_NoTasks(t *testing.T) { sv, _ := setupE2E(t) err := sv.RunNext() if err == nil { t.Fatal("expected error when no tasks available") } } func TestSupervisor_Run_TaskNotFound(t *testing.T) { sv, _ := setupE2E(t) err := sv.Run("T99") if err == nil { t.Fatal("expected error for nonexistent task") } } func TestSupervisor_Run_TaskNotReady(t *testing.T) { sv, _ := setupE2E(t) // Task in active, not ready writeTaskFile(t, sv.TasksDir, "active", "T01.md", e2eTask1) err := sv.Run("T01") if err == nil { t.Fatal("expected error for task not in ready/") } } func TestSupervisor_Run_FailedVerification(t *testing.T) { sv, _ := setupE2E(t) // Make the Go project fail by writing bad code os.WriteFile(filepath.Join(sv.CodeDir, "main.go"), []byte("package main\n\nfunc main() { BROKEN }\n"), 0644) writeTaskFile(t, sv.TasksDir, "ready", "T01.md", e2eTask1) err := sv.Run("T01") if err == nil { t.Fatal("expected error for failed verification") } // Task should still be moved to review even on failure if _, err := os.Stat(filepath.Join(sv.TasksDir, "review", "T01.md")); err != nil { t.Error("expected T01.md in review/ even after failure") } // Report should exist with failed status reportPath := filepath.Join(sv.ReportsDir, "T01-report.md") if _, err := os.Stat(reportPath); err != nil { t.Error("expected report to exist even for failed verification") } } func TestNewSupervisor(t *testing.T) { cfg := &config.Config{ Timeout: 10 * time.Minute, ProjectPath: "/tmp/code", TasksDir: "/tmp/tasks", } sv := NewSupervisor(cfg) if sv.Config != cfg { t.Error("expected config to be set") } if sv.TasksDir != "/tmp/tasks" { t.Errorf("expected TasksDir /tmp/tasks, got %s", sv.TasksDir) } if sv.CodeDir != "/tmp/code" { t.Errorf("expected CodeDir /tmp/code, got %s", sv.CodeDir) } if sv.ReportsDir != "/tmp/tasks/reports" { t.Errorf("expected ReportsDir /tmp/tasks/reports, got %s", sv.ReportsDir) } }