// agent_def_test.go 测试 Agent 类型注册表和工具集解析. // // 测试即文档:每个测试描述一个明确的行为约束. // 覆盖范围:Register/Get/List,三层过滤,内置类型约束,SetGlobalDisallowed. package engine import ( "sort" "testing" ) // --- Register / Get / List --- func TestAgentRegistry_RegisterAndGet(t *testing.T) { r := NewAgentRegistry() def := &AgentDefinition{ AgentType: "TestAgent", Description: "A test agent", } if err := r.Register(def); err != nil { t.Fatalf("Register failed: %v", err) } got, ok := r.Get("TestAgent") if !ok { t.Fatal("Get: expected true, got false") } if got.Description != "A test agent" { t.Errorf("Description mismatch: %q", got.Description) } } func TestAgentRegistry_GetNotFound(t *testing.T) { r := NewAgentRegistry() _, ok := r.Get("NonExistent") if ok { t.Error("expected Get to return false for unknown type") } } func TestAgentRegistry_RegisterOverwrite(t *testing.T) { r := NewAgentRegistry() _ = r.Register(&AgentDefinition{AgentType: "Dup", Description: "v1"}) _ = r.Register(&AgentDefinition{AgentType: "Dup", Description: "v2"}) got, _ := r.Get("Dup") if got.Description != "v2" { t.Errorf("expected overwrite to v2, got %q", got.Description) } } func TestAgentRegistry_RegisterNilError(t *testing.T) { r := NewAgentRegistry() err := r.Register(nil) if err == nil { t.Error("expected error for nil definition") } } func TestAgentRegistry_RegisterEmptyTypeError(t *testing.T) { r := NewAgentRegistry() err := r.Register(&AgentDefinition{AgentType: ""}) if err == nil { t.Error("expected error for empty AgentType") } } func TestAgentRegistry_ListSorted(t *testing.T) { r := NewAgentRegistry() _ = r.Register(&AgentDefinition{AgentType: "Zebra"}) _ = r.Register(&AgentDefinition{AgentType: "Apple"}) _ = r.Register(&AgentDefinition{AgentType: "Mango"}) list := r.List() if len(list) != 3 { t.Fatalf("expected 3 items, got %d", len(list)) } if list[0].AgentType != "Apple" || list[1].AgentType != "Mango" || list[2].AgentType != "Zebra" { t.Errorf("List not sorted: %v", list) } } // --- resolveAgentToolset 三层过滤 --- func TestResolveAgentToolset_NoFilter(t *testing.T) { // AllowedTools=nil, DisallowedTools=nil → 父工具集 - globalDisallowed r := NewAgentRegistry() // globalDisallowed = DefaultGlobalDisallowedTools(含 Agent 等) def := &AgentDefinition{AgentType: "All"} parent := []string{"Bash", "Edit", "Write", "Agent"} result := r.ResolveToolset(def, parent) // "Agent" 应被全局禁用过滤掉 for _, t2 := range result { if t2 == "Agent" { t.Error("globalDisallowed 应排除 Agent") } } // 其余工具应保留 if len(result) != 3 { t.Errorf("expected 3 tools, got %d: %v", len(result), result) } } func TestResolveAgentToolset_WhitelistFilter(t *testing.T) { // AllowedTools 非空时取交集 r := NewAgentRegistry() def := &AgentDefinition{ AgentType: "ReadOnly", AllowedTools: []string{"Bash", "Grep", "Glob"}, } parent := []string{"Bash", "Edit", "Write", "Grep", "Glob", "Agent"} result := r.ResolveToolset(def, parent) expected := []string{"Bash", "Glob", "Grep"} // sorted sort.Strings(expected) sort.Strings(result) if len(result) != len(expected) { t.Fatalf("expected %v, got %v", expected, result) } for i := range expected { if result[i] != expected[i] { t.Errorf("mismatch at %d: want %q got %q", i, expected[i], result[i]) } } } func TestResolveAgentToolset_DisallowedFilter(t *testing.T) { // DisallowedTools 移除特定工具 r := NewAgentRegistry() def := &AgentDefinition{ AgentType: "NoWrite", DisallowedTools: []string{"Edit", "Write"}, } parent := []string{"Bash", "Edit", "Write", "Grep"} result := r.ResolveToolset(def, parent) for _, t2 := range result { if t2 == "Edit" || t2 == "Write" { t.Errorf("DisallowedTools 应排除 %q", t2) } } if len(result) != 2 { // Bash, Grep t.Errorf("expected 2 tools, got %d: %v", len(result), result) } } func TestResolveAgentToolset_GlobalDisallowedPrecedence(t *testing.T) { // globalDisallowed 优先于 AllowedTools r := NewAgentRegistry() r.SetGlobalDisallowed([]string{"DangerTool", "Agent"}) def := &AgentDefinition{ AgentType: "WithDanger", AllowedTools: []string{"Bash", "DangerTool"}, // 试图允许被全局禁用的工具 } parent := []string{"Bash", "DangerTool", "Grep"} result := r.ResolveToolset(def, parent) // DangerTool 在全局禁用中,即使白名单包含也应排除 for _, t2 := range result { if t2 == "DangerTool" { t.Error("globalDisallowed 应优先于 AllowedTools") } } } func TestResolveAgentToolset_WhitelistToolNotInParent(t *testing.T) { // 白名单中有父工具集不存在的工具,不应出现在结果中 r := NewAgentRegistry() def := &AgentDefinition{ AgentType: "Phantom", AllowedTools: []string{"Bash", "NonExistentTool"}, } parent := []string{"Bash", "Edit"} result := r.ResolveToolset(def, parent) // 只有 Bash 同时在父工具集和白名单中 if len(result) != 1 || result[0] != "Bash" { t.Errorf("expected [Bash], got %v", result) } } // --- 内置 Agent 类型 --- func TestBuiltinAgents_ExploreDisablesWrite(t *testing.T) { r := NewAgentRegistry() RegisterBuiltinAgents(r) def, ok := r.Get("Explore") if !ok { t.Fatal("Explore 未注册") } parent := []string{"Bash", "Edit", "Write", "NotebookEdit", "Grep", "Glob", "Agent"} result := r.ResolveToolset(def, parent) for _, tool := range result { switch tool { case "Edit", "Write", "NotebookEdit", "Agent": t.Errorf("Explore 不应包含 %q", tool) } } // 应包含 Bash, Grep, Glob toolSet := make(map[string]bool) for _, t2 := range result { toolSet[t2] = true } for _, expected := range []string{"Bash", "Grep", "Glob"} { if !toolSet[expected] { t.Errorf("Explore 应包含 %q", expected) } } } func TestBuiltinAgents_PlanDisablesBashAndWrite(t *testing.T) { r := NewAgentRegistry() RegisterBuiltinAgents(r) def, ok := r.Get("Plan") if !ok { t.Fatal("Plan 未注册") } parent := []string{"Bash", "Edit", "Write", "NotebookEdit", "Grep", "Glob", "Agent"} result := r.ResolveToolset(def, parent) for _, tool := range result { switch tool { case "Edit", "Write", "NotebookEdit", "Bash", "Agent": t.Errorf("Plan 不应包含 %q", tool) } } // 应包含 Grep, Glob toolSet := make(map[string]bool) for _, t2 := range result { toolSet[t2] = true } for _, expected := range []string{"Grep", "Glob"} { if !toolSet[expected] { t.Errorf("Plan 应包含 %q", expected) } } } func TestBuiltinAgents_VerificationIsBackground(t *testing.T) { r := NewAgentRegistry() RegisterBuiltinAgents(r) def, ok := r.Get("Verification") if !ok { t.Fatal("Verification 未注册") } if !def.Background { t.Error("Verification 应默认后台运行") } } func TestBuiltinAgents_GeneralPurposeAllTools(t *testing.T) { r := NewAgentRegistry() RegisterBuiltinAgents(r) def, ok := r.Get("general-purpose") if !ok { t.Fatal("general-purpose 未注册") } parent := []string{"Bash", "Edit", "Write", "Grep", "Glob"} result := r.ResolveToolset(def, parent) // general-purpose 不限制工具,父工具集都应保留(Agent 被全局禁用) if len(result) != 5 { t.Errorf("general-purpose 应保留所有父工具(除 globalDisallowed),got %v", result) } } func TestBuiltinAgents_ExploreUsesHaikuModel(t *testing.T) { r := NewAgentRegistry() RegisterBuiltinAgents(r) def, _ := r.Get("Explore") // G9-C: DefaultRoles 为空,Explore.Model 跟随 DefaultRoles[RoleFast] = "". // 运行时由引擎的 ModelRegistry 解析实际模型--Model="" 表示"继承父 agent 模型". // 如果消费层设置了 RoleFast 角色,Explore 会自动使用该模型. if def.Model != "" { t.Errorf("DefaultRoles 为空时 Explore.Model 应为空(继承父模型),got %q", def.Model) } } // --- SetGlobalDisallowed --- func TestSetGlobalDisallowed_Override(t *testing.T) { r := NewAgentRegistry() // 默认 globalDisallowed = {"Agent"} // 覆盖为只有 "SuperDanger" r.SetGlobalDisallowed([]string{"SuperDanger"}) def := &AgentDefinition{AgentType: "Test"} // "Agent" 不再被全局禁用 parent := []string{"Agent", "SuperDanger", "Bash"} result := r.ResolveToolset(def, parent) toolSet := make(map[string]bool) for _, t2 := range result { toolSet[t2] = true } if !toolSet["Agent"] { t.Error("覆盖后 Agent 不应再被全局禁用") } if toolSet["SuperDanger"] { t.Error("SuperDanger 应被新全局禁用规则排除") } } func TestSetGlobalDisallowed_EmptyAllowsAll(t *testing.T) { r := NewAgentRegistry() r.SetGlobalDisallowed([]string{}) // 清空全局禁用 def := &AgentDefinition{AgentType: "Test"} parent := []string{"Agent", "Bash"} result := r.ResolveToolset(def, parent) if len(result) != 2 { t.Errorf("空全局禁用时应保留所有工具,got %v", result) } } // --- 内置类型数量 --- func TestBuiltinAgents_FourTypesRegistered(t *testing.T) { r := NewAgentRegistry() RegisterBuiltinAgents(r) list := r.List() if len(list) != 4 { t.Errorf("expected 4 builtin agent types, got %d", len(list)) } } // --- P0-B: globalDisallowed 补齐 --- func TestDefaultGlobalDisallowed_BlocksExitPlanMode(t *testing.T) { // ExitPlanMode 应在默认 globalDisallowed 中 r := NewAgentRegistry() def := &AgentDefinition{AgentType: "Test"} parent := []string{"Bash", "ExitPlanMode", "Read"} result := r.ResolveToolset(def, parent) for _, tool := range result { if tool == "ExitPlanMode" { t.Error("ExitPlanMode 应被默认 globalDisallowed 禁用") } } } func TestDefaultGlobalDisallowed_BlocksEnterPlanMode(t *testing.T) { r := NewAgentRegistry() def := &AgentDefinition{AgentType: "Test"} parent := []string{"Bash", "EnterPlanMode", "Read"} result := r.ResolveToolset(def, parent) for _, tool := range result { if tool == "EnterPlanMode" { t.Error("EnterPlanMode 应被默认 globalDisallowed 禁用") } } } func TestDefaultGlobalDisallowed_ContainsAllExpected(t *testing.T) { // DefaultGlobalDisallowedTools 应包含所有预期的安全工具 expected := map[string]bool{ "Agent": true, "ExitPlanMode": true, "EnterPlanMode": true, "AskUserQuestion": true, "TaskOutput": true, "TaskStop": true, } for _, tool := range DefaultGlobalDisallowedTools { delete(expected, tool) } if len(expected) > 0 { t.Errorf("DefaultGlobalDisallowedTools 缺少: %v", expected) } } // --- P0-A: MCP 前缀自动通过 --- func TestResolveAgentToolset_MCPToolsBypassAllowedTools(t *testing.T) { // mcp__ 前缀工具应跳过 AllowedTools 交集过滤 r := NewAgentRegistry() def := &AgentDefinition{ AgentType: "Explore", AllowedTools: []string{"Read", "Grep"}, // 明确白名单,不包含 mcp__ 工具 } parent := []string{"Read", "Grep", "Write", "mcp__github__search", "mcp__wms__query"} result := r.ResolveToolset(def, parent) toolSet := make(map[string]bool) for _, t := range result { toolSet[t] = true } // Read, Grep 应在(白名单内) if !toolSet["Read"] || !toolSet["Grep"] { t.Errorf("Read/Grep 应在结果中: %v", result) } // Write 不应在(不在白名单) if toolSet["Write"] { t.Error("Write 不在白名单,不应出现") } // MCP 工具应自动通过白名单过滤 if !toolSet["mcp__github__search"] { t.Error("mcp__github__search 应自动通过 AllowedTools 过滤") } if !toolSet["mcp__wms__query"] { t.Error("mcp__wms__query 应自动通过 AllowedTools 过滤") } } func TestResolveAgentToolset_MCPToolsRespectDisallowed(t *testing.T) { // MCP 工具虽然通过白名单,但仍受 DisallowedTools 约束 r := NewAgentRegistry() def := &AgentDefinition{ AgentType: "Restricted", AllowedTools: []string{"Read"}, DisallowedTools: []string{"mcp__internal__delete"}, } parent := []string{"Read", "mcp__internal__delete", "mcp__safe__query"} result := r.ResolveToolset(def, parent) toolSet := make(map[string]bool) for _, t := range result { toolSet[t] = true } if toolSet["mcp__internal__delete"] { t.Error("DisallowedTools 中的 MCP 工具应被排除") } if !toolSet["mcp__safe__query"] { t.Error("非 DisallowedTools 的 MCP 工具应保留") } } func TestResolveAgentToolset_MCPToolsBypassBackgroundLimit(t *testing.T) { // MCP 工具也应自动通过 BackgroundAllowedTools 过滤 r := NewAgentRegistry() def := &AgentDefinition{ AgentType: "BgAgent", Background: true, BackgroundAllowedTools: []string{"Grep"}, // 后台限制只有 Grep } parent := []string{"Grep", "Bash", "mcp__wms__scan"} result := r.ResolveToolset(def, parent) toolSet := make(map[string]bool) for _, t := range result { toolSet[t] = true } if !toolSet["Grep"] { t.Error("Grep 应在 BackgroundAllowedTools 中") } if toolSet["Bash"] { t.Error("Bash 不在 BackgroundAllowedTools 中,应被过滤") } if !toolSet["mcp__wms__scan"] { t.Error("MCP 工具应自动通过 BackgroundAllowedTools 过滤") } } // --- P1-B: Background agent 工具限制 --- func TestResolveAgentToolset_BackgroundLimitApplied(t *testing.T) { r := NewAgentRegistry() def := &AgentDefinition{ AgentType: "VerifyBg", Background: true, BackgroundAllowedTools: []string{"Read", "Grep", "Glob"}, } parent := []string{"Read", "Grep", "Glob", "Bash", "Edit", "Write"} result := r.ResolveToolset(def, parent) toolSet := make(map[string]bool) for _, t := range result { toolSet[t] = true } for _, expected := range []string{"Read", "Grep", "Glob"} { if !toolSet[expected] { t.Errorf("%s 应在 BackgroundAllowedTools 中", expected) } } for _, blocked := range []string{"Bash", "Edit", "Write"} { if toolSet[blocked] { t.Errorf("%s 不在 BackgroundAllowedTools 中,不应出现", blocked) } } } func TestResolveAgentToolset_BackgroundNoLimitWhenEmpty(t *testing.T) { // Background=true 但 BackgroundAllowedTools 为空 → 不附加限制 r := NewAgentRegistry() def := &AgentDefinition{ AgentType: "BgNoLimit", Background: true, // BackgroundAllowedTools 为 nil } parent := []string{"Bash", "Read", "Edit"} result := r.ResolveToolset(def, parent) // 不附加 BackgroundAllowedTools 限制,所有工具保留(除 globalDisallowed) if len(result) != 3 { t.Errorf("Background=true 且 BackgroundAllowedTools 空时不应附加限制,got %v", result) } } func TestResolveAgentToolset_NonBackgroundNotAffected(t *testing.T) { // Background=false 时 BackgroundAllowedTools 不生效 r := NewAgentRegistry() def := &AgentDefinition{ AgentType: "ForegroundAgent", Background: false, BackgroundAllowedTools: []string{"Grep"}, // 这个应该不生效 } parent := []string{"Bash", "Read", "Edit", "Grep"} result := r.ResolveToolset(def, parent) // Background=false,BackgroundAllowedTools 不生效,保留所有工具 if len(result) != 4 { t.Errorf("Background=false 时 BackgroundAllowedTools 应无效,got %v", result) } } // --- P1-A: AllowedSubAgentTypes 字段 --- func TestAgentDefinition_AllowedSubAgentTypes_Field(t *testing.T) { r := NewAgentRegistry() def := &AgentDefinition{ AgentType: "Explore", AllowedSubAgentTypes: []string{"Plan", "Verification"}, } if err := r.Register(def); err != nil { t.Fatal(err) } got, _ := r.Get("Explore") if len(got.AllowedSubAgentTypes) != 2 { t.Errorf("AllowedSubAgentTypes: want 2, got %v", got.AllowedSubAgentTypes) } } func TestBuiltinAgents_VerificationUsesBackgroundAllowedTools(t *testing.T) { // Verification agent 应是后台 agent r := NewAgentRegistry() RegisterBuiltinAgents(r) def, ok := r.Get("Verification") if !ok { t.Fatal("Verification 未注册") } if !def.Background { t.Error("Verification 应为后台 agent") } // 注:当前内置 Verification 没有 BackgroundAllowedTools,这是可选的 }