Files
Blizzard de36ed4cb3 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>
2026-07-02 09:56:26 +08:00

71 lines
2.4 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 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=3got %d", failed)
}
if len(secs) != 3 {
t.Fatalf("应返回 3 个 sectiongot %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=1got %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-空)")
}
}