package devagents import ( "encoding/json" "io" "log/slog" "os" "path/filepath" "strings" "testing" "github.com/enmanuel/agents/internal/config" ) func newSilentLogger() *slog.Logger { return slog.New(slog.NewJSONHandler(io.Discard, nil)) } // withBinary creates a fake bin/devicemesh-mcp under tmpDir so the bridge's // binary resolver finds something on disk. Returns the previous CWD. func withBinary(t *testing.T, tmpDir string) func() { t.Helper() binDir := filepath.Join(tmpDir, "bin") if err := os.MkdirAll(binDir, 0o755); err != nil { t.Fatalf("mkdir: %v", err) } binPath := filepath.Join(binDir, "devicemesh-mcp") if err := os.WriteFile(binPath, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil { t.Fatalf("write fake binary: %v", err) } prevDir, _ := os.Getwd() if err := os.Chdir(tmpDir); err != nil { t.Fatalf("chdir: %v", err) } return func() { _ = os.Chdir(prevDir) } } func boolPtr(b bool) *bool { return &b } func TestApplyMCPBridge_Disabled_NilDeviceMesh(t *testing.T) { cfg := &config.AgentConfig{} _, ok := ApplyMCPBridge(cfg, newSilentLogger()) if ok { t.Errorf("expected ok=false when DeviceMesh is nil") } } func TestApplyMCPBridge_Disabled_ExposeFalse(t *testing.T) { cfg := &config.AgentConfig{ DeviceMesh: &config.DeviceMeshConfig{ Enabled: true, ExposeViaMCP: boolPtr(false), }, } cfg.LLM.Primary.Provider = "claude-code" _, ok := ApplyMCPBridge(cfg, newSilentLogger()) if ok { t.Errorf("expected ok=false when ExposeViaMCP=false") } } func TestApplyMCPBridge_Disabled_WrongProvider(t *testing.T) { cfg := &config.AgentConfig{} cfg.Agent.ID = "test" cfg.LLM.Primary.Provider = "openai" cfg.DeviceMesh = &config.DeviceMeshConfig{ Enabled: true, DeviceAgentURL: "http://127.0.0.1:9999", Mode: "user", } _, ok := ApplyMCPBridge(cfg, newSilentLogger()) if ok { t.Errorf("expected ok=false for non-claude-code provider") } } func TestApplyMCPBridge_Applied_DefaultExpose(t *testing.T) { tmp := t.TempDir() defer withBinary(t, tmp)() cfg := &config.AgentConfig{} cfg.Agent.ID = "agent-test" cfg.LLM.Primary.Provider = "claude-code" cfg.LLM.Primary.ClaudeCode.DisableTools = true // expect override to false cfg.DeviceMesh = &config.DeviceMeshConfig{ Enabled: true, DeviceAgentURL: "http://10.42.0.10:7474", Mode: "user", ToolsAllowed: []string{"exec", "fs.read"}, } result, ok := ApplyMCPBridge(cfg, newSilentLogger()) if !ok { t.Fatalf("expected ok=true; bridge should have been applied") } // 1. Config path written and valid JSON. if result.ConfigPath == "" { t.Fatalf("missing ConfigPath in result") } defer os.Remove(result.ConfigPath) raw, err := os.ReadFile(result.ConfigPath) if err != nil { t.Fatalf("read config: %v", err) } var doc map[string]any if err := json.Unmarshal(raw, &doc); err != nil { t.Fatalf("config not valid JSON: %v\n%s", err, raw) } servers, _ := doc["mcpServers"].(map[string]any) srv, _ := servers["devicemesh"].(map[string]any) if srv == nil { t.Fatalf("mcpServers.devicemesh missing in config: %s", raw) } if cmd, _ := srv["command"].(string); !strings.HasSuffix(cmd, "devicemesh-mcp") { t.Errorf("expected command to end with devicemesh-mcp, got %q", cmd) } // 2. AllowedTools formatted as mcp____. if len(cfg.LLM.Primary.ClaudeCode.AllowedTools) != 2 { t.Fatalf("expected 2 allowed tools, got %v", cfg.LLM.Primary.ClaudeCode.AllowedTools) } for _, n := range cfg.LLM.Primary.ClaudeCode.AllowedTools { if !strings.HasPrefix(n, "mcp__devicemesh__") { t.Errorf("allowed tool %q missing mcp__devicemesh__ prefix", n) } } // 3. MCPConfigPath set on cfg. if cfg.LLM.Primary.ClaudeCode.MCPConfigPath != result.ConfigPath { t.Errorf("MCPConfigPath not propagated to cfg: got %q want %q", cfg.LLM.Primary.ClaudeCode.MCPConfigPath, result.ConfigPath) } // 4. DisableTools override applied. if cfg.LLM.Primary.ClaudeCode.DisableTools { t.Errorf("expected DisableTools=false after override, got true") } // 5. /tmp file mode is 0600. st, err := os.Stat(result.ConfigPath) if err == nil && st.Mode().Perm() != 0o600 { t.Errorf("expected config file mode 0600, got %v", st.Mode().Perm()) } } func TestApplyMCPBridge_URLEnvOverride(t *testing.T) { tmp := t.TempDir() defer withBinary(t, tmp)() t.Setenv("AGENT_TEST_DM_URL", "http://envurl.example:1234") cfg := &config.AgentConfig{} cfg.Agent.ID = "agent-test" cfg.LLM.Primary.Provider = "claude-code" cfg.DeviceMesh = &config.DeviceMeshConfig{ Enabled: true, DeviceAgentURL: "http://yaml-loses:9999", URLEnv: "AGENT_TEST_DM_URL", Mode: "user", } result, ok := ApplyMCPBridge(cfg, newSilentLogger()) if !ok { t.Fatalf("expected ok=true") } defer os.Remove(result.ConfigPath) if result.DeviceAgentURL != "http://envurl.example:1234" { t.Errorf("env URL override not applied: got %q", result.DeviceAgentURL) } } func TestApplyMCPBridge_BinaryMissing(t *testing.T) { // No fake binary on disk → should skip cleanly. tmp := t.TempDir() prev, _ := os.Getwd() _ = os.Chdir(tmp) defer os.Chdir(prev) cfg := &config.AgentConfig{} cfg.Agent.ID = "agent-test" cfg.LLM.Primary.Provider = "claude-code" cfg.DeviceMesh = &config.DeviceMeshConfig{ Enabled: true, DeviceAgentURL: "http://10.42.0.10:7474", } if _, ok := ApplyMCPBridge(cfg, newSilentLogger()); ok { t.Errorf("expected ok=false when binary is missing") } } func TestBuildClaudeAllowedToolNames(t *testing.T) { got := BuildClaudeAllowedToolNames("devicemesh", []string{"exec", "fs.read", "git.clone"}) if len(got) != 3 { t.Fatalf("expected 3 names, got %d", len(got)) } for _, n := range got { if !strings.HasPrefix(n, "mcp__devicemesh__") { t.Errorf("name %q missing prefix", n) } } // Sorted output for determinism. if got[0] >= got[1] || got[1] >= got[2] { t.Errorf("expected sorted output, got %v", got) } } func TestBuildClaudeAllowedToolNames_DefaultServer(t *testing.T) { got := BuildClaudeAllowedToolNames("", []string{"exec"}) if len(got) != 1 || !strings.HasPrefix(got[0], "mcp__devicemesh__") { t.Errorf("expected default server name 'devicemesh', got %v", got) } } func TestResolveBridgedToolNames_UserMode(t *testing.T) { names, err := ResolveBridgedToolNames(&config.DeviceMeshConfig{ Enabled: true, Mode: "user", }) if err != nil { t.Fatalf("err: %v", err) } if len(names) == 0 { t.Fatalf("expected non-empty names") } for _, n := range names { if n == "pkg.install" { t.Errorf("user mode should not include pkg.install") } } } func TestResolveBridgedToolNames_Filter(t *testing.T) { names, err := ResolveBridgedToolNames(&config.DeviceMeshConfig{ Enabled: true, Mode: "user", ToolsAllowed: []string{"exec", "fs.read", "unknown"}, }) if err != nil { t.Fatalf("err: %v", err) } if len(names) != 2 { t.Errorf("expected 2 names after filter, got %d (%v)", len(names), names) } } func TestShouldExposeViaMCP(t *testing.T) { if (*config.DeviceMeshConfig)(nil).ShouldExposeViaMCP() { t.Errorf("nil should not expose") } if (&config.DeviceMeshConfig{}).ShouldExposeViaMCP() { t.Errorf("disabled should not expose") } if !(&config.DeviceMeshConfig{Enabled: true}).ShouldExposeViaMCP() { t.Errorf("enabled + nil pointer should default to expose=true") } if (&config.DeviceMeshConfig{Enabled: true, ExposeViaMCP: boolPtr(false)}).ShouldExposeViaMCP() { t.Errorf("enabled + false should not expose") } if !(&config.DeviceMeshConfig{Enabled: true, ExposeViaMCP: boolPtr(true)}).ShouldExposeViaMCP() { t.Errorf("enabled + true should expose") } }