Files
Pulse/internal/api/ai_intelligence_handlers_test.go
rcourtman 27f1a11acb feat: add AI Intelligence system with investigation and forecasting
Major new AI capabilities for infrastructure monitoring:

Investigation System:
- Autonomous finding investigation with configurable autonomy levels
- Investigation orchestrator with rate limiting and guardrails
- Safety checks for read-only mode enforcement
- Chat-based investigation with approval workflows

Forecasting & Remediation:
- Trend forecasting for resource capacity planning
- Remediation engine for generating fix proposals
- Circuit breaker for AI operation protection

Unified Findings:
- Unified store bridging alerts and AI findings
- Correlation and root cause analysis
- Incident coordinator with metrics recording

New Frontend:
- AI Intelligence page with patrol controls
- Investigation drawer for finding details
- Unified findings panel with actions

Supporting Infrastructure:
- Learning store for user preference tracking
- Proxmox event ingestion and correlation
- Enhanced patrol with investigation triggers
2026-01-24 22:41:43 +00:00

389 lines
11 KiB
Go

package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
)
// createTestAIHandler creates a test AI handler with minimal setup
func createTestAIHandler(t *testing.T) *AISettingsHandler {
t.Helper()
tmp := t.TempDir()
cfg := &config.Config{DataPath: tmp}
persistence := config.NewConfigPersistence(tmp)
return newTestAISettingsHandler(cfg, persistence, nil)
}
// TestHandleGetPatterns tests the patterns endpoint
func TestHandleGetPatterns_MethodNotAllowed(t *testing.T) {
t.Parallel()
handler := createTestAIHandler(t)
req := httptest.NewRequest(http.MethodPost, "/api/ai/intelligence/patterns", nil)
rec := httptest.NewRecorder()
handler.HandleGetPatterns(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected status %d for POST, got %d", http.StatusMethodNotAllowed, rec.Code)
}
}
func TestHandleGetPatterns_NoPatrolService(t *testing.T) {
t.Parallel()
handler := createTestAIHandler(t)
req := httptest.NewRequest(http.MethodGet, "/api/ai/intelligence/patterns", nil)
rec := httptest.NewRecorder()
handler.HandleGetPatterns(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d: %s", http.StatusOK, rec.Code, rec.Body.String())
}
var resp map[string]interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
// Should return empty patterns array with message
patterns, ok := resp["patterns"].([]interface{})
if !ok {
t.Fatal("expected patterns array in response")
}
if len(patterns) != 0 {
t.Fatalf("expected empty patterns, got %d", len(patterns))
}
if resp["message"] != "Pulse Patrol is not enabled" {
t.Fatalf("unexpected message: %v", resp["message"])
}
}
func TestHandleGetPatterns_ResourceIDFilter(t *testing.T) {
t.Parallel()
handler := createTestAIHandler(t)
// Test with resource_id query parameter - should still return gracefully
req := httptest.NewRequest(http.MethodGet, "/api/ai/intelligence/patterns?resource_id=vm-100", nil)
rec := httptest.NewRecorder()
handler.HandleGetPatterns(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d: %s", http.StatusOK, rec.Code, rec.Body.String())
}
var resp map[string]interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
patterns, ok := resp["patterns"].([]interface{})
if !ok {
t.Fatal("expected patterns array in response")
}
if len(patterns) != 0 {
t.Fatalf("expected empty patterns for non-initialized service, got %d", len(patterns))
}
}
// TestHandleGetPredictions tests the predictions endpoint
func TestHandleGetPredictions_MethodNotAllowed(t *testing.T) {
t.Parallel()
handler := createTestAIHandler(t)
req := httptest.NewRequest(http.MethodPost, "/api/ai/intelligence/predictions", nil)
rec := httptest.NewRecorder()
handler.HandleGetPredictions(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected status %d for POST, got %d", http.StatusMethodNotAllowed, rec.Code)
}
}
func TestHandleGetPredictions_NoPatrolService(t *testing.T) {
t.Parallel()
handler := createTestAIHandler(t)
req := httptest.NewRequest(http.MethodGet, "/api/ai/intelligence/predictions", nil)
rec := httptest.NewRecorder()
handler.HandleGetPredictions(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d: %s", http.StatusOK, rec.Code, rec.Body.String())
}
var resp map[string]interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
predictions, ok := resp["predictions"].([]interface{})
if !ok {
t.Fatal("expected predictions array in response")
}
if len(predictions) != 0 {
t.Fatalf("expected empty predictions, got %d", len(predictions))
}
if resp["message"] != "Pulse Patrol is not enabled" {
t.Fatalf("unexpected message: %v", resp["message"])
}
}
func TestHandleGetPredictions_ResourceIDFilter(t *testing.T) {
t.Parallel()
handler := createTestAIHandler(t)
req := httptest.NewRequest(http.MethodGet, "/api/ai/intelligence/predictions?resource_id=vm-200", nil)
rec := httptest.NewRecorder()
handler.HandleGetPredictions(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d: %s", http.StatusOK, rec.Code, rec.Body.String())
}
var resp map[string]interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
// Verify response is valid JSON with expected structure
if _, ok := resp["predictions"]; !ok {
t.Fatal("expected predictions field in response")
}
}
// TestHandleGetCorrelations tests the correlations endpoint
func TestHandleGetCorrelations_MethodNotAllowed(t *testing.T) {
t.Parallel()
handler := createTestAIHandler(t)
req := httptest.NewRequest(http.MethodPost, "/api/ai/intelligence/correlations", nil)
rec := httptest.NewRecorder()
handler.HandleGetCorrelations(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected status %d for POST, got %d", http.StatusMethodNotAllowed, rec.Code)
}
}
func TestHandleGetCorrelations_NoPatrolService(t *testing.T) {
t.Parallel()
handler := createTestAIHandler(t)
req := httptest.NewRequest(http.MethodGet, "/api/ai/intelligence/correlations", nil)
rec := httptest.NewRecorder()
handler.HandleGetCorrelations(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d: %s", http.StatusOK, rec.Code, rec.Body.String())
}
var resp map[string]interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
correlations, ok := resp["correlations"].([]interface{})
if !ok {
t.Fatal("expected correlations array in response")
}
if len(correlations) != 0 {
t.Fatalf("expected empty correlations, got %d", len(correlations))
}
if resp["message"] != "Pulse Patrol is not enabled" {
t.Fatalf("unexpected message: %v", resp["message"])
}
}
func TestHandleGetCorrelations_ResourceIDFilter(t *testing.T) {
t.Parallel()
handler := createTestAIHandler(t)
req := httptest.NewRequest(http.MethodGet, "/api/ai/intelligence/correlations?resource_id=storage-1", nil)
rec := httptest.NewRecorder()
handler.HandleGetCorrelations(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d: %s", http.StatusOK, rec.Code, rec.Body.String())
}
var resp map[string]interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if _, ok := resp["correlations"]; !ok {
t.Fatal("expected correlations field in response")
}
}
// TestHandleGetRecentChanges tests the changes endpoint
func TestHandleGetRecentChanges_MethodNotAllowed(t *testing.T) {
t.Parallel()
handler := createTestAIHandler(t)
req := httptest.NewRequest(http.MethodPost, "/api/ai/intelligence/changes", nil)
rec := httptest.NewRecorder()
handler.HandleGetRecentChanges(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected status %d for POST, got %d", http.StatusMethodNotAllowed, rec.Code)
}
}
func TestHandleGetRecentChanges_NoPatrolService(t *testing.T) {
t.Parallel()
handler := createTestAIHandler(t)
req := httptest.NewRequest(http.MethodGet, "/api/ai/intelligence/changes", nil)
rec := httptest.NewRecorder()
handler.HandleGetRecentChanges(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d: %s", http.StatusOK, rec.Code, rec.Body.String())
}
var resp map[string]interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
changes, ok := resp["changes"].([]interface{})
if !ok {
t.Fatal("expected changes array in response")
}
if len(changes) != 0 {
t.Fatalf("expected empty changes, got %d", len(changes))
}
if resp["message"] != "Pulse Patrol is not enabled" {
t.Fatalf("unexpected message: %v", resp["message"])
}
}
// TestHandleGetRemediations tests the remediations endpoint
func TestHandleGetRemediations_MethodNotAllowed(t *testing.T) {
t.Parallel()
handler := createTestAIHandler(t)
req := httptest.NewRequest(http.MethodPost, "/api/ai/intelligence/remediations", nil)
rec := httptest.NewRecorder()
handler.HandleGetRemediations(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected status %d for POST, got %d", http.StatusMethodNotAllowed, rec.Code)
}
}
func TestHandleGetRemediations_NoPatrolService(t *testing.T) {
t.Parallel()
handler := createTestAIHandler(t)
req := httptest.NewRequest(http.MethodGet, "/api/ai/intelligence/remediations", nil)
rec := httptest.NewRecorder()
handler.HandleGetRemediations(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d: %s", http.StatusOK, rec.Code, rec.Body.String())
}
var resp map[string]interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
remediations, ok := resp["remediations"].([]interface{})
if !ok {
t.Fatal("expected remediations array in response")
}
if len(remediations) != 0 {
t.Fatalf("expected empty remediations, got %d", len(remediations))
}
if resp["message"] != "Pulse Patrol is not enabled" {
t.Fatalf("unexpected message: %v", resp["message"])
}
}
// TestHandleGetBaselines tests the baselines endpoint
func TestHandleGetBaselines_MethodNotAllowed(t *testing.T) {
t.Parallel()
handler := createTestAIHandler(t)
req := httptest.NewRequest(http.MethodPost, "/api/ai/intelligence/baselines", nil)
rec := httptest.NewRecorder()
handler.HandleGetBaselines(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected status %d for POST, got %d", http.StatusMethodNotAllowed, rec.Code)
}
}
func TestHandleGetBaselines_NoPatrolService(t *testing.T) {
t.Parallel()
handler := createTestAIHandler(t)
req := httptest.NewRequest(http.MethodGet, "/api/ai/intelligence/baselines", nil)
rec := httptest.NewRecorder()
handler.HandleGetBaselines(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d: %s", http.StatusOK, rec.Code, rec.Body.String())
}
var resp map[string]interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
baselines, ok := resp["baselines"].([]interface{})
if !ok {
t.Fatal("expected baselines array in response")
}
if len(baselines) != 0 {
t.Fatalf("expected empty baselines, got %d", len(baselines))
}
if resp["message"] != "Pulse Patrol is not enabled" {
t.Fatalf("unexpected message: %v", resp["message"])
}
}
func TestHandleGetBaselines_ResourceIDFilter(t *testing.T) {
t.Parallel()
handler := createTestAIHandler(t)
req := httptest.NewRequest(http.MethodGet, "/api/ai/intelligence/baselines?resource_id=node-1", nil)
rec := httptest.NewRecorder()
handler.HandleGetBaselines(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d: %s", http.StatusOK, rec.Code, rec.Body.String())
}
var resp map[string]interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if _, ok := resp["baselines"]; !ok {
t.Fatal("expected baselines field in response")
}
}