Files
AutoZero/agent/approval_mode_test.go
Sakurasan d5dac5a6fc feat: AutoZero 通用 Agent 框架(从 AutoGo 五阶段拆分完成)
llm(三协议 Provider)/ session(JSONL 持久化)/ tools(Registry、围栏、
builtin、computer)/ cdp(browser)/ skills / mcp / agent(循环、两级压缩、
编排、审批门控)+ 底座 jsonl/textx/execx/diag/scratch/spill/imagecache。

产品装配代码(config 桥接、schedule 工具、memory/plan/retro/prompt)
留在 AutoGo,宿主接入方式见 MIGRATION.md。
2026-10-26 00:54:46 +08:00

143 lines
5.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}