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。
143 lines
5.2 KiB
Go
143 lines
5.2 KiB
Go
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)
|
||
}
|
||
}
|