- Supervisor struct: NewSupervisor, Run, RunNext, execute - E2E tok: scan → find → active → run → verify → report → review - cmdRun u CLI koristi Supervisor - 8 e2e testova sa mock agentom, 67 ukupno — svi prolaze - T06 premešten u done/ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
221 lines
5.4 KiB
Go
221 lines
5.4 KiB
Go
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)
|
|
}
|
|
}
|