KAOS/code/internal/supervisor/task_test.go
djuka 79bcd52076 T02: Task loader — parsiranje markdown taskova
- 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>
2026-02-20 11:41:55 +00:00

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])
}
}
}
}