- Task struct sa svim poljima (ID, Title, Status, Agent, Model, DependsOn, Description) - LoadTask, ScanTasks, FindTask, NextTask, MoveTask - 17 testova — svi prolaze Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
382 lines
8.3 KiB
Go
382 lines
8.3 KiB
Go
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])
|
|
}
|
|
}
|
|
}
|
|
}
|