- 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
468 lines
11 KiB
Go
468 lines
11 KiB
Go
package ai
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
)
|
|
|
|
func TestNewService(t *testing.T) {
|
|
svc := NewService(nil, nil)
|
|
|
|
if svc == nil {
|
|
t.Fatal("Expected non-nil service")
|
|
}
|
|
|
|
// Should not be enabled without config
|
|
if svc.IsEnabled() {
|
|
t.Error("Expected service to not be enabled without config")
|
|
}
|
|
|
|
// Cost store should be initialized
|
|
if svc.costStore == nil {
|
|
t.Error("Expected cost store to be initialized")
|
|
}
|
|
}
|
|
|
|
func TestService_IsEnabled(t *testing.T) {
|
|
svc := NewService(nil, nil)
|
|
|
|
// Without config
|
|
if svc.IsEnabled() {
|
|
t.Error("Expected not enabled without config")
|
|
}
|
|
|
|
// Should remain not enabled since we can't create a real provider in unit tests
|
|
}
|
|
|
|
func TestService_GetConfig_Nil(t *testing.T) {
|
|
svc := NewService(nil, nil)
|
|
|
|
cfg := svc.GetConfig()
|
|
if cfg != nil {
|
|
t.Error("Expected nil config before loading")
|
|
}
|
|
}
|
|
|
|
func TestService_GetAIConfig(t *testing.T) {
|
|
svc := NewService(nil, nil)
|
|
|
|
cfg := svc.GetAIConfig()
|
|
if cfg != nil {
|
|
t.Error("Expected nil AI config before loading")
|
|
}
|
|
}
|
|
|
|
func TestService_GetPatrolService_Initial(t *testing.T) {
|
|
svc := NewService(nil, nil)
|
|
|
|
patrol := svc.GetPatrolService()
|
|
if patrol != nil {
|
|
t.Error("Expected nil patrol service before state provider is set")
|
|
}
|
|
}
|
|
|
|
func TestService_GetAlertTriggeredAnalyzer_Initial(t *testing.T) {
|
|
svc := NewService(nil, nil)
|
|
|
|
analyzer := svc.GetAlertTriggeredAnalyzer()
|
|
if analyzer != nil {
|
|
t.Error("Expected nil analyzer before state provider is set")
|
|
}
|
|
}
|
|
|
|
func TestService_SetStateProvider(t *testing.T) {
|
|
svc := NewService(nil, nil)
|
|
|
|
stateProvider := &mockStateProvider{
|
|
state: models.StateSnapshot{
|
|
Nodes: []models.Node{
|
|
{ID: "node-1", Name: "test-node"},
|
|
},
|
|
},
|
|
}
|
|
|
|
svc.SetStateProvider(stateProvider)
|
|
|
|
// Patrol service should now be initialized
|
|
patrol := svc.GetPatrolService()
|
|
if patrol == nil {
|
|
t.Error("Expected patrol service to be initialized after setting state provider")
|
|
}
|
|
|
|
// Alert triggered analyzer should now be initialized
|
|
analyzer := svc.GetAlertTriggeredAnalyzer()
|
|
if analyzer == nil {
|
|
t.Error("Expected alert analyzer to be initialized after setting state provider")
|
|
}
|
|
}
|
|
|
|
func TestService_GetCostSummary_NoStore(t *testing.T) {
|
|
svc := NewService(nil, nil)
|
|
svc.costStore = nil
|
|
|
|
summary := svc.GetCostSummary(30)
|
|
|
|
// Should return an empty summary
|
|
if summary.Days != 30 {
|
|
t.Errorf("Expected days 30, got %d", summary.Days)
|
|
}
|
|
}
|
|
|
|
func TestService_GetCostSummary_WithStore(t *testing.T) {
|
|
svc := NewService(nil, nil)
|
|
|
|
summary := svc.GetCostSummary(7)
|
|
|
|
// Should return a valid summary
|
|
if summary.Days != 7 {
|
|
t.Errorf("Expected days 7, got %d", summary.Days)
|
|
}
|
|
}
|
|
|
|
func TestService_ListCostEvents_NoStore(t *testing.T) {
|
|
svc := NewService(nil, nil)
|
|
svc.costStore = nil
|
|
|
|
events := svc.ListCostEvents(7)
|
|
if events != nil {
|
|
t.Error("Expected nil events when no store")
|
|
}
|
|
}
|
|
|
|
func TestService_ClearCostHistory_NoStore(t *testing.T) {
|
|
svc := NewService(nil, nil)
|
|
svc.costStore = nil
|
|
|
|
err := svc.ClearCostHistory()
|
|
if err != nil {
|
|
t.Errorf("Expected no error when no store, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestService_AcquireExecutionSlot(t *testing.T) {
|
|
svc := NewService(nil, nil)
|
|
|
|
ctx := context.Background()
|
|
|
|
// Acquire a chat slot
|
|
release, err := svc.acquireExecutionSlot(ctx, "chat")
|
|
if err != nil {
|
|
t.Fatalf("Failed to acquire chat slot: %v", err)
|
|
}
|
|
if release == nil {
|
|
t.Fatal("Expected non-nil release function")
|
|
}
|
|
release()
|
|
|
|
// Acquire a patrol slot
|
|
release, err = svc.acquireExecutionSlot(ctx, "patrol")
|
|
if err != nil {
|
|
t.Fatalf("Failed to acquire patrol slot: %v", err)
|
|
}
|
|
release()
|
|
|
|
// Empty use case should default to chat
|
|
release, err = svc.acquireExecutionSlot(ctx, "")
|
|
if err != nil {
|
|
t.Fatalf("Failed to acquire slot with empty use case: %v", err)
|
|
}
|
|
release()
|
|
}
|
|
|
|
// Note: TestService_AcquireExecutionSlot_Canceled removed - the slot acquisition
|
|
// doesn't immediately check context cancel in the select since slots are available
|
|
|
|
func TestService_EnforceBudget_NoBudget(t *testing.T) {
|
|
svc := NewService(nil, nil)
|
|
|
|
// No budget set - should pass
|
|
err := svc.enforceBudget("chat")
|
|
if err != nil {
|
|
t.Errorf("Expected no error without budget, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestService_StartPatrol_NoPatrol(t *testing.T) {
|
|
svc := NewService(nil, nil)
|
|
|
|
// Should not panic when patrol is nil
|
|
svc.StartPatrol(context.Background())
|
|
}
|
|
|
|
func TestService_StopPatrol_NoPatrol(t *testing.T) {
|
|
svc := NewService(nil, nil)
|
|
|
|
// Should not panic when patrol is nil
|
|
svc.StopPatrol()
|
|
}
|
|
|
|
func TestService_ReconfigurePatrol_NoPatrol(t *testing.T) {
|
|
svc := NewService(nil, nil)
|
|
|
|
// Should not panic when patrol is nil
|
|
svc.ReconfigurePatrol()
|
|
}
|
|
|
|
func TestService_LookupGuestsByVMID(t *testing.T) {
|
|
svc := NewService(nil, nil)
|
|
|
|
stateProvider := &mockStateProvider{
|
|
state: models.StateSnapshot{
|
|
VMs: []models.VM{
|
|
{ID: "vm-100", VMID: 100, Name: "web-server", Node: "pve-1"},
|
|
{ID: "vm-101", VMID: 101, Name: "database", Node: "pve-1"},
|
|
},
|
|
Containers: []models.Container{
|
|
{ID: "ct-200", VMID: 200, Name: "nginx", Node: "pve-1"},
|
|
},
|
|
},
|
|
}
|
|
svc.SetStateProvider(stateProvider)
|
|
|
|
// Test finding a VM
|
|
guests := svc.lookupGuestsByVMID(100, "")
|
|
if len(guests) != 1 {
|
|
t.Errorf("Expected 1 guest for VMID 100, got %d", len(guests))
|
|
}
|
|
if len(guests) > 0 && guests[0].Name != "web-server" {
|
|
t.Errorf("Expected name 'web-server', got '%s'", guests[0].Name)
|
|
}
|
|
|
|
// Test finding a container
|
|
guests = svc.lookupGuestsByVMID(200, "")
|
|
if len(guests) != 1 {
|
|
t.Errorf("Expected 1 guest for VMID 200, got %d", len(guests))
|
|
}
|
|
if len(guests) > 0 && guests[0].Type != "lxc" {
|
|
t.Errorf("Expected type 'lxc', got '%s'", guests[0].Type)
|
|
}
|
|
|
|
// Test not found
|
|
guests = svc.lookupGuestsByVMID(999, "")
|
|
if len(guests) != 0 {
|
|
t.Errorf("Expected 0 guests for VMID 999, got %d", len(guests))
|
|
}
|
|
}
|
|
|
|
func TestService_LookupGuestsByVMID_NoStateProvider(t *testing.T) {
|
|
svc := NewService(nil, nil)
|
|
|
|
guests := svc.lookupGuestsByVMID(100, "")
|
|
if guests != nil {
|
|
t.Error("Expected nil guests without state provider")
|
|
}
|
|
}
|
|
|
|
func TestService_LookupNodeForVMID(t *testing.T) {
|
|
svc := NewService(nil, nil)
|
|
|
|
stateProvider := &mockStateProvider{
|
|
state: models.StateSnapshot{
|
|
VMs: []models.VM{
|
|
{ID: "vm-100", VMID: 100, Name: "web-server", Node: "pve-1"},
|
|
},
|
|
},
|
|
}
|
|
svc.SetStateProvider(stateProvider)
|
|
|
|
node, name, guestType := svc.lookupNodeForVMID(100)
|
|
|
|
if node != "pve-1" {
|
|
t.Errorf("Expected node 'pve-1', got '%s'", node)
|
|
}
|
|
if name != "web-server" {
|
|
t.Errorf("Expected name 'web-server', got '%s'", name)
|
|
}
|
|
if guestType != "qemu" {
|
|
t.Errorf("Expected type 'qemu', got '%s'", guestType)
|
|
}
|
|
}
|
|
|
|
func TestExtractVMIDFromCommand(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
command string
|
|
expectedVMID int
|
|
expectedOwner bool
|
|
expectedFound bool
|
|
}{
|
|
{
|
|
name: "pct exec",
|
|
command: "pct exec 100 -- ls",
|
|
expectedVMID: 100,
|
|
expectedOwner: true,
|
|
expectedFound: true,
|
|
},
|
|
{
|
|
name: "pct enter",
|
|
command: "pct enter 101",
|
|
expectedVMID: 101,
|
|
expectedOwner: true,
|
|
expectedFound: true,
|
|
},
|
|
{
|
|
name: "qm start",
|
|
command: "qm start 200",
|
|
expectedVMID: 200,
|
|
expectedOwner: true,
|
|
expectedFound: true,
|
|
},
|
|
{
|
|
name: "qm guest exec",
|
|
command: "qm guest exec 201 ls",
|
|
expectedVMID: 201,
|
|
expectedOwner: true,
|
|
expectedFound: true,
|
|
},
|
|
{
|
|
name: "vzdump",
|
|
command: "vzdump 100",
|
|
expectedVMID: 100,
|
|
expectedOwner: false, // vzdump is cluster-aware
|
|
expectedFound: true,
|
|
},
|
|
{
|
|
name: "no vmid command",
|
|
command: "ls -la",
|
|
expectedVMID: 0,
|
|
expectedOwner: false,
|
|
expectedFound: false,
|
|
},
|
|
{
|
|
name: "empty command",
|
|
command: "",
|
|
expectedVMID: 0,
|
|
expectedOwner: false,
|
|
expectedFound: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
vmid, owner, found := extractVMIDFromCommand(tt.command)
|
|
if vmid != tt.expectedVMID {
|
|
t.Errorf("Expected VMID %d, got %d", tt.expectedVMID, vmid)
|
|
}
|
|
if owner != tt.expectedOwner {
|
|
t.Errorf("Expected owner %v, got %v", tt.expectedOwner, owner)
|
|
}
|
|
if found != tt.expectedFound {
|
|
t.Errorf("Expected found %v, got %v", tt.expectedFound, found)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFormatApprovalNeededToolResult(t *testing.T) {
|
|
result := formatApprovalNeededToolResult("rm -rf /", "tool-123", "Dangerous command")
|
|
|
|
if result == "" {
|
|
t.Error("Expected non-empty result")
|
|
}
|
|
|
|
// Should contain APPROVAL_REQUIRED prefix
|
|
if !hasPrefix(result, "APPROVAL_REQUIRED:") {
|
|
t.Error("Expected APPROVAL_REQUIRED prefix")
|
|
}
|
|
|
|
// Should contain the command
|
|
if !containsString(result, "rm -rf /") {
|
|
t.Error("Expected result to contain command")
|
|
}
|
|
}
|
|
|
|
func TestFormatPolicyBlockedToolResult(t *testing.T) {
|
|
result := formatPolicyBlockedToolResult("rm -rf /", "Command not allowed by policy")
|
|
|
|
if result == "" {
|
|
t.Error("Expected non-empty result")
|
|
}
|
|
|
|
// Should contain POLICY_BLOCKED prefix
|
|
if !hasPrefix(result, "POLICY_BLOCKED:") {
|
|
t.Error("Expected POLICY_BLOCKED prefix")
|
|
}
|
|
}
|
|
|
|
func TestParseApprovalNeededMarker(t *testing.T) {
|
|
// Valid approval needed response
|
|
validResult := formatApprovalNeededToolResult("rm -rf /", "tool-123", "test")
|
|
data, found := parseApprovalNeededMarker(validResult)
|
|
if !found {
|
|
t.Error("Expected to parse approval needed marker")
|
|
}
|
|
if data.Command != "rm -rf /" {
|
|
t.Errorf("Expected command 'rm -rf /', got '%s'", data.Command)
|
|
}
|
|
if data.ToolID != "tool-123" {
|
|
t.Errorf("Expected tool ID 'tool-123', got '%s'", data.ToolID)
|
|
}
|
|
|
|
// Invalid input
|
|
_, found = parseApprovalNeededMarker("not an approval marker")
|
|
if found {
|
|
t.Error("Expected not found for invalid input")
|
|
}
|
|
|
|
// Empty input
|
|
_, found = parseApprovalNeededMarker("")
|
|
if found {
|
|
t.Error("Expected not found for empty input")
|
|
}
|
|
|
|
// Only prefix without JSON
|
|
_, found = parseApprovalNeededMarker("APPROVAL_REQUIRED:")
|
|
if found {
|
|
t.Error("Expected not found for prefix without JSON")
|
|
}
|
|
}
|
|
|
|
// Note: GetDebugContext tests removed - they require persistence to be properly
|
|
// initialized and are better suited for integration tests
|
|
|
|
func TestService_SetProviders(t *testing.T) {
|
|
svc := NewService(nil, nil)
|
|
|
|
// Set up state provider first (needed for patrol service)
|
|
stateProvider := &mockStateProvider{}
|
|
svc.SetStateProvider(stateProvider)
|
|
|
|
// Test SetPatrolThresholdProvider
|
|
thresholdProvider := &mockThresholdProvider{
|
|
nodeCPU: 90,
|
|
nodeMemory: 85,
|
|
}
|
|
svc.SetPatrolThresholdProvider(thresholdProvider)
|
|
// No direct way to verify, but shouldn't panic
|
|
|
|
// Test SetChangeDetector with nil
|
|
svc.SetChangeDetector(nil)
|
|
|
|
// Test SetRemediationLog with nil
|
|
svc.SetRemediationLog(nil)
|
|
|
|
// Test SetPatternDetector with nil
|
|
svc.SetPatternDetector(nil)
|
|
|
|
// Test SetCorrelationDetector with nil
|
|
svc.SetCorrelationDetector(nil)
|
|
}
|
|
|
|
// Helper functions
|
|
func hasPrefix(s, prefix string) bool {
|
|
if len(s) < len(prefix) {
|
|
return false
|
|
}
|
|
return s[:len(prefix)] == prefix
|
|
}
|
|
|
|
func containsString(s, substr string) bool {
|
|
for i := 0; i <= len(s)-len(substr); i++ {
|
|
if s[i:i+len(substr)] == substr {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|