Pulse/internal/ai/target_host_test.go
rcourtman 3fdf753a5b Enhance devcontainer and CI workflows
- Add persistent volume mounts for Go/npm caches (faster rebuilds)
- Add shell config with helpful aliases and custom prompt
- Add comprehensive devcontainer documentation
- Add pre-commit hooks for Go formatting and linting
- Use go-version-file in CI workflows instead of hardcoded versions
- Simplify docker compose commands with --wait flag
- Add gitignore entries for devcontainer auth files

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 22:29:15 +00:00

187 lines
4.8 KiB
Go

package ai
import (
"testing"
"github.com/rcourtman/pulse-go-rewrite/internal/agentexec"
)
// TestRouteToAgent_TargetHostExplicit tests that explicit target_host in context
// takes priority for routing decisions
func TestRouteToAgent_TargetHostExplicit(t *testing.T) {
s := &Service{}
agents := []agentexec.ConnectedAgent{
{AgentID: "agent-1", Hostname: "delly"},
{AgentID: "agent-2", Hostname: "minipc"},
{AgentID: "agent-3", Hostname: "pimox"},
}
tests := []struct {
name string
req ExecuteRequest
command string
wantAgentID string
wantHostname string
wantMethod string
}{
{
name: "explicit node in context routes correctly",
req: ExecuteRequest{
TargetType: "host", // run_on_host=true sets this
TargetID: "", // run_on_host clears this
Context: map[string]interface{}{"node": "minipc"},
},
command: "pct exec 106 -- hostname",
wantAgentID: "agent-2",
wantHostname: "minipc",
wantMethod: "context_node",
},
{
name: "guest_node also routes correctly for host commands",
req: ExecuteRequest{
TargetType: "host",
TargetID: "",
Context: map[string]interface{}{"guest_node": "pimox"},
},
command: "qm guest exec 100 hostname",
wantAgentID: "agent-3",
wantHostname: "pimox",
wantMethod: "context_guest_node",
},
{
name: "node takes priority over guest_node",
req: ExecuteRequest{
TargetType: "host",
TargetID: "",
Context: map[string]interface{}{
"node": "delly",
"guest_node": "minipc", // Should be ignored when node is set
},
},
command: "uptime",
wantAgentID: "agent-1",
wantHostname: "delly",
wantMethod: "context_node",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := s.routeToAgent(tt.req, tt.command, agents)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.AgentID != tt.wantAgentID {
t.Errorf("AgentID = %q, want %q", result.AgentID, tt.wantAgentID)
}
if result.AgentHostname != tt.wantHostname {
t.Errorf("AgentHostname = %q, want %q", result.AgentHostname, tt.wantHostname)
}
if result.RoutingMethod != tt.wantMethod {
t.Errorf("RoutingMethod = %q, want %q", result.RoutingMethod, tt.wantMethod)
}
})
}
}
// TestRouteToAgent_SingleAgentFallback tests that with only one agent,
// we fall back to it with a warning
func TestRouteToAgent_SingleAgentFallback(t *testing.T) {
s := &Service{}
agents := []agentexec.ConnectedAgent{
{AgentID: "agent-1", Hostname: "delly"},
}
req := ExecuteRequest{
TargetType: "host",
TargetID: "",
Context: nil, // No context at all
}
result, err := s.routeToAgent(req, "uptime", agents)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.AgentID != "agent-1" {
t.Errorf("AgentID = %q, want %q", result.AgentID, "agent-1")
}
if result.RoutingMethod != "single_agent_fallback" {
t.Errorf("RoutingMethod = %q, want %q", result.RoutingMethod, "single_agent_fallback")
}
// Should have a warning about the fallback
if len(result.Warnings) == 0 {
t.Error("expected warning about fallback routing")
}
}
// TestRouteToAgent_MultiAgentNoContext tests that with multiple agents
// and no context, we get a clear error
func TestRouteToAgent_MultiAgentNoContext(t *testing.T) {
s := &Service{}
agents := []agentexec.ConnectedAgent{
{AgentID: "agent-1", Hostname: "delly"},
{AgentID: "agent-2", Hostname: "minipc"},
}
req := ExecuteRequest{
TargetType: "host",
TargetID: "",
Context: nil, // No context
}
_, err := s.routeToAgent(req, "uptime", agents)
if err == nil {
t.Fatal("expected error when no context with multiple agents")
}
routingErr, ok := err.(*RoutingError)
if !ok {
t.Fatalf("expected RoutingError, got %T", err)
}
// Should mention target_host in the suggestion
if routingErr.Suggestion == "" {
t.Error("expected suggestion in error")
}
// Should list available agents
if len(routingErr.AvailableAgents) != 2 {
t.Errorf("expected 2 available agents, got %d", len(routingErr.AvailableAgents))
}
}
// TestRouteToAgent_VMIDRoutingWithContext tests that VMID-based routing
// from context works correctly for pct/qm commands
func TestRouteToAgent_VMIDInCommandWithContext(t *testing.T) {
s := &Service{}
agents := []agentexec.ConnectedAgent{
{AgentID: "agent-1", Hostname: "delly"},
{AgentID: "agent-2", Hostname: "minipc"},
}
// Even with a VMID in the command, if we have node context, use it
req := ExecuteRequest{
TargetType: "host",
TargetID: "",
Context: map[string]interface{}{"node": "minipc"},
}
result, err := s.routeToAgent(req, "pct exec 106 -- hostname", agents)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.AgentHostname != "minipc" {
t.Errorf("AgentHostname = %q, want %q", result.AgentHostname, "minipc")
}
}