154 lines
4.5 KiB
Go
154 lines
4.5 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"nub/internal/llm"
|
|
"nub/internal/permission"
|
|
"nub/internal/tool"
|
|
)
|
|
|
|
// countingTool zählt, wie oft Run tatsächlich aufgerufen wurde — Grundlage,
|
|
// um zu beweisen, dass deny/ask-abgelehnte Calls das Tool gar nicht erst
|
|
// ausführen.
|
|
type countingTool struct{ calls *int32 }
|
|
|
|
func (t countingTool) Name() string { return "echo" }
|
|
func (t countingTool) Description() string { return "echoes input" }
|
|
func (t countingTool) Schema() json.RawMessage { return json.RawMessage(`{"type":"object"}`) }
|
|
func (t countingTool) Run(ctx context.Context, input json.RawMessage, env tool.Env) (tool.Result, error) {
|
|
atomic.AddInt32(t.calls, 1)
|
|
return tool.Result{ForModel: "ran"}, nil
|
|
}
|
|
|
|
func newPermissionTestLoop(t *testing.T, calls *int32) (*Loop, *fakeProvider) {
|
|
t.Helper()
|
|
provider := &fakeProvider{batches: [][]llm.Event{
|
|
toolCallEvents("call_1", "echo", `{}`),
|
|
textEvents("done"),
|
|
}}
|
|
loop, reg := newTestLoop(provider)
|
|
reg.Register(countingTool{calls: calls}) // überschreibt das echoTool aus newTestLoop
|
|
return loop, provider
|
|
}
|
|
|
|
func runOneRound(t *testing.T, loop *Loop) []tool.UIEvent {
|
|
t.Helper()
|
|
in := make(chan Input, 1)
|
|
in <- Input{Text: "go"}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
out := loop.Run(ctx, in)
|
|
close(in)
|
|
return drain(t, out, 3*time.Second)
|
|
}
|
|
|
|
func findToolCallOutput(events []tool.UIEvent) *tool.ToolCallOutput {
|
|
for _, ev := range events {
|
|
if o, ok := ev.(tool.ToolCallOutput); ok {
|
|
return &o
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestRunTools_DenyModeBlocksWithoutRunning(t *testing.T) {
|
|
var calls int32
|
|
loop, _ := newPermissionTestLoop(t, &calls)
|
|
loop.Permissions = &permission.Policy{Modes: map[string]permission.Mode{"echo": permission.ModeDeny}}
|
|
|
|
events := runOneRound(t, loop)
|
|
out := findToolCallOutput(events)
|
|
if out == nil {
|
|
t.Fatal("expected a ToolCallOutput event")
|
|
}
|
|
if !out.Result.IsError {
|
|
t.Error("expected IsError for a denied tool call")
|
|
}
|
|
if atomic.LoadInt32(&calls) != 0 {
|
|
t.Errorf("tool.Run was called %d times, want 0 (deny must block before execution)", calls)
|
|
}
|
|
}
|
|
|
|
func TestRunTools_AskModeWithNilHookIsHardError(t *testing.T) {
|
|
var calls int32
|
|
loop, _ := newPermissionTestLoop(t, &calls)
|
|
loop.Permissions = &permission.Policy{Modes: map[string]permission.Mode{"echo": permission.ModeAsk}}
|
|
// RequestPermission bleibt nil.
|
|
|
|
events := runOneRound(t, loop)
|
|
out := findToolCallOutput(events)
|
|
if out == nil {
|
|
t.Fatal("expected a ToolCallOutput event")
|
|
}
|
|
if !out.Result.IsError {
|
|
t.Error("expected IsError when ask has no RequestPermission hook")
|
|
}
|
|
if atomic.LoadInt32(&calls) != 0 {
|
|
t.Errorf("tool.Run was called %d times, want 0", calls)
|
|
}
|
|
}
|
|
|
|
func TestRunTools_AskModeAllowedByHookRuns(t *testing.T) {
|
|
var calls int32
|
|
loop, _ := newPermissionTestLoop(t, &calls)
|
|
loop.Permissions = &permission.Policy{Modes: map[string]permission.Mode{"echo": permission.ModeAsk}}
|
|
loop.RequestPermission = func(ctx context.Context, toolName string, input json.RawMessage) bool {
|
|
return true
|
|
}
|
|
|
|
events := runOneRound(t, loop)
|
|
out := findToolCallOutput(events)
|
|
if out == nil {
|
|
t.Fatal("expected a ToolCallOutput event")
|
|
}
|
|
if out.Result.IsError {
|
|
t.Errorf("expected success when the hook allows, got: %s", out.Result.ForModel)
|
|
}
|
|
if atomic.LoadInt32(&calls) != 1 {
|
|
t.Errorf("tool.Run was called %d times, want 1", calls)
|
|
}
|
|
}
|
|
|
|
func TestRunTools_AskModeDeniedByHookDoesNotRun(t *testing.T) {
|
|
var calls int32
|
|
loop, _ := newPermissionTestLoop(t, &calls)
|
|
loop.Permissions = &permission.Policy{Modes: map[string]permission.Mode{"echo": permission.ModeAsk}}
|
|
loop.RequestPermission = func(ctx context.Context, toolName string, input json.RawMessage) bool {
|
|
return false
|
|
}
|
|
|
|
events := runOneRound(t, loop)
|
|
out := findToolCallOutput(events)
|
|
if out == nil {
|
|
t.Fatal("expected a ToolCallOutput event")
|
|
}
|
|
if !out.Result.IsError {
|
|
t.Error("expected IsError when the user declines")
|
|
}
|
|
if atomic.LoadInt32(&calls) != 0 {
|
|
t.Errorf("tool.Run was called %d times, want 0", calls)
|
|
}
|
|
}
|
|
|
|
func TestRunTools_NilPolicyDefaultsToAuto(t *testing.T) {
|
|
var calls int32
|
|
loop, _ := newPermissionTestLoop(t, &calls)
|
|
// loop.Permissions bleibt nil.
|
|
|
|
events := runOneRound(t, loop)
|
|
out := findToolCallOutput(events)
|
|
if out == nil {
|
|
t.Fatal("expected a ToolCallOutput event")
|
|
}
|
|
if out.Result.IsError {
|
|
t.Errorf("expected success with no policy configured, got: %s", out.Result.ForModel)
|
|
}
|
|
if atomic.LoadInt32(&calls) != 1 {
|
|
t.Errorf("tool.Run was called %d times, want 1", calls)
|
|
}
|
|
}
|