mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 11:30:15 +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>
834 lines
21 KiB
Go
834 lines
21 KiB
Go
package ai
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/providers"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/resources"
|
|
)
|
|
|
|
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)
|
|
}
|
|
|
|
func TestService_Execute(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "pulse-ai-execute-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)
|
|
|
|
// Set enabled config
|
|
svc.cfg = &config.AIConfig{
|
|
Enabled: true,
|
|
}
|
|
|
|
// Set mock provider
|
|
mockP := &mockProvider{
|
|
chatFunc: func(ctx context.Context, req providers.ChatRequest) (*providers.ChatResponse, error) {
|
|
return &providers.ChatResponse{
|
|
Content: "Hello from mock AI",
|
|
Model: "mock-model",
|
|
}, nil
|
|
},
|
|
}
|
|
svc.provider = mockP
|
|
|
|
req := ExecuteRequest{
|
|
Prompt: "Hello",
|
|
Model: "anthropic:test-model", // Use known provider with no key to force fallback
|
|
}
|
|
|
|
resp, err := svc.Execute(context.Background(), req)
|
|
if err != nil {
|
|
t.Fatalf("Execute failed: %v", err)
|
|
}
|
|
|
|
if resp.Content != "Hello from mock AI" {
|
|
t.Errorf("Expected 'Hello from mock AI', got '%s'", resp.Content)
|
|
}
|
|
}
|
|
|
|
func TestService_Execute_Error(t *testing.T) {
|
|
tmpDir, _ := os.MkdirTemp("", "pulse-ai-execute-error-test-*")
|
|
defer os.RemoveAll(tmpDir)
|
|
persistence := config.NewConfigPersistence(tmpDir)
|
|
svc := NewService(persistence, nil)
|
|
svc.cfg = &config.AIConfig{Enabled: true}
|
|
|
|
mockP := &mockProvider{
|
|
chatFunc: func(ctx context.Context, req providers.ChatRequest) (*providers.ChatResponse, error) {
|
|
return nil, errors.New("API error")
|
|
},
|
|
}
|
|
svc.provider = mockP
|
|
|
|
_, err := svc.Execute(context.Background(), ExecuteRequest{
|
|
Prompt: "Hello",
|
|
Model: "anthropic:test-model",
|
|
})
|
|
if err == nil {
|
|
t.Error("Expected error, got nil")
|
|
}
|
|
}
|
|
|
|
func TestService_ExecuteStream(t *testing.T) {
|
|
tmpDir, _ := os.MkdirTemp("", "pulse-ai-execute-stream-test-*")
|
|
defer os.RemoveAll(tmpDir)
|
|
persistence := config.NewConfigPersistence(tmpDir)
|
|
svc := NewService(persistence, nil)
|
|
svc.cfg = &config.AIConfig{Enabled: true}
|
|
|
|
mockP := &mockProvider{
|
|
chatFunc: func(ctx context.Context, req providers.ChatRequest) (*providers.ChatResponse, error) {
|
|
return &providers.ChatResponse{
|
|
Content: "Streaming response",
|
|
Model: "mock-model",
|
|
}, nil
|
|
},
|
|
}
|
|
svc.provider = mockP
|
|
|
|
var events []StreamEvent
|
|
callback := func(ev StreamEvent) {
|
|
events = append(events, ev)
|
|
}
|
|
|
|
resp, err := svc.ExecuteStream(context.Background(), ExecuteRequest{
|
|
Prompt: "Hello",
|
|
Model: "anthropic:test-model", // Use known provider with no key to force fallback
|
|
}, callback)
|
|
if err != nil {
|
|
t.Fatalf("ExecuteStream failed: %v", err)
|
|
}
|
|
|
|
if resp.Content != "Streaming response" {
|
|
t.Errorf("Expected 'Streaming response', got '%s'", resp.Content)
|
|
}
|
|
|
|
// Should have at least one content event
|
|
foundContent := false
|
|
for _, ev := range events {
|
|
if ev.Type == "content" && ev.Data == "Streaming response" {
|
|
foundContent = true
|
|
break
|
|
}
|
|
}
|
|
if !foundContent {
|
|
t.Error("Expected content event in stream")
|
|
}
|
|
}
|
|
|
|
func TestService_Execute_WithTool(t *testing.T) {
|
|
tmpDir, _ := os.MkdirTemp("", "pulse-ai-execute-tool-test-*")
|
|
defer os.RemoveAll(tmpDir)
|
|
persistence := config.NewConfigPersistence(tmpDir)
|
|
svc := NewService(persistence, nil)
|
|
svc.cfg = &config.AIConfig{Enabled: true}
|
|
|
|
// Mock provider that returns a tool call
|
|
mockP := &mockProvider{
|
|
chatFunc: func(ctx context.Context, req providers.ChatRequest) (*providers.ChatResponse, error) {
|
|
// First call returns a tool call
|
|
if len(req.Messages) <= 1 {
|
|
return &providers.ChatResponse{
|
|
Content: "I will run a command",
|
|
Model: "mock-model",
|
|
ToolCalls: []providers.ToolCall{
|
|
{
|
|
ID: "call-123",
|
|
Name: "run_command",
|
|
Input: map[string]interface{}{"command": "uptime"},
|
|
},
|
|
},
|
|
StopReason: "tool_use",
|
|
}, nil
|
|
}
|
|
// Second call returns the final answer
|
|
return &providers.ChatResponse{
|
|
Content: "Command executed successfully",
|
|
Model: "mock-model",
|
|
}, nil
|
|
},
|
|
}
|
|
svc.provider = mockP
|
|
|
|
resp, err := svc.Execute(context.Background(), ExecuteRequest{
|
|
Prompt: "Run uptime",
|
|
Model: "anthropic:test-model",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Execute failed: %v", err)
|
|
}
|
|
|
|
if len(resp.ToolCalls) != 1 {
|
|
t.Errorf("Expected 1 tool call, got %d", len(resp.ToolCalls))
|
|
}
|
|
if resp.ToolCalls[0].Name != "run_command" {
|
|
t.Errorf("Expected run_command, got %s", resp.ToolCalls[0].Name)
|
|
}
|
|
}
|
|
|
|
func TestService_Execute_SystemPrompt(t *testing.T) {
|
|
tmpDir, _ := os.MkdirTemp("", "pulse-ai-prompt-test-*")
|
|
defer os.RemoveAll(tmpDir)
|
|
persistence := config.NewConfigPersistence(tmpDir)
|
|
svc := NewService(persistence, nil)
|
|
svc.cfg = &config.AIConfig{
|
|
Enabled: true,
|
|
CustomContext: "This is my home lab",
|
|
}
|
|
|
|
mockRP := &mockResourceProvider{
|
|
getStatsFunc: func() resources.StoreStats {
|
|
return resources.StoreStats{TotalResources: 1}
|
|
},
|
|
}
|
|
svc.SetResourceProvider(mockRP)
|
|
|
|
var capturedSystemPrompt string
|
|
mockP := &mockProvider{
|
|
chatFunc: func(ctx context.Context, req providers.ChatRequest) (*providers.ChatResponse, error) {
|
|
capturedSystemPrompt = req.System
|
|
return &providers.ChatResponse{Content: "OK"}, nil
|
|
},
|
|
}
|
|
svc.provider = mockP
|
|
|
|
_, err := svc.Execute(context.Background(), ExecuteRequest{
|
|
Prompt: "Hello",
|
|
Model: "anthropic:test-model",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Execute failed: %v", err)
|
|
}
|
|
|
|
if !containsString(capturedSystemPrompt, "This is my home lab") {
|
|
t.Error("System prompt should contain custom context")
|
|
}
|
|
if !containsString(capturedSystemPrompt, "## Unified Infrastructure View") {
|
|
t.Error("System prompt should contain unified infrastructure view section")
|
|
}
|
|
}
|
|
|
|
func TestService_KnowledgeMethods(t *testing.T) {
|
|
tmpDir, _ := os.MkdirTemp("", "pulse-ai-knowledge-test-*")
|
|
defer os.RemoveAll(tmpDir)
|
|
persistence := config.NewConfigPersistence(tmpDir)
|
|
svc := NewService(persistence, nil)
|
|
|
|
if svc.knowledgeStore == nil {
|
|
t.Fatal("knowledgeStore should be initialized")
|
|
}
|
|
|
|
guestID := "test-guest"
|
|
err := svc.SaveGuestNote(guestID, "Guest 1", "vm", "test", "Title", "Content")
|
|
if err != nil {
|
|
t.Fatalf("SaveGuestNote failed: %v", err)
|
|
}
|
|
|
|
kn, err := svc.GetGuestKnowledge(guestID)
|
|
if err != nil {
|
|
t.Fatalf("GetGuestKnowledge failed: %v", err)
|
|
}
|
|
if len(kn.Notes) != 1 {
|
|
t.Errorf("Expected 1 note, got %d", len(kn.Notes))
|
|
}
|
|
|
|
noteID := kn.Notes[0].ID
|
|
err = svc.DeleteGuestNote(guestID, noteID)
|
|
if err != nil {
|
|
t.Fatalf("DeleteGuestNote failed: %v", err)
|
|
}
|
|
|
|
kn, _ = svc.GetGuestKnowledge(guestID)
|
|
if len(kn.Notes) != 0 {
|
|
t.Error("Note should have been deleted")
|
|
}
|
|
}
|
|
|
|
func TestService_Reload(t *testing.T) {
|
|
tmpDir, _ := os.MkdirTemp("", "pulse-ai-reload-test-*")
|
|
defer os.RemoveAll(tmpDir)
|
|
persistence := config.NewConfigPersistence(tmpDir)
|
|
svc := NewService(persistence, nil)
|
|
|
|
err := svc.Reload()
|
|
if err != nil {
|
|
t.Fatalf("Reload failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestService_ListModels(t *testing.T) {
|
|
tmpDir, _ := os.MkdirTemp("", "pulse-ai-list-models-test-*")
|
|
defer os.RemoveAll(tmpDir)
|
|
persistence := config.NewConfigPersistence(tmpDir)
|
|
svc := NewService(persistence, nil)
|
|
|
|
// Mock config with no providers
|
|
svc.cfg = &config.AIConfig{Enabled: true}
|
|
|
|
// Should return empty list when no providers configured
|
|
models, err := svc.ListModels(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("ListModels failed: %v", err)
|
|
}
|
|
if len(models) != 0 {
|
|
t.Errorf("Expected 0 models, got %d", len(models))
|
|
}
|
|
}
|
|
|
|
func TestService_TestConnection(t *testing.T) {
|
|
tmpDir, _ := os.MkdirTemp("", "pulse-ai-test-conn-*")
|
|
defer os.RemoveAll(tmpDir)
|
|
persistence := config.NewConfigPersistence(tmpDir)
|
|
svc := NewService(persistence, nil)
|
|
|
|
// Test with no config
|
|
err := svc.TestConnection(context.Background())
|
|
if err == nil {
|
|
t.Error("Expected error with no config")
|
|
}
|
|
}
|
|
|
|
func TestService_SetMetricsHistoryProvider(t *testing.T) {
|
|
svc := NewService(nil, nil)
|
|
svc.SetMetricsHistoryProvider(nil)
|
|
if svc.stateProvider != nil {
|
|
t.Error("Expected stateProvider to be nil")
|
|
}
|
|
}
|
|
|
|
func TestService_LicenseGating(t *testing.T) {
|
|
svc := NewService(nil, nil)
|
|
|
|
// Default should be true when no checker is set (dev mode)
|
|
if !svc.HasLicenseFeature("test") {
|
|
t.Error("Expected true for no license checker (dev mode)")
|
|
}
|
|
|
|
mockLC := &mockLicenseChecker{hasFeature: true}
|
|
svc.SetLicenseChecker(mockLC)
|
|
|
|
if !svc.HasLicenseFeature("test") {
|
|
t.Error("Expected true with mock license checker")
|
|
}
|
|
|
|
tier, ok := svc.GetLicenseState()
|
|
if tier != "active" || !ok {
|
|
t.Errorf("Expected active tier from mock, got %s, %v", tier, ok)
|
|
}
|
|
}
|
|
|
|
func TestService_IsAutonomous(t *testing.T) {
|
|
svc := NewService(nil, nil)
|
|
svc.cfg = &config.AIConfig{AutonomousMode: true}
|
|
if !svc.IsAutonomous() {
|
|
t.Error("Expected true")
|
|
}
|
|
|
|
svc.cfg.AutonomousMode = false
|
|
if svc.IsAutonomous() {
|
|
t.Error("Expected false")
|
|
}
|
|
}
|
|
|
|
type mockLicenseChecker struct {
|
|
hasFeature bool
|
|
}
|
|
|
|
func (m *mockLicenseChecker) HasFeature(f string) bool { return m.hasFeature }
|
|
func (m *mockLicenseChecker) GetLicenseStateString() (string, bool) {
|
|
return "active", true
|
|
}
|
|
|
|
func TestService_RunCommand(t *testing.T) {
|
|
svc := NewService(nil, nil)
|
|
// Should fail with no agent server
|
|
resp, err := svc.RunCommand(context.Background(), RunCommandRequest{Command: "uptime"})
|
|
if err != nil {
|
|
t.Fatalf("RunCommand failed: %v", err)
|
|
}
|
|
if resp.Success {
|
|
t.Error("Expected success=false with no agent server")
|
|
}
|
|
}
|
|
|
|
func TestService_ExecuteTool(t *testing.T) {
|
|
svc := NewService(nil, nil)
|
|
ctx := context.Background()
|
|
req := ExecuteRequest{Prompt: "test"}
|
|
tc := providers.ToolCall{
|
|
ID: "call-1",
|
|
Name: "run_command",
|
|
Input: map[string]interface{}{"command": "uptime"},
|
|
}
|
|
|
|
output, exec := svc.executeTool(ctx, req, tc)
|
|
if !containsString(output, "agent server not available") {
|
|
t.Errorf("Expected agent server error, got: %s", output)
|
|
}
|
|
if exec.Success {
|
|
t.Error("Expected failure")
|
|
}
|
|
if exec.Name != "run_command" {
|
|
t.Errorf("Expected run_command, got %s", exec.Name)
|
|
}
|
|
}
|
|
|
|
// Helper functions (restored)
|
|
func hasPrefix(s, prefix string) bool {
|
|
return strings.HasPrefix(s, prefix)
|
|
}
|
|
|
|
func containsString(s, substr string) bool {
|
|
return strings.Contains(s, substr)
|
|
}
|