Files
agents_and_robots/pkg/tools/devicemesh/adapter_test.go
T
egutierrez bcd246bf85 feat(0144a): tool registry framework para device-mesh
Anade pkg/tools/devicemesh con Client HTTP al device_agent + ToolRegistry
con 16 tools standard (exec, fs.*, git.*, docker.*, proc.*, pkg.*, shell.eval).
RegisterBuiltins filtra por mode user/sudo via RequiresApproval flag.
Hook al pkg/decision con ActionKindDeviceMesh + DeviceMeshAction.
Runner soporta dispatch via NewRunnerWithDeviceMesh (back-compat NewRunner).

Tests: 25 nuevos en devicemesh + 4 en runner. Build clean.
2026-05-24 14:07:13 +02:00

220 lines
6.1 KiB
Go

package devicemesh
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestToolsForLLM_EmptyRegistry(t *testing.T) {
if got := ToolsForLLM(nil); len(got) != 0 {
t.Errorf("nil reg → expected 0 tools, got %d", len(got))
}
reg := NewToolRegistry(nil)
if got := ToolsForLLM(reg); len(got) != 0 {
t.Errorf("empty reg → expected 0 tools, got %d", len(got))
}
}
func TestToolsForLLM_PreservesNamesAndDescription(t *testing.T) {
reg := NewToolRegistry(NewClient("http://nowhere.invalid"))
reg.Register(ToolSpec{
Name: "exec",
Capability: "shell.exec",
Description: "Run a command",
InputSchema: map[string]any{
"type": "object",
"required": []string{"argv"},
"properties": map[string]any{
"argv": map[string]any{"type": "array", "description": "argument vector"},
},
},
})
reg.Register(ToolSpec{
Name: "pkg.install",
Capability: "pkg.install",
Description: "Install a package",
RequiresApproval: true,
})
got := ToolsForLLM(reg)
if len(got) != 2 {
t.Fatalf("expected 2 tools, got %d", len(got))
}
// Alpha-sorted by name
if got[0].Def.Name != "exec" || got[1].Def.Name != "pkg.install" {
t.Errorf("name order: %v", []string{got[0].Def.Name, got[1].Def.Name})
}
if !strings.Contains(got[0].Def.Description, "device_mesh: shell.exec") {
t.Errorf("description missing device_mesh marker: %q", got[0].Def.Description)
}
if !strings.Contains(got[1].Def.Description, "(approval required)") {
t.Errorf("approval-required marker missing: %q", got[1].Def.Description)
}
// Param extraction
if len(got[0].Def.Parameters) != 1 || got[0].Def.Parameters[0].Name != "argv" {
t.Errorf("expected one param 'argv', got %+v", got[0].Def.Parameters)
}
if !got[0].Def.Parameters[0].Required {
t.Errorf("expected argv to be required")
}
}
func TestAdaptTool_ExecRoutesThroughRegistry(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req CapabilityRequest
body, _ := io.ReadAll(r.Body)
_ = json.Unmarshal(body, &req)
// Echo the args back so we can assert ArgMapping ran.
_ = json.NewEncoder(w).Encode(CapabilityResponse{
RequestID: req.RequestID,
OK: true,
Result: map[string]any{"got": req.Args},
})
}))
defer srv.Close()
reg := NewToolRegistry(NewClient(srv.URL))
spec := ToolSpec{
Name: "echo",
Capability: "x.echo",
InputSchema: map[string]any{
"type": "object",
"required": []string{"msg"},
"properties": map[string]any{
"msg": map[string]any{"type": "string"},
},
},
ArgMapping: func(in map[string]any) (map[string]any, error) {
return map[string]any{"msg_upper": strings.ToUpper(in["msg"].(string))}, nil
},
}
reg.Register(spec)
tool := AdaptTool(reg, spec)
res := tool.Exec(context.Background(), map[string]any{"msg": "hi"})
if res.Err != nil {
t.Fatalf("exec err: %v", res.Err)
}
if !strings.Contains(res.Output, "HI") {
t.Errorf("expected HI in output, got %q", res.Output)
}
}
func TestAdaptTool_PropagatesValidationError(t *testing.T) {
reg := NewToolRegistry(NewClient("http://nowhere.invalid"))
spec := ToolSpec{
Name: "needs_int",
Capability: "x.y",
InputSchema: map[string]any{
"type": "object",
"required": []string{"n"},
"properties": map[string]any{
"n": map[string]any{"type": "integer"},
},
"additionalProperties": false,
},
}
reg.Register(spec)
tool := AdaptTool(reg, spec)
res := tool.Exec(context.Background(), map[string]any{"n": "not-an-int"})
if res.Err == nil {
t.Fatalf("expected validation error")
}
if !strings.Contains(res.Err.Error(), "needs_int") {
t.Errorf("error should mention tool name: %v", res.Err)
}
}
func TestFormatToolResult(t *testing.T) {
if got := formatToolResult(nil); got != "" {
t.Errorf("nil → expected empty, got %q", got)
}
if got := formatToolResult("plain"); got != "plain" {
t.Errorf("string passthrough: %q", got)
}
if got := formatToolResult(map[string]any{"a": 1}); got != `{"a":1}` {
t.Errorf("map encode: %q", got)
}
}
func TestFilterByAllowed(t *testing.T) {
reg := NewToolRegistry(NewClient("http://x"))
reg.Register(ToolSpec{Name: "a", Capability: "x.a"})
reg.Register(ToolSpec{Name: "b", Capability: "x.b"})
reg.Register(ToolSpec{Name: "c", Capability: "x.c"})
// Empty allow-list = passthrough
if got := FilterByAllowed(reg, nil); got.Len() != 3 {
t.Errorf("nil allowed → expected 3, got %d", got.Len())
}
// Subset
filtered := FilterByAllowed(reg, []string{"a", "c", "zzz"}) // zzz is silently dropped
if filtered.Len() != 2 {
t.Fatalf("expected 2 filtered, got %d", filtered.Len())
}
names := filtered.Names()
if names[0] != "a" || names[1] != "c" {
t.Errorf("unexpected names after filter: %v", names)
}
// Same Client shared
if filtered.Client() != reg.Client() {
t.Errorf("filtered should share Client with source")
}
// Nil source
if FilterByAllowed(nil, []string{"a"}) != nil {
t.Errorf("nil source → expected nil")
}
}
func TestParamsFromSchema_EdgeCases(t *testing.T) {
if got := paramsFromSchema(nil); got != nil {
t.Errorf("nil schema → expected nil, got %v", got)
}
// Missing properties
if got := paramsFromSchema(map[string]any{"type": "object"}); got != nil {
t.Errorf("no properties → expected nil, got %v", got)
}
// "required" as []any (json.Unmarshal default)
got := paramsFromSchema(map[string]any{
"required": []any{"foo"},
"properties": map[string]any{
"foo": map[string]any{"type": "string"},
"bar": map[string]any{"type": "integer"},
},
})
if len(got) != 2 {
t.Fatalf("expected 2 params, got %d", len(got))
}
// Sorted alpha: bar, foo
if got[0].Name != "bar" || got[1].Name != "foo" {
t.Errorf("expected sorted [bar, foo], got %+v", got)
}
if got[0].Required {
t.Errorf("bar should not be required")
}
if !got[1].Required {
t.Errorf("foo should be required")
}
// Type defaulting
got2 := paramsFromSchema(map[string]any{
"properties": map[string]any{
"x": map[string]any{},
},
})
if len(got2) != 1 || got2[0].Type != "string" {
t.Errorf("expected type default 'string', got %+v", got2)
}
}