Files
weichen a21a3b57b4 Route workspace Word delivery through the shared renderer
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
2026-10-15 19:41:14 +00:00

104 lines
4.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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")
}
}