package approval
import (
"context"
"encoding/json"
"strings"
"testing"
)
// fixedRules rules the same way on every call to one tool.
type fixedRules struct {
tool string
verdict Verdict
}
func (f fixedRules) Check(tool string, _ json.RawMessage) Ruling {
if tool != f.tool {
return Ruling{}
}
return Ruling{Verdict: f.verdict, Rule: "Rule(x)", Source: "settings.json"}
}
func (f fixedRules) Lines() []string { return []string{"settings.json", " rule"} }
func TestStandingRules(t *testing.T) {
tests := []struct {
name string
tool string
verdict Verdict
yolo bool
remembered bool
wantErr bool
wantPrompt int
}{
{name: "deny holds under yolo", tool: "bash", verdict: VerdictDeny, yolo: true, wantErr: true},
{name: "deny holds for a read-only tool", tool: "read_file", verdict: VerdictDeny, wantErr: true},
{name: "deny beats session memory", tool: "bash", verdict: VerdictDeny, remembered: true, wantErr: true},
{name: "allow skips the prompt", tool: "bash", verdict: VerdictAllow},
{name: "ask prompts under yolo", tool: "bash", verdict: VerdictAsk, yolo: true, wantPrompt: 1},
{name: "ask prompts for a read-only tool", tool: "read_file", verdict: VerdictAsk, wantPrompt: 1},
{name: "ask beats session memory", tool: "bash", verdict: VerdictAsk, remembered: true, wantPrompt: 1},
{name: "no verdict prompts as usual", tool: "bash", verdict: NoVerdict, wantPrompt: 1},
{name: "no verdict still honours memory", tool: "bash", verdict: NoVerdict, remembered: true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var asked int
var forced string
p := New(func(_ context.Context, r Request) (Decision, error) {
asked++
forced = r.Forced
return Once, nil
})
p.YOLO = tc.yolo
p.Rules = fixedRules{tool: tc.tool, verdict: tc.verdict}
if tc.remembered {
p.tools[tc.tool] = true
}
err := approve(t, p, tc.tool, `{"command":"ls","path":"a"}`)
if (err != nil) != tc.wantErr {
t.Fatalf("err = %v, want error %v", err, tc.wantErr)
}
if err != nil && !strings.Contains(err.Error(), "Rule(x) in settings.json") {
t.Errorf("denial %q should name the rule and its file", err)
}
if asked != tc.wantPrompt {
t.Errorf("prompted %d times, want %d", asked, tc.wantPrompt)
}
if tc.verdict == VerdictAsk && forced == "" {
t.Error("a prompt an ask rule forced should say so")
}
})
}
}
// Headless, an ask rule is a refusal with nobody to put the question to —
// which is what the rule asked for.
func TestAskRuleHeadlessDenies(t *testing.T) {
p := New(nil)
p.YOLO = true
p.Rules = fixedRules{tool: "bash", verdict: VerdictAsk}
if err := approve(t, p, "bash", `{"command":"ls"}`); err == nil {
t.Error("an ask rule with no prompt should not run the call")
}
}
func TestRuleLines(t *testing.T) {
var p *Policy
if p.RuleLines() != nil {
t.Error("nil policy")
}
p = New(nil)
if p.RuleLines() != nil {
t.Error("no rules")
}
p.Rules = fixedRules{}
if len(p.RuleLines()) != 2 {
t.Error("lines not passed through")
}
}