主人 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 改动前即未格式化,按铁律保留原样未动。
119 lines
3.1 KiB
Go
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 ""
|
|
}
|