mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-07 08:57:12 +00:00
433 lines
14 KiB
Go
433 lines
14 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
type stubDiscoveryProvider struct {
|
|
lastGetResourceType string
|
|
lastGetTargetID string
|
|
lastGetResourceID string
|
|
getResp *ResourceDiscoveryInfo
|
|
getErr error
|
|
|
|
lastListType string
|
|
lastListTargetID string
|
|
listResp []*ResourceDiscoveryInfo
|
|
listErr error
|
|
}
|
|
|
|
func (s *stubDiscoveryProvider) GetDiscovery(_ string) (*ResourceDiscoveryInfo, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (s *stubDiscoveryProvider) GetDiscoveryByResource(resourceType, targetID, resourceID string) (*ResourceDiscoveryInfo, error) {
|
|
s.lastGetResourceType = resourceType
|
|
s.lastGetTargetID = targetID
|
|
s.lastGetResourceID = resourceID
|
|
return s.getResp, s.getErr
|
|
}
|
|
|
|
func (s *stubDiscoveryProvider) ListDiscoveries() ([]*ResourceDiscoveryInfo, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (s *stubDiscoveryProvider) ListDiscoveriesByType(resourceType string) ([]*ResourceDiscoveryInfo, error) {
|
|
s.lastListType = resourceType
|
|
return s.listResp, s.listErr
|
|
}
|
|
|
|
func (s *stubDiscoveryProvider) ListDiscoveriesByTarget(targetID string) ([]*ResourceDiscoveryInfo, error) {
|
|
s.lastListTargetID = targetID
|
|
return s.listResp, s.listErr
|
|
}
|
|
|
|
func (s *stubDiscoveryProvider) FormatForAIContext(_ []*ResourceDiscoveryInfo) string {
|
|
return ""
|
|
}
|
|
|
|
func (s *stubDiscoveryProvider) TriggerDiscovery(_ context.Context, _, _, _ string) (*ResourceDiscoveryInfo, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func TestIsTransientError(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
err error
|
|
expected bool
|
|
}{
|
|
// Transient errors — should return true
|
|
{"nil error", nil, false},
|
|
{"rate limit 429", errors.New("API returned status 429"), true},
|
|
{"503 service unavailable", errors.New("HTTP 503 Service Unavailable"), true},
|
|
{"rate_limit underscore", errors.New("rate_limit: too many requests"), true},
|
|
{"rate limit space", errors.New("rate limit exceeded"), true},
|
|
{"ratelimit single word", errors.New("ratelimit error from provider"), true},
|
|
{"too many requests", errors.New("too many requests, slow down"), true},
|
|
{"timeout", errors.New("request timeout after 30s"), true},
|
|
{"context deadline", errors.New("context deadline exceeded"), true},
|
|
{"failed after retries", errors.New("failed after 3 retries"), true},
|
|
{"temporarily unavailable", errors.New("service temporarily unavailable"), true},
|
|
{"server overloaded", errors.New("server overloaded, try later"), true},
|
|
{"service unavailable text", errors.New("the service is service unavailable"), true},
|
|
{"connection refused", errors.New("dial tcp: connection refused"), true},
|
|
{"connection reset", errors.New("connection reset by peer"), true},
|
|
{"broken pipe", errors.New("write: broken pipe"), true},
|
|
{"i/o timeout", errors.New("i/o timeout"), true},
|
|
{"network unreachable", errors.New("network unreachable"), true},
|
|
|
|
// Anthropic-style rate limit
|
|
{"anthropic rate limit", errors.New("Error: 429 {\"type\":\"error\",\"error\":{\"type\":\"rate_limit_error\"}}"), true},
|
|
// OpenAI-style
|
|
{"openai rate limit", errors.New("Rate limit reached for gpt-4"), true},
|
|
// Gemini-style
|
|
{"gemini quota", errors.New("429 Too Many Requests"), true},
|
|
|
|
// Non-transient errors — should return false
|
|
{"resource not found", errors.New("resource not found"), false},
|
|
{"permission denied", errors.New("permission denied"), false},
|
|
{"invalid argument", errors.New("invalid resource_type: foo"), false},
|
|
{"generic error", errors.New("something went wrong"), false},
|
|
{"empty error", errors.New(""), false},
|
|
{"auth error", errors.New("authentication failed"), false},
|
|
{"not found", errors.New("404 not found"), false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := isTransientError(tt.err)
|
|
assert.Equal(t, tt.expected, result, "error: %v", tt.err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExecuteGetDiscovery_UsesTargetID(t *testing.T) {
|
|
provider := &stubDiscoveryProvider{
|
|
getResp: &ResourceDiscoveryInfo{
|
|
ID: "vm:node1:101",
|
|
ResourceType: "vm",
|
|
ResourceID: "101",
|
|
TargetID: "node1",
|
|
Hostname: "vm-101",
|
|
},
|
|
}
|
|
exec := NewPulseToolExecutor(ExecutorConfig{DiscoveryProvider: provider})
|
|
|
|
result, err := exec.executeGetDiscovery(context.Background(), map[string]interface{}{
|
|
"resource_type": "vm",
|
|
"resource_id": "101",
|
|
"target_id": "node1",
|
|
})
|
|
assert.NoError(t, err)
|
|
assert.False(t, result.IsError)
|
|
assert.Equal(t, "vm", provider.lastGetResourceType)
|
|
assert.Equal(t, "node1", provider.lastGetTargetID)
|
|
assert.Equal(t, "101", provider.lastGetResourceID)
|
|
|
|
var payload map[string]interface{}
|
|
assert.NoError(t, json.Unmarshal([]byte(result.Content[0].Text), &payload))
|
|
assert.Equal(t, "node1", payload["target_id"])
|
|
assert.NotContains(t, payload, "host_id")
|
|
}
|
|
|
|
func TestCanonicalDiscoveryResourceType(t *testing.T) {
|
|
tests := []struct {
|
|
in string
|
|
want string
|
|
}{
|
|
{in: "docker", want: "app-container"},
|
|
{in: "app-container", want: "app-container"},
|
|
{in: "k8s-cluster", want: "k8s-cluster"},
|
|
{in: "k8s-pod", want: "k8s-pod"},
|
|
{in: "docker-host", want: "docker-host"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.in, func(t *testing.T) {
|
|
assert.Equal(t, tc.want, CanonicalDiscoveryResourceType(tc.in))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDiscoveryProviderResourceType(t *testing.T) {
|
|
tests := []struct {
|
|
in string
|
|
want string
|
|
}{
|
|
{in: "app-container", want: "docker"},
|
|
{in: "docker", want: "docker"},
|
|
{in: "vm", want: "vm"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.in, func(t *testing.T) {
|
|
assert.Equal(t, tc.want, DiscoveryProviderResourceType(tc.in))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCanonicalDiscoveryTargetID(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in *ResourceDiscoveryInfo
|
|
fallback string
|
|
want string
|
|
}{
|
|
{
|
|
name: "uses discovery target",
|
|
in: &ResourceDiscoveryInfo{
|
|
TargetID: "node1",
|
|
},
|
|
fallback: "fallback",
|
|
want: "node1",
|
|
},
|
|
{
|
|
name: "falls back when target missing",
|
|
in: &ResourceDiscoveryInfo{
|
|
TargetID: " ",
|
|
},
|
|
fallback: "fallback",
|
|
want: "fallback",
|
|
},
|
|
{
|
|
name: "nil discovery falls back",
|
|
in: nil,
|
|
fallback: "fallback",
|
|
want: "fallback",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
assert.Equal(t, tc.want, CanonicalDiscoveryTargetID(tc.in, tc.fallback))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExecuteGetDiscovery_TargetIDRequired(t *testing.T) {
|
|
exec := NewPulseToolExecutor(ExecutorConfig{DiscoveryProvider: &stubDiscoveryProvider{}})
|
|
|
|
result, err := exec.executeGetDiscovery(context.Background(), map[string]interface{}{
|
|
"resource_type": "vm",
|
|
"resource_id": "101",
|
|
})
|
|
assert.NoError(t, err)
|
|
assert.True(t, result.IsError)
|
|
assert.Contains(t, result.Content[0].Text, "target_id is required")
|
|
}
|
|
|
|
func TestIsUnsupportedDiscoveryLegacyResourceTypeToken(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
want bool
|
|
}{
|
|
{name: "host rejected", in: "host", want: true},
|
|
{name: "mixed case host rejected", in: " HoSt ", want: true},
|
|
{name: "docker rejected", in: "docker", want: true},
|
|
{name: "docker container rejected", in: "docker-container", want: true},
|
|
{name: "lxc deferred to supported-type check", in: "lxc", want: false},
|
|
{name: "agent allowed", in: "agent", want: false},
|
|
{name: "vm allowed", in: "vm", want: false},
|
|
{name: "app container allowed", in: "app-container", want: false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert.Equal(t, tt.want, isUnsupportedDiscoveryLegacyResourceTypeToken(tt.in))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExecuteGetDiscovery_RejectsLegacyResourceTypeAlias(t *testing.T) {
|
|
provider := &stubDiscoveryProvider{}
|
|
exec := NewPulseToolExecutor(ExecutorConfig{DiscoveryProvider: provider})
|
|
|
|
for _, alias := range []string{"host", "lxc"} {
|
|
t.Run(alias, func(t *testing.T) {
|
|
result, err := exec.executeGetDiscovery(context.Background(), map[string]interface{}{
|
|
"resource_type": alias,
|
|
"resource_id": "101",
|
|
"target_id": "node1",
|
|
})
|
|
assert.NoError(t, err)
|
|
assert.True(t, result.IsError)
|
|
assert.Contains(t, result.Content[0].Text, "unsupported resource_type")
|
|
assert.Equal(t, "", provider.lastGetResourceType)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExecuteGetDiscovery_RejectsLegacyUnderscoreAppContainerAlias(t *testing.T) {
|
|
provider := &stubDiscoveryProvider{}
|
|
exec := NewPulseToolExecutor(ExecutorConfig{DiscoveryProvider: provider})
|
|
|
|
result, err := exec.executeGetDiscovery(context.Background(), map[string]interface{}{
|
|
"resource_type": "docker_container",
|
|
"resource_id": "abc123",
|
|
"target_id": "agent-1",
|
|
})
|
|
assert.NoError(t, err)
|
|
assert.True(t, result.IsError)
|
|
assert.Contains(t, result.Content[0].Text, "unsupported resource_type")
|
|
assert.Equal(t, "", provider.lastGetResourceType)
|
|
}
|
|
|
|
func TestExecuteGetDiscovery_RejectsLegacyDockerResourceTypeAliases(t *testing.T) {
|
|
for _, alias := range []string{"docker", "docker-container"} {
|
|
t.Run(alias, func(t *testing.T) {
|
|
provider := &stubDiscoveryProvider{}
|
|
exec := NewPulseToolExecutor(ExecutorConfig{DiscoveryProvider: provider})
|
|
|
|
result, err := exec.executeGetDiscovery(context.Background(), map[string]interface{}{
|
|
"resource_type": alias,
|
|
"resource_id": "abc123",
|
|
"target_id": "agent-1",
|
|
})
|
|
assert.NoError(t, err)
|
|
assert.True(t, result.IsError)
|
|
assert.Contains(t, result.Content[0].Text, "unsupported resource_type")
|
|
assert.Equal(t, "", provider.lastGetResourceType)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExecuteGetDiscovery_CanonicalAppContainerUsesDockerProviderType(t *testing.T) {
|
|
provider := &stubDiscoveryProvider{
|
|
getResp: &ResourceDiscoveryInfo{
|
|
ID: "docker:agent-1:abc123",
|
|
ResourceType: "docker",
|
|
ResourceID: "abc123",
|
|
TargetID: "agent-1",
|
|
Hostname: "docker-host-1",
|
|
},
|
|
}
|
|
exec := NewPulseToolExecutor(ExecutorConfig{DiscoveryProvider: provider})
|
|
|
|
result, err := exec.executeGetDiscovery(context.Background(), map[string]interface{}{
|
|
"resource_type": "app-container",
|
|
"resource_id": "abc123",
|
|
"target_id": "agent-1",
|
|
})
|
|
assert.NoError(t, err)
|
|
assert.False(t, result.IsError)
|
|
assert.Equal(t, "docker", provider.lastGetResourceType)
|
|
assert.Equal(t, "agent-1", provider.lastGetTargetID)
|
|
assert.Equal(t, "abc123", provider.lastGetResourceID)
|
|
|
|
var payload map[string]interface{}
|
|
assert.NoError(t, json.Unmarshal([]byte(result.Content[0].Text), &payload))
|
|
assert.Equal(t, "app-container", payload["resource_type"])
|
|
}
|
|
|
|
func TestExecuteListDiscoveries_FiltersByTargetID(t *testing.T) {
|
|
provider := &stubDiscoveryProvider{
|
|
listResp: []*ResourceDiscoveryInfo{
|
|
{
|
|
ID: "vm:node2:102",
|
|
ResourceType: "vm",
|
|
ResourceID: "102",
|
|
TargetID: "node2",
|
|
Hostname: "vm-102",
|
|
},
|
|
},
|
|
}
|
|
exec := NewPulseToolExecutor(ExecutorConfig{DiscoveryProvider: provider})
|
|
|
|
result, err := exec.executeListDiscoveries(context.Background(), map[string]interface{}{
|
|
"target_id": "node2",
|
|
})
|
|
assert.NoError(t, err)
|
|
assert.False(t, result.IsError)
|
|
assert.Equal(t, "node2", provider.lastListTargetID)
|
|
|
|
var payload map[string]interface{}
|
|
assert.NoError(t, json.Unmarshal([]byte(result.Content[0].Text), &payload))
|
|
assert.Equal(t, "node2", payload["filter_target_id"])
|
|
discoveries, ok := payload["discoveries"].([]interface{})
|
|
assert.True(t, ok)
|
|
if assert.NotEmpty(t, discoveries) {
|
|
first, castOK := discoveries[0].(map[string]interface{})
|
|
assert.True(t, castOK)
|
|
assert.NotContains(t, first, "host_id")
|
|
}
|
|
}
|
|
|
|
func TestExecuteListDiscoveries_RejectsLegacyTypeAlias(t *testing.T) {
|
|
exec := NewPulseToolExecutor(ExecutorConfig{DiscoveryProvider: &stubDiscoveryProvider{}})
|
|
|
|
for _, alias := range []string{"host", "lxc"} {
|
|
t.Run(alias, func(t *testing.T) {
|
|
result, err := exec.executeListDiscoveries(context.Background(), map[string]interface{}{
|
|
"type": alias,
|
|
})
|
|
assert.NoError(t, err)
|
|
assert.True(t, result.IsError)
|
|
assert.Contains(t, result.Content[0].Text, "unsupported type")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExecuteListDiscoveries_RejectsLegacyUnderscoreAppContainerAlias(t *testing.T) {
|
|
exec := NewPulseToolExecutor(ExecutorConfig{DiscoveryProvider: &stubDiscoveryProvider{}})
|
|
|
|
result, err := exec.executeListDiscoveries(context.Background(), map[string]interface{}{
|
|
"type": "app_container",
|
|
})
|
|
assert.NoError(t, err)
|
|
assert.True(t, result.IsError)
|
|
assert.Contains(t, result.Content[0].Text, "unsupported type")
|
|
}
|
|
|
|
func TestExecuteListDiscoveries_RejectsLegacyDockerTypeAliases(t *testing.T) {
|
|
for _, alias := range []string{"docker", "docker-container"} {
|
|
t.Run(alias, func(t *testing.T) {
|
|
exec := NewPulseToolExecutor(ExecutorConfig{DiscoveryProvider: &stubDiscoveryProvider{}})
|
|
|
|
result, err := exec.executeListDiscoveries(context.Background(), map[string]interface{}{
|
|
"type": alias,
|
|
})
|
|
assert.NoError(t, err)
|
|
assert.True(t, result.IsError)
|
|
assert.Contains(t, result.Content[0].Text, "unsupported type")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExecuteListDiscoveries_CanonicalAppContainerUsesDockerProviderType(t *testing.T) {
|
|
provider := &stubDiscoveryProvider{
|
|
listResp: []*ResourceDiscoveryInfo{
|
|
{
|
|
ID: "docker:agent-1:abc123",
|
|
ResourceType: "docker",
|
|
ResourceID: "abc123",
|
|
TargetID: "agent-1",
|
|
Hostname: "docker-host-1",
|
|
},
|
|
},
|
|
}
|
|
exec := NewPulseToolExecutor(ExecutorConfig{DiscoveryProvider: provider})
|
|
|
|
result, err := exec.executeListDiscoveries(context.Background(), map[string]interface{}{
|
|
"type": "app-container",
|
|
})
|
|
assert.NoError(t, err)
|
|
assert.False(t, result.IsError)
|
|
assert.Equal(t, "docker", provider.lastListType)
|
|
|
|
var payload map[string]interface{}
|
|
assert.NoError(t, json.Unmarshal([]byte(result.Content[0].Text), &payload))
|
|
assert.Equal(t, "app-container", payload["filter_type"])
|
|
discoveries, ok := payload["discoveries"].([]interface{})
|
|
assert.True(t, ok)
|
|
if assert.Len(t, discoveries, 1) {
|
|
first, castOK := discoveries[0].(map[string]interface{})
|
|
assert.True(t, castOK)
|
|
assert.Equal(t, "app-container", first["resource_type"])
|
|
}
|
|
}
|