mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 03:20:11 +00:00
- 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>
187 lines
4.8 KiB
Go
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")
|
|
}
|
|
}
|