package runtime import ( "strings" "testing" nruntime "agent-native/runtime" agentllm "agent-native/runtime/llm" core "agent-system-core" ) // ---- F1:delegate_task 回传子代理全文,不再只有 160 字摘要 ---- func TestDelegateToolReturnsFullText(t *testing.T) { full := strings.Repeat("子代理结论细节。", 60) // 480 字,远超 160 f := &fakeRunCtx{spawnAgentFn: func(agentID, name, task, hint string) (core.AgentResultPayload, error) { return core.AgentResultPayload{Status: core.RunCompleted, Text: full, Summary: "完成(160字内摘要)"}, nil }} st := &directRun{l: &LLMRunner{}, rc: f, eff: f.Settings(), inflight: map[string]*toolSlot{}} res := st.runTool("delegate_task", `{"name":"调研","task":"查证X"}`) if res.IsError { t.Fatalf("delegate failed: %s", res.Content) } if !strings.Contains(res.Content, "子代理结论细节。子代理结论细节。子代理结论细节。") { t.Fatalf("delegate result must carry the full child text, got: %.120s…", res.Content) } if strings.Contains(res.Content, "160字内摘要") { t.Fatalf("summary must be fallback only, got: %.120s…", res.Content) } } // ---- F5:并行执行下工具事件按 CallID 配对 ---- func TestToolEventsPairByCallID(t *testing.T) { f := &fakeRunCtx{} st := &directRun{l: &LLMRunner{}, rc: f, eff: f.Settings(), inflight: map[string]*toolSlot{}} st.queued = []agentllm.ToolCall{ {ID: "c1", Name: "knowledge_search", Arguments: `{}`}, {ID: "c2", Name: "web_search", Arguments: `{}`}, } // 乱序进出:c1 先开始,c2 先结束(并行下的真实时序)。 st.onEvent(nruntime.Event{Type: "tool_execution_start", Tool: "knowledge_search", CallID: "c1"}) st.onEvent(nruntime.Event{Type: "tool_execution_start", Tool: "web_search", CallID: "c2"}) st.onEvent(nruntime.Event{Type: "tool_execution_end", Tool: "web_search", CallID: "c2"}) st.onEvent(nruntime.Event{Type: "tool_execution_end", Tool: "knowledge_search", CallID: "c1"}) if len(st.inflight) != 0 { t.Fatalf("inflight must be empty after both ends: %v", st.inflight) } if len(st.finalParts) != 4 { t.Fatalf("finalParts = %d (want 4: call+result ×2)", len(st.finalParts)) } // 结束顺序决定部件顺序:先 c2 后 c1。 if got := st.finalParts[0].ToolCall.ID; got != "c2" { t.Fatalf("first tool part = %s (want c2)", got) } if got := st.finalParts[1].ToolResult.CallID; got != "c2" { t.Fatalf("first result part = %s (want c2)", got) } if got := st.finalParts[2].ToolCall.ID; got != "c1" { t.Fatalf("second tool part = %s (want c1)", got) } if got := st.finalParts[3].ToolResult.CallID; got != "c1" { t.Fatalf("second result part = %s (want c1)", got) } } // ---- F5:执行模式与串行集合 ---- func TestToolExecModeAndSerialSet(t *testing.T) { if got := toolExecMode(core.EffectiveSettings{}); got != nruntime.ExecParallel { t.Fatalf("default must be parallel, got %q", got) } off := core.EffectiveSettings{"runtime": map[string]any{"parallelTools": false}} if got := toolExecMode(off); got != "" { t.Fatalf("parallelTools=false must fall back to sequential, got %q", got) } // 顺序敏感工具必须串行;可并行的读检索与委派不得误入。 for _, name := range []string{"update_plan", "update_todos", "submit_card", "card_begin", "card_append", "card_commit", "run_workflow", "squad_orchestrate", "run_shell", "run_python", "register_artifact", "render_document"} { if !serialTools[name] { t.Fatalf("serialTools missing %s", name) } } for _, name := range []string{"delegate_task", "review_findings", "knowledge_search", "web_search", "fs_read_file", "write_artifact"} { if serialTools[name] { t.Fatalf("serialTools must not contain %s", name) } } } // ---- F2:MiniMax 前缀自动置位 reasoning_split ---- func TestOpenLLMClientReasoningSplitAuto(t *testing.T) { if c := openLLMClient("http://x", "k", "MiniMax-M2.7-highspeed"); !c.ReasoningSplit { t.Fatal("MiniMax model must set ReasoningSplit") } if c := openLLMClient("http://x", "k", "MiniMax-M3"); !c.ReasoningSplit { t.Fatal("MiniMax M3 must set ReasoningSplit") } if c := openLLMClient("http://x", "k", "claude-sonnet-5"); c.ReasoningSplit { t.Fatal("non-MiniMax model must not set ReasoningSplit") } }