nub/internal/agent/permission_test.go
Tom a97013d876 initial commit
- v 0.1.0 siehe CHANGELOG.md
2026-07-25 11:08:01 +02:00

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)
}
}