package main import ( "context" "testing" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry" "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/restrict" "git.yeij.top/AskaEth/Cyrene_v2/internal/config" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/agent" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/orchestration" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/tool" ) // TestWireSubagentToolRegistersOwnerOnly locks the composition-root wiring: the // dispatch tool must actually land in the capability registry (otherwise // orchestration stays library-only), and it must carry the default reachability // markers so a group/stranger session can neither see nor call it. func TestWireSubagentToolRegistersOwnerOnly(t *testing.T) { reg := registry.New() d, err := tool.New(reg) if err != nil { t.Fatalf("tool.New: %v", err) } a := &app{log: testLog(), dispatcher: d, scheduler: orchestration.New(orchestration.Options{})} a.wireSubagentTool() if _, gerr := reg.Get(orchestration.SpawnToolName); gerr != nil { t.Fatalf("capability %q is not registered: %v", orchestration.SpawnToolName, gerr) } names := make([]string, 0) for _, s := range d.ToolSchemas() { names = append(names, s.Name) } found := false for _, n := range names { if n == orchestration.SpawnToolName { found = true } } if !found { t.Fatalf("tool schemas = %v, want the spawn tool advertised", names) } if !restrict.SetContext(reg, restrict.ActorOwner).Allow(orchestration.SpawnToolName) { t.Fatal("owner was denied the spawn tool") } if restrict.SetContext(reg, restrict.ActorGroupMember).Allow(orchestration.SpawnToolName) { t.Fatal("group member may invoke the spawn tool; want owner-only") } if restrict.SetContext(reg, restrict.ActorStranger).Allow(orchestration.SpawnToolName) { t.Fatal("stranger may invoke the spawn tool; want owner-only") } // The registered capability must be the dispatch tool itself, not a placeholder. cap, _ := reg.Get(orchestration.SpawnToolName) sp, ok := cap.Value.(*orchestration.SpawnTool) if !ok { t.Fatalf("capability value = %T, want *orchestration.SpawnTool", cap.Value) } if _, err := sp.Execute(context.Background(), map[string]any{}); err == nil { t.Fatal("the registered tool accepted a goal-less call") } } // TestWireSubagentToolNilScheduler asserts a degraded boot (no scheduler) logs and // skips the registration instead of panicking. func TestWireSubagentToolNilScheduler(t *testing.T) { reg := registry.New() d, err := tool.New(reg) if err != nil { t.Fatalf("tool.New: %v", err) } a := &app{log: testLog(), dispatcher: d} a.wireSubagentTool() if _, gerr := reg.Get(orchestration.SpawnToolName); gerr == nil { t.Fatal("the tool was registered without a scheduler") } } // TestPersonaExistsMirrorsAgentPool locks the composition-root half of design/22 §8-2: // the resolver the scheduler validates a dispatched sub-agent's target persona with // must answer exactly like the per-role agent pool — a role is dispatchable precisely // when the loop has an agent carrying its instructions, and nothing else is. So a // persona with no agent is rejected at dispatch time rather than silently answering as // the default role. func TestPersonaExistsMirrorsAgentPool(t *testing.T) { t.Parallel() pool := map[string]*agent.Agent{"yuki": {}, "xilian": {}} exists := personaExists(pool) if !exists("yuki") || !exists("xilian") { t.Fatal("a pooled role was reported as unrunnable") } if exists("nobody") || exists("") { t.Fatal("an unpooled role id was accepted as runnable") } // The resolver is what orchestration gates the dispatch on, so wire it the same // way wire.go does and check the gate agrees. sched := orchestration.New(orchestration.Options{PersonaExists: personaExists(map[string]*agent.Agent{})}) if sched.PersonaKnown("yuki") { t.Fatal("an empty pool accepted a persona; want the dispatch rejected") } if !sched.PersonaKnown("") { t.Fatal("a persona-less dispatch was rejected by an empty pool; want it unaffected") } } // TestSubagentToolTimeoutFromConfig 锁定组合根的配置接法(design/05 §7 裁决 // 02.4.4):未配置时回落 10min 代码默认,SUBAGENT_TIMEOUT_SEC 生效时被真实 // 采用——派发工具拿到的是配置值而不是常量。 func TestSubagentToolTimeoutFromConfig(t *testing.T) { a := &app{log: testLog()} if got := a.subagentToolTimeout(); got != 10*time.Minute { t.Fatalf("subagentToolTimeout() = %v, want the 10min code default", got) } a.cfg = &config.Config{} a.cfg.Subagent.TimeoutSec = 90 if got := a.subagentToolTimeout(); got != 90*time.Second { t.Fatalf("subagentToolTimeout() = %v, want 90s from config", got) } // 非法(非正)配置不缩小时限:保持代码默认而不是把子代理变成「立即超时」。 a.cfg.Subagent.TimeoutSec = -5 if got := a.subagentToolTimeout(); got != 10*time.Minute { t.Fatalf("subagentToolTimeout() = %v for a non-positive config, want the code default", got) } }