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