ci / vet-and-test (pull_request) Successful in 43s
Why: Real runs were fully serial: the direct path never set the kernel's parallel tool mode, delegate_task handed the parent only the child's 160-char summary (the full text was dropped), MiniMax streamed its reasoning inside content <think> blocks, and the EXT-16 sandbox hid ~/.local so python-docx/openpyxl/pptx could not even import. All four were measured defects (F1/F2/F3/F5) in the delegation/routing proposal. Why: Serial-only mutual exclusion is not enough for order-sensitive tools - the card-stream test failed with out-of-order appends - so the ordering moved into the kernel (SerialTools partition, Agent-Native #44); process events pair by CallID (#42) and tool results write back through the injected call context (#43). Evidence: go test ./... green, including new regression anchors: TestDelegateToolReturnsFullText, TestToolEventsPairByCallID, TestToolExecModeAndSerialSet, TestOpenLLMClientReasoningSplitAuto, TestSandboxArgvDepsLayer, TestExecEnvJoinsPythonPath. Impact: Direct runs now execute independent same-turn calls (searches, reads, delegate_task, review_findings) concurrently by default (runtime.parallelTools=false restores sequential); card flow, plans, workflows, exec primitives and file edits keep model-declared order. delegate_task returns the capped full child text. MiniMax-* models get reasoning_split=true so thinking streams via reasoning_content. --exec-deps-dir/AGENT_SYSTEM_EXEC_DEPS_DIR mounts a read-only dependency layer into the sandbox and merges PYTHONPATH. Bumps deps/agent-native to c348441. Refs: proposal 2026-10-10-agent-delegation-routing-sandbox.md (E1, F1-F5); Agent-Native #42/#43/#44; Dev task feature/agent-e1-parallel-fixes.
104 lines
4.2 KiB
Go
104 lines
4.2 KiB
Go
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"} {
|
||
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")
|
||
}
|
||
}
|