Pulse/internal/ai/tools/tools_query_test.go
2026-04-09 20:15:17 +01:00

1931 lines
66 KiB
Go

package tools
import (
"context"
"encoding/json"
"strings"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/agentexec"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/rcourtman/pulse-go-rewrite/internal/truenas"
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
)
func TestQueryResponsesUseCanonicalEmptyCollections(t *testing.T) {
tests := []struct {
name string
raw any
keys []string
}{
{name: "resource_search", raw: EmptyResourceSearchResponse(), keys: []string{"matches"}},
{name: "topology_proxmox", raw: EmptyTopologyResponse(), keys: []string{"proxmox", "nodes"}},
{name: "topology_docker", raw: EmptyTopologyResponse(), keys: []string{"docker", "hosts"}},
{name: "topology_kubernetes", raw: EmptyTopologyResponse(), keys: []string{"kubernetes", "clusters"}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
payload, err := json.Marshal(tc.raw)
if err != nil {
t.Fatalf("marshal %s: %v", tc.name, err)
}
var decoded map[string]any
if err := json.Unmarshal(payload, &decoded); err != nil {
t.Fatalf("decode %s: %v", tc.name, err)
}
var current any = decoded
for _, key := range tc.keys {
obj, ok := current.(map[string]any)
if !ok {
t.Fatalf("%s expected object before %s, got %T", tc.name, key, current)
}
current = obj[key]
}
values, ok := current.([]any)
if !ok || len(values) != 0 {
t.Fatalf("expected %s to be an empty array, got %T (%v)", tc.name, current, current)
}
})
}
payload, err := json.Marshal(TopologyResponse{
Proxmox: ProxmoxTopology{
Nodes: []ProxmoxNodeTopology{{
Name: "node1",
Status: "online",
VMs: []TopologyVM{{VMID: 100, Name: "vm1", Status: "running"}},
}},
},
Docker: DockerTopology{
Hosts: []DockerHostTopology{{
Hostname: "docker-1",
}},
},
Kubernetes: KubernetesTopology{
Clusters: []KubernetesClusterTopology{{
Name: "cluster1",
Status: "online",
Nodes: []KubernetesNodeTopology{{
Name: "kube-node-1",
Status: "Ready",
Ready: true,
}},
}},
},
}.NormalizeCollections())
if err != nil {
t.Fatalf("marshal normalized topology: %v", err)
}
var decoded map[string]any
if err := json.Unmarshal(payload, &decoded); err != nil {
t.Fatalf("decode normalized topology: %v", err)
}
proxmox := decoded["proxmox"].(map[string]any)
nodes := proxmox["nodes"].([]any)
node := nodes[0].(map[string]any)
vms, ok := node["vms"].([]any)
if !ok || len(vms) != 1 {
t.Fatalf("expected proxmox node vms to contain normalized items, got %T (%v)", node["vms"], node["vms"])
}
vm := vms[0].(map[string]any)
if tags, ok := vm["tags"].([]any); !ok || len(tags) != 0 {
t.Fatalf("expected proxmox node vm tags to be an empty array, got %T (%v)", vm["tags"], vm["tags"])
}
if containers, ok := node["containers"].([]any); !ok || len(containers) != 0 {
t.Fatalf("expected proxmox node containers to be an empty array, got %T (%v)", node["containers"], node["containers"])
}
docker := decoded["docker"].(map[string]any)
hosts := docker["hosts"].([]any)
host := hosts[0].(map[string]any)
if containers, ok := host["containers"].([]any); !ok || len(containers) != 0 {
t.Fatalf("expected docker host containers to be an empty array, got %T (%v)", host["containers"], host["containers"])
}
kubernetes := decoded["kubernetes"].(map[string]any)
clusters := kubernetes["clusters"].([]any)
cluster := clusters[0].(map[string]any)
if deployments, ok := cluster["deployments"].([]any); !ok || len(deployments) != 0 {
t.Fatalf("expected kubernetes deployments to be an empty array, got %T (%v)", cluster["deployments"], cluster["deployments"])
}
if pods, ok := cluster["pods"].([]any); !ok || len(pods) != 0 {
t.Fatalf("expected kubernetes pods to be an empty array, got %T (%v)", cluster["pods"], cluster["pods"])
}
kubeNodes := cluster["nodes"].([]any)
kubeNode := kubeNodes[0].(map[string]any)
if roles, ok := kubeNode["roles"].([]any); !ok || len(roles) != 0 {
t.Fatalf("expected kubernetes node roles to be an empty array, got %T (%v)", kubeNode["roles"], kubeNode["roles"])
}
}
func TestResourceAndGuestResponsesUseCanonicalEmptyCollections(t *testing.T) {
tests := []struct {
name string
raw any
keys []string
}{
{name: "resource_tags", raw: EmptyResourceResponse(), keys: []string{"tags"}},
{name: "resource_networks", raw: EmptyResourceResponse(), keys: []string{"networks"}},
{name: "resource_ports", raw: EmptyResourceResponse(), keys: []string{"ports"}},
{name: "resource_mounts", raw: EmptyResourceResponse(), keys: []string{"mounts"}},
{name: "guest_mounts", raw: EmptyGuestConfigResponse(), keys: []string{"mounts"}},
{name: "guest_disks", raw: EmptyGuestConfigResponse(), keys: []string{"disks"}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
payload, err := json.Marshal(tc.raw)
if err != nil {
t.Fatalf("marshal %s: %v", tc.name, err)
}
var decoded map[string]any
if err := json.Unmarshal(payload, &decoded); err != nil {
t.Fatalf("decode %s: %v", tc.name, err)
}
current := decoded[tc.keys[0]]
values, ok := current.([]any)
if !ok || len(values) != 0 {
t.Fatalf("expected %s to be an empty array, got %T (%v)", tc.name, current, current)
}
})
}
payload, err := json.Marshal(EmptyResourceResponse())
if err != nil {
t.Fatalf("marshal empty resource: %v", err)
}
var decoded map[string]any
if err := json.Unmarshal(payload, &decoded); err != nil {
t.Fatalf("decode empty resource: %v", err)
}
labels, ok := decoded["labels"].(map[string]any)
if !ok || len(labels) != 0 {
t.Fatalf("expected labels to be an empty object, got %T (%v)", decoded["labels"], decoded["labels"])
}
payload, err = json.Marshal(ResourceResponse{
Type: "app-container",
ID: "abc",
Name: "app",
Networks: []NetworkInfo{{
Name: "bridge",
}},
}.NormalizeCollections())
if err != nil {
t.Fatalf("marshal normalized resource: %v", err)
}
if err := json.Unmarshal(payload, &decoded); err != nil {
t.Fatalf("decode normalized resource: %v", err)
}
networks := decoded["networks"].([]any)
network := networks[0].(map[string]any)
addresses, ok := network["addresses"].([]any)
if !ok || len(addresses) != 0 {
t.Fatalf("expected network addresses to be an empty array, got %T (%v)", network["addresses"], network["addresses"])
}
payload, err = json.Marshal(EmptyURLFetchResponse())
if err != nil {
t.Fatalf("marshal empty url fetch: %v", err)
}
if err := json.Unmarshal(payload, &decoded); err != nil {
t.Fatalf("decode empty url fetch: %v", err)
}
headers, ok := decoded["headers"].(map[string]any)
if !ok || len(headers) != 0 {
t.Fatalf("expected url fetch headers to be an empty object, got %T (%v)", decoded["headers"], decoded["headers"])
}
payload, err = json.Marshal(EmptyGuestConfigResponse())
if err != nil {
t.Fatalf("marshal empty guest config: %v", err)
}
if err := json.Unmarshal(payload, &decoded); err != nil {
t.Fatalf("decode empty guest config: %v", err)
}
raw, ok := decoded["raw"].(map[string]any)
if !ok || len(raw) != 0 {
t.Fatalf("expected guest raw to be an empty object, got %T (%v)", decoded["raw"], decoded["raw"])
}
}
type mockGuestConfigProvider struct {
lastGuestType string
lastInstance string
lastNode string
lastVMID int
config map[string]interface{}
}
func (m *mockGuestConfigProvider) GetGuestConfig(guestType, instance, node string, vmID int) (map[string]interface{}, error) {
m.lastGuestType = guestType
m.lastInstance = instance
m.lastNode = node
m.lastVMID = vmID
return m.config, nil
}
type registryUnifiedQueryProvider struct {
*unifiedresources.ResourceRegistry
}
func (p *registryUnifiedQueryProvider) GetByType(t unifiedresources.ResourceType) []unifiedresources.Resource {
return p.ListByType(t)
}
func newTrueNASUnifiedQueryProvider(t *testing.T) *registryUnifiedQueryProvider {
t.Helper()
previous := truenas.IsFeatureEnabled()
truenas.SetFeatureEnabled(true)
t.Cleanup(func() {
truenas.SetFeatureEnabled(previous)
})
registry := unifiedresources.NewRegistry(nil)
records := truenas.NewDefaultProvider().Records()
if len(records) == 0 {
t.Fatal("expected TrueNAS fixture records")
}
registry.IngestRecords(unifiedresources.SourceTrueNAS, records)
return &registryUnifiedQueryProvider{ResourceRegistry: registry}
}
func newVMwareUnifiedQueryProvider(t *testing.T) *registryUnifiedQueryProvider {
t.Helper()
now := time.Now().UTC()
diskUsed := int64(512 * 1024 * 1024 * 1024)
diskTotal := int64(1024 * 1024 * 1024 * 1024)
datastoreAccessible := true
registry := unifiedresources.NewRegistry(nil)
registry.IngestRecords(unifiedresources.SourceVMware, []unifiedresources.IngestRecord{
{
SourceID: "vc-1:host:host-101",
Resource: unifiedresources.Resource{
ID: "vmware-host-1",
Type: unifiedresources.ResourceTypeAgent,
Name: "esxi-01.lab.local",
Status: unifiedresources.StatusOnline,
LastSeen: now,
UpdatedAt: now,
VMware: &unifiedresources.VMwareData{
ConnectionID: "vc-1",
ConnectionName: "Lab VC",
ManagedObjectID: "host-101",
EntityType: "host",
},
},
Identity: unifiedresources.ResourceIdentity{Hostnames: []string{"esxi-01.lab.local"}},
},
{
SourceID: "vc-1:vm:vm-201",
Resource: unifiedresources.Resource{
ID: "vmware-vm-1",
Type: unifiedresources.ResourceTypeVM,
Name: "app-01",
Status: unifiedresources.StatusOnline,
LastSeen: now,
UpdatedAt: now,
ParentName: "esxi-01.lab.local",
VMware: &unifiedresources.VMwareData{
ConnectionID: "vc-1",
ConnectionName: "Lab VC",
ManagedObjectID: "vm-201",
EntityType: "vm",
RuntimeHostName: "esxi-01.lab.local",
GuestOSFamily: "ubuntu64Guest",
CPUCount: 4,
MemorySizeMiB: 8192,
},
},
Identity: unifiedresources.ResourceIdentity{
Hostnames: []string{"app-01"},
IPAddresses: []string{"192.0.2.50"},
},
},
{
SourceID: "vc-1:datastore:datastore-11",
Resource: unifiedresources.Resource{
Type: unifiedresources.ResourceTypeStorage,
Name: "nvme-primary",
Status: unifiedresources.StatusOnline,
LastSeen: now,
UpdatedAt: now,
Metrics: &unifiedresources.ResourceMetrics{
Disk: &unifiedresources.MetricValue{
Used: &diskUsed,
Total: &diskTotal,
Percent: 50,
},
},
Storage: &unifiedresources.StorageMeta{
Type: "vmfs",
Platform: "vmware-vsphere",
Topology: "datastore",
Enabled: true,
Active: true,
Shared: false,
Nodes: []string{"esxi-01.lab.local"},
},
VMware: &unifiedresources.VMwareData{
ConnectionID: "vc-1",
ConnectionName: "Lab VC",
ManagedObjectID: "datastore-11",
EntityType: "datastore",
DatastoreAccessible: &datastoreAccessible,
},
},
},
})
return &registryUnifiedQueryProvider{ResourceRegistry: registry}
}
func newCommandCapableAgentQueryProvider() *registryUnifiedQueryProvider {
now := time.Now().UTC()
registry := unifiedresources.NewRegistry(nil)
registry.IngestResources([]unifiedresources.Resource{{
ID: "agent:lab-host",
Type: unifiedresources.ResourceTypeAgent,
Name: "lab-host",
Status: unifiedresources.StatusOnline,
LastSeen: now,
UpdatedAt: now,
Agent: &unifiedresources.AgentData{
Hostname: "lab-host",
Platform: "linux",
CommandsEnabled: true,
},
Identity: unifiedresources.ResourceIdentity{
Hostnames: []string{"lab-host"},
},
}})
return &registryUnifiedQueryProvider{ResourceRegistry: registry}
}
func TestCanonicalQueryListType_StrictV6Tokens(t *testing.T) {
if got := canonicalQueryListType("k8s-pods"); got != "k8s-pods" {
t.Fatalf("canonicalQueryListType(k8s-pods) = %q, want k8s-pods", got)
}
if got := canonicalQueryListType("k8s_pods"); got != "k8s_pods" {
t.Fatalf("canonicalQueryListType(k8s_pods) = %q, want k8s_pods", got)
}
if got := canonicalQueryListType("kubernetes-clusters"); got != "kubernetes-clusters" {
t.Fatalf("canonicalQueryListType(kubernetes-clusters) = %q, want kubernetes-clusters", got)
}
if got := canonicalQueryListType("system"); got != "systems" {
t.Fatalf("canonicalQueryListType(system) = %q, want systems", got)
}
if got := canonicalQueryListType("agent"); got != "systems" {
t.Fatalf("canonicalQueryListType(agent) = %q, want systems", got)
}
if got := canonicalQueryListType("agents"); got != "systems" {
t.Fatalf("canonicalQueryListType(agents) = %q, want systems", got)
}
if got := canonicalQueryListType("storage"); got != "storage-pools" {
t.Fatalf("canonicalQueryListType(storage) = %q, want storage-pools", got)
}
if got := canonicalQueryListType("storage-pool"); got != "storage-pools" {
t.Fatalf("canonicalQueryListType(storage-pool) = %q, want storage-pools", got)
}
if got := canonicalQueryListType("physical-disk"); got != "physical-disks" {
t.Fatalf("canonicalQueryListType(physical-disk) = %q, want physical-disks", got)
}
}
func TestCanonicalQueryTopologyInclude_StrictV6Tokens(t *testing.T) {
if got := canonicalQueryTopologyInclude("app-container"); got != "app-containers" {
t.Fatalf("canonicalQueryTopologyInclude(app-container) = %q, want app-containers", got)
}
if got := canonicalQueryTopologyInclude("app_container"); got != "app_container" {
t.Fatalf("canonicalQueryTopologyInclude(app_container) = %q, want app_container", got)
}
if got := canonicalQueryTopologyInclude("docker"); got != "docker" {
t.Fatalf("canonicalQueryTopologyInclude(docker) = %q, want docker", got)
}
}
func TestCanonicalQuerySearchType_StrictV6Tokens(t *testing.T) {
if got := canonicalQuerySearchType("agent"); got != "agent" {
t.Fatalf("canonicalQuerySearchType(agent) = %q, want agent", got)
}
if got := canonicalQuerySearchType("docker-host"); got != "docker-host" {
t.Fatalf("canonicalQuerySearchType(docker-host) = %q, want docker-host", got)
}
if got := canonicalQuerySearchType("docker_host"); got != "docker_host" {
t.Fatalf("canonicalQuerySearchType(docker_host) = %q, want docker_host", got)
}
if got := canonicalQuerySearchType("systems"); got != "agent" {
t.Fatalf("canonicalQuerySearchType(systems) = %q, want agent", got)
}
if got := canonicalQuerySearchType("system"); got != "agent" {
t.Fatalf("canonicalQuerySearchType(system) = %q, want agent", got)
}
if got := canonicalQuerySearchType("storage"); got != "storage" {
t.Fatalf("canonicalQuerySearchType(storage) = %q, want storage", got)
}
if got := canonicalQuerySearchType("storage-pools"); got != "storage" {
t.Fatalf("canonicalQuerySearchType(storage-pools) = %q, want storage", got)
}
if got := canonicalQuerySearchType("physical-disks"); got != "physical-disk" {
t.Fatalf("canonicalQuerySearchType(physical-disks) = %q, want physical-disk", got)
}
}
func TestCanonicalQueryHelpers_NormalizeCanonicalAgentAndStorageAliases(t *testing.T) {
if got := canonicalQueryResourceType("system"); got != "agent" {
t.Fatalf("canonicalQueryResourceType(system) = %q, want agent", got)
}
if got := canonicalQueryResourceType("storage-pool"); got != "storage" {
t.Fatalf("canonicalQueryResourceType(storage-pool) = %q, want storage", got)
}
if got := canonicalQueryResourceType("container"); got != "container" {
t.Fatalf("canonicalQueryResourceType(container) = %q, want container", got)
}
if got := canonicalQueryResourceType("docker"); got != "docker" {
t.Fatalf("canonicalQueryResourceType(docker) = %q, want docker", got)
}
if got := canonicalQueryListType("container"); got != "container" {
t.Fatalf("canonicalQueryListType(container) = %q, want container", got)
}
if got := canonicalQuerySearchType("docker"); got != "docker" {
t.Fatalf("canonicalQuerySearchType(docker) = %q, want docker", got)
}
}
func TestExecuteListInfrastructureAndTopology(t *testing.T) {
state := models.StateSnapshot{
Nodes: []models.Node{{ID: "node1", Name: "node1", Instance: "pve1", Status: "online"}},
VMs: []models.VM{
{ID: "qemu/pve1/node1/100", Name: "vm1", VMID: 100, Instance: "pve1", Status: "running", Node: "node1"},
},
Containers: []models.Container{
{ID: "lxc/pve1/node1/200", Name: "ct1", VMID: 200, Instance: "pve1", Status: "stopped", Node: "node1"},
},
DockerHosts: []models.DockerHost{
{
ID: "host1",
Hostname: "h1",
DisplayName: "Host 1",
Containers: []models.DockerContainer{
{ID: "c1", Name: "nginx", State: "running", Image: "nginx"},
},
},
},
}
executor := NewPulseToolExecutor(ExecutorConfig{
StateProvider: &mockStateProvider{state: state},
AgentServer: &mockAgentServer{
agents: []agentexec.ConnectedAgent{{Hostname: "node1"}},
},
ControlLevel: ControlLevelControlled,
})
result, err := executor.executeListInfrastructure(context.Background(), map[string]interface{}{
"type": "vms",
"status": "running",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var infra InfrastructureResponse
if err := json.Unmarshal([]byte(result.Content[0].Text), &infra); err != nil {
t.Fatalf("decode infra: %v", err)
}
if len(infra.VMs) != 1 || infra.VMs[0].Name != "vm1" {
t.Fatalf("unexpected infra response: %+v", infra)
}
// Topology includes derived node for VM reference if missing
state.Nodes = nil
executor.stateProvider = &mockStateProvider{state: state}
topologyResult, err := executor.executeGetTopology(context.Background(), map[string]interface{}{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var topology TopologyResponse
if err := json.Unmarshal([]byte(topologyResult.Content[0].Text), &topology); err != nil {
t.Fatalf("decode topology: %v", err)
}
if topology.Summary.TotalVMs != 1 || len(topology.Proxmox.Nodes) == 0 {
t.Fatalf("unexpected topology: %+v", topology)
}
}
func TestExecuteGetTopologySummaryOnly(t *testing.T) {
state := models.StateSnapshot{
Nodes: []models.Node{{ID: "node1", Name: "node1", Instance: "pve1", Status: "online"}},
VMs: []models.VM{
{ID: "qemu/pve1/node1/100", Name: "vm1", VMID: 100, Instance: "pve1", Status: "running", Node: "node1"},
},
Containers: []models.Container{
{ID: "lxc/pve1/node1/200", Name: "ct1", VMID: 200, Instance: "pve1", Status: "stopped", Node: "node1"},
},
DockerHosts: []models.DockerHost{
{
ID: "host1",
Hostname: "host1",
Containers: []models.DockerContainer{
{ID: "c1", Name: "nginx", State: "running", Image: "nginx"},
},
},
},
}
executor := NewPulseToolExecutor(ExecutorConfig{
StateProvider: &mockStateProvider{state: state},
})
result, err := executor.executeGetTopology(context.Background(), map[string]interface{}{
"summary_only": true,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var topology TopologyResponse
if err := json.Unmarshal([]byte(result.Content[0].Text), &topology); err != nil {
t.Fatalf("decode topology: %v", err)
}
if len(topology.Proxmox.Nodes) != 0 {
t.Fatalf("expected no proxmox nodes, got: %+v", topology.Proxmox.Nodes)
}
if len(topology.Docker.Hosts) != 0 {
t.Fatalf("expected no docker hosts, got: %+v", topology.Docker.Hosts)
}
if len(topology.Kubernetes.Clusters) != 0 {
t.Fatalf("expected no kubernetes clusters, got: %+v", topology.Kubernetes.Clusters)
}
if topology.Summary.TotalVMs != 1 || topology.Summary.TotalDockerHosts != 1 || topology.Summary.TotalDockerContainers != 1 {
t.Fatalf("unexpected summary: %+v", topology.Summary)
}
if topology.Summary.RunningVMs != 1 || topology.Summary.RunningDocker != 1 {
t.Fatalf("unexpected running summary: %+v", topology.Summary)
}
}
func TestExecuteGetTopology_UsesCanonicalMaxProxmoxNodesInput(t *testing.T) {
state := models.StateSnapshot{
Nodes: []models.Node{
{ID: "node-1", Name: "node-1", Instance: "pve1", Status: "online"},
{ID: "node-2", Name: "node-2", Instance: "pve1", Status: "online"},
},
}
executor := NewPulseToolExecutor(ExecutorConfig{
StateProvider: &mockStateProvider{state: state},
})
result, err := executor.executeGetTopology(context.Background(), map[string]interface{}{
"max_proxmox_nodes": 1,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var topology TopologyResponse
if err := json.Unmarshal([]byte(result.Content[0].Text), &topology); err != nil {
t.Fatalf("decode topology: %v", err)
}
if len(topology.Proxmox.Nodes) != 1 {
t.Fatalf("expected one proxmox node after max_proxmox_nodes cap, got %+v", topology.Proxmox.Nodes)
}
}
func TestExecuteGetTopology_RejectsLegacyDockerIncludeAlias(t *testing.T) {
executor := NewPulseToolExecutor(ExecutorConfig{})
result, _ := executor.executeGetTopology(context.Background(), map[string]interface{}{
"include": "docker",
})
if !result.IsError {
t.Fatal("expected error for legacy include alias")
}
if !strings.Contains(result.Content[0].Text, "invalid include") {
t.Fatalf("unexpected error text: %s", result.Content[0].Text)
}
}
func TestExecuteGetTopology_KubernetesInclude(t *testing.T) {
state := models.StateSnapshot{
KubernetesClusters: []models.KubernetesCluster{
{
ID: "cluster-1",
Name: "prod-cluster",
Status: "online",
Nodes: []models.KubernetesNode{
{Name: "worker-1", UID: "node-1", Ready: true, Roles: []string{"worker"}},
},
Pods: []models.KubernetesPod{
{
UID: "pod-1",
Name: "api-6f8d5c",
Namespace: "default",
Phase: "Running",
Restarts: 2,
OwnerKind: "Deployment",
OwnerName: "api",
},
},
Deployments: []models.KubernetesDeployment{
{
UID: "deploy-1",
Name: "api",
Namespace: "default",
DesiredReplicas: 3,
ReadyReplicas: 2,
},
},
},
},
}
executor := NewPulseToolExecutor(ExecutorConfig{
StateProvider: &mockStateProvider{state: state},
})
result, err := executor.executeGetTopology(context.Background(), map[string]interface{}{
"include": "kubernetes",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var topology TopologyResponse
if err := json.Unmarshal([]byte(result.Content[0].Text), &topology); err != nil {
t.Fatalf("decode topology: %v", err)
}
if len(topology.Proxmox.Nodes) != 0 {
t.Fatalf("expected no proxmox topology, got: %+v", topology.Proxmox.Nodes)
}
if len(topology.Docker.Hosts) != 0 {
t.Fatalf("expected no docker topology, got: %+v", topology.Docker.Hosts)
}
if len(topology.Kubernetes.Clusters) != 1 {
t.Fatalf("expected one kubernetes cluster, got: %+v", topology.Kubernetes.Clusters)
}
cluster := topology.Kubernetes.Clusters[0]
if cluster.Name != "prod-cluster" || cluster.NodeCount != 1 || cluster.DeploymentCount != 1 || cluster.PodCount != 1 {
t.Fatalf("unexpected cluster topology: %+v", cluster)
}
if len(cluster.Nodes) != 1 || cluster.Nodes[0].Name != "worker-1" || !cluster.Nodes[0].Ready {
t.Fatalf("unexpected cluster nodes: %+v", cluster.Nodes)
}
if len(cluster.Deployments) != 1 || cluster.Deployments[0].Name != "api" || cluster.Deployments[0].ReadyReplicas != 2 {
t.Fatalf("unexpected cluster deployments: %+v", cluster.Deployments)
}
if len(cluster.Pods) != 1 || cluster.Pods[0].Name != "api-6f8d5c" || cluster.Pods[0].OwnerName != "api" {
t.Fatalf("unexpected cluster pods: %+v", cluster.Pods)
}
if topology.Summary.TotalK8sClusters != 1 || topology.Summary.TotalK8sNodes != 1 || topology.Summary.TotalK8sDeployments != 1 || topology.Summary.TotalK8sPods != 1 {
t.Fatalf("unexpected k8s summary totals: %+v", topology.Summary)
}
if topology.Summary.RunningK8sPods != 1 {
t.Fatalf("unexpected k8s running summary: %+v", topology.Summary)
}
}
func TestExecuteSearchResources(t *testing.T) {
state := models.StateSnapshot{
Nodes: []models.Node{{ID: "node1", Name: "node1", Status: "online"}},
VMs: []models.VM{
{ID: "vm1", VMID: 100, Name: "web-vm", Status: "running", Node: "node1"},
},
Containers: []models.Container{
{ID: "ct1", VMID: 200, Name: "db-ct", Status: "stopped", Node: "node1"},
},
DockerHosts: []models.DockerHost{
{
ID: "host1",
Hostname: "dock1",
DisplayName: "Dock 1",
Status: "online",
Containers: []models.DockerContainer{
{ID: "c1", Name: "nginx", State: "running", Image: "nginx:latest"},
},
},
},
}
executor := NewPulseToolExecutor(ExecutorConfig{
StateProvider: &mockStateProvider{state: state},
})
result, err := executor.executeSearchResources(context.Background(), map[string]interface{}{
"query": "nginx",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var response ResourceSearchResponse
if err := json.Unmarshal([]byte(result.Content[0].Text), &response); err != nil {
t.Fatalf("decode response: %v", err)
}
if len(response.Matches) != 1 || response.Matches[0].Type != "app-container" || response.Matches[0].Name != "nginx" {
t.Fatalf("unexpected search response: %+v", response)
}
result, err = executor.executeSearchResources(context.Background(), map[string]interface{}{
"query": "web",
"type": "vm",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
response = ResourceSearchResponse{}
if err := json.Unmarshal([]byte(result.Content[0].Text), &response); err != nil {
t.Fatalf("decode response: %v", err)
}
if len(response.Matches) != 1 || response.Matches[0].Type != "vm" || response.Matches[0].Name != "web-vm" {
t.Fatalf("unexpected search response: %+v", response)
}
result, err = executor.executeSearchResources(context.Background(), map[string]interface{}{
"query": "dock",
"type": "docker-host",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
response = ResourceSearchResponse{}
if err := json.Unmarshal([]byte(result.Content[0].Text), &response); err != nil {
t.Fatalf("decode response: %v", err)
}
if len(response.Matches) != 1 || response.Matches[0].Type != "docker-host" || response.Matches[0].Name != "Dock 1" {
t.Fatalf("unexpected docker-host search response: %+v", response)
}
// Canonical type+VMID patterns: "VM100", "system-container200"
for _, tc := range []struct {
query string
wantType string
wantName string
}{
{"system-container200", "system-container", "db-ct"},
{"VM100", "vm", "web-vm"},
{"vm100", "vm", "web-vm"},
} {
result, err = executor.executeSearchResources(context.Background(), map[string]interface{}{
"query": tc.query,
})
if err != nil {
t.Fatalf("query %q: unexpected error: %v", tc.query, err)
}
response = ResourceSearchResponse{}
if err := json.Unmarshal([]byte(result.Content[0].Text), &response); err != nil {
t.Fatalf("query %q: decode response: %v", tc.query, err)
}
if len(response.Matches) != 1 || response.Matches[0].Type != tc.wantType || response.Matches[0].Name != tc.wantName {
t.Fatalf("query %q: expected 1 match (%s %s), got %+v", tc.query, tc.wantType, tc.wantName, response.Matches)
}
}
for _, legacyQuery := range []string{"LXC200", "qemu100", "CT200"} {
result, err = executor.executeSearchResources(context.Background(), map[string]interface{}{
"query": legacyQuery,
})
if err != nil {
t.Fatalf("legacy query %q: unexpected error: %v", legacyQuery, err)
}
response = ResourceSearchResponse{}
if err := json.Unmarshal([]byte(result.Content[0].Text), &response); err != nil {
t.Fatalf("legacy query %q: decode response: %v", legacyQuery, err)
}
if len(response.Matches) != 0 {
t.Fatalf("legacy query %q: expected no matches, got %+v", legacyQuery, response.Matches)
}
}
}
func TestExecuteSearchResources_Errors(t *testing.T) {
executor := NewPulseToolExecutor(ExecutorConfig{
StateProvider: &mockStateProvider{state: models.StateSnapshot{}},
})
result, _ := executor.executeSearchResources(context.Background(), map[string]interface{}{
"query": "",
})
if !result.IsError {
t.Fatal("expected error for empty query")
}
result, _ = executor.executeSearchResources(context.Background(), map[string]interface{}{
"query": "node",
"type": "bad",
})
if !result.IsError {
t.Fatal("expected error for invalid type")
}
result, _ = executor.executeSearchResources(context.Background(), map[string]interface{}{
"query": "node",
"type": "host",
})
if !result.IsError {
t.Fatal("expected error for legacy host type")
}
if !strings.Contains(result.Content[0].Text, "invalid type: host") {
t.Fatalf("unexpected legacy host type error: %s", result.Content[0].Text)
}
}
func TestExecuteGetResource(t *testing.T) {
state := models.StateSnapshot{
VMs: []models.VM{{ID: "vm1", VMID: 100, Name: "vm1", Status: "running", Node: "node1"}},
Containers: []models.Container{{ID: "ct1", VMID: 200, Name: "ct1", Status: "running", Node: "node1"}},
DockerHosts: []models.DockerHost{{
Hostname: "host",
Containers: []models.DockerContainer{{
ID: "abc123",
Name: "nginx",
State: "running",
Image: "nginx",
}},
}},
}
executor := NewPulseToolExecutor(ExecutorConfig{
StateProvider: &mockStateProvider{state: state},
})
resource, _ := executor.executeGetResource(context.Background(), map[string]interface{}{
"resource_type": "vm",
"resource_id": "100",
})
var res ResourceResponse
if err := json.Unmarshal([]byte(resource.Content[0].Text), &res); err != nil {
t.Fatalf("decode resource: %v", err)
}
if res.Type != "vm" || res.Name != "vm1" {
t.Fatalf("unexpected resource: %+v", res)
}
resource, _ = executor.executeGetResource(context.Background(), map[string]interface{}{
"resource_type": "app-container",
"resource_id": "abc",
})
if err := json.Unmarshal([]byte(resource.Content[0].Text), &res); err != nil {
t.Fatalf("decode docker resource: %v", err)
}
if res.Type != "app-container" || res.Name != "nginx" {
t.Fatalf("unexpected docker resource: %+v", res)
}
}
func TestExecuteQuery_UsesCanonicalTrueNASUnifiedResources(t *testing.T) {
provider := newTrueNASUnifiedQueryProvider(t)
executor := NewPulseToolExecutor(ExecutorConfig{
UnifiedResourceProvider: provider,
})
listSystems, err := executor.executeListInfrastructure(context.Background(), map[string]interface{}{
"type": "systems",
})
if err != nil {
t.Fatalf("list systems: unexpected error: %v", err)
}
var systemsResp InfrastructureResponse
if err := json.Unmarshal([]byte(listSystems.Content[0].Text), &systemsResp); err != nil {
t.Fatalf("decode systems response: %v", err)
}
if len(systemsResp.Systems) != 1 {
t.Fatalf("expected one TrueNAS system, got %+v", systemsResp.Systems)
}
if systemsResp.Systems[0].Platform != "truenas" || systemsResp.Systems[0].Name != "truenas-main" {
t.Fatalf("unexpected system summary: %+v", systemsResp.Systems[0])
}
listApps, err := executor.executeListInfrastructure(context.Background(), map[string]interface{}{
"type": "app-containers",
})
if err != nil {
t.Fatalf("list app containers: unexpected error: %v", err)
}
var appsResp InfrastructureResponse
if err := json.Unmarshal([]byte(listApps.Content[0].Text), &appsResp); err != nil {
t.Fatalf("decode app container response: %v", err)
}
if len(appsResp.AppContainers) == 0 {
t.Fatal("expected canonical app containers from TrueNAS fixtures")
}
foundNextcloud := false
for _, app := range appsResp.AppContainers {
if app.Name != "Nextcloud" {
continue
}
foundNextcloud = true
if app.Platform != "truenas" || app.Host != "truenas-main" || app.ID != "nextcloud" {
t.Fatalf("unexpected Nextcloud summary: %+v", app)
}
}
if !foundNextcloud {
t.Fatalf("expected Nextcloud in app containers, got %+v", appsResp.AppContainers)
}
listPools, err := executor.executeListInfrastructure(context.Background(), map[string]interface{}{
"type": "storage-pools",
})
if err != nil {
t.Fatalf("list storage pools: unexpected error: %v", err)
}
var poolsResp InfrastructureResponse
if err := json.Unmarshal([]byte(listPools.Content[0].Text), &poolsResp); err != nil {
t.Fatalf("decode storage pools response: %v", err)
}
if len(poolsResp.StoragePools) != len(truenas.DefaultFixtures().Pools) {
t.Fatalf("expected only top-level pools, got %+v", poolsResp.StoragePools)
}
for _, pool := range poolsResp.StoragePools {
if strings.Contains(pool.Name, "/") {
t.Fatalf("expected storage-pools list to exclude datasets, got %+v", poolsResp.StoragePools)
}
}
listDisks, err := executor.executeListInfrastructure(context.Background(), map[string]interface{}{
"type": "physical-disks",
})
if err != nil {
t.Fatalf("list physical disks: unexpected error: %v", err)
}
var disksResp InfrastructureResponse
if err := json.Unmarshal([]byte(listDisks.Content[0].Text), &disksResp); err != nil {
t.Fatalf("decode physical disks response: %v", err)
}
if len(disksResp.PhysicalDisks) == 0 {
t.Fatal("expected physical disks from TrueNAS fixtures")
}
foundHotDisk := false
for _, disk := range disksResp.PhysicalDisks {
if disk.DevPath != "/dev/sdc" {
continue
}
foundHotDisk = true
if disk.Node != "truenas-main" || disk.Health == "" {
t.Fatalf("unexpected canonical disk summary: %+v", disk)
}
}
if !foundHotDisk {
t.Fatalf("expected sdc disk in list response, got %+v", disksResp.PhysicalDisks)
}
searchPool, err := executor.executeSearchResources(context.Background(), map[string]interface{}{
"query": "archive",
"type": "storage",
})
if err != nil {
t.Fatalf("search storage pools: unexpected error: %v", err)
}
var searchResp ResourceSearchResponse
if err := json.Unmarshal([]byte(searchPool.Content[0].Text), &searchResp); err != nil {
t.Fatalf("decode storage search response: %v", err)
}
if len(searchResp.Matches) != 1 || searchResp.Matches[0].Type != "storage" || searchResp.Matches[0].Name != "archive" || searchResp.Matches[0].Platform != "truenas" {
t.Fatalf("unexpected storage search response: %+v", searchResp.Matches)
}
searchDisk, err := executor.executeSearchResources(context.Background(), map[string]interface{}{
"query": "sdc",
"type": "physical-disk",
})
if err != nil {
t.Fatalf("search physical disks: unexpected error: %v", err)
}
searchResp = ResourceSearchResponse{}
if err := json.Unmarshal([]byte(searchDisk.Content[0].Text), &searchResp); err != nil {
t.Fatalf("decode physical-disk search response: %v", err)
}
if len(searchResp.Matches) != 1 || searchResp.Matches[0].Type != "physical-disk" || searchResp.Matches[0].Platform != "truenas" || searchResp.Matches[0].Host != "truenas-main" {
t.Fatalf("unexpected physical-disk search response: %+v", searchResp.Matches)
}
getSystem, err := executor.executeGetResource(context.Background(), map[string]interface{}{
"resource_type": "agent",
"resource_id": "truenas-main",
})
if err != nil {
t.Fatalf("get agent: unexpected error: %v", err)
}
var systemRes ResourceResponse
if err := json.Unmarshal([]byte(getSystem.Content[0].Text), &systemRes); err != nil {
t.Fatalf("decode agent resource: %v", err)
}
if systemRes.Type != "agent" || systemRes.Platform != "truenas" || systemRes.Host != "truenas-main" {
t.Fatalf("unexpected canonical agent resource: %+v", systemRes)
}
getApp, err := executor.executeGetResource(context.Background(), map[string]interface{}{
"resource_type": "app-container",
"resource_id": "nextcloud",
})
if err != nil {
t.Fatalf("get app-container: unexpected error: %v", err)
}
var appRes ResourceResponse
if err := json.Unmarshal([]byte(getApp.Content[0].Text), &appRes); err != nil {
t.Fatalf("decode app resource: %v", err)
}
if appRes.Type != "app-container" || appRes.Name != "Nextcloud" || appRes.Platform != "truenas" || appRes.Host != "truenas-main" {
t.Fatalf("unexpected canonical app resource: %+v", appRes)
}
}
func TestExecuteGetResource_RegistersTrueNASAppContainerForCanonicalControl(t *testing.T) {
provider := newTrueNASUnifiedQueryProvider(t)
resolved := &mockResolvedContext{
resources: make(map[string]ResolvedResourceInfo),
aliases: make(map[string]ResolvedResourceInfo),
}
executor := NewPulseToolExecutor(ExecutorConfig{
UnifiedResourceProvider: provider,
})
executor.SetResolvedContext(resolved)
_, err := executor.executeGetResource(context.Background(), map[string]interface{}{
"resource_type": "app-container",
"resource_id": "nextcloud",
})
if err != nil {
t.Fatalf("get app-container: unexpected error: %v", err)
}
info, found := resolved.GetResolvedResourceByAlias("Nextcloud")
if !found {
t.Fatal("expected resolved context to include TrueNAS app by alias")
}
if info.GetAdapter() != "truenas" {
t.Fatalf("expected truenas adapter, got %q", info.GetAdapter())
}
if info.GetTargetHost() != "truenas-main" {
t.Fatalf("expected truenas-main target host, got %q", info.GetTargetHost())
}
allowed := strings.Join(info.GetAllowedActions(), ",")
for _, action := range []string{"query", "get", "start", "stop", "restart"} {
if !strings.Contains(allowed, action) {
t.Fatalf("expected allowed actions to include %q, got %v", action, info.GetAllowedActions())
}
}
}
func TestExecuteGetResource_RegistersVMwareVMAsReadOnly(t *testing.T) {
provider := newVMwareUnifiedQueryProvider(t)
resolved := &mockResolvedContext{
resources: make(map[string]ResolvedResourceInfo),
aliases: make(map[string]ResolvedResourceInfo),
}
executor := NewPulseToolExecutor(ExecutorConfig{
UnifiedResourceProvider: provider,
ReadState: provider.ResourceRegistry,
})
executor.SetResolvedContext(resolved)
result, err := executor.executeGetResource(context.Background(), map[string]interface{}{
"resource_type": "vm",
"resource_id": "app-01",
})
if err != nil {
t.Fatalf("get vm: unexpected error: %v", err)
}
var response ResourceResponse
if err := json.Unmarshal([]byte(result.Content[0].Text), &response); err != nil {
t.Fatalf("decode response: %v", err)
}
if response.Platform != "vmware-vsphere" {
t.Fatalf("expected VMware platform on response, got %+v", response)
}
if response.Node != "esxi-01.lab.local" {
t.Fatalf("expected VMware runtime host in node field, got %+v", response)
}
info, found := resolved.GetResolvedResourceByAlias("app-01")
if !found {
t.Fatal("expected resolved context to include VMware VM by alias")
}
if info.GetAdapter() != "vmware-vsphere" {
t.Fatalf("expected VMware adapter, got %q", info.GetAdapter())
}
if got := strings.Join(info.GetAllowedActions(), ","); got != "query,get" && got != "get,query" {
t.Fatalf("expected VMware VM to register read-only actions, got %v", info.GetAllowedActions())
}
controlResult, err := executor.executeControl(context.Background(), map[string]interface{}{
"type": "resource",
"resource_id": "app-01",
"action": "restart",
})
if err != nil {
t.Fatalf("executeControl(type=resource): unexpected error: %v", err)
}
if !controlResult.IsError {
t.Fatalf("expected VMware VM restart to be rejected, got %+v", controlResult)
}
if !strings.Contains(controlResult.Content[0].Text, "not permitted") {
t.Fatalf("expected read-only rejection, got %q", controlResult.Content[0].Text)
}
}
func TestExecuteGetResource_RegistersVMwareAgentAsReadOnly(t *testing.T) {
provider := newVMwareUnifiedQueryProvider(t)
hostResources := provider.GetByType(unifiedresources.ResourceTypeAgent)
if len(hostResources) != 1 {
t.Fatalf("expected one VMware host resource, got %d", len(hostResources))
}
resolved := &mockResolvedContext{
resources: make(map[string]ResolvedResourceInfo),
aliases: make(map[string]ResolvedResourceInfo),
}
executor := NewPulseToolExecutor(ExecutorConfig{
UnifiedResourceProvider: provider,
ReadState: provider.ResourceRegistry,
})
executor.SetResolvedContext(resolved)
result, err := executor.executeGetResource(context.Background(), map[string]interface{}{
"resource_type": "agent",
"resource_id": hostResources[0].ID,
})
if err != nil {
t.Fatalf("get agent: unexpected error: %v", err)
}
var response ResourceResponse
if err := json.Unmarshal([]byte(result.Content[0].Text), &response); err != nil {
t.Fatalf("decode response: %v", err)
}
if response.Type != "agent" || response.Platform != "vmware-vsphere" || response.Host != "esxi-01.lab.local" {
t.Fatalf("unexpected VMware agent response: %+v", response)
}
info, found := resolved.GetResolvedResourceByAlias("esxi-01.lab.local")
if !found {
t.Fatal("expected resolved context to include VMware host by alias")
}
if info.GetKind() != "agent" {
t.Fatalf("expected agent kind, got %q", info.GetKind())
}
if info.GetAdapter() != "vmware-vsphere" {
t.Fatalf("expected VMware adapter, got %q", info.GetAdapter())
}
if got := strings.Join(info.GetAllowedActions(), ","); got != "query,get" && got != "get,query" {
t.Fatalf("expected VMware host to register read-only actions, got %v", info.GetAllowedActions())
}
controlResult, err := executor.executeControl(context.Background(), map[string]interface{}{
"type": "resource",
"resource_id": "esxi-01.lab.local",
"action": "restart",
})
if err != nil {
t.Fatalf("executeControl(type=resource): unexpected error: %v", err)
}
if !controlResult.IsError {
t.Fatalf("expected VMware host restart to be rejected, got %+v", controlResult)
}
if !strings.Contains(controlResult.Content[0].Text, "not permitted") {
t.Fatalf("expected read-only rejection, got %q", controlResult.Content[0].Text)
}
}
func TestExecuteGetResource_RegistersVMwareStorageAsReadOnly(t *testing.T) {
provider := newVMwareUnifiedQueryProvider(t)
storageResources := provider.GetByType(unifiedresources.ResourceTypeStorage)
if len(storageResources) != 1 {
t.Fatalf("expected one VMware storage resource, got %d", len(storageResources))
}
resolved := &mockResolvedContext{
resources: make(map[string]ResolvedResourceInfo),
aliases: make(map[string]ResolvedResourceInfo),
}
executor := NewPulseToolExecutor(ExecutorConfig{
UnifiedResourceProvider: provider,
ReadState: provider.ResourceRegistry,
})
executor.SetResolvedContext(resolved)
result, err := executor.executeGetResource(context.Background(), map[string]interface{}{
"resource_type": "storage",
"resource_id": storageResources[0].ID,
})
if err != nil {
t.Fatalf("get storage: unexpected error: %v", err)
}
var response ResourceResponse
if err := json.Unmarshal([]byte(result.Content[0].Text), &response); err != nil {
t.Fatalf("decode response: %v", err)
}
if response.Type != "storage" || response.Platform != "vmware-vsphere" || response.Name != "nvme-primary" {
t.Fatalf("unexpected VMware storage response: %+v", response)
}
if response.Disk == nil || response.Disk.TotalGB == 0 {
t.Fatalf("expected VMware storage capacity details, got %+v", response)
}
info, found := resolved.GetResolvedResourceByAlias("nvme-primary")
if !found {
t.Fatal("expected resolved context to include VMware storage by alias")
}
if info.GetKind() != "storage" {
t.Fatalf("expected storage kind, got %q", info.GetKind())
}
if got := strings.Join(info.GetAllowedActions(), ","); got != "query,get" && got != "get,query" {
t.Fatalf("expected VMware storage to register read-only actions, got %v", info.GetAllowedActions())
}
}
func TestExecuteSearchResources_RegistersVMwareVMAsReadOnly(t *testing.T) {
provider := newVMwareUnifiedQueryProvider(t)
resolved := &mockResolvedContext{
resources: make(map[string]ResolvedResourceInfo),
aliases: make(map[string]ResolvedResourceInfo),
}
executor := NewPulseToolExecutor(ExecutorConfig{
UnifiedResourceProvider: provider,
ReadState: provider.ResourceRegistry,
})
executor.SetResolvedContext(resolved)
result, err := executor.executeSearchResources(context.Background(), map[string]interface{}{
"query": "app-01",
"type": "vm",
})
if err != nil {
t.Fatalf("search vm: unexpected error: %v", err)
}
var response ResourceSearchResponse
if err := json.Unmarshal([]byte(result.Content[0].Text), &response); err != nil {
t.Fatalf("decode search response: %v", err)
}
if len(response.Matches) != 1 {
t.Fatalf("expected one VMware search match, got %+v", response.Matches)
}
if response.Matches[0].Platform != "vmware-vsphere" {
t.Fatalf("expected VMware platform on search match, got %+v", response.Matches[0])
}
info, found := resolved.GetResolvedResourceByAlias("app-01")
if !found {
t.Fatal("expected VMware VM search result to register in resolved context")
}
if got := strings.Join(info.GetAllowedActions(), ","); got != "query,get" && got != "get,query" {
t.Fatalf("expected VMware search registration to remain read-only, got %v", info.GetAllowedActions())
}
}
func TestExecuteSearchResources_RegistersVMwareAgentAsReadOnly(t *testing.T) {
provider := newVMwareUnifiedQueryProvider(t)
resolved := &mockResolvedContext{
resources: make(map[string]ResolvedResourceInfo),
aliases: make(map[string]ResolvedResourceInfo),
}
executor := NewPulseToolExecutor(ExecutorConfig{
UnifiedResourceProvider: provider,
ReadState: provider.ResourceRegistry,
})
executor.SetResolvedContext(resolved)
result, err := executor.executeSearchResources(context.Background(), map[string]interface{}{
"query": "esxi-01",
"type": "agent",
})
if err != nil {
t.Fatalf("search agent: unexpected error: %v", err)
}
var response ResourceSearchResponse
if err := json.Unmarshal([]byte(result.Content[0].Text), &response); err != nil {
t.Fatalf("decode search response: %v", err)
}
if len(response.Matches) != 1 {
t.Fatalf("expected one VMware host search match, got %+v", response.Matches)
}
if response.Matches[0].Type != "agent" || response.Matches[0].Platform != "vmware-vsphere" {
t.Fatalf("unexpected VMware host search match: %+v", response.Matches[0])
}
info, found := resolved.GetResolvedResourceByAlias("esxi-01.lab.local")
if !found {
t.Fatal("expected VMware host search result to register in resolved context")
}
if got := strings.Join(info.GetAllowedActions(), ","); got != "query,get" && got != "get,query" {
t.Fatalf("expected VMware host search registration to remain read-only, got %v", info.GetAllowedActions())
}
}
func TestExecuteSearchResources_RegistersVMwareStorageAsReadOnly(t *testing.T) {
provider := newVMwareUnifiedQueryProvider(t)
resolved := &mockResolvedContext{
resources: make(map[string]ResolvedResourceInfo),
aliases: make(map[string]ResolvedResourceInfo),
}
executor := NewPulseToolExecutor(ExecutorConfig{
UnifiedResourceProvider: provider,
ReadState: provider.ResourceRegistry,
})
executor.SetResolvedContext(resolved)
result, err := executor.executeSearchResources(context.Background(), map[string]interface{}{
"query": "nvme-primary",
"type": "storage",
})
if err != nil {
t.Fatalf("search storage: unexpected error: %v", err)
}
var response ResourceSearchResponse
if err := json.Unmarshal([]byte(result.Content[0].Text), &response); err != nil {
t.Fatalf("decode search response: %v", err)
}
if len(response.Matches) != 1 {
t.Fatalf("expected one VMware storage search match, got %+v", response.Matches)
}
if response.Matches[0].Type != "storage" || response.Matches[0].Platform != "vmware-vsphere" {
t.Fatalf("unexpected VMware storage search match: %+v", response.Matches[0])
}
info, found := resolved.GetResolvedResourceByAlias("nvme-primary")
if !found {
t.Fatal("expected VMware storage search result to register in resolved context")
}
if got := strings.Join(info.GetAllowedActions(), ","); got != "query,get" && got != "get,query" {
t.Fatalf("expected VMware storage search registration to remain read-only, got %v", info.GetAllowedActions())
}
}
func TestExecuteGetResource_RegistersCommandCapableAgentForExec(t *testing.T) {
provider := newCommandCapableAgentQueryProvider()
resolved := &mockResolvedContext{
resources: make(map[string]ResolvedResourceInfo),
aliases: make(map[string]ResolvedResourceInfo),
}
executor := NewPulseToolExecutor(ExecutorConfig{
UnifiedResourceProvider: provider,
ReadState: provider.ResourceRegistry,
})
executor.SetResolvedContext(resolved)
_, err := executor.executeGetResource(context.Background(), map[string]interface{}{
"resource_type": "agent",
"resource_id": "lab-host",
})
if err != nil {
t.Fatalf("get agent: unexpected error: %v", err)
}
info, found := resolved.GetResolvedResourceByAlias("lab-host")
if !found {
t.Fatal("expected command-capable agent to register in resolved context")
}
allowed := strings.Join(info.GetAllowedActions(), ",")
for _, action := range []string{"query", "get", "exec"} {
if !strings.Contains(allowed, action) {
t.Fatalf("expected allowed actions to include %q, got %v", action, info.GetAllowedActions())
}
}
}
func TestExecuteQuerySurfacesIncludeGovernedMetadata(t *testing.T) {
state := models.StateSnapshot{
Nodes: []models.Node{
{ID: "node1", Name: "node1.internal", Status: "online"},
},
VMs: []models.VM{
{ID: "qemu/pve1/node1/100", VMID: 100, Name: "finance-vm", Status: "running", Node: "node1.internal"},
},
}
executor := NewPulseToolExecutor(ExecutorConfig{
StateProvider: &mockStateProvider{state: state},
})
getResult, _ := executor.executeGetResource(context.Background(), map[string]interface{}{
"resource_type": "vm",
"resource_id": "100",
})
var resource ResourceResponse
if err := json.Unmarshal([]byte(getResult.Content[0].Text), &resource); err != nil {
t.Fatalf("decode resource response: %v", err)
}
if resource.Policy == nil {
t.Fatal("expected governed policy metadata on get response")
}
if resource.Policy.Sensitivity != "sensitive" {
t.Fatalf("unexpected get response sensitivity: %+v", resource.Policy)
}
if resource.AISafeSummary == "" {
t.Fatal("expected aiSafeSummary on get response")
}
if strings.Contains(resource.AISafeSummary, "finance-vm") {
t.Fatalf("aiSafeSummary leaked raw VM name: %q", resource.AISafeSummary)
}
searchResult, _ := executor.executeSearchResources(context.Background(), map[string]interface{}{
"query": "finance",
"type": "vm",
})
var search ResourceSearchResponse
if err := json.Unmarshal([]byte(searchResult.Content[0].Text), &search); err != nil {
t.Fatalf("decode search response: %v", err)
}
if len(search.Matches) != 1 {
t.Fatalf("expected 1 search match, got %+v", search.Matches)
}
if search.Matches[0].Policy == nil {
t.Fatal("expected governed policy metadata on search match")
}
if search.Matches[0].AISafeSummary == "" {
t.Fatal("expected aiSafeSummary on search match")
}
topologyResult, _ := executor.executeGetTopology(context.Background(), map[string]interface{}{
"include": "proxmox",
})
var topology TopologyResponse
if err := json.Unmarshal([]byte(topologyResult.Content[0].Text), &topology); err != nil {
t.Fatalf("decode topology response: %v", err)
}
if len(topology.Proxmox.Nodes) != 1 {
t.Fatalf("expected 1 proxmox node, got %+v", topology.Proxmox.Nodes)
}
if topology.Proxmox.Nodes[0].Policy == nil {
t.Fatal("expected governed policy metadata on topology node")
}
if len(topology.Proxmox.Nodes[0].VMs) != 1 || topology.Proxmox.Nodes[0].VMs[0].Policy == nil {
t.Fatalf("expected governed VM metadata in topology response, got %+v", topology.Proxmox.Nodes[0].VMs)
}
}
func TestGovernedQueryMetadataFromResolvedResourceClonesPolicy(t *testing.T) {
resource := &unifiedresources.Resource{
ID: "vm-1",
Name: "customer-payments",
Tags: []string{"customer-data"},
Identity: unifiedresources.ResourceIdentity{
Hostnames: []string{"payments.internal"},
IPAddresses: []string{"10.0.0.44"},
},
Canonical: &unifiedresources.CanonicalIdentity{
PlatformID: "payments.internal",
PrimaryID: "vm:100",
Aliases: []string{"vm-100"},
},
}
metadata := governedQueryMetadataFromResolvedResource(resource)
if metadata.Policy == nil {
t.Fatal("expected policy metadata")
}
if metadata.Policy == resource.Policy {
t.Fatal("expected metadata policy to be a clone")
}
if len(metadata.Policy.Routing.Redact) == 0 {
t.Fatal("expected redactions on governed metadata policy")
}
baselinePolicy := unifiedresources.CloneResourcePolicy(metadata.Policy)
metadata.Policy.Routing.Redact[0] = unifiedresources.ResourceRedactionAlias
if baselinePolicy.Routing.Redact[0] != unifiedresources.ResourceRedactionHostname {
t.Fatalf("expected clone to deep copy redactions, baseline mutated to %#v", baselinePolicy.Routing.Redact)
}
if metadata.AISafeSummary == "" {
t.Fatal("expected ai safe summary")
}
if metadata.AISafeSummary == resource.Name {
t.Fatalf("expected safe summary to avoid raw resource name: %q", metadata.AISafeSummary)
}
if metadata.Policy.Sensitivity != unifiedresources.ResourceSensitivityRestricted {
t.Fatalf("expected restricted sensitivity, got %q", metadata.Policy.Sensitivity)
}
if metadata.Policy.Routing.Scope != unifiedresources.ResourceRoutingScopeLocalOnly {
t.Fatalf("expected local-only routing, got %q", metadata.Policy.Routing.Scope)
}
}
func TestExecuteGetResource_DockerDetails(t *testing.T) {
state := models.StateSnapshot{
DockerHosts: []models.DockerHost{{
Hostname: "dock1",
Containers: []models.DockerContainer{{
ID: "abcd1234",
Name: "web",
State: "running",
Image: "nginx:latest",
Health: "healthy",
CPUPercent: 1.2,
MemoryPercent: 3.4,
MemoryUsage: 1024,
MemoryLimit: 2048,
RestartCount: 2,
Labels: map[string]string{
"service": "web",
},
UpdateStatus: &models.DockerContainerUpdateStatus{
UpdateAvailable: true,
},
Ports: []models.DockerContainerPort{
{PrivatePort: 80, PublicPort: 8080, Protocol: "tcp", IP: "0.0.0.0"},
},
Networks: []models.DockerContainerNetworkLink{
{Name: "bridge", IPv4: "172.17.0.2"},
},
Mounts: []models.DockerContainerMount{
{Source: "/src", Destination: "/dst", RW: true},
},
}},
}},
}
executor := NewPulseToolExecutor(ExecutorConfig{
StateProvider: &mockStateProvider{state: state},
})
result, _ := executor.executeGetResource(context.Background(), map[string]interface{}{
"resource_type": "app-container",
"resource_id": "web",
})
var res ResourceResponse
if err := json.Unmarshal([]byte(result.Content[0].Text), &res); err != nil {
t.Fatalf("decode docker resource: %v", err)
}
if !res.UpdateAvailable || len(res.Ports) != 1 || len(res.Networks) != 1 || len(res.Mounts) != 1 {
t.Fatalf("unexpected docker details: %+v", res)
}
}
func TestIntArg(t *testing.T) {
if got := intArg(map[string]interface{}{}, "limit", 10); got != 10 {
t.Fatalf("unexpected default: %d", got)
}
if got := intArg(map[string]interface{}{"limit": float64(5)}, "limit", 10); got != 5 {
t.Fatalf("unexpected value: %d", got)
}
}
func TestExecuteListInfrastructurePaginationAndDockerFilter(t *testing.T) {
state := models.StateSnapshot{
Nodes: []models.Node{
{ID: "node1", Name: "node1", Status: "online"},
{ID: "node2", Name: "node2", Status: "offline"},
},
DockerHosts: []models.DockerHost{
{
ID: "host1",
Hostname: "dock1",
Containers: []models.DockerContainer{
{ID: "c1", Name: "app", State: "running"},
{ID: "c2", Name: "db", State: "stopped"},
},
},
{
ID: "host2",
Hostname: "dock2",
Containers: []models.DockerContainer{
{ID: "c3", Name: "cache", State: "stopped"},
},
},
},
}
executor := NewPulseToolExecutor(ExecutorConfig{
StateProvider: &mockStateProvider{state: state},
})
result, err := executor.executeListInfrastructure(context.Background(), map[string]interface{}{
"type": "nodes",
"limit": 1,
"offset": 1,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var nodesResp InfrastructureResponse
if err := json.Unmarshal([]byte(result.Content[0].Text), &nodesResp); err != nil {
t.Fatalf("decode nodes response: %v", err)
}
if len(nodesResp.Nodes) != 1 || nodesResp.Nodes[0].Name != "node2" {
t.Fatalf("unexpected nodes response: %+v", nodesResp.Nodes)
}
if nodesResp.Pagination == nil || nodesResp.Pagination.Total != 2 || nodesResp.Pagination.Offset != 1 {
t.Fatalf("unexpected pagination: %+v", nodesResp.Pagination)
}
result, err = executor.executeListInfrastructure(context.Background(), map[string]interface{}{
"type": "app-containers",
"status": "running",
"max_docker_containers_per_host": 1,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var dockerResp InfrastructureResponse
if err := json.Unmarshal([]byte(result.Content[0].Text), &dockerResp); err != nil {
t.Fatalf("decode docker response: %v", err)
}
if len(dockerResp.AppContainers) != 1 || dockerResp.AppContainers[0].Name != "app" {
t.Fatalf("unexpected app containers: %+v", dockerResp.AppContainers)
}
if dockerResp.AppContainers[0].Host != "dock1" || dockerResp.AppContainers[0].Status != "running" || dockerResp.AppContainers[0].Platform != "docker" {
t.Fatalf("unexpected app container summary: %+v", dockerResp.AppContainers[0])
}
}
func TestExecuteListInfrastructure_KubernetesFilters(t *testing.T) {
state := models.StateSnapshot{
KubernetesClusters: []models.KubernetesCluster{
{
ID: "cluster-1",
Name: "prod-cluster",
Status: "online",
Nodes: []models.KubernetesNode{
{Name: "worker-1", UID: "node-1", Ready: true, Roles: []string{"worker"}},
},
Pods: []models.KubernetesPod{
{
UID: "pod-1",
Name: "api-123",
Namespace: "default",
Phase: "Running",
OwnerKind: "Deployment",
OwnerName: "api",
},
{
UID: "pod-2",
Name: "job-123",
Namespace: "batch",
Phase: "Succeeded",
OwnerKind: "Job",
OwnerName: "cleanup",
},
},
Deployments: []models.KubernetesDeployment{
{
UID: "dep-1",
Name: "api",
Namespace: "default",
DesiredReplicas: 3,
ReadyReplicas: 2,
AvailableReplicas: 2,
},
},
},
{
ID: "cluster-2",
Name: "dev-cluster",
Status: "warning",
Nodes: []models.KubernetesNode{
{Name: "dev-worker-1", UID: "node-2", Ready: false},
},
},
},
}
executor := NewPulseToolExecutor(ExecutorConfig{
StateProvider: &mockStateProvider{state: state},
})
result, err := executor.executeListInfrastructure(context.Background(), map[string]interface{}{
"type": "kubernetes",
"cluster_name": "prod-cluster",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var clustersResp InfrastructureResponse
if err := json.Unmarshal([]byte(result.Content[0].Text), &clustersResp); err != nil {
t.Fatalf("decode kubernetes clusters response: %v", err)
}
if len(clustersResp.K8sClusters) != 1 || clustersResp.K8sClusters[0].Name != "prod-cluster" {
t.Fatalf("unexpected k8s clusters: %+v", clustersResp.K8sClusters)
}
if clustersResp.K8sClusters[0].NodeCount != 1 || clustersResp.K8sClusters[0].DeploymentCount != 1 || clustersResp.K8sClusters[0].PodCount != 2 {
t.Fatalf("unexpected k8s cluster counts: %+v", clustersResp.K8sClusters[0])
}
if clustersResp.Total.K8sClusters != 2 || clustersResp.Total.K8sNodes != 2 || clustersResp.Total.K8sPods != 2 || clustersResp.Total.K8sDeployments != 1 {
t.Fatalf("unexpected k8s totals: %+v", clustersResp.Total)
}
result, err = executor.executeListInfrastructure(context.Background(), map[string]interface{}{
"type": "k8s-pods",
"status": "running",
"cluster_name": "prod-cluster",
"namespace": "default",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var podsResp InfrastructureResponse
if err := json.Unmarshal([]byte(result.Content[0].Text), &podsResp); err != nil {
t.Fatalf("decode k8s pods response: %v", err)
}
if len(podsResp.K8sPods) != 1 || podsResp.K8sPods[0].Name != "api-123" {
t.Fatalf("unexpected k8s pods: %+v", podsResp.K8sPods)
}
result, err = executor.executeListInfrastructure(context.Background(), map[string]interface{}{
"type": "k8s-deployments",
"namespace": "default",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var deploymentsResp InfrastructureResponse
if err := json.Unmarshal([]byte(result.Content[0].Text), &deploymentsResp); err != nil {
t.Fatalf("decode k8s deployments response: %v", err)
}
if len(deploymentsResp.K8sDeployments) != 1 || deploymentsResp.K8sDeployments[0].Name != "api" {
t.Fatalf("unexpected k8s deployments: %+v", deploymentsResp.K8sDeployments)
}
}
func TestExecuteGetResourceErrorsAndContainer(t *testing.T) {
executor := NewPulseToolExecutor(ExecutorConfig{})
result, _ := executor.executeGetResource(context.Background(), map[string]interface{}{
"resource_type": "vm",
"resource_id": "100",
})
if result.Content[0].Text != "State information not available." {
t.Fatalf("unexpected response: %s", result.Content[0].Text)
}
executor.stateProvider = &mockStateProvider{state: models.StateSnapshot{
Containers: []models.Container{
{ID: "ct1", VMID: 200, Name: "ct1", Status: "running", Node: "node1"},
},
}}
result, _ = executor.executeGetResource(context.Background(), map[string]interface{}{
"resource_type": "system-container",
"resource_id": "ct1",
})
var res ResourceResponse
if err := json.Unmarshal([]byte(result.Content[0].Text), &res); err != nil {
t.Fatalf("decode container response: %v", err)
}
if res.Type != "system-container" || res.Name != "ct1" {
t.Fatalf("unexpected container response: %+v", res)
}
result, _ = executor.executeGetResource(context.Background(), map[string]interface{}{
"resource_type": "vm",
"resource_id": "999",
})
var notFound map[string]interface{}
if err := json.Unmarshal([]byte(result.Content[0].Text), &notFound); err != nil {
t.Fatalf("decode not found response: %v", err)
}
if notFound["error"] != "not_found" {
t.Fatalf("unexpected not found response: %+v", notFound)
}
result, _ = executor.executeGetResource(context.Background(), map[string]interface{}{
"resource_type": "bad",
"resource_id": "1",
})
if !result.IsError {
t.Fatal("expected error for invalid resource_type")
}
result, _ = executor.executeGetResource(context.Background(), map[string]interface{}{
"resource_type": "host",
"resource_id": "1",
})
if !result.IsError {
t.Fatal("expected error for legacy host resource_type")
}
if !strings.Contains(result.Content[0].Text, "invalid resource_type: host") {
t.Fatalf("unexpected legacy host resource_type error: %s", result.Content[0].Text)
}
}
func TestExecuteListInfrastructure_NoStateProvider(t *testing.T) {
executor := NewPulseToolExecutor(ExecutorConfig{})
result, _ := executor.executeListInfrastructure(context.Background(), map[string]interface{}{})
if !result.IsError {
t.Fatal("expected error without state provider")
}
}
func TestInfrastructureResponse_UsesCanonicalEmptyCollections(t *testing.T) {
payload, err := json.Marshal(EmptyInfrastructureResponse())
if err != nil {
t.Fatalf("marshal empty infrastructure response: %v", err)
}
var decoded map[string]any
if err := json.Unmarshal(payload, &decoded); err != nil {
t.Fatalf("decode empty infrastructure response: %v", err)
}
for _, key := range []string{"systems", "nodes", "vms", "containers", "app_containers", "docker_hosts", "storage_pools", "physical_disks", "k8s_clusters", "k8s_nodes", "k8s_pods", "k8s_deployments"} {
values, ok := decoded[key].([]any)
if !ok || len(values) != 0 {
t.Fatalf("expected %s to serialize as an empty array, got %T (%v)", key, decoded[key], decoded[key])
}
}
}
func TestExecuteGetResource_MissingArgs(t *testing.T) {
executor := NewPulseToolExecutor(ExecutorConfig{
StateProvider: &mockStateProvider{state: models.StateSnapshot{}},
})
result, _ := executor.executeGetResource(context.Background(), map[string]interface{}{})
if !result.IsError {
t.Fatal("expected error for missing resource_type")
}
result, _ = executor.executeGetResource(context.Background(), map[string]interface{}{
"resource_type": "vm",
})
if !result.IsError {
t.Fatal("expected error for missing resource_id")
}
}
func TestExecuteGetResourceConfig_SystemContainerUsesCanonicalResolution(t *testing.T) {
guestCfg := &mockGuestConfigProvider{
config: map[string]interface{}{
"hostname": "ct1",
"ostype": "debian",
"onboot": "1",
"rootfs": "local-lvm:vm-200-disk-0,size=8G",
"mp0": "local:200/vm-200-disk-1.raw,mp=/data",
},
}
executor := NewPulseToolExecutor(ExecutorConfig{
StateProvider: &mockStateProvider{state: models.StateSnapshot{
Containers: []models.Container{
{ID: "ct1", VMID: 200, Name: "ct1", Status: "running", Node: "node1", Instance: "pve1"},
},
}},
GuestConfigProvider: guestCfg,
})
result, _ := executor.executeGetResourceConfig(context.Background(), map[string]interface{}{
"resource_type": "system-container",
"resource_id": "ct1",
})
if result.IsError {
t.Fatalf("unexpected error result: %s", result.Content[0].Text)
}
var response GuestConfigResponse
if err := json.Unmarshal([]byte(result.Content[0].Text), &response); err != nil {
t.Fatalf("decode guest config response: %v", err)
}
if response.GuestType != "system-container" || response.VMID != 200 || response.Node != "node1" {
t.Fatalf("unexpected guest config response: %+v", response)
}
if response.Policy == nil {
t.Fatal("expected governed policy metadata on guest config response")
}
if response.AISafeSummary == "" {
t.Fatal("expected aiSafeSummary on guest config response")
}
if guestCfg.lastGuestType != "container" {
t.Fatalf("expected provider guestType=container, got %q", guestCfg.lastGuestType)
}
if guestCfg.lastInstance != "pve1" || guestCfg.lastNode != "node1" || guestCfg.lastVMID != 200 {
t.Fatalf("unexpected provider call context: instance=%q node=%q vmid=%d", guestCfg.lastInstance, guestCfg.lastNode, guestCfg.lastVMID)
}
}
func TestExecuteGetGuestConfig_RejectsLegacyResourceTypes(t *testing.T) {
executor := NewPulseToolExecutor(ExecutorConfig{
StateProvider: &mockStateProvider{state: models.StateSnapshot{
Containers: []models.Container{
{ID: "ct1", VMID: 200, Name: "ct1", Status: "running", Node: "node1", Instance: "pve1"},
},
}},
GuestConfigProvider: &mockGuestConfigProvider{config: map[string]interface{}{}},
})
for _, resourceType := range []string{"lxc", "host"} {
t.Run(resourceType, func(t *testing.T) {
result, _ := executor.executeGetResourceConfig(context.Background(), map[string]interface{}{
"resource_type": resourceType,
"resource_id": "ct1",
})
if !result.IsError {
t.Fatalf("expected error for legacy %s resource_type", resourceType)
}
if !strings.Contains(result.Content[0].Text, "invalid resource_type: "+resourceType) {
t.Fatalf("unexpected error text for %s: %s", resourceType, result.Content[0].Text)
}
})
}
}