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-空)") } }