package supervisor import ( "os" "path/filepath" "testing" ) const sampleTask = `# T01: Inicijalizacija Go projekta **Kreirao:** planer **Datum:** 2026-02-20 **Agent:** coder **Model:** Sonnet **Zavisi od:** — --- ## Opis Kreirati Go projekat u code/ folderu sa osnovnom strukturom za KAOS supervisor. --- ## Pitanja ` const sampleTaskWithDeps = `# T02: Task loader **Agent:** coder **Model:** Sonnet **Zavisi od:** T01, T03 --- ## Opis Implementirati task loader koji parsira markdown. --- ` const sampleTaskWithDoneDeps = `# T05: Deploy **Agent:** deployer **Model:** Haiku **Zavisi od:** T01 ✅, T02 ✅ --- ## Opis Deploy aplikacije. --- ` func writeTempTask(t *testing.T, dir, folder, filename, content string) string { t.Helper() folderPath := filepath.Join(dir, folder) if err := os.MkdirAll(folderPath, 0755); err != nil { t.Fatalf("mkdir: %v", err) } path := filepath.Join(folderPath, filename) if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatalf("write: %v", err) } return path } func TestLoadTask_ParsesAllFields(t *testing.T) { dir := t.TempDir() path := writeTempTask(t, dir, "ready", "T01.md", sampleTask) task, err := LoadTask(path) if err != nil { t.Fatalf("unexpected error: %v", err) } if task.ID != "T01" { t.Errorf("expected ID T01, got %s", task.ID) } if task.Title != "Inicijalizacija Go projekta" { t.Errorf("expected title 'Inicijalizacija Go projekta', got %q", task.Title) } if task.Agent != "coder" { t.Errorf("expected agent coder, got %s", task.Agent) } if task.Model != "Sonnet" { t.Errorf("expected model Sonnet, got %s", task.Model) } if len(task.DependsOn) != 0 { t.Errorf("expected no dependencies, got %v", task.DependsOn) } if task.Status != "ready" { t.Errorf("expected status ready, got %s", task.Status) } if task.Description == "" { t.Error("expected non-empty description") } } func TestLoadTask_WithDependencies(t *testing.T) { dir := t.TempDir() path := writeTempTask(t, dir, "backlog", "T02.md", sampleTaskWithDeps) task, err := LoadTask(path) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(task.DependsOn) != 2 { t.Fatalf("expected 2 dependencies, got %d: %v", len(task.DependsOn), task.DependsOn) } if task.DependsOn[0] != "T01" || task.DependsOn[1] != "T03" { t.Errorf("expected [T01, T03], got %v", task.DependsOn) } } func TestLoadTask_WithCheckmarkDependencies(t *testing.T) { dir := t.TempDir() path := writeTempTask(t, dir, "ready", "T05.md", sampleTaskWithDoneDeps) task, err := LoadTask(path) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(task.DependsOn) != 2 { t.Fatalf("expected 2 dependencies, got %d: %v", len(task.DependsOn), task.DependsOn) } if task.DependsOn[0] != "T01" || task.DependsOn[1] != "T02" { t.Errorf("expected [T01, T02], got %v", task.DependsOn) } } func TestLoadTask_NoID(t *testing.T) { dir := t.TempDir() content := "No header here\nJust some text\n" path := writeTempTask(t, dir, "backlog", "bad.md", content) _, err := LoadTask(path) if err == nil { t.Fatal("expected error for file without task ID") } } func TestLoadTask_NonexistentFile(t *testing.T) { _, err := LoadTask("/nonexistent/path/T99.md") if err == nil { t.Fatal("expected error for nonexistent file") } } func TestScanTasks_FindsAllFolders(t *testing.T) { dir := t.TempDir() writeTempTask(t, dir, "backlog", "T01.md", sampleTask) writeTempTask(t, dir, "ready", "T02.md", sampleTaskWithDeps) writeTempTask(t, dir, "done", "T03.md", `# T03: Done task **Agent:** coder **Model:** Haiku **Zavisi od:** — --- ## Opis Already done. --- `) // Create empty folders that should be handled gracefully os.MkdirAll(filepath.Join(dir, "active"), 0755) os.MkdirAll(filepath.Join(dir, "review"), 0755) tasks, err := ScanTasks(dir) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(tasks) != 3 { t.Fatalf("expected 3 tasks, got %d", len(tasks)) } // Check statuses statuses := map[string]string{} for _, task := range tasks { statuses[task.ID] = task.Status } if statuses["T01"] != "backlog" { t.Errorf("T01 expected backlog, got %s", statuses["T01"]) } if statuses["T02"] != "ready" { t.Errorf("T02 expected ready, got %s", statuses["T02"]) } if statuses["T03"] != "done" { t.Errorf("T03 expected done, got %s", statuses["T03"]) } } func TestScanTasks_EmptyFolder(t *testing.T) { dir := t.TempDir() // Only create empty folders for _, f := range taskFolders { os.MkdirAll(filepath.Join(dir, f), 0755) } tasks, err := ScanTasks(dir) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(tasks) != 0 { t.Errorf("expected 0 tasks, got %d", len(tasks)) } } func TestScanTasks_MissingFolders(t *testing.T) { dir := t.TempDir() // Don't create any subfolders tasks, err := ScanTasks(dir) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(tasks) != 0 { t.Errorf("expected 0 tasks, got %d", len(tasks)) } } func TestFindTask(t *testing.T) { tasks := []Task{ {ID: "T01", Title: "First"}, {ID: "T02", Title: "Second"}, {ID: "T03", Title: "Third"}, } found := FindTask(tasks, "T02") if found == nil { t.Fatal("expected to find T02") } if found.Title != "Second" { t.Errorf("expected title Second, got %s", found.Title) } notFound := FindTask(tasks, "T99") if notFound != nil { t.Error("expected nil for nonexistent task") } } func TestNextTask_ReadyWithDoneDeps(t *testing.T) { tasks := []Task{ {ID: "T01", Status: "done"}, {ID: "T02", Status: "ready", DependsOn: []string{"T01"}}, {ID: "T03", Status: "ready", DependsOn: []string{"T01", "T02"}}, } next := NextTask(tasks) if next == nil { t.Fatal("expected a next task") } if next.ID != "T02" { t.Errorf("expected T02, got %s", next.ID) } } func TestNextTask_BlockedByNonDone(t *testing.T) { tasks := []Task{ {ID: "T01", Status: "active"}, {ID: "T02", Status: "ready", DependsOn: []string{"T01"}}, } next := NextTask(tasks) if next != nil { t.Errorf("expected nil (T02 blocked), got %s", next.ID) } } func TestNextTask_NoDependencies(t *testing.T) { tasks := []Task{ {ID: "T01", Status: "ready", DependsOn: nil}, } next := NextTask(tasks) if next == nil { t.Fatal("expected T01 (no deps)") } if next.ID != "T01" { t.Errorf("expected T01, got %s", next.ID) } } func TestNextTask_NoReadyTasks(t *testing.T) { tasks := []Task{ {ID: "T01", Status: "done"}, {ID: "T02", Status: "active"}, } next := NextTask(tasks) if next != nil { t.Errorf("expected nil, got %s", next.ID) } } func TestMoveTask(t *testing.T) { dir := t.TempDir() writeTempTask(t, dir, "ready", "T01.md", sampleTask) err := MoveTask(dir, "T01", "ready", "active") if err != nil { t.Fatalf("unexpected error: %v", err) } // Verify file moved if _, err := os.Stat(filepath.Join(dir, "ready", "T01.md")); !os.IsNotExist(err) { t.Error("expected file removed from ready/") } if _, err := os.Stat(filepath.Join(dir, "active", "T01.md")); err != nil { t.Error("expected file in active/") } } func TestMoveTask_NonexistentSource(t *testing.T) { dir := t.TempDir() os.MkdirAll(filepath.Join(dir, "ready"), 0755) err := MoveTask(dir, "T99", "ready", "active") if err == nil { t.Fatal("expected error for nonexistent source") } } func TestMoveTask_CreatesDestFolder(t *testing.T) { dir := t.TempDir() writeTempTask(t, dir, "ready", "T01.md", sampleTask) // Don't create active/ — MoveTask should create it err := MoveTask(dir, "T01", "ready", "active") if err != nil { t.Fatalf("unexpected error: %v", err) } if _, err := os.Stat(filepath.Join(dir, "active", "T01.md")); err != nil { t.Error("expected file in active/") } } func TestParseDependencies(t *testing.T) { tests := []struct { input string expected []string }{ {"—", nil}, {"-", nil}, {"", nil}, {"T01", []string{"T01"}}, {"T01, T03", []string{"T01", "T03"}}, {"T01 ✅", []string{"T01"}}, {"T01 ✅, T02 ✅", []string{"T01", "T02"}}, } for _, tt := range tests { result := parseDependencies(tt.input) if len(result) != len(tt.expected) { t.Errorf("parseDependencies(%q): expected %v, got %v", tt.input, tt.expected, result) continue } for i := range result { if result[i] != tt.expected[i] { t.Errorf("parseDependencies(%q)[%d]: expected %s, got %s", tt.input, i, tt.expected[i], result[i]) } } } }