mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 03:20:11 +00:00
- Add persistent volume mounts for Go/npm caches (faster rebuilds) - Add shell config with helpful aliases and custom prompt - Add comprehensive devcontainer documentation - Add pre-commit hooks for Go formatting and linting - Use go-version-file in CI workflows instead of hardcoded versions - Simplify docker compose commands with --wait flag - Add gitignore entries for devcontainer auth files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1083 lines
31 KiB
Go
1083 lines
31 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
)
|
|
|
|
func TestAISettingsHandler_GetAndUpdateSettings_RoundTrip(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
// GET should return defaults if no config has been saved yet.
|
|
{
|
|
req := httptest.NewRequest(http.MethodGet, "/api/settings/ai", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleGetAISettings(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("GET status = %d, body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var resp AISettingsResponse
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if resp.Enabled {
|
|
t.Fatalf("expected default Enabled=false, got %+v", resp)
|
|
}
|
|
}
|
|
|
|
// Update settings to enable AI via Ollama.
|
|
{
|
|
body, _ := json.Marshal(AISettingsUpdateRequest{
|
|
Enabled: ptr(true),
|
|
Provider: ptr("ollama"),
|
|
Model: ptr("ollama:llama3"),
|
|
OllamaBaseURL: ptr("http://localhost:11434"),
|
|
})
|
|
req := httptest.NewRequest(http.MethodPut, "/api/settings/ai", bytes.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleUpdateAISettings(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("PUT status = %d, body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var resp AISettingsResponse
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if !resp.Enabled || !resp.OllamaConfigured {
|
|
t.Fatalf("expected enabled + ollama configured, got %+v", resp)
|
|
}
|
|
if resp.OllamaBaseURL != "http://localhost:11434" {
|
|
t.Fatalf("unexpected ollama base url: %+v", resp)
|
|
}
|
|
}
|
|
|
|
// GET again should reflect persisted updates.
|
|
{
|
|
req := httptest.NewRequest(http.MethodGet, "/api/settings/ai", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleGetAISettings(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("GET status = %d, body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var resp AISettingsResponse
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if !resp.Enabled || !resp.OllamaConfigured {
|
|
t.Fatalf("expected enabled + ollama configured, got %+v", resp)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAISettingsHandler_ListModels_Ollama(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ollama := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/api/tags":
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"models": []map[string]any{
|
|
{"name": "llama3:latest"},
|
|
{"name": "tinyllama:latest"},
|
|
},
|
|
})
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer ollama.Close()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
|
|
aiCfg := config.NewDefaultAIConfig()
|
|
aiCfg.Enabled = true
|
|
aiCfg.Model = "ollama:llama3"
|
|
aiCfg.OllamaBaseURL = ollama.URL
|
|
if err := persistence.SaveAIConfig(*aiCfg); err != nil {
|
|
t.Fatalf("SaveAIConfig: %v", err)
|
|
}
|
|
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/models", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleListModels(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var resp struct {
|
|
Models []struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
} `json:"models"`
|
|
Error string `json:"error"`
|
|
}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if resp.Error != "" {
|
|
t.Fatalf("unexpected error: %s", resp.Error)
|
|
}
|
|
if len(resp.Models) != 2 {
|
|
t.Fatalf("expected 2 models, got %+v", resp.Models)
|
|
}
|
|
}
|
|
|
|
func TestAISettingsHandler_Execute_Ollama(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ollama := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/api/chat":
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"model": "llama3",
|
|
"created_at": time.Now().Format(time.RFC3339),
|
|
"message": map[string]any{
|
|
"role": "assistant",
|
|
"content": "hello from ollama",
|
|
},
|
|
"done": true,
|
|
"done_reason": "stop",
|
|
"prompt_eval_count": 3,
|
|
"eval_count": 5,
|
|
})
|
|
case "/api/version":
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"version": "0.1.0"})
|
|
case "/api/tags":
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"models": []map[string]any{{"name": "llama3"}}})
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer ollama.Close()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
|
|
aiCfg := config.NewDefaultAIConfig()
|
|
aiCfg.Enabled = true
|
|
aiCfg.Model = "ollama:llama3"
|
|
aiCfg.OllamaBaseURL = ollama.URL
|
|
if err := persistence.SaveAIConfig(*aiCfg); err != nil {
|
|
t.Fatalf("SaveAIConfig: %v", err)
|
|
}
|
|
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
body, _ := json.Marshal(AIExecuteRequest{Prompt: "hi"})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/execute", bytes.NewReader(body))
|
|
req = req.WithContext(context.Background())
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleExecute(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var resp AIExecuteResponse
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if resp.Content != "hello from ollama" || resp.Model == "" {
|
|
t.Fatalf("unexpected response: %+v", resp)
|
|
}
|
|
}
|
|
|
|
func ptr[T any](v T) *T { return &v }
|
|
|
|
func TestAISettingsHandler_TestConnection_Ollama(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ollama := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/version" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"version": "0.1.0"})
|
|
}))
|
|
defer ollama.Close()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
|
|
aiCfg := config.NewDefaultAIConfig()
|
|
aiCfg.Enabled = true
|
|
aiCfg.Model = "ollama:llama3"
|
|
aiCfg.OllamaBaseURL = ollama.URL
|
|
if err := persistence.SaveAIConfig(*aiCfg); err != nil {
|
|
t.Fatalf("SaveAIConfig: %v", err)
|
|
}
|
|
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/test", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleTestAIConnection(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var resp struct {
|
|
Success bool `json:"success"`
|
|
Message string `json:"message"`
|
|
}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if !resp.Success {
|
|
t.Fatalf("expected success, got %+v", resp)
|
|
}
|
|
}
|
|
|
|
func TestAISettingsHandler_TestProvider_Ollama(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ollama := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/version" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"version": "0.1.0"})
|
|
}))
|
|
defer ollama.Close()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
|
|
aiCfg := config.NewDefaultAIConfig()
|
|
aiCfg.Enabled = true
|
|
aiCfg.Model = "ollama:llama3"
|
|
aiCfg.OllamaBaseURL = ollama.URL
|
|
if err := persistence.SaveAIConfig(*aiCfg); err != nil {
|
|
t.Fatalf("SaveAIConfig: %v", err)
|
|
}
|
|
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/test/ollama", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleTestProvider(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var resp struct {
|
|
Success bool `json:"success"`
|
|
Provider string `json:"provider"`
|
|
}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if !resp.Success || resp.Provider != "ollama" {
|
|
t.Fatalf("unexpected response: %+v", resp)
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// HandleGetAICostSummary tests
|
|
// ========================================
|
|
|
|
func TestHandleGetAICostSummary_MethodNotAllowed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/cost/summary", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleGetAICostSummary(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleGetAICostSummary_NoAIService(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/cost/summary", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleGetAICostSummary(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
|
|
}
|
|
|
|
var resp struct {
|
|
Days int `json:"days"`
|
|
PricingAsOf string `json:"pricing_as_of"`
|
|
}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if resp.Days != 30 {
|
|
t.Fatalf("expected Days=30 default, got %d", resp.Days)
|
|
}
|
|
}
|
|
|
|
func TestHandleGetAICostSummary_CustomDays(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/cost/summary?days=7", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleGetAICostSummary(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
|
|
}
|
|
|
|
var resp struct {
|
|
Days int `json:"days"`
|
|
}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if resp.Days != 7 {
|
|
t.Fatalf("expected Days=7, got %d", resp.Days)
|
|
}
|
|
}
|
|
|
|
func TestHandleGetAICostSummary_MaxDays(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
// Test that days > 365 is capped at 365
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/cost/summary?days=1000", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleGetAICostSummary(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
|
|
}
|
|
|
|
var resp struct {
|
|
Days int `json:"days"`
|
|
}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if resp.Days != 365 {
|
|
t.Fatalf("expected Days=365 (capped), got %d", resp.Days)
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// HandleResetAICostHistory tests
|
|
// ========================================
|
|
|
|
func TestHandleResetAICostHistory_MethodNotAllowed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/cost/reset", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleResetAICostHistory(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleResetAICostHistory_Success(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/cost/reset", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleResetAICostHistory(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
|
|
}
|
|
|
|
var resp struct {
|
|
Ok bool `json:"ok"`
|
|
}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if !resp.Ok {
|
|
t.Fatalf("expected ok=true")
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// HandleExportAICostHistory tests
|
|
// ========================================
|
|
|
|
func TestHandleExportAICostHistory_MethodNotAllowed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/cost/export", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleExportAICostHistory(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// HandleGetSuppressionRules tests
|
|
// ========================================
|
|
|
|
func TestHandleGetSuppressionRules_MethodNotAllowed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/patrol/suppressions", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleGetSuppressionRules(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// HandleAddSuppressionRule tests
|
|
// ========================================
|
|
|
|
func TestHandleAddSuppressionRule_MethodNotAllowed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/patrol/suppressions", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleAddSuppressionRule(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// HandleDeleteSuppressionRule tests
|
|
// ========================================
|
|
|
|
func TestHandleDeleteSuppressionRule_MethodNotAllowed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/patrol/suppressions/rule-123", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleDeleteSuppressionRule(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// HandleGetDismissedFindings tests
|
|
// ========================================
|
|
|
|
func TestHandleGetDismissedFindings_MethodNotAllowed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/patrol/dismissed", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleGetDismissedFindings(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// HandleGetGuestKnowledge tests
|
|
// ========================================
|
|
|
|
func TestHandleGetGuestKnowledge_MissingGuestID(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/knowledge", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleGetGuestKnowledge(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rec.Code)
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// HandleSaveGuestNote tests
|
|
// ========================================
|
|
|
|
func TestHandleSaveGuestNote_InvalidBody(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/knowledge", bytes.NewReader([]byte(`{invalid json}`)))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleSaveGuestNote(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleSaveGuestNote_MissingFields(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
body := []byte(`{"guest_id": "vm-100"}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/knowledge", bytes.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleSaveGuestNote(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rec.Code)
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// HandleDeleteGuestNote tests
|
|
// ========================================
|
|
|
|
func TestHandleDeleteGuestNote_InvalidBody(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/knowledge/delete", bytes.NewReader([]byte(`{invalid json}`)))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleDeleteGuestNote(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleDeleteGuestNote_MissingFields(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
body := []byte(`{"guest_id": "vm-100"}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/knowledge/delete", bytes.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleDeleteGuestNote(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rec.Code)
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// HandleClearGuestKnowledge tests
|
|
// ========================================
|
|
|
|
func TestHandleClearGuestKnowledge_MethodNotAllowed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/knowledge/clear", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleClearGuestKnowledge(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleClearGuestKnowledge_MissingGuestID(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
body := []byte(`{}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/knowledge/clear", bytes.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleClearGuestKnowledge(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rec.Code)
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// HandleDebugContext tests
|
|
// ========================================
|
|
|
|
func TestHandleDebugContext_MethodNotAllowed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/debug/context", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleDebugContext(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// HandleGetConnectedAgents tests
|
|
// ========================================
|
|
|
|
func TestHandleGetConnectedAgents_MethodNotAllowed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/agents", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleGetConnectedAgents(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleGetConnectedAgents_NoAgentServer(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
// handler created with nil agentServer
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/agents", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleGetConnectedAgents(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
|
|
}
|
|
|
|
var resp struct {
|
|
Count int `json:"count"`
|
|
Note string `json:"note"`
|
|
}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if resp.Count != 0 {
|
|
t.Fatalf("expected count=0, got %d", resp.Count)
|
|
}
|
|
if resp.Note == "" {
|
|
t.Fatalf("expected note to be present")
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// HandleRunCommand tests
|
|
// ========================================
|
|
|
|
func TestHandleRunCommand_MethodNotAllowed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/run-command", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleRunCommand(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleRunCommand_InvalidBody(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/run-command", bytes.NewReader([]byte(`{invalid json}`)))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleRunCommand(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rec.Code)
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// HandleAnalyzeKubernetesCluster tests
|
|
// ========================================
|
|
|
|
func TestHandleAnalyzeKubernetesCluster_MethodNotAllowed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/kubernetes/analyze", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleAnalyzeKubernetesCluster(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleAnalyzeKubernetesCluster_InvalidBody(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/kubernetes/analyze", bytes.NewReader([]byte(`{invalid json}`)))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleAnalyzeKubernetesCluster(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rec.Code)
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// HandleInvestigateAlert tests
|
|
// ========================================
|
|
|
|
func TestHandleInvestigateAlert_MethodNotAllowed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/investigate", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleInvestigateAlert(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleInvestigateAlert_InvalidBody(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/investigate", bytes.NewReader([]byte(`{invalid json}`)))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleInvestigateAlert(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleInvestigateAlert_MissingAlertID(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
body := []byte(`{}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/investigate", bytes.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleInvestigateAlert(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rec.Code)
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// AISettingsHandler setter method tests
|
|
// ========================================
|
|
|
|
func TestAISettingsHandler_SetConfig(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
// SetConfig with nil should be a no-op
|
|
handler.SetConfig(nil)
|
|
|
|
// SetConfig with new config should update the handler's config
|
|
newCfg := &config.Config{DataPath: tmp, BackendPort: 9999}
|
|
handler.SetConfig(newCfg)
|
|
// No assertion needed - just verifying it doesn't panic
|
|
}
|
|
|
|
func TestAISettingsHandler_StopPatrol(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
// StopPatrol should be safe to call even when patrol is not running
|
|
handler.StopPatrol()
|
|
// No assertion needed - just verifying it doesn't panic
|
|
}
|
|
|
|
func TestAISettingsHandler_GetAlertTriggeredAnalyzer(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
// Should return the analyzer (may be nil if not initialized)
|
|
analyzer := handler.GetAlertTriggeredAnalyzer()
|
|
// Just verify it doesn't panic and returns something
|
|
_ = analyzer
|
|
}
|
|
|
|
func TestAISettingsHandler_StartPatrol(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
// Start patrol with a cancellable context
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
handler.StartPatrol(ctx)
|
|
|
|
// Give it a brief moment then stop
|
|
time.Sleep(10 * time.Millisecond)
|
|
cancel()
|
|
handler.StopPatrol()
|
|
}
|
|
|
|
func TestAISettingsHandler_SetPatrolFindingsPersistence(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
// Set nil persistence should not panic
|
|
err := handler.SetPatrolFindingsPersistence(nil)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAISettingsHandler_SetPatrolRunHistoryPersistence(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
// Set nil persistence should not panic
|
|
err := handler.SetPatrolRunHistoryPersistence(nil)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAISettingsHandler_SetPatrolThresholdProvider(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
// Set nil threshold provider should not panic
|
|
handler.SetPatrolThresholdProvider(nil)
|
|
}
|
|
|
|
func TestAISettingsHandler_SetMetricsHistoryProvider(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
// Set nil metrics provider should not panic
|
|
handler.SetMetricsHistoryProvider(nil)
|
|
}
|
|
|
|
func TestAISettingsHandler_SetBaselineStore(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
// Set nil baseline store should not panic
|
|
handler.SetBaselineStore(nil)
|
|
}
|
|
|
|
func TestAISettingsHandler_SetChangeDetector(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
// Set nil change detector should not panic
|
|
handler.SetChangeDetector(nil)
|
|
}
|
|
|
|
func TestAISettingsHandler_SetRemediationLog(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
// Set nil remediation log should not panic
|
|
handler.SetRemediationLog(nil)
|
|
}
|
|
|
|
func TestAISettingsHandler_SetPatternDetector(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
// Set nil pattern detector should not panic
|
|
handler.SetPatternDetector(nil)
|
|
}
|
|
|
|
func TestAISettingsHandler_SetCorrelationDetector(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmp}
|
|
persistence := config.NewConfigPersistence(tmp)
|
|
handler := NewAISettingsHandler(cfg, persistence, nil)
|
|
|
|
// Set nil correlation detector should not panic
|
|
handler.SetCorrelationDetector(nil)
|
|
}
|