- Add integration tests for Ollama provider (17 tests against real API) - Add unit tests for baseline, correlation, patterns, memory, knowledge, cost packages - Add context formatter and builder tests - Add factory tests for provider initialization - Add Makefile targets: test-integration, test-all - Clean up test theatre (removed struct field tests) Integration tests require Ollama at OLLAMA_URL (default: 192.168.0.124:11434) Run with: make test-integration
474 lines
12 KiB
Go
474 lines
12 KiB
Go
package ai
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// mockThresholdProvider implements ThresholdProvider for testing
|
|
type mockThresholdProvider struct {
|
|
nodeCPU float64
|
|
nodeMemory float64
|
|
guestMem float64
|
|
guestDisk float64
|
|
storage float64
|
|
}
|
|
|
|
func (m *mockThresholdProvider) GetNodeCPUThreshold() float64 { return m.nodeCPU }
|
|
func (m *mockThresholdProvider) GetNodeMemoryThreshold() float64 { return m.nodeMemory }
|
|
func (m *mockThresholdProvider) GetGuestMemoryThreshold() float64 { return m.guestMem }
|
|
func (m *mockThresholdProvider) GetGuestDiskThreshold() float64 { return m.guestDisk }
|
|
func (m *mockThresholdProvider) GetStorageThreshold() float64 { return m.storage }
|
|
|
|
func TestDefaultPatrolThresholds(t *testing.T) {
|
|
thresholds := DefaultPatrolThresholds()
|
|
|
|
// Verify defaults are set
|
|
if thresholds.NodeCPUWatch != 75 {
|
|
t.Errorf("Expected NodeCPUWatch 75, got %f", thresholds.NodeCPUWatch)
|
|
}
|
|
if thresholds.NodeCPUWarning != 85 {
|
|
t.Errorf("Expected NodeCPUWarning 85, got %f", thresholds.NodeCPUWarning)
|
|
}
|
|
if thresholds.StorageWatch != 70 {
|
|
t.Errorf("Expected StorageWatch 70, got %f", thresholds.StorageWatch)
|
|
}
|
|
}
|
|
|
|
func TestCalculatePatrolThresholds_NilProvider(t *testing.T) {
|
|
thresholds := CalculatePatrolThresholds(nil)
|
|
|
|
// Should return defaults when provider is nil
|
|
defaults := DefaultPatrolThresholds()
|
|
if thresholds.NodeCPUWatch != defaults.NodeCPUWatch {
|
|
t.Errorf("Expected defaults when provider is nil")
|
|
}
|
|
}
|
|
|
|
func TestCalculatePatrolThresholds_FromProvider(t *testing.T) {
|
|
provider := &mockThresholdProvider{
|
|
nodeCPU: 90,
|
|
nodeMemory: 85,
|
|
guestMem: 80,
|
|
guestDisk: 75,
|
|
storage: 70,
|
|
}
|
|
|
|
thresholds := CalculatePatrolThresholds(provider)
|
|
|
|
// Watch thresholds should be alertThreshold - 15
|
|
expectedNodeCPUWatch := 90 - 15
|
|
if thresholds.NodeCPUWatch != float64(expectedNodeCPUWatch) {
|
|
t.Errorf("Expected NodeCPUWatch %d, got %f", expectedNodeCPUWatch, thresholds.NodeCPUWatch)
|
|
}
|
|
|
|
// Warning thresholds should be alertThreshold - 5
|
|
expectedNodeCPUWarning := 90 - 5
|
|
if thresholds.NodeCPUWarning != float64(expectedNodeCPUWarning) {
|
|
t.Errorf("Expected NodeCPUWarning %d, got %f", expectedNodeCPUWarning, thresholds.NodeCPUWarning)
|
|
}
|
|
}
|
|
|
|
func TestClampThreshold(t *testing.T) {
|
|
tests := []struct {
|
|
input float64
|
|
expected float64
|
|
}{
|
|
{50, 50}, // Normal value passes through
|
|
{5, 10}, // Below minimum, clamped to 10
|
|
{-5, 10}, // Negative, clamped to 10
|
|
{100, 99}, // Above maximum, clamped to 99
|
|
{150, 99}, // Way above, clamped to 99
|
|
{10, 10}, // Exactly at minimum
|
|
{99, 99}, // Exactly at maximum
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
result := clampThreshold(tt.input)
|
|
if result != tt.expected {
|
|
t.Errorf("clampThreshold(%f) = %f, want %f", tt.input, result, tt.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPatrolConfig_GetInterval(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config PatrolConfig
|
|
expected time.Duration
|
|
}{
|
|
{
|
|
name: "uses primary interval",
|
|
config: PatrolConfig{Interval: 30 * time.Minute},
|
|
expected: 30 * time.Minute,
|
|
},
|
|
{
|
|
name: "falls back to quick check interval",
|
|
config: PatrolConfig{QuickCheckInterval: 20 * time.Minute},
|
|
expected: 20 * time.Minute,
|
|
},
|
|
{
|
|
name: "defaults to 15 minutes",
|
|
config: PatrolConfig{},
|
|
expected: 15 * time.Minute,
|
|
},
|
|
{
|
|
name: "primary interval takes precedence",
|
|
config: PatrolConfig{Interval: 45 * time.Minute, QuickCheckInterval: 10 * time.Minute},
|
|
expected: 45 * time.Minute,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := tt.config.GetInterval()
|
|
if result != tt.expected {
|
|
t.Errorf("GetInterval() = %v, want %v", result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDefaultPatrolConfig(t *testing.T) {
|
|
cfg := DefaultPatrolConfig()
|
|
|
|
if !cfg.Enabled {
|
|
t.Error("Expected patrol to be enabled by default")
|
|
}
|
|
if cfg.Interval != 15*time.Minute {
|
|
t.Errorf("Expected 15 minute default interval, got %v", cfg.Interval)
|
|
}
|
|
if !cfg.AnalyzeNodes {
|
|
t.Error("Expected AnalyzeNodes to be true by default")
|
|
}
|
|
if !cfg.AnalyzeGuests {
|
|
t.Error("Expected AnalyzeGuests to be true by default")
|
|
}
|
|
if !cfg.AnalyzeDocker {
|
|
t.Error("Expected AnalyzeDocker to be true by default")
|
|
}
|
|
if !cfg.AnalyzeStorage {
|
|
t.Error("Expected AnalyzeStorage to be true by default")
|
|
}
|
|
if !cfg.AnalyzePBS {
|
|
t.Error("Expected AnalyzePBS to be true by default")
|
|
}
|
|
if !cfg.AnalyzeHosts {
|
|
t.Error("Expected AnalyzeHosts to be true by default")
|
|
}
|
|
}
|
|
|
|
func TestNewPatrolService(t *testing.T) {
|
|
ps := NewPatrolService(nil, nil)
|
|
|
|
if ps == nil {
|
|
t.Fatal("Expected non-nil patrol service")
|
|
}
|
|
|
|
// Should have initialized with defaults
|
|
cfg := ps.GetConfig()
|
|
if !cfg.Enabled {
|
|
t.Error("Expected patrol to be enabled by default")
|
|
}
|
|
|
|
// Findings store should be initialized
|
|
if ps.GetFindings() == nil {
|
|
t.Error("Expected findings store to be initialized")
|
|
}
|
|
|
|
// Should not be running initially
|
|
status := ps.GetStatus()
|
|
if status.Running {
|
|
t.Error("Expected patrol to not be running initially")
|
|
}
|
|
}
|
|
|
|
func TestPatrolService_SetConfig(t *testing.T) {
|
|
ps := NewPatrolService(nil, nil)
|
|
|
|
newConfig := PatrolConfig{
|
|
Enabled: false,
|
|
Interval: 30 * time.Minute,
|
|
AnalyzeNodes: false,
|
|
AnalyzeGuests: true,
|
|
}
|
|
|
|
ps.SetConfig(newConfig)
|
|
cfg := ps.GetConfig()
|
|
|
|
if cfg.Enabled != false {
|
|
t.Error("Expected enabled to be false after SetConfig")
|
|
}
|
|
if cfg.Interval != 30*time.Minute {
|
|
t.Errorf("Expected interval to be 30 minutes, got %v", cfg.Interval)
|
|
}
|
|
if cfg.AnalyzeNodes != false {
|
|
t.Error("Expected AnalyzeNodes to be false")
|
|
}
|
|
}
|
|
|
|
func TestPatrolService_SetThresholdProvider(t *testing.T) {
|
|
ps := NewPatrolService(nil, nil)
|
|
|
|
provider := &mockThresholdProvider{
|
|
nodeCPU: 95,
|
|
nodeMemory: 90,
|
|
guestMem: 85,
|
|
guestDisk: 80,
|
|
storage: 75,
|
|
}
|
|
|
|
ps.SetThresholdProvider(provider)
|
|
|
|
// Verify thresholds were calculated
|
|
ps.mu.RLock()
|
|
thresholds := ps.thresholds
|
|
ps.mu.RUnlock()
|
|
|
|
// Watch = alert - 15
|
|
expectedWatch := 95.0 - 15.0
|
|
if thresholds.NodeCPUWatch != expectedWatch {
|
|
t.Errorf("Expected NodeCPUWatch %f, got %f", expectedWatch, thresholds.NodeCPUWatch)
|
|
}
|
|
}
|
|
|
|
func TestPatrolService_GetStatus(t *testing.T) {
|
|
ps := NewPatrolService(nil, nil)
|
|
|
|
status := ps.GetStatus()
|
|
|
|
// Default status checks
|
|
if status.Running {
|
|
t.Error("Expected running to be false initially")
|
|
}
|
|
if !status.Enabled {
|
|
t.Error("Expected enabled to be true by default")
|
|
}
|
|
if status.FindingsCount != 0 {
|
|
t.Errorf("Expected 0 findings count, got %d", status.FindingsCount)
|
|
}
|
|
if !status.Healthy {
|
|
t.Error("Expected healthy to be true with no findings")
|
|
}
|
|
if status.IntervalMs == 0 {
|
|
t.Error("Expected non-zero interval")
|
|
}
|
|
}
|
|
|
|
func TestPatrolService_GetStatus_WithFindings(t *testing.T) {
|
|
ps := NewPatrolService(nil, nil)
|
|
|
|
// Add a warning finding
|
|
finding := &Finding{
|
|
ID: "test-finding",
|
|
Severity: FindingSeverityWarning,
|
|
ResourceID: "test-resource",
|
|
ResourceName: "test",
|
|
Title: "Test Warning",
|
|
}
|
|
ps.findings.Add(finding)
|
|
|
|
status := ps.GetStatus()
|
|
|
|
if status.FindingsCount != 1 {
|
|
t.Errorf("Expected 1 finding, got %d", status.FindingsCount)
|
|
}
|
|
if status.Healthy {
|
|
t.Error("Expected healthy to be false with warning finding")
|
|
}
|
|
}
|
|
|
|
func TestPatrolService_StreamSubscription(t *testing.T) {
|
|
ps := NewPatrolService(nil, nil)
|
|
|
|
// Subscribe
|
|
ch := ps.SubscribeToStream()
|
|
if ch == nil {
|
|
t.Fatal("Expected non-nil channel")
|
|
}
|
|
|
|
// Verify it's tracked
|
|
ps.streamMu.RLock()
|
|
_, exists := ps.streamSubscribers[ch]
|
|
ps.streamMu.RUnlock()
|
|
|
|
if !exists {
|
|
t.Error("Expected channel to be in subscribers")
|
|
}
|
|
|
|
// Unsubscribe
|
|
ps.UnsubscribeFromStream(ch)
|
|
|
|
ps.streamMu.RLock()
|
|
_, stillExists := ps.streamSubscribers[ch]
|
|
ps.streamMu.RUnlock()
|
|
|
|
if stillExists {
|
|
t.Error("Expected channel to be removed from subscribers")
|
|
}
|
|
}
|
|
|
|
func TestPatrolService_Broadcast(t *testing.T) {
|
|
ps := NewPatrolService(nil, nil)
|
|
|
|
ch := ps.SubscribeToStream()
|
|
|
|
// Broadcast an event
|
|
event := PatrolStreamEvent{
|
|
Type: "test",
|
|
Content: "test content",
|
|
}
|
|
ps.broadcast(event)
|
|
|
|
// Check for the event
|
|
select {
|
|
case received := <-ch:
|
|
if received.Type != "test" {
|
|
t.Errorf("Expected type 'test', got '%s'", received.Type)
|
|
}
|
|
if received.Content != "test content" {
|
|
t.Errorf("Expected content 'test content', got '%s'", received.Content)
|
|
}
|
|
case <-time.After(100 * time.Millisecond):
|
|
t.Error("Expected to receive broadcast event")
|
|
}
|
|
|
|
ps.UnsubscribeFromStream(ch)
|
|
}
|
|
|
|
func TestPatrolService_SetStreamPhase(t *testing.T) {
|
|
ps := NewPatrolService(nil, nil)
|
|
|
|
// Default phase
|
|
ps.streamMu.RLock()
|
|
initialPhase := ps.streamPhase
|
|
ps.streamMu.RUnlock()
|
|
|
|
if initialPhase != "idle" {
|
|
t.Errorf("Expected initial phase 'idle', got '%s'", initialPhase)
|
|
}
|
|
|
|
// Change phase
|
|
ps.setStreamPhase("analyzing")
|
|
|
|
ps.streamMu.RLock()
|
|
newPhase := ps.streamPhase
|
|
ps.streamMu.RUnlock()
|
|
|
|
if newPhase != "analyzing" {
|
|
t.Errorf("Expected phase 'analyzing', got '%s'", newPhase)
|
|
}
|
|
|
|
// Reset to idle should clear output
|
|
ps.streamMu.Lock()
|
|
ps.currentOutput.WriteString("some content")
|
|
ps.streamMu.Unlock()
|
|
|
|
ps.setStreamPhase("idle")
|
|
|
|
output, phase := ps.GetCurrentStreamOutput()
|
|
if phase != "idle" {
|
|
t.Errorf("Expected phase 'idle', got '%s'", phase)
|
|
}
|
|
if output != "" {
|
|
t.Errorf("Expected empty output after reset to idle, got '%s'", output)
|
|
}
|
|
}
|
|
|
|
func TestPatrolService_GetCurrentStreamOutput(t *testing.T) {
|
|
ps := NewPatrolService(nil, nil)
|
|
|
|
ps.setStreamPhase("analyzing")
|
|
ps.appendStreamContent("test output 1")
|
|
ps.appendStreamContent("test output 2")
|
|
|
|
output, phase := ps.GetCurrentStreamOutput()
|
|
|
|
if phase != "analyzing" {
|
|
t.Errorf("Expected phase 'analyzing', got '%s'", phase)
|
|
}
|
|
if output != "test output 1test output 2" {
|
|
t.Errorf("Expected concatenated output, got '%s'", output)
|
|
}
|
|
}
|
|
|
|
func TestPatrolService_SetMemoryProviders(t *testing.T) {
|
|
ps := NewPatrolService(nil, nil)
|
|
|
|
// Test SetChangeDetector
|
|
changeDetector := &ChangeDetector{} // Would need proper initialization
|
|
ps.mu.Lock()
|
|
ps.changeDetector = changeDetector
|
|
ps.mu.Unlock()
|
|
|
|
if ps.GetChangeDetector() != changeDetector {
|
|
t.Error("Expected change detector to be set")
|
|
}
|
|
|
|
// Test SetRemediationLog
|
|
remLog := &RemediationLog{} // Would need proper initialization
|
|
ps.mu.Lock()
|
|
ps.remediationLog = remLog
|
|
ps.mu.Unlock()
|
|
|
|
if ps.GetRemediationLog() != remLog {
|
|
t.Error("Expected remediation log to be set")
|
|
}
|
|
}
|
|
|
|
func TestPatrolRunRecord(t *testing.T) {
|
|
now := time.Now()
|
|
record := PatrolRunRecord{
|
|
ID: "test-run-1",
|
|
StartedAt: now,
|
|
CompletedAt: now.Add(5 * time.Second),
|
|
Duration: 5 * time.Second,
|
|
Type: "patrol",
|
|
ResourcesChecked: 10,
|
|
NodesChecked: 2,
|
|
GuestsChecked: 5,
|
|
NewFindings: 1,
|
|
Status: "issues_found",
|
|
}
|
|
|
|
if record.ID != "test-run-1" {
|
|
t.Errorf("Expected ID 'test-run-1', got '%s'", record.ID)
|
|
}
|
|
if record.ResourcesChecked != 10 {
|
|
t.Errorf("Expected 10 resources checked, got %d", record.ResourcesChecked)
|
|
}
|
|
if record.Status != "issues_found" {
|
|
t.Errorf("Expected status 'issues_found', got '%s'", record.Status)
|
|
}
|
|
}
|
|
|
|
func TestPatrolStatus_Fields(t *testing.T) {
|
|
now := time.Now()
|
|
next := now.Add(15 * time.Minute)
|
|
|
|
status := PatrolStatus{
|
|
Running: true,
|
|
Enabled: true,
|
|
LastPatrolAt: &now,
|
|
NextPatrolAt: &next,
|
|
LastDuration: 5 * time.Second,
|
|
ResourcesChecked: 25,
|
|
FindingsCount: 3,
|
|
ErrorCount: 0,
|
|
Healthy: false,
|
|
IntervalMs: 900000,
|
|
}
|
|
|
|
if !status.Running {
|
|
t.Error("Expected running to be true")
|
|
}
|
|
if status.FindingsCount != 3 {
|
|
t.Errorf("Expected 3 findings, got %d", status.FindingsCount)
|
|
}
|
|
if status.LastPatrolAt == nil {
|
|
t.Error("Expected LastPatrolAt to be set")
|
|
}
|
|
if status.IntervalMs != 900000 {
|
|
t.Errorf("Expected interval 900000ms, got %d", status.IntervalMs)
|
|
}
|
|
}
|