Files
pulse/internal/ai/tools/tools_patrol_test.go
T
2026-07-16 06:40:17 +01:00

837 lines
28 KiB
Go

package tools
import (
"context"
"encoding/json"
"fmt"
"strings"
"testing"
"github.com/rcourtman/pulse-go-rewrite/internal/agentcapabilities"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- Mock PatrolFindingCreator ---
type mockPatrolFindingCreator struct {
createFindingFunc func(input PatrolFindingInput) (string, bool, error)
assessFindingFunc func(input PatrolFindingAssessmentInput) error
resolveFindingFunc func(findingID, reason string) error
getActiveFn func(resourceID, minSeverity string) []PatrolFindingInfo
// Track calls for assertions
createCalls []PatrolFindingInput
assessCalls []PatrolFindingAssessmentInput
resolveCalls []struct {
FindingID string
Reason string
}
getCalls []struct {
ResourceID string
MinSeverity string
}
checked bool
}
func (m *mockPatrolFindingCreator) AssessFinding(input PatrolFindingAssessmentInput) error {
m.assessCalls = append(m.assessCalls, input)
if m.assessFindingFunc != nil {
return m.assessFindingFunc(input)
}
return nil
}
func (m *mockPatrolFindingCreator) CreateFinding(input PatrolFindingInput) (string, bool, error) {
m.createCalls = append(m.createCalls, input)
if m.createFindingFunc != nil {
return m.createFindingFunc(input)
}
return "finding-123", true, nil
}
func (m *mockPatrolFindingCreator) ResolveFinding(findingID, reason string) error {
m.resolveCalls = append(m.resolveCalls, struct {
FindingID string
Reason string
}{findingID, reason})
if m.resolveFindingFunc != nil {
return m.resolveFindingFunc(findingID, reason)
}
return nil
}
func (m *mockPatrolFindingCreator) GetActiveFindings(resourceID, minSeverity string) []PatrolFindingInfo {
m.getCalls = append(m.getCalls, struct {
ResourceID string
MinSeverity string
}{resourceID, minSeverity})
m.checked = true
if m.getActiveFn != nil {
return m.getActiveFn(resourceID, minSeverity)
}
return nil
}
func (m *mockPatrolFindingCreator) HasCheckedFindings() bool {
return m.checked
}
// --- Helper to create executor with patrol creator ---
func newPatrolTestExecutor(creator PatrolFindingCreator) *PulseToolExecutor {
exec := NewPulseToolExecutor(ExecutorConfig{})
exec.SetPatrolFindingCreator(creator)
return exec
}
func validReportArgs() map[string]interface{} {
return map[string]interface{}{
"key": "high-cpu",
"severity": "warning",
"category": "performance",
"resource_id": "node/pve1",
"resource_name": "pve1",
"resource_type": "node",
"title": "High CPU usage on pve1",
"description": "CPU usage is at 92% for the last 15 minutes",
"recommendation": "Inspect the busiest processes and rebalance workloads if the pressure persists.",
"evidence": "CPU usage remained at 92% for 15 minutes.",
}
}
// --- patrol_report_finding tests ---
func TestHandlePatrolReportFinding_NilCreator(t *testing.T) {
exec := NewPulseToolExecutor(ExecutorConfig{})
// Creator is nil (not in a patrol run)
result, err := handlePatrolReportFinding(context.Background(), exec, validReportArgs())
require.NoError(t, err)
text := extractText(result)
assert.Contains(t, text, "only available during a patrol run")
}
func TestHandlePatrolReportFinding_RequiresGetFindings(t *testing.T) {
creator := &mockPatrolFindingCreator{}
exec := newPatrolTestExecutor(creator)
result, err := handlePatrolReportFinding(context.Background(), exec, validReportArgs())
require.NoError(t, err)
text := extractText(result)
assert.Contains(t, text, "patrol_get_findings")
}
func TestHandlePatrolReportFinding_ValidInput(t *testing.T) {
creator := &mockPatrolFindingCreator{checked: true}
exec := newPatrolTestExecutor(creator)
args := validReportArgs()
args["recommendation"] = "Consider migrating VMs to reduce load"
args["evidence"] = "CPU at 92% over 15 minutes"
result, err := handlePatrolReportFinding(context.Background(), exec, args)
require.NoError(t, err)
var parsed map[string]interface{}
text := extractText(result)
require.NoError(t, json.Unmarshal([]byte(text), &parsed))
assert.Equal(t, true, parsed["ok"])
assert.Equal(t, "finding-123", parsed[agentcapabilities.FindingIDArgumentName])
assert.Equal(t, true, parsed["is_new"])
assert.Equal(t, true, result.StructuredContent["ok"])
assert.Equal(t, "finding-123", result.StructuredContent[agentcapabilities.FindingIDArgumentName])
// Verify the creator was called with correct input
require.Len(t, creator.createCalls, 1)
input := creator.createCalls[0]
assert.Equal(t, "high-cpu", input.Key)
assert.Equal(t, "warning", input.Severity)
assert.Equal(t, "performance", input.Category)
assert.Equal(t, "node/pve1", input.ResourceID)
assert.Equal(t, "pve1", input.ResourceName)
assert.Equal(t, "node", input.ResourceType)
assert.Equal(t, "High CPU usage on pve1", input.Title)
assert.Equal(t, "CPU usage is at 92% for the last 15 minutes", input.Description)
assert.Equal(t, "Consider migrating VMs to reduce load", input.Recommendation)
assert.Equal(t, "CPU at 92% over 15 minutes", input.Evidence)
}
func TestHandlePatrolReportFinding_PassesImpactWhenAuthored(t *testing.T) {
creator := &mockPatrolFindingCreator{checked: true}
exec := newPatrolTestExecutor(creator)
args := validReportArgs()
args["impact"] = "Customer-facing latency rises and request queue depth grows; sustained CPU pressure will time out user requests."
result, err := handlePatrolReportFinding(context.Background(), exec, args)
require.NoError(t, err)
var parsed map[string]interface{}
require.NoError(t, json.Unmarshal([]byte(extractText(result)), &parsed))
assert.Equal(t, true, parsed["ok"])
require.Len(t, creator.createCalls, 1)
assert.Equal(t,
"Customer-facing latency rises and request queue depth grows; sustained CPU pressure will time out user requests.",
creator.createCalls[0].Impact,
)
}
func TestHandlePatrolReportFinding_LeavesImpactEmptyWhenOmitted(t *testing.T) {
creator := &mockPatrolFindingCreator{checked: true}
exec := newPatrolTestExecutor(creator)
// validReportArgs intentionally omits impact, mirroring the LLM choosing
// to leave it empty rather than fabricating one. The contract is that an
// omitted impact is honored verbatim — no default copy is synthesized.
result, err := handlePatrolReportFinding(context.Background(), exec, validReportArgs())
require.NoError(t, err)
var parsed map[string]interface{}
require.NoError(t, json.Unmarshal([]byte(extractText(result)), &parsed))
assert.Equal(t, true, parsed["ok"])
require.Len(t, creator.createCalls, 1)
assert.Equal(t, "", creator.createCalls[0].Impact)
}
func TestHandlePatrolReportFinding_AcceptsPhysicalDiskResourceType(t *testing.T) {
creator := &mockPatrolFindingCreator{checked: true}
exec := newPatrolTestExecutor(creator)
args := validReportArgs()
args["resource_id"] = "disk-1"
args["resource_name"] = "/dev/sda"
args["resource_type"] = "physical_disk"
args["title"] = "Disk health warning"
args["description"] = "Disk health reported FAILED"
result, err := handlePatrolReportFinding(context.Background(), exec, args)
require.NoError(t, err)
var parsed map[string]interface{}
text := extractText(result)
require.NoError(t, json.Unmarshal([]byte(text), &parsed))
assert.Equal(t, true, parsed["ok"])
require.Len(t, creator.createCalls, 1)
assert.Equal(t, "physical_disk", creator.createCalls[0].ResourceType)
}
func TestHandlePatrolReportFinding_MissingRequiredFields(t *testing.T) {
creator := &mockPatrolFindingCreator{checked: true}
exec := newPatrolTestExecutor(creator)
tests := []struct {
name string
remove string
missing string
}{
{"missing key", "key", "key"},
{"missing severity", "severity", "severity"},
{"missing category", "category", "category"},
{"missing resource_id", "resource_id", "resource_id"},
{"missing resource_name", "resource_name", "resource_name"},
{"missing resource_type", "resource_type", "resource_type"},
{"missing title", "title", "title"},
{"missing description", "description", "description"},
{"missing recommendation", "recommendation", "recommendation"},
{"missing evidence", "evidence", "evidence"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
args := validReportArgs()
delete(args, tt.remove)
result, err := handlePatrolReportFinding(context.Background(), exec, args)
require.NoError(t, err)
text := extractText(result)
assert.Contains(t, text, "missing required fields")
assert.Contains(t, text, tt.missing)
})
}
}
func TestHandlePatrolReportFinding_AllRequiredFieldsMissing(t *testing.T) {
creator := &mockPatrolFindingCreator{checked: true}
exec := newPatrolTestExecutor(creator)
result, err := handlePatrolReportFinding(context.Background(), exec, map[string]interface{}{})
require.NoError(t, err)
text := extractText(result)
assert.Contains(t, text, "missing required fields")
// Should mention all required fields.
for _, field := range []string{"key", "severity", "category", "resource_id", "resource_name", "resource_type", "title", "description", "recommendation", "evidence"} {
assert.Contains(t, text, field)
}
}
func TestHandlePatrolReportFinding_InvalidSeverity(t *testing.T) {
creator := &mockPatrolFindingCreator{checked: true}
exec := newPatrolTestExecutor(creator)
args := validReportArgs()
args["severity"] = "extreme"
result, err := handlePatrolReportFinding(context.Background(), exec, args)
require.NoError(t, err)
text := extractText(result)
assert.Contains(t, text, "invalid severity")
assert.Contains(t, text, "extreme")
}
func TestHandlePatrolReportFinding_InvalidCategory(t *testing.T) {
creator := &mockPatrolFindingCreator{checked: true}
exec := newPatrolTestExecutor(creator)
args := validReportArgs()
args["category"] = "networking"
result, err := handlePatrolReportFinding(context.Background(), exec, args)
require.NoError(t, err)
text := extractText(result)
assert.Contains(t, text, "invalid category")
assert.Contains(t, text, "networking")
}
func TestHandlePatrolReportFinding_RejectsLegacyResourceTypeAliases(t *testing.T) {
for _, resourceType := range []string{"host", "container", "docker", "docker-container", "docker_container", "app_container", "k8s_cluster", "kubernetes_cluster"} {
t.Run(resourceType, func(t *testing.T) {
creator := &mockPatrolFindingCreator{checked: true}
exec := newPatrolTestExecutor(creator)
args := validReportArgs()
args["resource_type"] = resourceType
result, err := handlePatrolReportFinding(context.Background(), exec, args)
require.NoError(t, err)
text := extractText(result)
assert.Contains(t, text, "unsupported resource_type")
assert.Contains(t, text, resourceType)
assert.Empty(t, creator.createCalls)
})
}
}
func TestHandlePatrolReportFinding_InvalidResourceType(t *testing.T) {
creator := &mockPatrolFindingCreator{checked: true}
exec := newPatrolTestExecutor(creator)
args := validReportArgs()
args["resource_type"] = "bad-type"
result, err := handlePatrolReportFinding(context.Background(), exec, args)
require.NoError(t, err)
text := extractText(result)
assert.Contains(t, text, "unsupported resource_type")
assert.Contains(t, text, "bad-type")
}
func TestHandlePatrolReportFinding_ValidSeverities(t *testing.T) {
for _, sev := range []string{"critical", "warning"} {
t.Run(sev, func(t *testing.T) {
creator := &mockPatrolFindingCreator{checked: true}
exec := newPatrolTestExecutor(creator)
args := validReportArgs()
args["severity"] = sev
result, err := handlePatrolReportFinding(context.Background(), exec, args)
require.NoError(t, err)
text := extractText(result)
assert.NotContains(t, text, "invalid severity")
var parsed map[string]interface{}
require.NoError(t, json.Unmarshal([]byte(text), &parsed))
assert.Equal(t, true, parsed["ok"])
})
}
}
func TestHandlePatrolReportFinding_RejectedSeverities(t *testing.T) {
for _, sev := range []string{"watch", "info"} {
t.Run(sev, func(t *testing.T) {
creator := &mockPatrolFindingCreator{checked: true}
exec := newPatrolTestExecutor(creator)
args := validReportArgs()
args["severity"] = sev
result, err := handlePatrolReportFinding(context.Background(), exec, args)
require.NoError(t, err)
text := extractText(result)
assert.Contains(t, text, "invalid severity")
})
}
}
func TestHandlePatrolReportFinding_ValidCategories(t *testing.T) {
for _, cat := range []string{"performance", "capacity", "reliability", "backup", "security", "general"} {
t.Run(cat, func(t *testing.T) {
creator := &mockPatrolFindingCreator{checked: true}
exec := newPatrolTestExecutor(creator)
args := validReportArgs()
args["category"] = cat
result, err := handlePatrolReportFinding(context.Background(), exec, args)
require.NoError(t, err)
text := extractText(result)
assert.NotContains(t, text, "invalid category")
var parsed map[string]interface{}
require.NoError(t, json.Unmarshal([]byte(text), &parsed))
assert.Equal(t, true, parsed["ok"])
})
}
}
func TestHandlePatrolReportFinding_DuplicateFinding(t *testing.T) {
creator := &mockPatrolFindingCreator{
createFindingFunc: func(input PatrolFindingInput) (string, bool, error) {
return "finding-existing", false, nil // isNew = false
},
checked: true,
}
exec := newPatrolTestExecutor(creator)
result, err := handlePatrolReportFinding(context.Background(), exec, validReportArgs())
require.NoError(t, err)
var parsed map[string]interface{}
text := extractText(result)
require.NoError(t, json.Unmarshal([]byte(text), &parsed))
assert.Equal(t, true, parsed["ok"])
assert.Equal(t, "finding-existing", parsed[agentcapabilities.FindingIDArgumentName])
assert.Equal(t, false, parsed["is_new"])
assert.Equal(t, "finding-existing", result.StructuredContent[agentcapabilities.FindingIDArgumentName])
}
func TestHandlePatrolReportFinding_CreatorError(t *testing.T) {
creator := &mockPatrolFindingCreator{
createFindingFunc: func(input PatrolFindingInput) (string, bool, error) {
return "", false, fmt.Errorf("validation failed: CPU is below threshold")
},
checked: true,
}
exec := newPatrolTestExecutor(creator)
result, err := handlePatrolReportFinding(context.Background(), exec, validReportArgs())
require.NoError(t, err)
text := extractText(result)
assert.Contains(t, text, "failed to create finding")
assert.Contains(t, text, "validation failed")
}
func TestHandlePatrolReportFinding_RejectsBlankGroundingFields(t *testing.T) {
for _, field := range []string{"recommendation", "evidence"} {
t.Run(field, func(t *testing.T) {
creator := &mockPatrolFindingCreator{checked: true}
exec := newPatrolTestExecutor(creator)
args := validReportArgs()
args[field] = " \n\t"
result, err := handlePatrolReportFinding(context.Background(), exec, args)
require.NoError(t, err)
assert.Contains(t, extractText(result), "missing required fields")
assert.Contains(t, extractText(result), field)
assert.Empty(t, creator.createCalls)
})
}
}
// --- patrol_assess_finding tests ---
func TestHandlePatrolAssessFinding_RequiresGetFindings(t *testing.T) {
creator := &mockPatrolFindingCreator{}
exec := newPatrolTestExecutor(creator)
result, err := handlePatrolAssessFinding(context.Background(), exec, map[string]interface{}{
agentcapabilities.FindingIDArgumentName: "finding-1",
"verdict": "present",
"evidence": "restart count is 12",
agentcapabilities.ReasonArgumentName: "the restart loop remains active",
})
require.NoError(t, err)
assert.Contains(t, extractText(result), "patrol_get_findings")
assert.Empty(t, creator.assessCalls)
}
func TestHandlePatrolAssessFinding_AcceptsExplicitVerdicts(t *testing.T) {
for _, verdict := range []string{"present", "resolved", "uncertain"} {
t.Run(verdict, func(t *testing.T) {
creator := &mockPatrolFindingCreator{checked: true}
exec := newPatrolTestExecutor(creator)
result, err := handlePatrolAssessFinding(context.Background(), exec, map[string]interface{}{
agentcapabilities.FindingIDArgumentName: "finding-1",
"verdict": verdict,
"evidence": "current evidence",
agentcapabilities.ReasonArgumentName: "evidence supports verdict",
})
require.NoError(t, err)
require.Len(t, creator.assessCalls, 1)
assert.Equal(t, verdict, creator.assessCalls[0].Verdict)
assert.Contains(t, extractText(result), verdict)
})
}
}
func TestHandlePatrolAssessFinding_RejectsIncompleteOrInvalidVerdict(t *testing.T) {
creator := &mockPatrolFindingCreator{checked: true}
exec := newPatrolTestExecutor(creator)
result, err := handlePatrolAssessFinding(context.Background(), exec, map[string]interface{}{
agentcapabilities.FindingIDArgumentName: "finding-1",
"verdict": "fixed-ish",
"evidence": "current evidence",
agentcapabilities.ReasonArgumentName: "reason",
})
require.NoError(t, err)
assert.Contains(t, extractText(result), "invalid verdict")
result, err = handlePatrolAssessFinding(context.Background(), exec, map[string]interface{}{
agentcapabilities.FindingIDArgumentName: "finding-1",
"verdict": "present",
})
require.NoError(t, err)
assert.Contains(t, extractText(result), "evidence")
assert.Empty(t, creator.assessCalls)
}
// --- patrol_resolve_finding tests ---
func TestHandlePatrolResolveFinding_NilCreator(t *testing.T) {
exec := NewPulseToolExecutor(ExecutorConfig{})
result, err := handlePatrolResolveFinding(context.Background(), exec, map[string]interface{}{
agentcapabilities.FindingIDArgumentName: "f-123",
agentcapabilities.ReasonArgumentName: "CPU recovered",
})
require.NoError(t, err)
text := extractText(result)
assert.Contains(t, text, "only available during a patrol run")
}
func TestHandlePatrolResolveFinding_RequiresGetFindings(t *testing.T) {
creator := &mockPatrolFindingCreator{}
exec := newPatrolTestExecutor(creator)
result, err := handlePatrolResolveFinding(context.Background(), exec, map[string]interface{}{
agentcapabilities.FindingIDArgumentName: "f-123",
agentcapabilities.ReasonArgumentName: "CPU recovered",
})
require.NoError(t, err)
text := extractText(result)
assert.Contains(t, text, "patrol_get_findings")
}
func TestHandlePatrolResolveFinding_ValidInput(t *testing.T) {
creator := &mockPatrolFindingCreator{checked: true}
exec := newPatrolTestExecutor(creator)
result, err := handlePatrolResolveFinding(context.Background(), exec, map[string]interface{}{
agentcapabilities.FindingIDArgumentName: "f-123",
agentcapabilities.ReasonArgumentName: "CPU has returned to 35%",
})
require.NoError(t, err)
var parsed map[string]interface{}
text := extractText(result)
require.NoError(t, json.Unmarshal([]byte(text), &parsed))
assert.Equal(t, true, parsed["ok"])
assert.Equal(t, true, parsed["resolved"])
assert.Equal(t, true, result.StructuredContent["ok"])
assert.Equal(t, true, result.StructuredContent["resolved"])
require.Len(t, creator.assessCalls, 1)
assert.Equal(t, "f-123", creator.assessCalls[0].FindingID)
assert.Equal(t, "resolved", creator.assessCalls[0].Verdict)
assert.Equal(t, "CPU has returned to 35%", creator.assessCalls[0].Evidence)
}
func TestHandlePatrolResolveFinding_MissingFindingID(t *testing.T) {
creator := &mockPatrolFindingCreator{checked: true}
exec := newPatrolTestExecutor(creator)
result, err := handlePatrolResolveFinding(context.Background(), exec, map[string]interface{}{
agentcapabilities.ReasonArgumentName: "CPU recovered",
})
require.NoError(t, err)
text := extractText(result)
assert.Contains(t, text, "missing required field: finding_id")
}
func TestHandlePatrolResolveFinding_MissingReason(t *testing.T) {
creator := &mockPatrolFindingCreator{checked: true}
exec := newPatrolTestExecutor(creator)
result, err := handlePatrolResolveFinding(context.Background(), exec, map[string]interface{}{
agentcapabilities.FindingIDArgumentName: "f-123",
})
require.NoError(t, err)
text := extractText(result)
assert.Contains(t, text, "missing required field: reason")
}
func TestHandlePatrolResolveFinding_ResolveError(t *testing.T) {
creator := &mockPatrolFindingCreator{
assessFindingFunc: func(input PatrolFindingAssessmentInput) error {
return fmt.Errorf("finding not found: %s", input.FindingID)
},
checked: true,
}
exec := newPatrolTestExecutor(creator)
result, err := handlePatrolResolveFinding(context.Background(), exec, map[string]interface{}{
agentcapabilities.FindingIDArgumentName: "f-nonexistent",
agentcapabilities.ReasonArgumentName: "CPU recovered",
})
require.NoError(t, err)
text := extractText(result)
assert.Contains(t, text, "failed to resolve finding")
assert.Contains(t, text, "finding not found")
}
// --- patrol_get_findings tests ---
func TestHandlePatrolGetFindings_NilCreator(t *testing.T) {
exec := NewPulseToolExecutor(ExecutorConfig{})
result, err := handlePatrolGetFindings(context.Background(), exec, map[string]interface{}{})
require.NoError(t, err)
text := extractText(result)
assert.Contains(t, text, "only available during a patrol run")
}
func TestHandlePatrolGetFindings_EmptyResult(t *testing.T) {
creator := &mockPatrolFindingCreator{}
exec := newPatrolTestExecutor(creator)
result, err := handlePatrolGetFindings(context.Background(), exec, map[string]interface{}{})
require.NoError(t, err)
var parsed map[string]interface{}
text := extractText(result)
require.NoError(t, json.Unmarshal([]byte(text), &parsed))
assert.Equal(t, true, parsed["ok"])
assert.Equal(t, float64(0), parsed["count"])
assert.Equal(t, true, result.StructuredContent["ok"])
assert.Equal(t, "0", fmt.Sprint(result.StructuredContent["count"]))
}
func TestHandlePatrolGetFindings_WithFindings(t *testing.T) {
creator := &mockPatrolFindingCreator{
getActiveFn: func(resourceID, minSeverity string) []PatrolFindingInfo {
return []PatrolFindingInfo{
{
ID: "f-1",
Key: "high-cpu",
Severity: "warning",
Category: "performance",
ResourceID: "node/pve1",
ResourceName: "pve1",
ResourceType: "node",
Title: "High CPU on pve1",
DetectedAt: "2024-01-15T10:00:00Z",
},
{
ID: "f-2",
Key: "high-disk",
Severity: "critical",
Category: "capacity",
ResourceID: "node/pve2",
ResourceName: "pve2",
ResourceType: "node",
Title: "Disk almost full on pve2",
DetectedAt: "2024-01-15T09:00:00Z",
},
}
},
}
exec := newPatrolTestExecutor(creator)
result, err := handlePatrolGetFindings(context.Background(), exec, map[string]interface{}{})
require.NoError(t, err)
var parsed map[string]interface{}
text := extractText(result)
require.NoError(t, json.Unmarshal([]byte(text), &parsed))
assert.Equal(t, true, parsed["ok"])
assert.Equal(t, float64(2), parsed["count"])
assert.Equal(t, true, result.StructuredContent["ok"])
assert.Equal(t, "2", fmt.Sprint(result.StructuredContent["count"]))
findings := parsed["findings"].([]interface{})
require.Len(t, findings, 2)
f1 := findings[0].(map[string]interface{})
assert.Equal(t, "f-1", f1["id"])
assert.Equal(t, "high-cpu", f1["key"])
assert.Equal(t, "warning", f1["severity"])
f2 := findings[1].(map[string]interface{})
assert.Equal(t, "f-2", f2["id"])
assert.Equal(t, "critical", f2["severity"])
}
func TestHandlePatrolGetFindings_WithFilters(t *testing.T) {
creator := &mockPatrolFindingCreator{
getActiveFn: func(resourceID, minSeverity string) []PatrolFindingInfo {
// Verify filters are passed through
return nil
},
}
exec := newPatrolTestExecutor(creator)
_, err := handlePatrolGetFindings(context.Background(), exec, map[string]interface{}{
"resource_id": "node/pve1",
"severity": "warning",
})
require.NoError(t, err)
require.Len(t, creator.getCalls, 1)
assert.Equal(t, "node/pve1", creator.getCalls[0].ResourceID)
assert.Equal(t, "warning", creator.getCalls[0].MinSeverity)
}
func TestHandlePatrolGetFindings_NoFilters(t *testing.T) {
creator := &mockPatrolFindingCreator{}
exec := newPatrolTestExecutor(creator)
_, err := handlePatrolGetFindings(context.Background(), exec, map[string]interface{}{})
require.NoError(t, err)
require.Len(t, creator.getCalls, 1)
assert.Equal(t, "", creator.getCalls[0].ResourceID)
assert.Equal(t, "", creator.getCalls[0].MinSeverity)
}
// --- SetPatrolFindingCreator tests ---
func TestSetPatrolFindingCreator(t *testing.T) {
exec := NewPulseToolExecutor(ExecutorConfig{})
// Initially nil
assert.Nil(t, exec.GetPatrolFindingCreator())
// Set a creator
creator := &mockPatrolFindingCreator{}
exec.SetPatrolFindingCreator(creator)
assert.NotNil(t, exec.GetPatrolFindingCreator())
// Clear it
exec.SetPatrolFindingCreator(nil)
assert.Nil(t, exec.GetPatrolFindingCreator())
}
// --- Tool registration tests ---
func TestPatrolToolsRegistered(t *testing.T) {
exec := NewPulseToolExecutor(ExecutorConfig{})
// Patrol tools should be registered but availability depends on patrolFindingCreator
tools := exec.registry.ListTools(InvocationPolicy{
ControlLevel: ControlLevelReadOnly,
PulseStateAllowlist: patrolDetectionPulseStateAllowlist(),
Profile: ProfilePatrolDetection,
})
found := map[string]bool{}
var resolveTool Tool
var reportTool Tool
var getFindingsTool Tool
for _, tool := range tools {
if tool.Name == "patrol_report_finding" || tool.Name == "patrol_assess_finding" || tool.Name == "patrol_resolve_finding" || tool.Name == "patrol_get_findings" {
found[tool.Name] = true
}
if tool.Name == "patrol_resolve_finding" {
resolveTool = tool
}
if tool.Name == "patrol_report_finding" {
reportTool = tool
}
if tool.Name == "patrol_get_findings" {
getFindingsTool = tool
}
}
assert.True(t, found["patrol_report_finding"], "patrol_report_finding should be registered")
assert.True(t, found["patrol_assess_finding"], "patrol_assess_finding should be registered")
assert.True(t, found["patrol_resolve_finding"], "patrol_resolve_finding should be registered")
assert.True(t, found["patrol_get_findings"], "patrol_get_findings should be registered")
require.NotEmpty(t, resolveTool.Name)
assert.Contains(t, resolveTool.InputSchema.Required, agentcapabilities.FindingIDArgumentName)
assert.Contains(t, resolveTool.InputSchema.Required, agentcapabilities.ReasonArgumentName)
assert.Contains(t, resolveTool.InputSchema.Properties, agentcapabilities.FindingIDArgumentName)
assert.Contains(t, resolveTool.InputSchema.Properties, agentcapabilities.ReasonArgumentName)
require.NotEmpty(t, reportTool.Name)
assert.Contains(t, reportTool.Description, "failed health check")
assert.Contains(t, reportTool.Description, "root cause is unknown")
assert.Contains(t, reportTool.Description, "safe, actionable recommendation")
assert.Equal(t, PatrolReportFindingRequiredArguments(), reportTool.InputSchema.Required)
assert.Contains(t, reportTool.Description, strings.Join(PatrolReportFindingRequiredArguments(), ", "))
assert.Contains(t, reportTool.Description, "reporting several findings in parallel")
assert.Contains(t, reportTool.InputSchema.Required, "recommendation")
assert.Contains(t, reportTool.InputSchema.Required, "evidence")
assert.NotContains(t, reportTool.InputSchema.Required, "impact")
require.NotEmpty(t, getFindingsTool.Name)
assert.Contains(t, getFindingsTool.Description, "exactly once")
}
func TestPatrolReportFindingRequiredArgumentsReturnsCopy(t *testing.T) {
first := PatrolReportFindingRequiredArguments()
first[0] = "mutated"
second := PatrolReportFindingRequiredArguments()
assert.Equal(t, "key", second[0])
}
func TestPatrolToolsAvailability(t *testing.T) {
exec := NewPulseToolExecutor(ExecutorConfig{})
// Without creator, patrol tools should not be available
assert.False(t, exec.isToolAvailable("patrol_report_finding"))
assert.False(t, exec.isToolAvailable("patrol_assess_finding"))
assert.False(t, exec.isToolAvailable("patrol_resolve_finding"))
assert.False(t, exec.isToolAvailable("patrol_get_findings"))
// Set creator
exec.SetPatrolFindingCreator(&mockPatrolFindingCreator{})
// Now they should be available
assert.True(t, exec.isToolAvailable("patrol_report_finding"))
assert.True(t, exec.isToolAvailable("patrol_assess_finding"))
assert.True(t, exec.isToolAvailable("patrol_resolve_finding"))
assert.True(t, exec.isToolAvailable("patrol_get_findings"))
}
// --- Helper ---
func extractText(result CallToolResult) string {
if len(result.Content) == 0 {
return ""
}
return result.Content[0].Text
}