package agent import ( "context" "testing" "git.gopher.ink/mirrors2/AutoZero/llm" "git.gopher.ink/mirrors2/AutoZero/tools" ) // kindTool 造一个指定类别与副作用标注的假工具。 func kindTool(name string, kind tools.ToolKind, sideEffect bool) *tools.Tool { t := &tools.Tool{Name: name, SideEffect: sideEffect, Kind: kind} if !sideEffect && kind == tools.KindRead { t.ReadOnly = true } return t } // TestNeedsApprovalByMode 覆盖「档位 × 工具类别 × 副作用」的门控矩阵。 func TestNeedsApprovalByMode(t *testing.T) { read := kindTool("read", tools.KindRead, false) write := kindTool("write", tools.KindWrite, true) exec := kindTool("bash", tools.KindExec, true) other := kindTool("agent", tools.KindOther, true) unlabeled := &tools.Tool{Name: "mcp_thing", SideEffect: true} // 零值 Kind → other cases := []struct { mode string tool *tools.Tool wantAsk bool name string }{ {"", read, false, "zero-mode read"}, {"", write, true, "zero-mode write (= old dangerous-only)"}, {"", exec, true, "zero-mode exec"}, {ModeAsk, read, false, "ask read"}, {ModeAsk, write, true, "ask write"}, {ModeAsk, exec, true, "ask exec"}, {ModeAcceptEdits, read, false, "accept-edits read"}, {ModeAcceptEdits, write, false, "accept-edits write auto-approved"}, {ModeAcceptEdits, exec, true, "accept-edits exec still asks"}, {ModeAcceptEdits, other, true, "accept-edits other still asks"}, {ModeAcceptEdits, unlabeled, true, "accept-edits unlabeled kind still asks (fail-closed)"}, {ModeBypass, read, false, "bypass read"}, {ModeBypass, write, false, "bypass write"}, {ModeBypass, exec, false, "bypass exec"}, {ModeBypass, other, false, "bypass other"}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { a := &Agent{Mode: c.mode, trusted: map[string]bool{}} a.Opt.Approval = func(ApprovalRequest) ApprovalVerdict { return ApprovalDeny } if got := a.needsApproval(c.tool.Name, c.tool); got != c.wantAsk { t.Errorf("needsApproval(%s, %s) = %v, want %v", c.mode, c.tool.Name, got, c.wantAsk) } }) } } // TestNeedsApprovalShortCircuits:ApproveAll 与会话信任缓存优先于档位。 func TestNeedsApprovalShortCircuits(t *testing.T) { exec := kindTool("bash", tools.KindExec, true) a := &Agent{Mode: ModeAsk, trusted: map[string]bool{}} a.Opt.ApproveAll = true if a.needsApproval("bash", exec) { t.Error("ApproveAll must short-circuit to no-approval") } b := &Agent{Mode: ModeAsk, trusted: map[string]bool{"bash": true}} if b.needsApproval("bash", exec) { t.Error("session-trusted tool must not ask again") } } // TestNeedsApprovalLegacyPolicyAlways:旧 policy=always 仍会对只读工具发问 // (仅在挂了交互回调时)。 func TestNeedsApprovalLegacyPolicyAlways(t *testing.T) { read := kindTool("read", tools.KindRead, false) a := &Agent{Mode: ModeAsk, ApprovalPolicy: "always", trusted: map[string]bool{}} a.Opt.Approval = func(ApprovalRequest) ApprovalVerdict { return ApprovalOnce } if !a.needsApproval("read", read) { t.Error("policy=always should still ask for read-only tools") } // 没有交互回调时不该问(无人应答)。 b := &Agent{Mode: ModeAsk, ApprovalPolicy: "always", trusted: map[string]bool{}} if b.needsApproval("read", read) { t.Error("policy=always without an approval callback should not gate") } } // TestEffectiveKind:零值推断是 fail-closed 的。 func TestEffectiveKind(t *testing.T) { cases := []struct { tool *tools.Tool want tools.ToolKind }{ {&tools.Tool{ReadOnly: true}, tools.KindRead}, {&tools.Tool{ReadOnly: true, SideEffect: true}, tools.KindOther}, // 有副作用不算 read {&tools.Tool{}, tools.KindOther}, // 未知 → other {&tools.Tool{SideEffect: true}, tools.KindOther}, {&tools.Tool{Kind: tools.KindWrite}, tools.KindWrite}, // 显式优先 } for _, c := range cases { if got := c.tool.EffectiveKind(); got != c.want { t.Errorf("%+v: EffectiveKind() = %q, want %q", c.tool, got, c.want) } } } // TestExecOneAcceptEdits:端到端——自动编辑档不放行 bash,但放行 write。 func TestExecOneAcceptEdits(t *testing.T) { newAgent := func(ran *int) *Agent { reg := tools.NewRegistry() _ = reg.Register(&tools.Tool{ Name: "write", SideEffect: true, Kind: tools.KindWrite, Run: func(context.Context, string) (tools.Result, error) { *ran++; return tools.TextResult("wrote"), nil }, }) _ = reg.Register(&tools.Tool{ Name: "bash", SideEffect: true, Kind: tools.KindExec, Run: func(context.Context, string) (tools.Result, error) { *ran++; return tools.TextResult("ran"), nil }, }) a := &Agent{Tools: reg, Mode: ModeAcceptEdits, trusted: map[string]bool{}} a.Opt.Approval = func(ApprovalRequest) ApprovalVerdict { return ApprovalDeny } return a } ran := 0 a := newAgent(&ran) if _, err := a.execOne(context.Background(), llm.ToolCall{Name: "write", Input: "{}"}); err != nil { t.Fatalf("write should be auto-approved in accept-edits: %v", err) } if _, err := a.execOne(context.Background(), llm.ToolCall{Name: "bash", Input: "{}"}); err == nil { t.Fatal("bash must still require approval in accept-edits (denied here)") } if ran != 1 { t.Errorf("only write should have run, got %d runs", ran) } }