Files
agents_and_robots/shell/effects/runner_devicemesh_test.go
egutierrez bcd246bf85 feat(0144a): tool registry framework para device-mesh
Anade pkg/tools/devicemesh con Client HTTP al device_agent + ToolRegistry
con 16 tools standard (exec, fs.*, git.*, docker.*, proc.*, pkg.*, shell.eval).
RegisterBuiltins filtra por mode user/sudo via RequiresApproval flag.
Hook al pkg/decision con ActionKindDeviceMesh + DeviceMeshAction.
Runner soporta dispatch via NewRunnerWithDeviceMesh (back-compat NewRunner).

Tests: 25 nuevos en devicemesh + 4 en runner. Build clean.
2026-05-24 14:07:13 +02:00

102 lines
3.0 KiB
Go

package effects
import (
"context"
"errors"
"io"
"log/slog"
"strings"
"testing"
"github.com/enmanuel/agents/pkg/decision"
)
// stubMeshCaller is a minimal DeviceMeshCaller for runner tests.
type stubMeshCaller struct {
tool string
input map[string]any
result any
err error
}
func (s *stubMeshCaller) Call(_ context.Context, toolName string, input map[string]any) (any, error) {
s.tool = toolName
s.input = input
return s.result, s.err
}
func newSilentLogger() *slog.Logger {
return slog.New(slog.NewTextHandler(io.Discard, nil))
}
func TestRunner_DeviceMesh_Success(t *testing.T) {
stub := &stubMeshCaller{result: map[string]any{"stdout": "hello", "exit_code": 0}}
r := NewRunnerWithDeviceMesh(nil, nil, stub, newSilentLogger())
results := r.Execute(context.Background(), "!room", []decision.Action{{
Kind: decision.ActionKindDeviceMesh,
DeviceMesh: &decision.DeviceMeshAction{
Tool: "exec",
Input: map[string]any{"argv": []string{"echo", "hello"}},
},
}})
if len(results) != 1 {
t.Fatalf("expected 1 result, got %d", len(results))
}
res := results[0]
if res.Err != nil {
t.Fatalf("expected no error, got %v", res.Err)
}
if stub.tool != "exec" {
t.Errorf("stub.tool=%q", stub.tool)
}
if !strings.Contains(res.Output, "hello") {
t.Errorf("output missing 'hello': %q", res.Output)
}
if !strings.Contains(res.Output, "exit_code") {
t.Errorf("output should be JSON containing exit_code: %q", res.Output)
}
}
func TestRunner_DeviceMesh_PropagatesError(t *testing.T) {
stub := &stubMeshCaller{err: errors.New("approval timeout")}
r := NewRunnerWithDeviceMesh(nil, nil, stub, newSilentLogger())
results := r.Execute(context.Background(), "!room", []decision.Action{{
Kind: decision.ActionKindDeviceMesh,
DeviceMesh: &decision.DeviceMeshAction{Tool: "pkg.install", Input: map[string]any{"name": "jq"}},
}})
if results[0].Err == nil {
t.Fatalf("expected error to propagate")
}
if !strings.Contains(results[0].Err.Error(), "approval") {
t.Errorf("error mismatch: %v", results[0].Err)
}
}
func TestRunner_DeviceMesh_NilAction(t *testing.T) {
r := NewRunnerWithDeviceMesh(nil, nil, &stubMeshCaller{}, newSilentLogger())
results := r.Execute(context.Background(), "!room", []decision.Action{{
Kind: decision.ActionKindDeviceMesh,
// DeviceMesh field is nil
}})
if results[0].Err == nil {
t.Fatalf("expected error for nil DeviceMesh field")
}
}
func TestRunner_DeviceMesh_NoCaller(t *testing.T) {
// Using NewRunner (legacy) — should fail gracefully on DeviceMesh action.
r := NewRunner(nil, nil, newSilentLogger())
results := r.Execute(context.Background(), "!room", []decision.Action{{
Kind: decision.ActionKindDeviceMesh,
DeviceMesh: &decision.DeviceMeshAction{Tool: "exec", Input: map[string]any{"argv": []string{"x"}}},
}})
if results[0].Err == nil {
t.Fatalf("expected error when Runner has no DeviceMeshCaller")
}
if !strings.Contains(results[0].Err.Error(), "DeviceMeshCaller") {
t.Errorf("error should mention DeviceMeshCaller: %v", results[0].Err)
}
}