Pulse/internal/ai/service_remediation_test.go
rcourtman 8019acd6b6 feat(ai): add unified Intelligence orchestrator
- Create Intelligence struct that aggregates all AI subsystems
- Add /api/ai/intelligence endpoint for system-wide and per-resource insights
- Wire Intelligence into PatrolService as a facade (not replacement)
- Add TypeScript types and API client for frontend
- Add unit tests for Intelligence orchestrator
- Fix pre-existing test failures using diagnostic commands instead of actionable ones

The Intelligence orchestrator provides:
- System-wide health scoring (A-F grades)
- Aggregated findings, predictions, correlations
- Per-resource context generation for AI prompts
- Learning progress tracking

This unifies access to AI subsystems without replacing existing code paths.
2025-12-21 10:32:02 +00:00

158 lines
4.2 KiB
Go

package ai
import (
"os"
"testing"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
)
// ============================================================================
// Remediation Tests
// ============================================================================
func TestService_Remediation(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "pulse-ai-rem-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
persistence := config.NewConfigPersistence(tmpDir)
svc := NewService(persistence, nil)
patrol := NewPatrolService(svc, nil)
svc.patrolService = patrol // Manual link for test
remLog := NewRemediationLog(RemediationLogConfig{DataDir: tmpDir})
svc.SetRemediationLog(remLog)
// Test logRemediation - use an actionable command (not diagnostic)
req := ExecuteRequest{
TargetID: "vm-101",
TargetType: "vm",
Prompt: "High CPU",
}
svc.logRemediation(req, "systemctl restart myservice", "output", true)
// Verify it was logged
history := remLog.GetForResource("vm-101", 5)
if len(history) != 1 {
t.Fatalf("Expected 1 remediation record, got %d", len(history))
}
if history[0].Action != "systemctl restart myservice" {
t.Errorf("Expected action 'systemctl restart myservice', got %s", history[0].Action)
}
// Test buildRemediationContext
ctx := svc.buildRemediationContext("vm-101", "High CPU")
if !containsString(ctx, "Remediation History for This Resource") {
t.Error("Expected remediation history section in context")
}
if !containsString(ctx, "systemctl restart myservice") {
t.Error("Expected logged action in context")
}
// Test with similar problem
ctx2 := svc.buildRemediationContext("other-vm", "High CPU")
if !containsString(ctx2, "Past Successful Fixes for Similar Issues") {
t.Error("Expected successful fixes section in context for similar problem")
}
}
func TestService_Remediation_NoPatrolService(t *testing.T) {
svc := NewService(nil, nil)
// patrolService is nil, logRemediation should handle gracefully
req := ExecuteRequest{
TargetID: "vm-102",
TargetType: "vm",
Prompt: "Test",
}
// Should not panic
svc.logRemediation(req, "test", "output", true)
}
func TestService_Remediation_NoRemediationLog(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "pulse-ai-rem-test-nolog-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
persistence := config.NewConfigPersistence(tmpDir)
svc := NewService(persistence, nil)
patrol := NewPatrolService(svc, nil)
svc.patrolService = patrol
// remediationLog is nil on patrol
req := ExecuteRequest{
TargetID: "vm-103",
TargetType: "vm",
Prompt: "Test",
}
// Should not panic
svc.logRemediation(req, "test", "output", true)
}
func TestService_BuildRemediationContext_Empty(t *testing.T) {
svc := NewService(nil, nil)
// With no remediationLog set, should return empty
ctx := svc.buildRemediationContext("unknown", "Unknown problem")
if ctx != "" {
t.Error("Expected empty context when no remediation log")
}
}
// ============================================================================
// String Utility Tests
// ============================================================================
func TestTruncateString(t *testing.T) {
tests := []struct {
name string
input string
max int
expected string
}{
{"empty string", "", 5, ""},
{"under limit", "123", 5, "123"},
{"at limit", "12345", 5, "12345"},
{"over limit", "1234567890", 5, "12..."},
{"exactly at max minus ellipsis", "1234", 4, "1234"},
{"just over", "12345", 4, "1..."},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := truncateString(tt.input, tt.max)
if result != tt.expected {
t.Errorf("truncateString(%q, %d) = %q, expected %q",
tt.input, tt.max, result, tt.expected)
}
})
}
}
func TestContainsString(t *testing.T) {
tests := []struct {
haystack string
needle string
expected bool
}{
{"hello world", "world", true},
{"hello world", "foo", false},
{"", "test", false},
{"test", "", true},
}
for _, tt := range tests {
if containsString(tt.haystack, tt.needle) != tt.expected {
t.Errorf("containsString(%q, %q) expected %v",
tt.haystack, tt.needle, tt.expected)
}
}
}