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) } }