61606d450d
Schema DeviceMeshConfig en AgentConfig. Adapter ToolsForLLM convierte ToolSpec → tools.Tool transparente al LLM existente. URL via env var override. tools_allowed filter. agent-wsl-lucas blank import en launcher. LLM ve los tools como cualquier otra herramienta. Effects runner ya soporta ActionKindDeviceMesh como fallback. Build + tests verdes.
318 lines
11 KiB
Go
318 lines
11 KiB
Go
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")
|
|
}
|