Why: Task 10 replaces Backend's independent Markdown-to-Word production path with the reviewed canonical parser, embedded organization-default candidate, and Word adapter. Evidence: Canonical parity, deterministic package, routing, safety, cancellation, comparison, persistence, affected-package race, full test, vet, build, and static build checks passed locally. Impact: Ordinary workspace renders now persist Markdown, deterministic DOCX, manifest, and archive with hashes; controlled/project/review/approved routes fail closed to RDM, and legacy Go rendering is comparison-only. Refs: Unified Document Renderer Task 10; Renderer d449b9f779f2f968f8ac556b89940a6c9aaf3fdc
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", "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")
|
||
}
|
||
}
|