T06: CLI — komandni interfejs
- 5 komandi: run, status, next, verify, history - Config proširen sa KAOS_TASKS_DIR - Tabelarni status sa emoji ikonama - run: scan → find → move → run → verify → report → review - 8 CLI testova, 59 ukupno — svi prolaze - T05 premešten u done/ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
028872be43
commit
38e1e1029c
47
TASKS/reports/T06-report.md
Normal file
47
TASKS/reports/T06-report.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# T06 Izveštaj: CLI — komandni interfejs
|
||||||
|
|
||||||
|
**Agent:** coder
|
||||||
|
**Model:** Opus
|
||||||
|
**Datum:** 2026-02-20
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Šta je urađeno
|
||||||
|
|
||||||
|
Implementiran CLI u `code/cmd/kaos-supervisor/main.go`:
|
||||||
|
|
||||||
|
### Izmenjeni/kreirani fajlovi
|
||||||
|
|
||||||
|
| Fajl | Opis |
|
||||||
|
|------|------|
|
||||||
|
| `cmd/kaos-supervisor/main.go` | 5 komandi: run, status, next, verify, history |
|
||||||
|
| `cmd/kaos-supervisor/main_test.go` | 8 testova za CLI komande |
|
||||||
|
| `internal/config/config.go` | Dodat KAOS_TASKS_DIR |
|
||||||
|
| `internal/config/config_test.go` | Dodat test za KAOS_TASKS_DIR |
|
||||||
|
| `.env.example` | Dodat KAOS_TASKS_DIR |
|
||||||
|
|
||||||
|
### Komande
|
||||||
|
|
||||||
|
- **run [TASK_ID]** — pokreni task (ili sledeći), verify, report, premesti u review/
|
||||||
|
- **status** — tabelarni prikaz svih taskova sa emoji statusima
|
||||||
|
- **next** — sledeći task za rad
|
||||||
|
- **verify [PATH]** — verifikacija Go projekta
|
||||||
|
- **history** — lista izveštaja
|
||||||
|
|
||||||
|
### Testovi — 8/8 PASS (CLI)
|
||||||
|
|
||||||
|
```
|
||||||
|
TestCmdStatus_ShowsTasks PASS
|
||||||
|
TestCmdNext_FindsReadyTask PASS
|
||||||
|
TestCmdVerify_OnTestProject PASS (0.46s)
|
||||||
|
TestCmdHistory_ShowsReports PASS
|
||||||
|
TestUnknownCommand PASS
|
||||||
|
TestCmdRun_NoReadyTasks PASS
|
||||||
|
TestStatusIcon PASS
|
||||||
|
TestNextTask_Integration PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ukupno projekat: 59 testova, svi prolaze
|
||||||
|
|
||||||
|
- `go vet ./...` — čist
|
||||||
|
- `go build ./...` — prolazi
|
||||||
90
TASKS/review/T06.md
Normal file
90
TASKS/review/T06.md
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
# T06: CLI — komandni interfejs
|
||||||
|
|
||||||
|
**Kreirao:** planer
|
||||||
|
**Datum:** 2026-02-20
|
||||||
|
**Agent:** coder
|
||||||
|
**Model:** Sonnet
|
||||||
|
**Zavisi od:** T02 ✅, T03 ✅, T04 ✅, T05
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Opis
|
||||||
|
|
||||||
|
Entry point za supervisor. Parsira komande, poziva supervisor funkcije.
|
||||||
|
Koristi sve module iz T02-T05.
|
||||||
|
|
||||||
|
## Fajlovi za izmenu
|
||||||
|
|
||||||
|
```
|
||||||
|
code/cmd/kaos-supervisor/main.go ← komande
|
||||||
|
```
|
||||||
|
|
||||||
|
## Komande
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kaos-supervisor run [TASK_ID] # pokreni task (ili sledeći iz ready/)
|
||||||
|
kaos-supervisor status # prikaži status svih taskova
|
||||||
|
kaos-supervisor next # prikaži šta je sledeće za rad
|
||||||
|
kaos-supervisor verify [PATH] # pokreni verifikaciju na projektu
|
||||||
|
kaos-supervisor history # prikaži izvršene taskove iz reports/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Detalji komandi
|
||||||
|
|
||||||
|
### run [TASK_ID]
|
||||||
|
1. ScanTasks() — učitaj sve taskove
|
||||||
|
2. Ako je TASK_ID dat → FindTask, proveri da je u ready/
|
||||||
|
3. Ako nije dat → NextTask (prvi iz ready/ sa ispunjenim zavisnostima)
|
||||||
|
4. MoveTask → active/
|
||||||
|
5. RunTask (iz runner.go)
|
||||||
|
6. Verify (iz checker.go)
|
||||||
|
7. WriteReport (iz reporter.go)
|
||||||
|
8. MoveTask → review/
|
||||||
|
9. Prikaži rezime
|
||||||
|
|
||||||
|
### status
|
||||||
|
- ScanTasks → tabelarni prikaz svih taskova
|
||||||
|
- Format: ID | Naslov | Folder | Zavisi od
|
||||||
|
|
||||||
|
### next
|
||||||
|
- NextTask → prikaži koji task je sledeći za rad
|
||||||
|
|
||||||
|
### verify [PATH]
|
||||||
|
- Verify(path) → prikaži rezultat
|
||||||
|
- Default path: code/
|
||||||
|
|
||||||
|
### history
|
||||||
|
- Čitaj fajlove iz TASKS/reports/ → prikaži listu
|
||||||
|
|
||||||
|
## Pravila
|
||||||
|
|
||||||
|
- Bez eksternih CLI biblioteka (flag package ili os.Args)
|
||||||
|
- Formatiran terminal output sa emoji za status
|
||||||
|
- Poruke na srpskom
|
||||||
|
- Config čita iz .env (putanja do TASKS/, timeout)
|
||||||
|
- Exit code: 0 uspeh, 1 greška
|
||||||
|
|
||||||
|
## Testovi
|
||||||
|
|
||||||
|
- status sa primer taskovima → ispravan output
|
||||||
|
- next → vraća pravi task
|
||||||
|
- verify na test projektu → prikaže rezultat
|
||||||
|
- Nepoznata komanda → help poruka
|
||||||
|
- run bez taskova u ready/ → poruka "nema taskova"
|
||||||
|
|
||||||
|
## Očekivani izlaz
|
||||||
|
|
||||||
|
`make build` → `bin/kaos-supervisor` radi sve komande.
|
||||||
|
`go test ./... -v` — svi testovi zeleni.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pitanja
|
||||||
|
|
||||||
|
*(agent piše pitanja ovde, planer odgovara)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Odgovori
|
||||||
|
|
||||||
|
*(planer piše odgovore ovde)*
|
||||||
@ -1,2 +1,3 @@
|
|||||||
KAOS_TIMEOUT=30m
|
KAOS_TIMEOUT=30m
|
||||||
KAOS_PROJECT_PATH=.
|
KAOS_PROJECT_PATH=.
|
||||||
|
KAOS_TASKS_DIR=../TASKS
|
||||||
|
|||||||
@ -1,22 +1,242 @@
|
|||||||
// Package main is the entry point for the KAOS supervisor process.
|
// Package main is the entry point for the KAOS supervisor process.
|
||||||
// It loads configuration and starts the supervisor.
|
// It parses CLI commands and delegates to supervisor functions.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
"github.com/dal/kaos/internal/config"
|
"github.com/dal/kaos/internal/config"
|
||||||
_ "github.com/dal/kaos/internal/supervisor"
|
"github.com/dal/kaos/internal/supervisor"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cfg, err := config.Load()
|
cfg, err := config.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to load config: %v", err)
|
fmt.Fprintf(os.Stderr, "Greška pri učitavanju konfiguracije: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(os.Stdout, "KAOS Supervisor started (timeout=%s, project_path=%s)\n",
|
if len(os.Args) < 2 {
|
||||||
cfg.Timeout, cfg.ProjectPath)
|
printHelp()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
command := os.Args[1]
|
||||||
|
|
||||||
|
switch command {
|
||||||
|
case "run":
|
||||||
|
os.Exit(cmdRun(cfg))
|
||||||
|
case "status":
|
||||||
|
os.Exit(cmdStatus(cfg))
|
||||||
|
case "next":
|
||||||
|
os.Exit(cmdNext(cfg))
|
||||||
|
case "verify":
|
||||||
|
os.Exit(cmdVerify(cfg))
|
||||||
|
case "history":
|
||||||
|
os.Exit(cmdHistory(cfg))
|
||||||
|
case "help", "--help", "-h":
|
||||||
|
printHelp()
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "Nepoznata komanda: %s\n\n", command)
|
||||||
|
printHelp()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printHelp() {
|
||||||
|
fmt.Println("KAOS Supervisor")
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Komande:")
|
||||||
|
fmt.Println(" run [TASK_ID] Pokreni task (ili sledeći iz ready/)")
|
||||||
|
fmt.Println(" status Prikaži status svih taskova")
|
||||||
|
fmt.Println(" next Prikaži sledeći task za rad")
|
||||||
|
fmt.Println(" verify [PATH] Pokreni verifikaciju na projektu")
|
||||||
|
fmt.Println(" history Prikaži izvršene taskove iz reports/")
|
||||||
|
fmt.Println(" help Prikaži ovu pomoć")
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdRun(cfg *config.Config) int {
|
||||||
|
tasks, err := supervisor.ScanTasks(cfg.TasksDir)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Greška pri skeniranju taskova: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var task *supervisor.Task
|
||||||
|
|
||||||
|
if len(os.Args) > 2 {
|
||||||
|
taskID := os.Args[2]
|
||||||
|
task = supervisor.FindTask(tasks, taskID)
|
||||||
|
if task == nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Task %s nije pronađen\n", taskID)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if task.Status != "ready" {
|
||||||
|
fmt.Fprintf(os.Stderr, "Task %s nije u ready/ (trenutno: %s)\n", taskID, task.Status)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
task = supervisor.NextTask(tasks)
|
||||||
|
if task == nil {
|
||||||
|
fmt.Println("Nema taskova spremnih za rad")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("▶ Pokrećem: %s — %s\n", task.ID, task.Title)
|
||||||
|
|
||||||
|
// Move to active
|
||||||
|
if err := supervisor.MoveTask(cfg.TasksDir, task.ID, "ready", "active"); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Greška pri premještanju u active/: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the task
|
||||||
|
result := supervisor.RunTask(*task, cfg.ProjectPath, cfg.Timeout, nil)
|
||||||
|
|
||||||
|
fmt.Printf("⏱ Trajanje: %s\n", result.Duration.Round(1000000000)) // round to seconds
|
||||||
|
fmt.Printf("📤 Exit code: %d\n", result.ExitCode)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
fmt.Println("\n🔍 Verifikacija...")
|
||||||
|
verify := supervisor.Verify(cfg.ProjectPath)
|
||||||
|
fmt.Print(supervisor.FormatResult(verify))
|
||||||
|
|
||||||
|
// Write report
|
||||||
|
reportsDir := filepath.Join(cfg.TasksDir, "reports")
|
||||||
|
reportPath, err := supervisor.WriteReport(*task, result, verify, reportsDir)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Greška pri pisanju izveštaja: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("\n📝 Izveštaj: %s\n", reportPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to review
|
||||||
|
if err := supervisor.MoveTask(cfg.TasksDir, task.ID, "active", "review"); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Greška pri premještanju u review/: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\n✅ %s premešten u review/\n", task.ID)
|
||||||
|
|
||||||
|
if !verify.AllPassed {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdStatus(cfg *config.Config) int {
|
||||||
|
tasks, err := supervisor.ScanTasks(cfg.TasksDir)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Greška pri skeniranju taskova: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tasks) == 0 {
|
||||||
|
fmt.Println("Nema taskova")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||||
|
fmt.Fprintln(w, "ID\tNaslov\tFolder\tZavisi od")
|
||||||
|
fmt.Fprintln(w, "──\t──────\t──────\t─────────")
|
||||||
|
|
||||||
|
for _, t := range tasks {
|
||||||
|
deps := "—"
|
||||||
|
if len(t.DependsOn) > 0 {
|
||||||
|
deps = strings.Join(t.DependsOn, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
icon := statusIcon(t.Status)
|
||||||
|
fmt.Fprintf(w, "%s\t%s\t%s %s\t%s\n", t.ID, t.Title, icon, t.Status, deps)
|
||||||
|
}
|
||||||
|
w.Flush()
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdNext(cfg *config.Config) int {
|
||||||
|
tasks, err := supervisor.ScanTasks(cfg.TasksDir)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Greška pri skeniranju taskova: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
task := supervisor.NextTask(tasks)
|
||||||
|
if task == nil {
|
||||||
|
fmt.Println("Nema taskova spremnih za rad")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Sledeći: %s — %s\n", task.ID, task.Title)
|
||||||
|
fmt.Printf("Agent: %s | Model: %s\n", task.Agent, task.Model)
|
||||||
|
if len(task.DependsOn) > 0 {
|
||||||
|
fmt.Printf("Zavisi od: %s\n", strings.Join(task.DependsOn, ", "))
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdVerify(cfg *config.Config) int {
|
||||||
|
path := cfg.ProjectPath
|
||||||
|
if len(os.Args) > 2 {
|
||||||
|
path = os.Args[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("🔍 Verifikacija: %s\n\n", path)
|
||||||
|
result := supervisor.Verify(path)
|
||||||
|
fmt.Print(supervisor.FormatResult(result))
|
||||||
|
|
||||||
|
if !result.AllPassed {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdHistory(cfg *config.Config) int {
|
||||||
|
reportsDir := filepath.Join(cfg.TasksDir, "reports")
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(reportsDir)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
fmt.Println("Nema izveštaja")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "Greška pri čitanju reports/: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(entries) == 0 {
|
||||||
|
fmt.Println("Nema izveštaja")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Izveštaji:")
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() || !strings.HasSuffix(entry.Name(), "-report.md") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := strings.TrimSuffix(entry.Name(), "-report.md")
|
||||||
|
fmt.Printf(" 📝 %s (%s)\n", name, filepath.Join(reportsDir, entry.Name()))
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func statusIcon(status string) string {
|
||||||
|
switch status {
|
||||||
|
case "done":
|
||||||
|
return "✅"
|
||||||
|
case "active":
|
||||||
|
return "🔄"
|
||||||
|
case "ready":
|
||||||
|
return "📋"
|
||||||
|
case "review":
|
||||||
|
return "👀"
|
||||||
|
case "backlog":
|
||||||
|
return "📦"
|
||||||
|
default:
|
||||||
|
return "❓"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
268
code/cmd/kaos-supervisor/main_test.go
Normal file
268
code/cmd/kaos-supervisor/main_test.go
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/dal/kaos/internal/config"
|
||||||
|
"github.com/dal/kaos/internal/supervisor"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupTestTasks(t *testing.T) (string, *config.Config) {
|
||||||
|
t.Helper()
|
||||||
|
dir := t.TempDir()
|
||||||
|
|
||||||
|
tasksDir := filepath.Join(dir, "TASKS")
|
||||||
|
for _, folder := range []string{"backlog", "ready", "active", "review", "done", "reports"} {
|
||||||
|
os.MkdirAll(filepath.Join(tasksDir, folder), 0755)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create test tasks
|
||||||
|
t1 := `# T01: Prvi task
|
||||||
|
|
||||||
|
**Agent:** coder
|
||||||
|
**Model:** Sonnet
|
||||||
|
**Zavisi od:** —
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Opis
|
||||||
|
|
||||||
|
Opis prvog taska.
|
||||||
|
|
||||||
|
---
|
||||||
|
`
|
||||||
|
t2 := `# T02: Drugi task
|
||||||
|
|
||||||
|
**Agent:** coder
|
||||||
|
**Model:** Sonnet
|
||||||
|
**Zavisi od:** T01
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Opis
|
||||||
|
|
||||||
|
Opis drugog taska.
|
||||||
|
|
||||||
|
---
|
||||||
|
`
|
||||||
|
os.WriteFile(filepath.Join(tasksDir, "done", "T01.md"), []byte(t1), 0644)
|
||||||
|
os.WriteFile(filepath.Join(tasksDir, "ready", "T02.md"), []byte(t2), 0644)
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
TasksDir: tasksDir,
|
||||||
|
ProjectPath: dir,
|
||||||
|
}
|
||||||
|
|
||||||
|
return dir, cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmdStatus_ShowsTasks(t *testing.T) {
|
||||||
|
_, cfg := setupTestTasks(t)
|
||||||
|
|
||||||
|
// Capture output by redirecting stdout
|
||||||
|
old := os.Stdout
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stdout = w
|
||||||
|
|
||||||
|
exitCode := cmdStatus(cfg)
|
||||||
|
|
||||||
|
w.Close()
|
||||||
|
os.Stdout = old
|
||||||
|
|
||||||
|
var buf [4096]byte
|
||||||
|
n, _ := r.Read(buf[:])
|
||||||
|
output := string(buf[:n])
|
||||||
|
|
||||||
|
if exitCode != 0 {
|
||||||
|
t.Errorf("expected exit code 0, got %d", exitCode)
|
||||||
|
}
|
||||||
|
if !strings.Contains(output, "T01") {
|
||||||
|
t.Errorf("expected T01 in output, got:\n%s", output)
|
||||||
|
}
|
||||||
|
if !strings.Contains(output, "T02") {
|
||||||
|
t.Errorf("expected T02 in output, got:\n%s", output)
|
||||||
|
}
|
||||||
|
if !strings.Contains(output, "done") {
|
||||||
|
t.Errorf("expected 'done' status in output, got:\n%s", output)
|
||||||
|
}
|
||||||
|
if !strings.Contains(output, "ready") {
|
||||||
|
t.Errorf("expected 'ready' status in output, got:\n%s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmdNext_FindsReadyTask(t *testing.T) {
|
||||||
|
_, cfg := setupTestTasks(t)
|
||||||
|
|
||||||
|
old := os.Stdout
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stdout = w
|
||||||
|
|
||||||
|
exitCode := cmdNext(cfg)
|
||||||
|
|
||||||
|
w.Close()
|
||||||
|
os.Stdout = old
|
||||||
|
|
||||||
|
var buf [4096]byte
|
||||||
|
n, _ := r.Read(buf[:])
|
||||||
|
output := string(buf[:n])
|
||||||
|
|
||||||
|
if exitCode != 0 {
|
||||||
|
t.Errorf("expected exit code 0, got %d", exitCode)
|
||||||
|
}
|
||||||
|
if !strings.Contains(output, "T02") {
|
||||||
|
t.Errorf("expected T02 as next task, got:\n%s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmdVerify_OnTestProject(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
|
||||||
|
// Create a minimal passing Go project
|
||||||
|
os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module testproj\n\ngo 1.22\n"), 0644)
|
||||||
|
os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n\nfunc main() {}\n"), 0644)
|
||||||
|
|
||||||
|
cfg := &config.Config{ProjectPath: dir}
|
||||||
|
|
||||||
|
// Set os.Args so cmdVerify doesn't pick up test flags
|
||||||
|
oldArgs := os.Args
|
||||||
|
os.Args = []string{"kaos-supervisor", "verify"}
|
||||||
|
defer func() { os.Args = oldArgs }()
|
||||||
|
|
||||||
|
old := os.Stdout
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stdout = w
|
||||||
|
|
||||||
|
exitCode := cmdVerify(cfg)
|
||||||
|
|
||||||
|
w.Close()
|
||||||
|
os.Stdout = old
|
||||||
|
|
||||||
|
var buf [4096]byte
|
||||||
|
n, _ := r.Read(buf[:])
|
||||||
|
output := string(buf[:n])
|
||||||
|
|
||||||
|
if exitCode != 0 {
|
||||||
|
t.Errorf("expected exit code 0, got %d\noutput: %s", exitCode, output)
|
||||||
|
}
|
||||||
|
if !strings.Contains(output, "All checks passed") {
|
||||||
|
t.Errorf("expected 'All checks passed', got:\n%s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmdHistory_ShowsReports(t *testing.T) {
|
||||||
|
_, cfg := setupTestTasks(t)
|
||||||
|
|
||||||
|
// Create a fake report
|
||||||
|
reportsDir := filepath.Join(cfg.TasksDir, "reports")
|
||||||
|
os.WriteFile(filepath.Join(reportsDir, "T01-report.md"), []byte("# T01 report\n"), 0644)
|
||||||
|
|
||||||
|
old := os.Stdout
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stdout = w
|
||||||
|
|
||||||
|
exitCode := cmdHistory(cfg)
|
||||||
|
|
||||||
|
w.Close()
|
||||||
|
os.Stdout = old
|
||||||
|
|
||||||
|
var buf [4096]byte
|
||||||
|
n, _ := r.Read(buf[:])
|
||||||
|
output := string(buf[:n])
|
||||||
|
|
||||||
|
if exitCode != 0 {
|
||||||
|
t.Errorf("expected exit code 0, got %d", exitCode)
|
||||||
|
}
|
||||||
|
if !strings.Contains(output, "T01") {
|
||||||
|
t.Errorf("expected T01 in history, got:\n%s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnknownCommand(t *testing.T) {
|
||||||
|
// Test printHelp doesn't panic
|
||||||
|
old := os.Stdout
|
||||||
|
_, w, _ := os.Pipe()
|
||||||
|
os.Stdout = w
|
||||||
|
printHelp()
|
||||||
|
w.Close()
|
||||||
|
os.Stdout = old
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmdRun_NoReadyTasks(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
tasksDir := filepath.Join(dir, "TASKS")
|
||||||
|
for _, folder := range []string{"backlog", "ready", "active", "review", "done", "reports"} {
|
||||||
|
os.MkdirAll(filepath.Join(tasksDir, folder), 0755)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
TasksDir: tasksDir,
|
||||||
|
ProjectPath: dir,
|
||||||
|
}
|
||||||
|
|
||||||
|
old := os.Stdout
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stdout = w
|
||||||
|
|
||||||
|
// Save and restore os.Args
|
||||||
|
oldArgs := os.Args
|
||||||
|
os.Args = []string{"kaos-supervisor", "run"}
|
||||||
|
defer func() { os.Args = oldArgs }()
|
||||||
|
|
||||||
|
exitCode := cmdRun(cfg)
|
||||||
|
|
||||||
|
w.Close()
|
||||||
|
os.Stdout = old
|
||||||
|
|
||||||
|
var buf [4096]byte
|
||||||
|
n, _ := r.Read(buf[:])
|
||||||
|
output := string(buf[:n])
|
||||||
|
|
||||||
|
if exitCode != 0 {
|
||||||
|
t.Errorf("expected exit code 0, got %d", exitCode)
|
||||||
|
}
|
||||||
|
if !strings.Contains(output, "Nema taskova") {
|
||||||
|
t.Errorf("expected 'Nema taskova', got:\n%s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatusIcon(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
status string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"done", "✅"},
|
||||||
|
{"active", "🔄"},
|
||||||
|
{"ready", "📋"},
|
||||||
|
{"review", "👀"},
|
||||||
|
{"backlog", "📦"},
|
||||||
|
{"unknown", "❓"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
got := statusIcon(tt.status)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("statusIcon(%q) = %q, want %q", tt.status, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that ScanTasks and NextTask integration works
|
||||||
|
func TestNextTask_Integration(t *testing.T) {
|
||||||
|
_, cfg := setupTestTasks(t)
|
||||||
|
|
||||||
|
tasks, err := supervisor.ScanTasks(cfg.TasksDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ScanTasks error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
next := supervisor.NextTask(tasks)
|
||||||
|
if next == nil {
|
||||||
|
t.Fatal("expected a next task")
|
||||||
|
}
|
||||||
|
if next.ID != "T02" {
|
||||||
|
t.Errorf("expected T02, got %s", next.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -16,6 +16,8 @@ type Config struct {
|
|||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
// ProjectPath is the root path of the project being supervised.
|
// ProjectPath is the root path of the project being supervised.
|
||||||
ProjectPath string
|
ProjectPath string
|
||||||
|
// TasksDir is the path to the TASKS directory.
|
||||||
|
TasksDir string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load reads configuration from environment variables.
|
// Load reads configuration from environment variables.
|
||||||
@ -39,9 +41,15 @@ func Load() (*Config, error) {
|
|||||||
return nil, fmt.Errorf("KAOS_PROJECT_PATH environment variable is required")
|
return nil, fmt.Errorf("KAOS_PROJECT_PATH environment variable is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasksDir := os.Getenv("KAOS_TASKS_DIR")
|
||||||
|
if tasksDir == "" {
|
||||||
|
return nil, fmt.Errorf("KAOS_TASKS_DIR environment variable is required")
|
||||||
|
}
|
||||||
|
|
||||||
return &Config{
|
return &Config{
|
||||||
Timeout: timeout,
|
Timeout: timeout,
|
||||||
ProjectPath: projectPath,
|
ProjectPath: projectPath,
|
||||||
|
TasksDir: tasksDir,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import (
|
|||||||
func TestLoad_WithValidEnv(t *testing.T) {
|
func TestLoad_WithValidEnv(t *testing.T) {
|
||||||
t.Setenv("KAOS_TIMEOUT", "30m")
|
t.Setenv("KAOS_TIMEOUT", "30m")
|
||||||
t.Setenv("KAOS_PROJECT_PATH", "/tmp/test")
|
t.Setenv("KAOS_PROJECT_PATH", "/tmp/test")
|
||||||
|
t.Setenv("KAOS_TASKS_DIR", "/tmp/tasks")
|
||||||
|
|
||||||
cfg, err := Load()
|
cfg, err := Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -25,11 +26,16 @@ func TestLoad_WithValidEnv(t *testing.T) {
|
|||||||
if cfg.ProjectPath != "/tmp/test" {
|
if cfg.ProjectPath != "/tmp/test" {
|
||||||
t.Errorf("expected project path /tmp/test, got %s", cfg.ProjectPath)
|
t.Errorf("expected project path /tmp/test, got %s", cfg.ProjectPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.TasksDir != "/tmp/tasks" {
|
||||||
|
t.Errorf("expected tasks dir /tmp/tasks, got %s", cfg.TasksDir)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoad_MissingTimeout(t *testing.T) {
|
func TestLoad_MissingTimeout(t *testing.T) {
|
||||||
t.Setenv("KAOS_TIMEOUT", "")
|
t.Setenv("KAOS_TIMEOUT", "")
|
||||||
t.Setenv("KAOS_PROJECT_PATH", "/tmp/test")
|
t.Setenv("KAOS_PROJECT_PATH", "/tmp/test")
|
||||||
|
t.Setenv("KAOS_TASKS_DIR", "/tmp/tasks")
|
||||||
|
|
||||||
_, err := Load()
|
_, err := Load()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -40,6 +46,7 @@ func TestLoad_MissingTimeout(t *testing.T) {
|
|||||||
func TestLoad_InvalidTimeout(t *testing.T) {
|
func TestLoad_InvalidTimeout(t *testing.T) {
|
||||||
t.Setenv("KAOS_TIMEOUT", "not-a-duration")
|
t.Setenv("KAOS_TIMEOUT", "not-a-duration")
|
||||||
t.Setenv("KAOS_PROJECT_PATH", "/tmp/test")
|
t.Setenv("KAOS_PROJECT_PATH", "/tmp/test")
|
||||||
|
t.Setenv("KAOS_TASKS_DIR", "/tmp/tasks")
|
||||||
|
|
||||||
_, err := Load()
|
_, err := Load()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -50,6 +57,7 @@ func TestLoad_InvalidTimeout(t *testing.T) {
|
|||||||
func TestLoad_MissingProjectPath(t *testing.T) {
|
func TestLoad_MissingProjectPath(t *testing.T) {
|
||||||
t.Setenv("KAOS_TIMEOUT", "30m")
|
t.Setenv("KAOS_TIMEOUT", "30m")
|
||||||
t.Setenv("KAOS_PROJECT_PATH", "")
|
t.Setenv("KAOS_PROJECT_PATH", "")
|
||||||
|
t.Setenv("KAOS_TASKS_DIR", "/tmp/tasks")
|
||||||
|
|
||||||
_, err := Load()
|
_, err := Load()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -57,6 +65,17 @@ func TestLoad_MissingProjectPath(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoad_MissingTasksDir(t *testing.T) {
|
||||||
|
t.Setenv("KAOS_TIMEOUT", "30m")
|
||||||
|
t.Setenv("KAOS_PROJECT_PATH", "/tmp/test")
|
||||||
|
t.Setenv("KAOS_TASKS_DIR", "")
|
||||||
|
|
||||||
|
_, err := Load()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for missing KAOS_TASKS_DIR")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLoadEnvFile(t *testing.T) {
|
func TestLoadEnvFile(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
envPath := filepath.Join(dir, ".env")
|
envPath := filepath.Join(dir, ".env")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user