package devicemesh import ( "context" "encoding/json" "io" "net/http" "net/http/httptest" "testing" ) func TestRegisterBuiltins_UserExcludesApprovalTools(t *testing.T) { reg := NewToolRegistry(nil) names := RegisterBuiltins(reg, ModeUser) want := map[string]bool{ "exec": true, "shell.eval": true, "fs.read": true, "fs.write": true, "fs.list": true, "fs.stat": true, "git.clone": true, "git.commit": true, "git.push": true, "pkg.search": true, "proc.list": true, "docker.list": true, "docker.exec": true, "docker.logs": true, } got := map[string]bool{} for _, n := range names { got[n] = true } for w := range want { if !got[w] { t.Errorf("user mode missing tool %q", w) } } if got["pkg.install"] { t.Errorf("user mode should NOT include pkg.install") } if got["proc.kill"] { t.Errorf("user mode should NOT include proc.kill (RequiresApproval)") } } func TestRegisterBuiltins_SudoIncludesOnlyApprovalTools(t *testing.T) { reg := NewToolRegistry(nil) names := RegisterBuiltins(reg, ModeSudo) got := map[string]bool{} for _, n := range names { got[n] = true } if !got["pkg.install"] { t.Errorf("sudo mode should include pkg.install") } if !got["proc.kill"] { t.Errorf("sudo mode should include proc.kill") } if !got["shell.eval"] { t.Errorf("sudo mode should include shell.eval (special-cased with RequiresApproval=true)") } if got["exec"] { t.Errorf("sudo mode should NOT include exec (no RequiresApproval)") } if got["fs.read"] { t.Errorf("sudo mode should NOT include fs.read") } } func TestRegisterBuiltins_ModeAll(t *testing.T) { reg := NewToolRegistry(nil) names := RegisterBuiltins(reg, ModeAll) if len(names) < 16 { t.Errorf("expected all 16 builtins, got %d: %v", len(names), names) } got := map[string]bool{} for _, n := range names { got[n] = true } if !got["exec"] || !got["pkg.install"] { t.Errorf("ModeAll should include both exec and pkg.install") } } func TestBuiltins_Exec_HappyPath(t *testing.T) { var received CapabilityRequest srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) _ = json.Unmarshal(body, &received) _ = json.NewEncoder(w).Encode(CapabilityResponse{ RequestID: received.RequestID, OK: true, Result: map[string]any{ "stdout": "hello\n", "stderr": "", "exit_code": float64(0), // JSON numbers decode as float64 "duration_ms": float64(12), }, }) })) defer srv.Close() reg := NewToolRegistry(NewClient(srv.URL)) RegisterBuiltins(reg, ModeUser) out, err := reg.Call(context.Background(), "exec", map[string]any{ "argv": []string{"echo", "hello"}, "cwd": "/tmp", "timeout_s": 5, }) if err != nil { t.Fatalf("exec call: %v", err) } // Result should be a normalized map. m, ok := out.(map[string]any) if !ok { t.Fatalf("expected map result, got %T", out) } if m["stdout"].(string) != "hello\n" { t.Errorf("stdout: %v", m["stdout"]) } if m["exit_code"].(int) != 0 { t.Errorf("exit_code: %v (%T)", m["exit_code"], m["exit_code"]) } // Verify the request that was sent. if received.Capability != "shell.exec" { t.Errorf("capability: %q", received.Capability) } argv, ok := received.Args["argv"].([]any) if !ok { t.Fatalf("argv not []any: %T", received.Args["argv"]) } if len(argv) != 2 || argv[0].(string) != "echo" { t.Errorf("argv content: %v", argv) } if received.Args["cwd"].(string) != "/tmp" { t.Errorf("cwd: %v", received.Args["cwd"]) } if int(received.Args["timeout_s"].(float64)) != 5 { t.Errorf("timeout_s: %v", received.Args["timeout_s"]) } } func TestBuiltins_Exec_RejectsEmptyArgv(t *testing.T) { reg := NewToolRegistry(NewClient("http://nowhere.invalid")) RegisterBuiltins(reg, ModeUser) _, err := reg.Call(context.Background(), "exec", map[string]any{ "argv": []string{}, }) if err == nil { t.Fatalf("expected error for empty argv") } } func TestBuiltins_FSRead_HappyPath(t *testing.T) { var received CapabilityRequest srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) _ = json.Unmarshal(body, &received) _ = json.NewEncoder(w).Encode(CapabilityResponse{ RequestID: received.RequestID, OK: true, Result: map[string]any{ "content": "file contents here", "size": float64(18), }, }) })) defer srv.Close() reg := NewToolRegistry(NewClient(srv.URL)) RegisterBuiltins(reg, ModeUser) out, err := reg.Call(context.Background(), "fs.read", map[string]any{ "path": "/etc/os-release", "max_bytes": 1024, }) if err != nil { t.Fatalf("fs.read: %v", err) } m := out.(map[string]any) if m["content"].(string) != "file contents here" { t.Errorf("content: %v", m["content"]) } if received.Capability != "fs.read" { t.Errorf("capability: %q", received.Capability) } if received.Args["path"].(string) != "/etc/os-release" { t.Errorf("path: %v", received.Args["path"]) } if int(received.Args["max_bytes"].(float64)) != 1024 { t.Errorf("max_bytes: %v", received.Args["max_bytes"]) } } func TestBuiltins_FSWrite_RequiresContentOrB64(t *testing.T) { reg := NewToolRegistry(NewClient("http://nowhere.invalid")) RegisterBuiltins(reg, ModeUser) _, err := reg.Call(context.Background(), "fs.write", map[string]any{ "path": "/tmp/x", }) if err == nil { t.Fatalf("expected error when neither content nor content_b64 provided") } } func TestBuiltins_FSWrite_AcceptsContent(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(CapabilityResponse{OK: true, Result: map[string]any{"bytes_written": float64(11)}}) })) defer srv.Close() reg := NewToolRegistry(NewClient(srv.URL)) RegisterBuiltins(reg, ModeUser) _, err := reg.Call(context.Background(), "fs.write", map[string]any{ "path": "/tmp/x", "content": "hello world", }) if err != nil { t.Fatalf("fs.write: %v", err) } } func TestBuiltins_PkgInstall_RegisteredOnlyInSudo(t *testing.T) { // Build user reg user := NewToolRegistry(nil) RegisterBuiltins(user, ModeUser) if _, ok := user.Get("pkg.install"); ok { t.Errorf("pkg.install should NOT be in user registry") } // Build sudo reg sudo := NewToolRegistry(nil) RegisterBuiltins(sudo, ModeSudo) if _, ok := sudo.Get("pkg.install"); !ok { t.Errorf("pkg.install should be in sudo registry") } } // ----- shell.eval ----- func TestBuiltins_ShellEval_PresentInUserModeWithoutApproval(t *testing.T) { reg := NewToolRegistry(nil) RegisterBuiltins(reg, ModeUser) spec, ok := reg.Get("shell.eval") if !ok { t.Fatalf("shell.eval should be registered in ModeUser") } if spec.RequiresApproval { t.Errorf("shell.eval in ModeUser should have RequiresApproval=false, got true") } if spec.Capability != "shell.eval" { t.Errorf("capability mismatch: %q", spec.Capability) } } func TestBuiltins_ShellEval_PresentInSudoModeWithApproval(t *testing.T) { reg := NewToolRegistry(nil) RegisterBuiltins(reg, ModeSudo) spec, ok := reg.Get("shell.eval") if !ok { t.Fatalf("shell.eval should be registered in ModeSudo") } if !spec.RequiresApproval { t.Errorf("shell.eval in ModeSudo should have RequiresApproval=true, got false") } // Ensure withApprovalRequired did not mutate the original spec returned // from builtinSpecs (other registries should still see false). userReg := NewToolRegistry(nil) RegisterBuiltins(userReg, ModeUser) userSpec, _ := userReg.Get("shell.eval") if userSpec.RequiresApproval { t.Errorf("ModeUser shell.eval should remain RequiresApproval=false; sudo registration leaked") } } func TestBuiltins_ShellEval_InputSchemaValidation(t *testing.T) { reg := NewToolRegistry(nil) RegisterBuiltins(reg, ModeUser) spec, ok := reg.Get("shell.eval") if !ok { t.Fatalf("shell.eval not registered") } // Happy: minimal valid input. if err := ValidateInput(spec, map[string]any{"cmd": "git status"}); err != nil { t.Errorf("expected valid input to pass, got %v", err) } // Happy: with shell enum. if err := ValidateInput(spec, map[string]any{"cmd": "ls -la", "shell": "bash"}); err != nil { t.Errorf("shell=bash should be valid, got %v", err) } if err := ValidateInput(spec, map[string]any{"cmd": "Get-Process", "shell": "powershell"}); err != nil { t.Errorf("shell=powershell should be valid, got %v", err) } if err := ValidateInput(spec, map[string]any{"cmd": "ls", "shell": "auto"}); err != nil { t.Errorf("shell=auto should be valid, got %v", err) } // Reject: shell not in enum. if err := ValidateInput(spec, map[string]any{"cmd": "ls", "shell": "zsh"}); err == nil { t.Errorf("shell=zsh should be rejected by enum") } // Reject: missing required cmd. if err := ValidateInput(spec, map[string]any{}); err == nil { t.Errorf("empty input should fail (cmd required)") } // Reject: unknown property (additionalProperties=false). if err := ValidateInput(spec, map[string]any{"cmd": "ls", "extra": "x"}); err == nil { t.Errorf("unknown property should be rejected by additionalProperties=false") } // Reject: cmd not a string. if err := ValidateInput(spec, map[string]any{"cmd": 42}); err == nil { t.Errorf("cmd as integer should be rejected") } } func TestBuiltins_ShellEval_ArgMapping(t *testing.T) { spec := shellEvalSpec() // Pass cmd alone. out, err := spec.ArgMapping(map[string]any{"cmd": "git status"}) if err != nil { t.Fatalf("argmap cmd-only: %v", err) } if out["cmd"].(string) != "git status" { t.Errorf("cmd not passed through: %v", out["cmd"]) } if _, ok := out["shell"]; ok { t.Errorf("shell should be absent when not provided") } if _, ok := out["cwd"]; ok { t.Errorf("cwd should be absent when not provided") } // Pass all fields. out, err = spec.ArgMapping(map[string]any{ "cmd": "ls -la", "shell": "bash", "cwd": "/home/lucas", }) if err != nil { t.Fatalf("argmap full: %v", err) } if out["shell"].(string) != "bash" { t.Errorf("shell not propagated: %v", out["shell"]) } if out["cwd"].(string) != "/home/lucas" { t.Errorf("cwd not propagated: %v", out["cwd"]) } // Empty strings for optional fields are filtered out. out, err = spec.ArgMapping(map[string]any{"cmd": "ls", "shell": "", "cwd": ""}) if err != nil { t.Fatalf("argmap empty optionals: %v", err) } if _, ok := out["shell"]; ok { t.Errorf("empty shell should be filtered, got %v", out["shell"]) } if _, ok := out["cwd"]; ok { t.Errorf("empty cwd should be filtered, got %v", out["cwd"]) } // Missing cmd is an error. if _, err := spec.ArgMapping(map[string]any{}); err == nil { t.Errorf("ArgMapping should error on missing cmd") } } func TestBuiltins_ShellEval_SmokeCall(t *testing.T) { var received CapabilityRequest srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) _ = json.Unmarshal(body, &received) _ = json.NewEncoder(w).Encode(CapabilityResponse{ RequestID: received.RequestID, OK: true, Result: map[string]any{ "stdout": "hola\n", "stderr": "", "exit_code": float64(0), "approval_status": "auto_approved", "cmd_executed": "echo hola", "truncated": false, "duration_ms": float64(7), }, }) })) defer srv.Close() reg := NewToolRegistry(NewClient(srv.URL)) RegisterBuiltins(reg, ModeUser) out, err := reg.Call(context.Background(), "shell.eval", map[string]any{ "cmd": "echo hola", }) if err != nil { t.Fatalf("shell.eval call: %v", err) } m, ok := out.(map[string]any) if !ok { t.Fatalf("expected map result, got %T", out) } if m["stdout"].(string) != "hola\n" { t.Errorf("stdout: %v", m["stdout"]) } if m["approval_status"].(string) != "auto_approved" { t.Errorf("approval_status: %v", m["approval_status"]) } if m["cmd_executed"].(string) != "echo hola" { t.Errorf("cmd_executed: %v", m["cmd_executed"]) } // Verify the device-facing request envelope. if received.Capability != "shell.eval" { t.Errorf("capability: %q", received.Capability) } if received.Args["cmd"].(string) != "echo hola" { t.Errorf("cmd: %v", received.Args["cmd"]) } if _, ok := received.Args["shell"]; ok { t.Errorf("shell should be absent when omitted by caller") } }