Files
sundynix-agentix/sundynix-dispatcher/internal/eino/compose_graph_test.go
T
Blizzard 2ee16d1f99 feat(dispatcher): Eino 采纳 Phase C —— 对话主流程跑 compose.Graph + callbacks 归一
按"并存 + 等价回归"策略,对话主流程可跑在 Eino compose.Graph 上:
- compose_graph.go:runConversation 按 EINO_COMPOSE 灰度开关分流;
  runComposeConversation 建图 START→ChatModel→END,Compile→Stream 回流 token;
  模型未就绪/编译失败降级回 runAgent。默认关,graph.go 仍是默认且权威。
- compose_callbacks.go:composeTracer 用 utils/callbacks 把 ChatModel/Tool 的
  start/end/error 桥到 ExecEvent(可观测归一,不再各处手写 emit)。
- LLM 接口 + Pool 增 ChatModel();fakeLLM 加 cm 字段 + stub Eino 模型。
- 测试:compose 图编译运行 / compose 对话流式 / 开关关→走 runAgent。

顺带修真 bug:SubjectTaskStatus 原 sundynix.tasks.status 落在任务流通配
sundynix.tasks.> 内 → 状态事件被当成"幽灵任务"自我放大(实测污染 2300+ 条)。
挪到 sundynix.status.task + dispatcher 加空任务护栏。

验收:make test-go 全绿;live compose 路径 54 字答复 eval 1.00、默认路径
eval 1.00、幽灵任务 0 复发。剩余 branch/map/render 等节点逐步迁移后 graph.go 退役。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 10:42:50 +08:00

90 lines
3.0 KiB
Go
Raw 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"
"strings"
"testing"
"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/schema"
"github.com/sundynix/sundynix-dispatcher/internal/harness"
"github.com/sundynix/sundynix-dispatcher/internal/llm"
)
// stubModel 是实现 Eino model.BaseChatModel 的测试桩,固定回一段文本(确定性)。
type stubModel struct{ reply string }
func (s *stubModel) Generate(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) {
return schema.AssistantMessage(s.reply, nil), nil
}
func (s *stubModel) Stream(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) {
sr, sw := schema.Pipe[*schema.Message](1)
go func() {
sw.Send(schema.AssistantMessage(s.reply, nil), nil)
sw.Close()
}()
return sr, nil
}
// TestComposeGraphRuns 直接验证 compose.GraphSTART→ChatModel→END)能编译并流式产出。
func TestComposeGraphRuns(t *testing.T) {
g := compose.NewGraph[[]*schema.Message, *schema.Message]()
if err := g.AddChatModelNode("model", &stubModel{reply: "你好世界"}); err != nil {
t.Fatal(err)
}
_ = g.AddEdge(compose.START, "model")
_ = g.AddEdge("model", compose.END)
r, err := g.Compile(context.Background())
if err != nil {
t.Fatalf("compile: %v", err)
}
out, err := r.Invoke(context.Background(), []*schema.Message{schema.UserMessage("hi")})
if err != nil {
t.Fatalf("invoke: %v", err)
}
if out.Content != "你好世界" {
t.Fatalf("got %q", out.Content)
}
}
// TestComposeConversationEquivalent 验证 EINO_COMPOSE=1 时对话主流程走 compose 路径,
// 把模型输出流式回流到 sink 并累计成稿——与自研 runAgent 行为等价(都逐片转发模型输出)。
func TestComposeConversationEquivalent(t *testing.T) {
t.Setenv("EINO_COMPOSE", "1")
fs := &fakeSink{}
o := &Orchestrator{
pool: &fakeLLM{ready: true, cm: &stubModel{reply: "我是 compose 路径的回答"}},
breaker: harness.NewCircuitBreaker(),
sink: fs,
}
b := &board{query: "你好"}
tr := &execTracer{} // sink 为 nil → 轨迹发射空操作
o.runConversation(context.Background(), "task_compose", b, "", tr, "agent")
if !strings.Contains(b.answer, "compose 路径") {
t.Fatalf("成稿未含模型输出: %q", b.answer)
}
if !strings.Contains(fs.text(), "compose 路径") {
t.Fatalf("sink 未收到流式 token: %q", fs.text())
}
}
// TestComposeDisabledUsesRunAgent 验证开关关闭时仍走自研 runAgent(默认行为不变)。
func TestComposeDisabledUsesRunAgent(t *testing.T) {
t.Setenv("EINO_COMPOSE", "0")
fs := &fakeSink{}
o := &Orchestrator{
pool: &fakeLLM{ready: true, stream: func([]llm.ChatMessage) string { return "自研路径回答" }},
breaker: harness.NewCircuitBreaker(),
sink: fs,
}
b := &board{query: "你好"}
o.runConversation(context.Background(), "task_legacy", b, "", &execTracer{}, "agent")
if !strings.Contains(b.answer, "自研路径") {
t.Fatalf("未走自研路径: %q", b.answer)
}
}