package devagents import ( "log/slog" "os" "testing" "github.com/enmanuel/agents/internal/config" toolmemory "github.com/enmanuel/agents/tools/memorytools" ) func TestBuildToolRegistry_MinimalConfig(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})) cfg := &config.AgentConfig{ Agent: config.AgentMeta{ID: "test-agent"}, } roomCtx := &toolmemory.RoomContext{} reg := buildToolRegistry(cfg, nil, nil, nil, nil, nil, nil, nil, nil, roomCtx, logger) // Always-registered tools: current_time, weather, matrix_send names := reg.Names() if len(names) < 3 { t.Fatalf("expected at least 3 always-on tools, got %d: %v", len(names), names) } assertToolRegistered(t, reg, "current_time") assertToolRegistered(t, reg, "get_weather") assertToolRegistered(t, reg, "matrix_send") } func TestBuildToolRegistry_HTTPEnabled(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})) cfg := &config.AgentConfig{ Agent: config.AgentMeta{ID: "test-agent"}, Tools: config.ToolsCfg{ HTTP: config.HTTPToolCfg{Enabled: true, AllowedDomains: []string{"example.com"}}, }, } roomCtx := &toolmemory.RoomContext{} reg := buildToolRegistry(cfg, nil, nil, nil, nil, nil, nil, nil, nil, roomCtx, logger) assertToolRegistered(t, reg, "http_get") assertToolRegistered(t, reg, "http_post") } func TestBuildToolRegistry_HTTPDisabled(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})) cfg := &config.AgentConfig{ Agent: config.AgentMeta{ID: "test-agent"}, } roomCtx := &toolmemory.RoomContext{} reg := buildToolRegistry(cfg, nil, nil, nil, nil, nil, nil, nil, nil, roomCtx, logger) assertToolNotRegistered(t, reg, "http_get") assertToolNotRegistered(t, reg, "http_post") } func TestBuildToolRegistry_FileOpsReadOnly(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})) cfg := &config.AgentConfig{ Agent: config.AgentMeta{ID: "test-agent"}, Tools: config.ToolsCfg{ FileOps: config.FileOpsCfg{Enabled: true, ReadOnly: true, AllowedPaths: []string{"/tmp"}}, }, } roomCtx := &toolmemory.RoomContext{} reg := buildToolRegistry(cfg, nil, nil, nil, nil, nil, nil, nil, nil, roomCtx, logger) assertToolRegistered(t, reg, "read_file") assertToolRegistered(t, reg, "list_directory") assertToolNotRegistered(t, reg, "write_file") assertToolNotRegistered(t, reg, "append_file") assertToolNotRegistered(t, reg, "delete_file") } func TestBuildToolRegistry_FileOpsReadWrite(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})) cfg := &config.AgentConfig{ Agent: config.AgentMeta{ID: "test-agent"}, Tools: config.ToolsCfg{ FileOps: config.FileOpsCfg{Enabled: true, ReadOnly: false, AllowedPaths: []string{"/tmp"}}, }, } roomCtx := &toolmemory.RoomContext{} reg := buildToolRegistry(cfg, nil, nil, nil, nil, nil, nil, nil, nil, roomCtx, logger) assertToolRegistered(t, reg, "read_file") assertToolRegistered(t, reg, "list_directory") assertToolRegistered(t, reg, "write_file") assertToolRegistered(t, reg, "append_file") assertToolRegistered(t, reg, "delete_file") } func TestBuildToolRegistry_IMDbEnabled(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})) cfg := &config.AgentConfig{ Agent: config.AgentMeta{ID: "test-agent"}, Tools: config.ToolsCfg{ IMDb: config.IMDbToolCfg{Enabled: true}, }, } roomCtx := &toolmemory.RoomContext{} reg := buildToolRegistry(cfg, nil, nil, nil, nil, nil, nil, nil, nil, roomCtx, logger) assertToolRegistered(t, reg, "imdb_search") } func TestBuildToolRegistry_SSHEnabled(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})) cfg := &config.AgentConfig{ Agent: config.AgentMeta{ID: "test-agent"}, Tools: config.ToolsCfg{ SSH: config.SSHToolCfg{Enabled: true}, }, } roomCtx := &toolmemory.RoomContext{} // SSH tool requires an executor; passing nil is fine for registration (only used at exec time) reg := buildToolRegistry(cfg, nil, nil, nil, nil, nil, nil, nil, nil, roomCtx, logger) assertToolRegistered(t, reg, "ssh_command") } func TestBuildToolRegistry_ToolCount(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})) // Enable everything that doesn't need external deps cfg := &config.AgentConfig{ Agent: config.AgentMeta{ID: "test-agent"}, Tools: config.ToolsCfg{ HTTP: config.HTTPToolCfg{Enabled: true}, SSH: config.SSHToolCfg{Enabled: true}, FileOps: config.FileOpsCfg{Enabled: true, AllowedPaths: []string{"/tmp"}}, IMDb: config.IMDbToolCfg{Enabled: true}, }, } roomCtx := &toolmemory.RoomContext{} reg := buildToolRegistry(cfg, nil, nil, nil, nil, nil, nil, nil, nil, roomCtx, logger) // 4 always-on (current_time, get_weather, wikipedia_search, matrix_send) + 2 HTTP + 1 SSH + 5 file + 1 IMDb = 13 expected := 13 if got := reg.Len(); got != expected { t.Errorf("expected %d tools, got %d: %v", expected, got, reg.Names()) } } // ── Test helpers ──────────────────────────────────────────────────────────── func assertToolRegistered(t *testing.T, reg interface{ Names() []string }, name string) { t.Helper() for _, n := range reg.Names() { if n == name { return } } t.Errorf("expected tool %q to be registered, but it was not. Registered: %v", name, reg.Names()) } func assertToolNotRegistered(t *testing.T, reg interface{ Names() []string }, name string) { t.Helper() for _, n := range reg.Names() { if n == name { t.Errorf("expected tool %q NOT to be registered, but it was", name) return } } } func TestBuildToolRegistry_DeviceMeshDisabled(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})) cfg := &config.AgentConfig{ Agent: config.AgentMeta{ID: "test-agent"}, DeviceMesh: nil, } roomCtx := &toolmemory.RoomContext{} reg := buildToolRegistry(cfg, nil, nil, nil, nil, nil, nil, nil, nil, roomCtx, logger) // None of the device_mesh tool names should appear when the block is nil. assertToolNotRegistered(t, reg, "exec") assertToolNotRegistered(t, reg, "shell.eval") assertToolNotRegistered(t, reg, "fs.read") } func TestBuildDeviceMeshRegistry_NoURLReturnsNil(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})) cfg := &config.AgentConfig{ Agent: config.AgentMeta{ID: "agent-x"}, DeviceMesh: &config.DeviceMeshConfig{ Enabled: true, Mode: "user", // no URL, no URLEnv }, } if got := buildDeviceMeshRegistry(cfg, logger); got != nil { t.Errorf("expected nil registry when no URL is set, got %d tools", got.Len()) } } func TestBuildDeviceMeshRegistry_URLEnvOverride(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})) t.Setenv("TEST_DM_URL", "http://10.42.0.99:7474") cfg := &config.AgentConfig{ Agent: config.AgentMeta{ID: "agent-x"}, DeviceMesh: &config.DeviceMeshConfig{ Enabled: true, Mode: "user", DeviceAgentURL: "http://stale-url", URLEnv: "TEST_DM_URL", }, } reg := buildDeviceMeshRegistry(cfg, logger) if reg == nil { t.Fatalf("expected non-nil registry") } if reg.Client().BaseURL != "http://10.42.0.99:7474" { t.Errorf("URLEnv override failed: got %q", reg.Client().BaseURL) } } func TestBuildDeviceMeshRegistry_UserModeFiltersApproval(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})) cfg := &config.AgentConfig{ Agent: config.AgentMeta{ID: "agent-x"}, DeviceMesh: &config.DeviceMeshConfig{ Enabled: true, Mode: "user", DeviceAgentURL: "http://dummy:7474", }, } reg := buildDeviceMeshRegistry(cfg, logger) if reg == nil { t.Fatalf("expected non-nil registry") } for _, n := range reg.Names() { // User mode: pkg.install (requires approval) must not be present. if n == "pkg.install" { t.Errorf("user mode leaked approval-only tool: %s", n) } } } func TestBuildDeviceMeshRegistry_SudoModeKeepsOnlyApproval(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})) cfg := &config.AgentConfig{ Agent: config.AgentMeta{ID: "agent-x-sudo"}, DeviceMesh: &config.DeviceMeshConfig{ Enabled: true, Mode: "sudo", DeviceAgentURL: "http://dummy:7474", }, } reg := buildDeviceMeshRegistry(cfg, logger) if reg == nil { t.Fatalf("expected non-nil registry") } // pkg.install MUST be there in sudo mode. assertToolRegistered(t, reg, "pkg.install") // shell.eval is always registered (special-cased) and promoted to approval. spec, ok := reg.Get("shell.eval") if !ok { t.Fatalf("shell.eval should be registered in sudo mode too") } if !spec.RequiresApproval { t.Errorf("shell.eval in sudo mode should have RequiresApproval=true") } } func TestBuildDeviceMeshRegistry_ToolsAllowedNarrows(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})) cfg := &config.AgentConfig{ Agent: config.AgentMeta{ID: "agent-x"}, DeviceMesh: &config.DeviceMeshConfig{ Enabled: true, Mode: "user", DeviceAgentURL: "http://dummy:7474", ToolsAllowed: []string{"exec", "fs.read", "zzz.unknown"}, }, } reg := buildDeviceMeshRegistry(cfg, logger) if reg == nil { t.Fatalf("expected non-nil registry") } if reg.Len() != 2 { t.Errorf("expected 2 tools after filter, got %d: %v", reg.Len(), reg.Names()) } assertToolRegistered(t, reg, "exec") assertToolRegistered(t, reg, "fs.read") } func TestBuildToolRegistry_DeviceMeshAdaptedIntoMainRegistry(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})) cfg := &config.AgentConfig{ Agent: config.AgentMeta{ID: "agent-x"}, DeviceMesh: &config.DeviceMeshConfig{ Enabled: true, Mode: "user", DeviceAgentURL: "http://dummy:7474", ToolsAllowed: []string{"exec"}, }, } roomCtx := &toolmemory.RoomContext{} reg := buildToolRegistry(cfg, nil, nil, nil, nil, nil, nil, nil, nil, roomCtx, logger) // The "exec" tool should appear in the main agent tool registry, alongside // the always-on tools, ready for the LLM tool-use loop to invoke. assertToolRegistered(t, reg, "exec") assertToolRegistered(t, reg, "current_time") }