bcd246bf85
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.
102 lines
3.0 KiB
Go
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)
|
|
}
|
|
}
|