package server import ( "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "github.com/dal/kaos/internal/config" ) const testTask1 = `# T01: Prvi task **Agent:** coder **Model:** Sonnet **Zavisi od:** — --- ## Opis Opis prvog taska. --- ` const testTask2 = `# T08: HTTP server **Agent:** coder **Model:** Sonnet **Zavisi od:** T07 --- ## Opis Implementacija HTTP servera. --- ` func setupTestServer(t *testing.T) *Server { 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) } os.WriteFile(filepath.Join(tasksDir, "done", "T01.md"), []byte(testTask1), 0644) os.WriteFile(filepath.Join(tasksDir, "backlog", "T08.md"), []byte(testTask2), 0644) // Docs: create markdown files in project root os.WriteFile(filepath.Join(dir, "CLAUDE.md"), []byte("# CLAUDE.md\n\nGlavni fajl.\n\n| Kolona | Opis |\n|--------|------|\n| A | B |\n"), 0644) os.WriteFile(filepath.Join(dir, "README.md"), []byte("# README\n\nOpis projekta.\n"), 0644) os.MkdirAll(filepath.Join(dir, "agents", "coder"), 0755) os.WriteFile(filepath.Join(dir, "agents", "coder", "CLAUDE.md"), []byte("# Coder Agent\n\nPravila kodiranja.\n"), 0644) os.MkdirAll(filepath.Join(dir, "agents", "checker"), 0755) os.WriteFile(filepath.Join(dir, "agents", "checker", "CLAUDE.md"), []byte("# Checker Agent\n\nBuild + Test verifikacija.\n"), 0644) os.WriteFile(filepath.Join(tasksDir, "reports", "T01-report.md"), []byte("# T01 Report\n\n10 testova, svi prolaze.\n"), 0644) cfg := &config.Config{ TasksDir: tasksDir, ProjectPath: dir, Port: "0", } return New(cfg) } func TestAPIGetTasks(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/api/tasks", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } var tasks []taskResponse if err := json.Unmarshal(w.Body.Bytes(), &tasks); err != nil { t.Fatalf("invalid JSON: %v", err) } if len(tasks) != 2 { t.Fatalf("expected 2 tasks, got %d", len(tasks)) } // Check that tasks have correct statuses statuses := map[string]string{} for _, task := range tasks { statuses[task.ID] = task.Status } if statuses["T01"] != "done" { t.Errorf("expected T01 status done, got %s", statuses["T01"]) } if statuses["T08"] != "backlog" { t.Errorf("expected T08 status backlog, got %s", statuses["T08"]) } } func TestAPIGetTask(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/api/task/T01", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } var detail taskDetailResponse if err := json.Unmarshal(w.Body.Bytes(), &detail); err != nil { t.Fatalf("invalid JSON: %v", err) } if detail.ID != "T01" { t.Errorf("expected T01, got %s", detail.ID) } if detail.Content == "" { t.Error("expected non-empty content") } } func TestAPIGetTask_NotFound(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/api/task/T99", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Fatalf("expected 404, got %d", w.Code) } } func TestAPIMoveTask(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodPost, "/api/task/T08/move?to=ready", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } // Verify file was moved if _, err := os.Stat(filepath.Join(srv.Config.TasksDir, "ready", "T08.md")); err != nil { t.Error("expected T08.md in ready/") } if _, err := os.Stat(filepath.Join(srv.Config.TasksDir, "backlog", "T08.md")); !os.IsNotExist(err) { t.Error("expected T08.md removed from backlog/") } } func TestAPIMoveTask_NotFound(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodPost, "/api/task/T99/move?to=ready", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Fatalf("expected 404, got %d", w.Code) } } func TestAPIMoveTask_InvalidFolder(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodPost, "/api/task/T08/move?to=invalid", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d", w.Code) } } func TestDashboardHTML(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } body := w.Body.String() if !containsStr(body, "KAOS Dashboard") { t.Error("expected 'KAOS Dashboard' in HTML") } if !containsStr(body, "T01") { t.Error("expected T01 in HTML") } if !containsStr(body, "T08") { t.Error("expected T08 in HTML") } if !containsStr(body, "BACKLOG") { t.Error("expected BACKLOG column in HTML") } if !containsStr(body, "DONE") { t.Error("expected DONE column in HTML") } } func TestTaskDetailHTML(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/task/T01", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } body := w.Body.String() if !containsStr(body, "T01") { t.Error("expected T01 in detail HTML") } if !containsStr(body, "Prvi task") { t.Error("expected task title in detail HTML") } } func TestTaskDetailHTML_NotFound(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/task/T99", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Fatalf("expected 404, got %d", w.Code) } } func TestHTMLMoveTask(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodPost, "/task/T08/move?to=ready", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } // Should return updated dashboard HTML body := w.Body.String() if !containsStr(body, "KAOS Dashboard") { t.Error("expected dashboard HTML after move") } } func TestDashboardHTML_HasAllColumns(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) body := w.Body.String() for _, col := range []string{"BACKLOG", "READY", "ACTIVE", "REVIEW", "DONE"} { if !containsStr(body, col) { t.Errorf("expected %s column in dashboard", col) } } } func TestDashboardHTML_HasHTMXAttributes(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) body := w.Body.String() if !containsStr(body, "hx-get") { t.Error("expected hx-get attributes in HTML") } if !containsStr(body, "hx-target") { t.Error("expected hx-target attributes in HTML") } } func TestDashboardHTML_TasksInCorrectColumns(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) body := w.Body.String() // T01 should be in done column, T08 in backlog if !containsStr(body, `id="col-done"`) { t.Error("expected col-done in HTML") } if !containsStr(body, `id="col-backlog"`) { t.Error("expected col-backlog in HTML") } } func TestReport_Exists(t *testing.T) { srv := setupTestServer(t) // Create a report reportsDir := filepath.Join(srv.Config.TasksDir, "reports") os.MkdirAll(reportsDir, 0755) os.WriteFile(filepath.Join(reportsDir, "T01-report.md"), []byte("# T01 Report\nSve ok."), 0644) req := httptest.NewRequest(http.MethodGet, "/report/T01", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } if !containsStr(w.Body.String(), "T01 Report") { t.Error("expected report content") } } func TestReport_NotFound(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/report/T99", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Fatalf("expected 404, got %d", w.Code) } } func TestTaskDetail_HasMoveButtons(t *testing.T) { srv := setupTestServer(t) // T08 is in backlog, should have "Premesti u Ready" button req := httptest.NewRequest(http.MethodGet, "/task/T08", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } body := w.Body.String() if !containsStr(body, "Ready") { t.Error("expected 'Ready' move button for backlog task") } } func TestAPIMoveTask_ForbiddenToActive(t *testing.T) { srv := setupTestServer(t) // Put T08 in ready first os.Rename( filepath.Join(srv.Config.TasksDir, "backlog", "T08.md"), filepath.Join(srv.Config.TasksDir, "ready", "T08.md"), ) // Try to move ready → active (agent-only) req := httptest.NewRequest(http.MethodPost, "/api/task/T08/move?to=active", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusForbidden { t.Fatalf("expected 403 for ready→active, got %d: %s", w.Code, w.Body.String()) } } func TestAPIMoveTask_ForbiddenActiveToReview(t *testing.T) { srv := setupTestServer(t) // Put T08 in active os.Rename( filepath.Join(srv.Config.TasksDir, "backlog", "T08.md"), filepath.Join(srv.Config.TasksDir, "active", "T08.md"), ) // Try to move active → review (agent-only) req := httptest.NewRequest(http.MethodPost, "/api/task/T08/move?to=review", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusForbidden { t.Fatalf("expected 403 for active→review, got %d: %s", w.Code, w.Body.String()) } } func TestAPIMoveTask_AllowedBacklogToReady(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodPost, "/api/task/T08/move?to=ready", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200 for backlog→ready, got %d: %s", w.Code, w.Body.String()) } } func TestAPIMoveTask_AllowedReviewToDone(t *testing.T) { srv := setupTestServer(t) // Put T08 in review os.Rename( filepath.Join(srv.Config.TasksDir, "backlog", "T08.md"), filepath.Join(srv.Config.TasksDir, "review", "T08.md"), ) req := httptest.NewRequest(http.MethodPost, "/api/task/T08/move?to=done", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200 for review→done, got %d: %s", w.Code, w.Body.String()) } } func TestDashboardHTML_HasSortableScript(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) body := w.Body.String() if !containsStr(body, "sortable.min.js") { t.Error("expected sortable.min.js script tag") } if !containsStr(body, "initSortable") { t.Error("expected initSortable function") } } func TestDashboardHTML_HasDataFolderAttributes(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) body := w.Body.String() if !containsStr(body, `data-folder="backlog"`) { t.Error("expected data-folder attribute on column-tasks") } if !containsStr(body, `data-folder="ready"`) { t.Error("expected data-folder=ready attribute") } } func TestIsMoveAllowed(t *testing.T) { tests := []struct { from, to string allowed bool }{ {"backlog", "ready", true}, {"ready", "backlog", true}, {"review", "done", true}, {"review", "ready", true}, {"done", "review", true}, {"ready", "active", false}, {"active", "review", false}, {"backlog", "done", false}, {"backlog", "active", false}, {"done", "backlog", false}, {"ready", "ready", false}, } for _, tt := range tests { got := isMoveAllowed(tt.from, tt.to) if got != tt.allowed { t.Errorf("isMoveAllowed(%s, %s) = %v, want %v", tt.from, tt.to, got, tt.allowed) } } } func TestNoCacheHeaders(t *testing.T) { srv := setupTestServer(t) // Dynamic route should have no-cache headers req := httptest.NewRequest(http.MethodGet, "/", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) cc := w.Header().Get("Cache-Control") if !containsStr(cc, "no-store") { t.Errorf("expected Cache-Control no-store on dashboard, got %q", cc) } // API route should also have no-cache headers req2 := httptest.NewRequest(http.MethodGet, "/api/tasks", nil) w2 := httptest.NewRecorder() srv.Router.ServeHTTP(w2, req2) cc2 := w2.Header().Get("Cache-Control") if !containsStr(cc2, "no-store") { t.Errorf("expected Cache-Control no-store on API, got %q", cc2) } } func TestDashboardReflectsDiskChanges(t *testing.T) { srv := setupTestServer(t) // Initial state: T08 in backlog req := httptest.NewRequest(http.MethodGet, "/api/tasks", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) var tasks1 []taskResponse json.Unmarshal(w.Body.Bytes(), &tasks1) found := false for _, task := range tasks1 { if task.ID == "T08" && task.Status == "backlog" { found = true } } if !found { t.Fatal("expected T08 in backlog initially") } // Move file on disk (simulating external change) os.Rename( filepath.Join(srv.Config.TasksDir, "backlog", "T08.md"), filepath.Join(srv.Config.TasksDir, "ready", "T08.md"), ) // Second request should reflect the change without server restart req2 := httptest.NewRequest(http.MethodGet, "/api/tasks", nil) w2 := httptest.NewRecorder() srv.Router.ServeHTTP(w2, req2) var tasks2 []taskResponse json.Unmarshal(w2.Body.Bytes(), &tasks2) found = false for _, task := range tasks2 { if task.ID == "T08" && task.Status == "ready" { found = true } } if !found { t.Fatal("expected T08 in ready after disk move — server did not read fresh state") } } func TestDocsList(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/docs", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } body := w.Body.String() if !containsStr(body, "CLAUDE.md") { t.Error("expected CLAUDE.md in docs list") } if !containsStr(body, "README.md") { t.Error("expected README.md in docs list") } if !containsStr(body, "agents/coder/CLAUDE.md") { t.Error("expected agents/coder/CLAUDE.md in docs list") } } func TestDocsView_CLAUDE(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/docs/CLAUDE.md", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } body := w.Body.String() if !containsStr(body, "Glavni fajl") { t.Error("expected rendered markdown content") } // Should have table rendered as HTML if !containsStr(body, "") || !containsStr(body, "
") { t.Error("expected HTML table from markdown") } } func TestDocsView_NestedFile(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/docs/agents/coder/CLAUDE.md", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } body := w.Body.String() if !containsStr(body, "Coder Agent") { t.Error("expected nested file content") } // Breadcrumbs if !containsStr(body, "agents") { t.Error("expected breadcrumb for agents") } } func TestDocsView_PathTraversal(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/docs/../../etc/passwd", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusForbidden { t.Fatalf("expected 403 for path traversal, got %d", w.Code) } } func TestDocsView_NonMarkdown(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/docs/main.go", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusForbidden { t.Fatalf("expected 403 for non-.md file, got %d", w.Code) } } func TestDocsView_NotFound(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/docs/nonexistent.md", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Fatalf("expected 404, got %d", w.Code) } } func TestDocsView_HasBreadcrumbs(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/docs/agents/coder/CLAUDE.md", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) body := w.Body.String() if !containsStr(body, "Dokumenti") { t.Error("expected 'Dokumenti' in breadcrumbs") } if !containsStr(body, "coder") { t.Error("expected 'coder' in breadcrumbs") } } func TestSearch_FindsTask(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/search?q=Prvi", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } body := w.Body.String() if !containsStr(body, "T01") { t.Error("expected T01 in search results for 'Prvi'") } } func TestSearch_FindsTaskByID(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/search?q=T08", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) body := w.Body.String() if !containsStr(body, "T08") { t.Error("expected T08 in search results") } } func TestSearch_FindsDocument(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/search?q=checker", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) body := w.Body.String() if !containsStr(body, "checker/CLAUDE.md") { t.Error("expected checker CLAUDE.md in search results") } } func TestSearch_FindsReport(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/search?q=prolaze", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) body := w.Body.String() if !containsStr(body, "T01-report.md") { t.Error("expected T01 report in search results") } } func TestSearch_EmptyQuery(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/search?q=", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } if w.Body.Len() != 0 { t.Errorf("expected empty response for empty query, got %d bytes", w.Body.Len()) } } func TestSearch_NoResults(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/search?q=xyznepostoji", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) body := w.Body.String() if !containsStr(body, "Nema rezultata") { t.Error("expected 'Nema rezultata' message") } } func TestSearch_CaseInsensitive(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/search?q=CODER", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) body := w.Body.String() if !containsStr(body, "coder") { t.Error("expected case-insensitive match for 'CODER'") } } func TestSearch_HasSnippet(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/search?q=kodiranja", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) body := w.Body.String() if !containsStr(body, "kodiranja") { t.Error("expected snippet with 'kodiranja' text") } } func TestConsolePage(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/console", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } body := w.Body.String() if !containsStr(body, "Sesija 1") { t.Error("expected 'Sesija 1' in console page") } if !containsStr(body, "Sesija 2") { t.Error("expected 'Sesija 2' in console page") } } func TestConsoleSessions(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/console/sessions", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } var statuses []sessionStatus if err := json.Unmarshal(w.Body.Bytes(), &statuses); err != nil { t.Fatalf("invalid JSON: %v", err) } if len(statuses) != 2 { t.Fatalf("expected 2 sessions, got %d", len(statuses)) } if statuses[0].Status != "idle" || statuses[1].Status != "idle" { t.Error("expected both sessions idle") } } func TestConsoleExec_InvalidSession(t *testing.T) { srv := setupTestServer(t) body := `{"cmd":"status","session":3}` req := httptest.NewRequest(http.MethodPost, "/console/exec", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("expected 400 for invalid session, got %d", w.Code) } } func TestConsoleExec_ValidRequest(t *testing.T) { srv := setupTestServer(t) body := `{"cmd":"echo test","session":1}` req := httptest.NewRequest(http.MethodPost, "/console/exec", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } var resp execResponse if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("invalid JSON: %v", err) } if resp.ExecID == "" { t.Error("expected non-empty exec ID") } if resp.Session != 1 { t.Errorf("expected session 1, got %d", resp.Session) } } func TestConsoleKill_IdleSession(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodPost, "/console/kill/1", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } } func TestConsoleHistory_Empty(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/console/history/1", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } var history []historyEntry if err := json.Unmarshal(w.Body.Bytes(), &history); err != nil { t.Fatalf("invalid JSON: %v", err) } if len(history) != 0 { t.Errorf("expected empty history, got %d entries", len(history)) } } func TestConsoleHistory_AfterExec(t *testing.T) { srv := setupTestServer(t) // Execute a command first body := `{"cmd":"test command","session":2}` req := httptest.NewRequest(http.MethodPost, "/console/exec", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) // Check history req2 := httptest.NewRequest(http.MethodGet, "/console/history/2", nil) w2 := httptest.NewRecorder() srv.Router.ServeHTTP(w2, req2) var history []historyEntry json.Unmarshal(w2.Body.Bytes(), &history) if len(history) != 1 { t.Fatalf("expected 1 history entry, got %d", len(history)) } if history[0].Command != "test command" { t.Errorf("expected 'test command', got %s", history[0].Command) } } func TestRewriteLinksSimple(t *testing.T) { input := `link and ext` result := rewriteLinksSimple(input, ".") if !containsStr(result, `/docs/README.md`) { t.Errorf("expected rewritten link, got: %s", result) } if !containsStr(result, `https://example.com`) { t.Error("external link should not be rewritten") } } func TestRewriteLinksSimple_NestedDir(t *testing.T) { input := `link` result := rewriteLinksSimple(input, "agents/coder") if !containsStr(result, `/docs/agents/coder/CLAUDE.md`) { t.Errorf("expected nested rewritten link, got: %s", result) } } func containsStr(s, substr string) bool { return len(s) >= len(substr) && findStr(s, substr) } func findStr(s, substr string) bool { for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return true } } return false }