Files
sundynix-agentix/sundynix-dispatcher/internal/eino/graph_test.go
T
Blizzard 9b0520e020 test(backend): 编排引擎/DSL/docx/报告导出 首批 Go 单测(19 用例,纯逻辑无依赖)
补齐核心后端逻辑的自动化测试,全部纯函数级、不依赖 docker/NATS/LLM,毫秒级跑完,
把"手动起全栈 curl 验证"变成 `go test`:

- dispatcher/internal/eino (graph_test.go)
  evalCondition(各运算符+refs/tools/answer/profile 关键字+兜底)、resolveOperand、
  aggregate(拼接/去重合并/默认/全空)、branchNode(真假边标签精确选路 + 无标签退回边序 +
  单边剪枝)、cstr/cbool/countLines/labelOf/targetsOf。
- dispatcher/internal/dsl (compile_test.go)
  Topo(线性序 + 有环不丢节点)、Compile(system/query/tools 抽取 + 无输入兜底 + 空图用原文)、
  ToolBinding(tool/retriever/非工具)、Parse(合法/非法)。
- mcp-go/internal/office (unioffice_test.go)
  RenderReport 产物为合法 docx(zip 三部件 + 标题/章节文本 + XML 转义)、escapeXML。
- mcp-go/internal/mcp (report_test.go)
  reportMarkdown(标题+多章 / 无标题)。
- gateway/internal/dsl (parser_test.go)
  ParseAndAssemble(task_ 前缀 + Graph 透传 + Meta 初始化 + 空/非法报错)。

`go test ./...` 各模块全绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 14:13:13 +08:00

137 lines
4.5 KiB
Go

package eino
import (
"testing"
"github.com/sundynix/sundynix-dispatcher/internal/dsl"
)
func TestEvalCondition(t *testing.T) {
b := &board{refs: []string{"a", "b"}, toolOut: []string{"x"}, answer: "12345", profile: "p"}
cases := []struct {
cond string
want bool
}{
{"", true}, // 空 → 真
{"2>1", true}, // 字面量比较
{"1>2", false}, //
{"refs > 0", true}, // refs=2
{"refs == 2", true}, //
{"refs < 1", false}, //
{"tools >= 1", true}, // tools=1
{"tools != 0", true}, //
{"answer <= 5", true}, // answer 长度 5
{"answer > 5", false}, //
{"profile == 1", true},
{"乱写的条件", true}, // 非空但无法解析 → 默认真
}
for _, c := range cases {
if got := evalCondition(c.cond, b); got != c.want {
t.Errorf("evalCondition(%q) = %v, want %v", c.cond, got, c.want)
}
}
}
func TestResolveOperand(t *testing.T) {
b := &board{refs: []string{"a"}, toolOut: []string{"x", "y"}, answer: "汉字三", profile: ""}
cases := []struct {
s string
want float64
}{
{"refs", 1},
{"tools", 2},
{"answer", 3}, // 3 个 rune
{"profile", 0},
{"42", 42},
{"unknown", 0}, // 非关键字非数字 → 0
}
for _, c := range cases {
if got := resolveOperand(c.s, b); got != c.want {
t.Errorf("resolveOperand(%q) = %v, want %v", c.s, got, c.want)
}
}
}
func TestAggregate(t *testing.T) {
parts := []string{"甲", "乙", "甲", " ", ""}
if got := aggregate("拼接", parts); len(got) != 1 || got[0] != "甲\n---\n乙\n---\n甲" {
t.Errorf("拼接 = %v", got)
}
if got := aggregate("去重合并", parts); len(got) != 1 || got[0] != "甲\n---\n乙" {
t.Errorf("去重合并 = %v", got)
}
if got := aggregate("未知策略", []string{"a", "b"}); len(got) != 1 || got[0] != "a\n---\nb" {
t.Errorf("默认应为拼接, got %v", got)
}
if got := aggregate("拼接", []string{"", " "}); got != nil {
t.Errorf("全空应返回 nil, got %v", got)
}
}
func TestBranchNode_Labeled(t *testing.T) {
o := &Orchestrator{}
tr := &execTracer{} // sink 为 nil → 所有发射均空操作
byID := map[string]dsl.Node{"A": {ID: "A"}, "B": {ID: "B"}}
// true 边故意列在第二位,验证按标签而非边序选路。
outs := []dsl.Edge{
{Source: "br", Target: "B", SourceHandle: "false"},
{Source: "br", Target: "A", SourceHandle: "true"},
}
n := dsl.Node{ID: "br", Kind: "branch", Config: map[string]any{"condition": "2>1"}}
if got := o.branchNode(n, &board{}, outs, byID, tr); len(got) != 1 || got[0] != "A" {
t.Errorf("条件真应走 true 标签(A), got %v", got)
}
n.Config["condition"] = "1>2"
if got := o.branchNode(n, &board{}, outs, byID, tr); len(got) != 1 || got[0] != "B" {
t.Errorf("条件假应走 false 标签(B), got %v", got)
}
}
func TestBranchNode_OrderFallback(t *testing.T) {
o := &Orchestrator{}
tr := &execTracer{}
byID := map[string]dsl.Node{"A": {ID: "A"}, "B": {ID: "B"}}
outs := []dsl.Edge{{Source: "br", Target: "A"}, {Source: "br", Target: "B"}} // 无标签
n := dsl.Node{ID: "br", Kind: "branch", Config: map[string]any{"condition": "2>1"}}
if got := o.branchNode(n, &board{}, outs, byID, tr); len(got) != 1 || got[0] != "A" {
t.Errorf("无标签条件真应走第一条边(A), got %v", got)
}
n.Config["condition"] = "1>2"
if got := o.branchNode(n, &board{}, outs, byID, tr); len(got) != 1 || got[0] != "B" {
t.Errorf("无标签条件假应走第二条边(B), got %v", got)
}
// 单出边 + 条件假 → 不继续。
single := []dsl.Edge{{Source: "br", Target: "A"}}
n.Config["condition"] = "1>2"
if got := o.branchNode(n, &board{}, single, byID, tr); len(got) != 0 {
t.Errorf("单边条件假应剪掉, got %v", got)
}
}
func TestSmallHelpers(t *testing.T) {
cfg := map[string]any{"s": " hi ", "n": 7, "b": true, "nil": nil}
if cstr(cfg, "s") != "hi" {
t.Error("cstr 应去空白")
}
if cstr(cfg, "n") != "7" {
t.Error("cstr 非字符串应 fmt 转换")
}
if cstr(cfg, "missing") != "" || cstr(cfg, "nil") != "" || cstr(nil, "x") != "" {
t.Error("cstr 缺失/nil 应空串")
}
if !cbool(cfg, "b") || cbool(cfg, "s") || cbool(nil, "b") {
t.Error("cbool 行为不符")
}
if countLines("") != 0 || countLines("a") != 1 || countLines("a\nb\nc") != 3 {
t.Error("countLines 行为不符")
}
if labelOf(dsl.Node{Label: "L"}, "d") != "L" || labelOf(dsl.Node{}, "d") != "d" {
t.Error("labelOf 行为不符")
}
es := []dsl.Edge{{Target: "x"}, {Target: "y"}}
got := targetsOf(es)
if len(got) != 2 || got[0] != "x" || got[1] != "y" {
t.Errorf("targetsOf = %v", got)
}
}