fix(dispatcher): Map 节点错误传播 —— 并行子项失败不再静默(T4.E)
- writeSection 返回 (body, error):仅真·LLM 调用失败返 err;预算触顶/模型未配置 是主动降级(可见降级正文,err=nil)不计失败 - writeSections 返回 ([]section, failed):失败项 Body 带可见「撰写失败」标记 + 汇总失败数 - mapNode:全部子项失败 → 置 b.fatalErr(任务判 failed 而非静默 done-空); 部分失败 → trace span + 流式 ⚠️ 告警 - report handleReport:部分章节失败时流式提示,不再当全成功 - 3 单测:全失败/部分精确计数(=1 非 all-or-nothing)/mapNode 置 fatalErr Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
package eino
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/sundynix/sundynix-dispatcher/internal/dsl"
|
||||
"github.com/sundynix/sundynix-dispatcher/internal/llm"
|
||||
)
|
||||
|
||||
// writeSections:真·LLM 失败应逐项标记(Body 带可见失败标记)并汇总失败数,不静默丢失。
|
||||
func TestWriteSections_PropagatesFailures(t *testing.T) {
|
||||
ll := &fakeLLM{ready: true, chat: func(_ []llm.ChatMessage) (string, error) {
|
||||
return "", errors.New("boom")
|
||||
}}
|
||||
o := newOrch(ll, kbTool(nil), &fakeSink{}, &fakeExec{})
|
||||
|
||||
secs, failed := o.writeSections(context.Background(), "主题", "", []string{"A", "B", "C"}, o.tracer("t1"))
|
||||
if failed != 3 {
|
||||
t.Fatalf("全失败应 failed=3,got %d", failed)
|
||||
}
|
||||
if len(secs) != 3 {
|
||||
t.Fatalf("应返回 3 个 section,got %d", len(secs))
|
||||
}
|
||||
for _, s := range secs {
|
||||
if !strings.Contains(s.Body, "撰写失败") {
|
||||
t.Errorf("失败项 Body 应含可见失败标记,got %q", s.Body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 部分失败:仅命中 heading B 的调用失败 → failed 精确为 1(非 all-or-nothing),A/C 正常。
|
||||
func TestWriteSections_PartialFailureCount(t *testing.T) {
|
||||
ll := &fakeLLM{ready: true, chat: func(m []llm.ChatMessage) (string, error) {
|
||||
for _, msg := range m {
|
||||
if strings.Contains(msg.Content, "本章标题:B") {
|
||||
return "", errors.New("boom-B")
|
||||
}
|
||||
}
|
||||
return "正文", nil
|
||||
}}
|
||||
o := newOrch(ll, kbTool(nil), &fakeSink{}, &fakeExec{})
|
||||
|
||||
secs, failed := o.writeSections(context.Background(), "主题", "", []string{"A", "B", "C"}, o.tracer("t1"))
|
||||
if failed != 1 {
|
||||
t.Fatalf("仅 B 失败应 failed=1,got %d", failed)
|
||||
}
|
||||
if !strings.Contains(secs[1].Body, "撰写失败") {
|
||||
t.Errorf("B 应标记失败,got %q", secs[1].Body)
|
||||
}
|
||||
if strings.Contains(secs[0].Body, "撰写失败") || strings.Contains(secs[2].Body, "撰写失败") {
|
||||
t.Errorf("A/C 不应被误判失败")
|
||||
}
|
||||
}
|
||||
|
||||
// mapNode:并行 fan-out 全部子项失败 → 置 fatalErr,任务判 failed 而非静默 done-空。
|
||||
func TestMapNode_AllFailSetsFatal(t *testing.T) {
|
||||
ll := &fakeLLM{ready: true, chat: func(_ []llm.ChatMessage) (string, error) {
|
||||
return "", errors.New("boom")
|
||||
}}
|
||||
o := newOrch(ll, kbTool(nil), &fakeSink{}, &fakeExec{})
|
||||
b := &board{query: "分析一下"}
|
||||
|
||||
o.mapNode(context.Background(), "t1", dsl.Node{ID: "m1"}, b, o.tracer("t1"))
|
||||
if b.fatalErr == nil {
|
||||
t.Fatalf("全部子项失败时应置 fatalErr(否则任务会静默判 done-空)")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user