Files
Cyrene_v2/internal/runtime/loop/flow_test.go
T
AskaEth 91e8d7a029 docs(runtime): internal/runtime/loop 44 个文件注释改中文(逐行 1:1,代码零字节改动)
主人 2026-10-03 拍板「注释一律中文」;本模块按长期分批协议(一个模块一次派工)执行。
注释行数 1329 → 1329 逐行 1:1(含中文 33 → 1304);行尾注释 5 条一并译。
校验:机械校验(非注释改动行)= 空;独立 Go AST 程序(剥注释复渲)证明 44/44 文件代码全等;
全树 go test ./... 58 包 ok / 0 FAIL;交叉编译绿。
gofmt:capacity_loop_test.go / flow.go 改动前即未格式化,按铁律保留原样未动。
2026-10-03 22:28:21 +08:00

119 lines
3.1 KiB
Go

package loop
import (
"context"
"sync"
"testing"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace"
)
// TestRun_PublishesFlowGraph_Trunk 给 FlowPublisher 接上捕获式 publish
// 函数与实时 tracer,跑一轮工具,并断言 ws.graph_update
// 以 input→llm→tool→output 主干发布(design/19 §1 P1)。
func TestRun_PublishesFlowGraph_Trunk(t *testing.T) {
var mu sync.Mutex
var seen []*trace.FlowGraph
flow := NewFlowPublisher(func(subject string, payload any) {
if subject != graphUpdateSubject {
return
}
if g, ok := payload.(*trace.FlowGraph); ok {
mu.Lock()
seen = append(seen, g)
mu.Unlock()
}
}, func(string) bool { return true })
call := `{"tool_calls":[{"name":"echo","args":{"q":"hi"}}]}`
ad := &mockAdapter{chat: []mgResp{
{resp: model.Response{Content: call}},
{resp: model.Response{Content: "Echoed!"}},
}}
td := dispatcherWith(t, &mockTool{name: "echo", result: "hi"})
a := newTestAgent(t, ad, td)
l := New(Options{Agent: a, MaxRounds: 8, Flow: flow, Tracer: trace.New(nil)})
res, err := l.Run(context.Background(), "web", []model.Message{model.TextMessage(model.RoleUser, "echo this")})
if err != nil {
t.Fatalf("Run: %v", err)
}
if res.Text != "Echoed!" {
t.Fatalf("Text = %q, want Echoed!", res.Text)
}
mu.Lock()
defer mu.Unlock()
if len(seen) == 0 {
t.Fatal("no ws.graph_update published")
}
last := seen[len(seen)-1]
if last.SessionID != "web" {
t.Fatalf("SessionID = %q, want web", last.SessionID)
}
kinds := make(map[string]string)
for _, n := range last.Nodes {
kinds[n.ID] = n.Kind
}
nodeKind := map[string]bool{"input": false, "llm": false, "tool": false, "output": false}
inputID, outputID := "", ""
for _, n := range last.Nodes {
if n.Kind == "input" {
inputID = n.ID
}
if n.Kind == "output" {
outputID = n.ID
}
if _, ok := nodeKind[n.Kind]; ok {
nodeKind[n.Kind] = true
}
}
for _, want := range []string{"input", "llm", "tool", "output"} {
if !nodeKind[want] {
t.Fatalf("trunk incomplete: no %q node; nodes=%+v", want, last.Nodes)
}
}
// 从 input 节点开始走链;它必须经 llm/tool 到达 output。
chain := []string{}
cur := inputID
for cur != "" {
chain = append(chain, kinds[cur])
if cur == outputID {
break
}
cur = nextNode(last, cur)
}
hasLLM, hasTool := false, false
for _, k := range chain {
if k == "llm" {
hasLLM = true
}
if k == "tool" {
hasTool = true
}
}
if chain[len(chain)-1] != "output" || !hasLLM || !hasTool {
t.Fatalf("trunk chain broken: %v", chain)
}
// 最终帧应显示该工具已完成(done),而不是仍在运行。
for _, n := range last.Nodes {
if n.Kind == "tool" && n.Status != trace.FlowStatusDone {
t.Fatalf("tool status = %q, want done", n.Status)
}
}
}
// nextNode 返回 cur 指向的节点;cur 没有出边时返回 ""(终端的
// output 节点)。线性链至多有一个后继。
func nextNode(g *trace.FlowGraph, cur string) string {
for _, e := range g.Edges {
if e.From == cur {
return e.To
}
}
return ""
}