mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 19:41:17 +00:00
Cover gatherGuestIntelligence (discovery matching, instance fallback, reachability via mock prober, edge cases), parsePingOutput parsing, DetectReachabilitySignals, enriched seed context (Service/Reachable columns, quiet mode variants, health issues fallback), and extend signal helper tests for SignalGuestUnreachable.
149 lines
5.7 KiB
Go
149 lines
5.7 KiB
Go
package ai
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/tools"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
)
|
|
|
|
func TestIsInvestigationTool(t *testing.T) {
|
|
if !isInvestigationTool("pulse_query") || !isInvestigationTool("pulse_metrics") || !isInvestigationTool("pulse_storage") || !isInvestigationTool("pulse_read") {
|
|
t.Fatal("expected investigation tools to be recognized")
|
|
}
|
|
if isInvestigationTool("pulse_control") {
|
|
t.Fatal("expected non-investigation tool to be false")
|
|
}
|
|
}
|
|
|
|
func TestFormatToolResult(t *testing.T) {
|
|
result := tools.CallToolResult{
|
|
Content: []tools.Content{
|
|
{Type: "text", Text: "first"},
|
|
{Type: "resource", URI: "file://ignored"},
|
|
{Type: "text", Text: "second"},
|
|
},
|
|
}
|
|
if got := formatToolResult(result); got != "first\nsecond" {
|
|
t.Fatalf("formatToolResult returned %q", got)
|
|
}
|
|
|
|
if got := formatToolResult(tools.CallToolResult{}); got != "" {
|
|
t.Fatalf("expected empty result for no content, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestEvalPromptBuilders(t *testing.T) {
|
|
systemPrompt := buildEvalSystemPrompt()
|
|
if !strings.Contains(systemPrompt, "patrol_report_finding") || !strings.Contains(systemPrompt, "patrol_get_findings") {
|
|
t.Fatalf("expected eval system prompt to include tool instructions")
|
|
}
|
|
|
|
signals := []DetectedSignal{
|
|
{
|
|
SignalType: SignalHighCPU,
|
|
ResourceID: "node-1",
|
|
ResourceName: "node-1",
|
|
ResourceType: "node",
|
|
SuggestedSeverity: "warning",
|
|
Category: "performance",
|
|
Summary: "CPU high",
|
|
Evidence: "cpu=99%",
|
|
},
|
|
}
|
|
userPrompt := buildEvalUserPrompt(signals)
|
|
if !strings.Contains(userPrompt, "Signal 1") || !strings.Contains(userPrompt, "CPU high") || !strings.Contains(userPrompt, "cpu=99%") {
|
|
t.Fatalf("unexpected eval user prompt: %s", userPrompt)
|
|
}
|
|
}
|
|
|
|
func TestSignalHelpersAndFindingsFromSignals(t *testing.T) {
|
|
cases := []struct {
|
|
signal DetectedSignal
|
|
wantKey string
|
|
wantTitle string
|
|
recSubstring string
|
|
}{
|
|
{signal: DetectedSignal{SignalType: SignalSMARTFailure}, wantKey: "smart-failure", wantTitle: "SMART health check failed", recSubstring: "disk"},
|
|
{signal: DetectedSignal{SignalType: SignalHighCPU}, wantKey: "cpu-high", wantTitle: "High CPU usage detected", recSubstring: "CPU"},
|
|
{signal: DetectedSignal{SignalType: SignalHighMemory}, wantKey: "memory-high", wantTitle: "High memory usage detected", recSubstring: "memory"},
|
|
{signal: DetectedSignal{SignalType: SignalHighDisk}, wantKey: "disk-high", wantTitle: "Storage usage is high", recSubstring: "storage"},
|
|
{signal: DetectedSignal{SignalType: SignalBackupFailed}, wantKey: "backup-failed", wantTitle: "Backup failed", recSubstring: "backup"},
|
|
{signal: DetectedSignal{SignalType: SignalBackupStale}, wantKey: "backup-stale", wantTitle: "Backup is stale", recSubstring: "backup"},
|
|
{signal: DetectedSignal{SignalType: SignalActiveAlert}, wantKey: "active-alert", wantTitle: "Active alert detected", recSubstring: "alert"},
|
|
{signal: DetectedSignal{SignalType: SignalGuestUnreachable, ResourceName: "db-server"}, wantKey: "guest-unreachable", wantTitle: "Guest unreachable: db-server", recSubstring: "ping"},
|
|
{signal: DetectedSignal{SignalType: SignalType("unknown")}, wantKey: "deterministic-signal", wantTitle: "Infrastructure signal detected", recSubstring: "Investigate"},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
if got := signalKey(c.signal); got != c.wantKey {
|
|
t.Fatalf("signalKey(%s) = %s, want %s", c.signal.SignalType, got, c.wantKey)
|
|
}
|
|
if got := signalTitle(c.signal); got != c.wantTitle {
|
|
t.Fatalf("signalTitle(%s) = %s, want %s", c.signal.SignalType, got, c.wantTitle)
|
|
}
|
|
if !strings.Contains(defaultRecommendationForSignal(c.signal), c.recSubstring) {
|
|
t.Fatalf("unexpected recommendation for %s: %s", c.signal.SignalType, defaultRecommendationForSignal(c.signal))
|
|
}
|
|
}
|
|
|
|
ps := NewPatrolService(nil, nil)
|
|
adapter := newPatrolFindingCreatorAdapter(ps, models.StateSnapshot{})
|
|
|
|
signals := []DetectedSignal{
|
|
{
|
|
SignalType: SignalHighCPU,
|
|
ResourceID: "node-1",
|
|
ResourceName: "",
|
|
ResourceType: "",
|
|
SuggestedSeverity: "",
|
|
Category: "",
|
|
Summary: "CPU high",
|
|
Evidence: "cpu=99%",
|
|
},
|
|
{
|
|
SignalType: SignalBackupFailed,
|
|
ResourceID: "vm-101",
|
|
ResourceName: "vm-101",
|
|
ResourceType: "vm",
|
|
SuggestedSeverity: "warning",
|
|
Category: "backup",
|
|
Summary: "Backup failed",
|
|
Evidence: "job failed",
|
|
},
|
|
}
|
|
|
|
created := ps.createFindingsFromSignals(adapter, signals)
|
|
if created != len(signals) {
|
|
t.Fatalf("expected %d findings created, got %d", len(signals), created)
|
|
}
|
|
}
|
|
|
|
func TestRunEvaluationPass(t *testing.T) {
|
|
ps := NewPatrolService(&Service{}, nil)
|
|
_, err := ps.runEvaluationPass(context.Background(), nil, []DetectedSignal{{SignalType: SignalHighCPU}})
|
|
if err == nil {
|
|
t.Fatal("expected error when chat service is unavailable")
|
|
}
|
|
|
|
persistence := config.NewConfigPersistence(t.TempDir())
|
|
svc := NewService(persistence, nil)
|
|
mockCS := &patrolMockChatService{
|
|
executePatrolStreamFunc: func(ctx context.Context, req PatrolExecuteRequest, callback ChatStreamCallback) (*PatrolStreamResponse, error) {
|
|
return &PatrolStreamResponse{Content: "ok", InputTokens: 10, OutputTokens: 20}, nil
|
|
},
|
|
}
|
|
svc.SetChatService(mockCS)
|
|
|
|
ps.aiService = svc
|
|
resp, err := ps.runEvaluationPass(context.Background(), nil, []DetectedSignal{{SignalType: SignalHighCPU}})
|
|
if err != nil {
|
|
t.Fatalf("expected evaluation pass to succeed, got %v", err)
|
|
}
|
|
if resp == nil || resp.InputTokens != 10 || resp.OutputTokens != 20 {
|
|
t.Fatalf("unexpected evaluation response: %+v", resp)
|
|
}
|
|
}
|