9b0520e020
补齐核心后端逻辑的自动化测试,全部纯函数级、不依赖 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>
103 lines
3.0 KiB
Go
103 lines
3.0 KiB
Go
package dsl
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
)
|
|
|
|
func graphJSON(t *testing.T, f Flow) json.RawMessage {
|
|
t.Helper()
|
|
b, err := json.Marshal(f)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return b
|
|
}
|
|
|
|
func TestTopo_Linear(t *testing.T) {
|
|
f := Flow{
|
|
Nodes: []Node{{ID: "i"}, {ID: "a"}, {ID: "o"}},
|
|
Edges: []Edge{{Source: "i", Target: "a"}, {Source: "a", Target: "o"}},
|
|
}
|
|
order := f.Topo()
|
|
ids := []string{order[0].ID, order[1].ID, order[2].ID}
|
|
want := []string{"i", "a", "o"}
|
|
for k := range want {
|
|
if ids[k] != want[k] {
|
|
t.Fatalf("Topo 顺序 = %v, want %v", ids, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTopo_CycleFallback(t *testing.T) {
|
|
// 有环:Kahn 排不出,退化为声明序补齐,但不能丢节点。
|
|
f := Flow{
|
|
Nodes: []Node{{ID: "a"}, {ID: "b"}},
|
|
Edges: []Edge{{Source: "a", Target: "b"}, {Source: "b", Target: "a"}},
|
|
}
|
|
if got := f.Topo(); len(got) != 2 {
|
|
t.Fatalf("有环也应返回全部节点, got %d", len(got))
|
|
}
|
|
}
|
|
|
|
func TestCompile(t *testing.T) {
|
|
raw := graphJSON(t, Flow{Nodes: []Node{
|
|
{ID: "i", Kind: "input", Config: map[string]any{"text": "你好"}},
|
|
{ID: "a", Kind: "agent", Config: map[string]any{"system": "你是助手", "prompt": "再见"}},
|
|
{ID: "t", Kind: "tool", Config: map[string]any{"tool": "wiki_search"}},
|
|
}})
|
|
p := Compile(raw)
|
|
if p.System != "你是助手" {
|
|
t.Errorf("System = %q", p.System)
|
|
}
|
|
if p.Query != "你好\n再见" {
|
|
t.Errorf("Query = %q", p.Query)
|
|
}
|
|
if len(p.Tools) != 1 || p.Tools[0] != "wiki_search" {
|
|
t.Errorf("Tools = %v", p.Tools)
|
|
}
|
|
}
|
|
|
|
func TestCompile_NoInputFallback(t *testing.T) {
|
|
// 有节点但无 input/agent 输入 → System 兜底默认,Query 兜底 "你好"。
|
|
raw := graphJSON(t, Flow{Nodes: []Node{{ID: "t", Kind: "tool", Config: map[string]any{"tool": "wiki_search"}}}})
|
|
p := Compile(raw)
|
|
if p.System != defaultSystem {
|
|
t.Errorf("System 应兜底, got %q", p.System)
|
|
}
|
|
if p.Query != "你好" {
|
|
t.Errorf("无输入 Query 应兜底为 你好, got %q", p.Query)
|
|
}
|
|
}
|
|
|
|
func TestCompile_EmptyGraphUsesRaw(t *testing.T) {
|
|
// 无节点(无法解析为结构化图)→ 兼容旧行为:原文当输入。
|
|
p := Compile(json.RawMessage(`帮我写个东西`))
|
|
if p.Query != "帮我写个东西" {
|
|
t.Errorf("空图应把原文当 Query, got %q", p.Query)
|
|
}
|
|
}
|
|
|
|
func TestToolBinding(t *testing.T) {
|
|
tool, args := ToolBinding(Node{Kind: "tool", Config: map[string]any{"tool": "x", "args": `{"q":"v"}`}})
|
|
if tool != "x" || args["q"] != "v" {
|
|
t.Errorf("tool 节点 = %q %v", tool, args)
|
|
}
|
|
rtool, rargs := ToolBinding(Node{Kind: "retriever", Config: map[string]any{"kb": "docs"}})
|
|
if rtool != "wiki_search" || rargs["kb"] != "docs" {
|
|
t.Errorf("retriever 节点 = %q %v", rtool, rargs)
|
|
}
|
|
if ntool, _ := ToolBinding(Node{Kind: "agent"}); ntool != "" {
|
|
t.Errorf("非工具节点应返回空工具, got %q", ntool)
|
|
}
|
|
}
|
|
|
|
func TestParse(t *testing.T) {
|
|
if _, err := Parse(json.RawMessage(`{"nodes":[{"id":"a"}],"edges":[]}`)); err != nil {
|
|
t.Errorf("合法 DSL 不应报错: %v", err)
|
|
}
|
|
if _, err := Parse(json.RawMessage(`{bad json`)); err == nil {
|
|
t.Error("非法 JSON 应报错")
|
|
}
|
|
}
|