主人 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 改动前即未格式化,按铁律保留原样未动。
161 lines
5.1 KiB
Go
161 lines
5.1 KiB
Go
package loop
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/agent"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
|
)
|
|
|
|
// TestRunCallsOnTurnEndOnce 钉住 turn 边界:一个落定的 turn 恰好
|
|
// 上报一次 —— 携带该 turn 自己的用户文本、循环绑定的角色
|
|
// 以及答案 —— 钩子返回的标签记录在 Result.Mood 上,
|
|
// 使持久化层能标记 assistant 消息。
|
|
func TestRunCallsOnTurnEndOnce(t *testing.T) {
|
|
t.Parallel()
|
|
ad := &mockAdapter{chat: []mgResp{{resp: model.Response{Content: "在的呀♪"}}}}
|
|
a := newTestAgent(t, ad, nil)
|
|
|
|
var ends []TurnEnd
|
|
l := New(Options{
|
|
Agent: a,
|
|
PersonaID: "cyrene",
|
|
OnTurnEnd: func(end TurnEnd) string {
|
|
ends = append(ends, end)
|
|
return "happy"
|
|
},
|
|
})
|
|
|
|
res, err := l.Run(context.Background(), "web", []model.Message{model.TextMessage(model.RoleUser, "今天谢谢你")})
|
|
if err != nil {
|
|
t.Fatalf("Run: %v", err)
|
|
}
|
|
if len(ends) != 1 {
|
|
t.Fatalf("OnTurnEnd calls = %d, want 1", len(ends))
|
|
}
|
|
got := ends[0]
|
|
if got.SessionID != "web" || got.PersonaID != "cyrene" {
|
|
t.Fatalf("TurnEnd = %+v, want session web / persona cyrene", got)
|
|
}
|
|
if got.UserText != "今天谢谢你" {
|
|
t.Fatalf("TurnEnd.UserText = %q, want the user turn text", got.UserText)
|
|
}
|
|
if got.AssistantText != "在的呀♪" {
|
|
t.Fatalf("TurnEnd.AssistantText = %q, want the final answer", got.AssistantText)
|
|
}
|
|
if got.StopReason != agent.StopReasonStop || got.Steps != 1 {
|
|
t.Fatalf("TurnEnd stop/steps = %q/%d, want stop/1", got.StopReason, got.Steps)
|
|
}
|
|
if !got.Env.MemWritable {
|
|
t.Fatalf("TurnEnd.Env = %+v, want the owner lane by default", got.Env)
|
|
}
|
|
if res.Mood != "happy" {
|
|
t.Fatalf("Result.Mood = %q, want the hook's label", res.Mood)
|
|
}
|
|
}
|
|
|
|
// TestRunPassesRestrictedEnvToTurnEnd 钉住会话的隔离
|
|
// 环境到达钩子,使群聊/陌生人 turn 能被保持只读。
|
|
func TestRunPassesRestrictedEnvToTurnEnd(t *testing.T) {
|
|
t.Parallel()
|
|
ad := &mockAdapter{chat: []mgResp{{resp: model.Response{Content: "好的"}}}}
|
|
a := newTestAgent(t, ad, nil)
|
|
|
|
var got TurnEnd
|
|
l := New(Options{
|
|
Agent: a,
|
|
OnTurnEnd: func(end TurnEnd) string {
|
|
got = end
|
|
return ""
|
|
},
|
|
})
|
|
env := SessionEnv{ActorEnv: "group_member", MemWritable: false}
|
|
ctx := WithSessionEnv(context.Background(), env)
|
|
if _, err := l.Run(ctx, "sess-group", []model.Message{model.TextMessage(model.RoleUser, "你好")}); err != nil {
|
|
t.Fatalf("Run: %v", err)
|
|
}
|
|
if !got.Env.Restricted() || got.Env != env {
|
|
t.Fatalf("TurnEnd.Env = %+v, want the restricted env", got.Env)
|
|
}
|
|
}
|
|
|
|
// TestRunTurnEndOnceForMultiStepTurn 钉住「每 turn 一次,而非每步
|
|
// 一次」:一个工具循环 turn(模型 → 工具调用 → 最终答案)只上报其边界
|
|
// 一次,且上报的用户文本仍是该 turn 的用户消息,而不是
|
|
// 回灌的工具结果。
|
|
func TestRunTurnEndOnceForMultiStepTurn(t *testing.T) {
|
|
t.Parallel()
|
|
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)
|
|
|
|
var ends []TurnEnd
|
|
l := New(Options{
|
|
Agent: a,
|
|
PersonaID: "cyrene",
|
|
OnTurnEnd: func(end TurnEnd) string {
|
|
ends = append(ends, end)
|
|
return "calm"
|
|
},
|
|
})
|
|
|
|
res, err := l.Run(context.Background(), "web", []model.Message{model.TextMessage(model.RoleUser, "echo this")})
|
|
if err != nil {
|
|
t.Fatalf("Run: %v", err)
|
|
}
|
|
if len(ends) != 1 {
|
|
t.Fatalf("OnTurnEnd calls = %d, want 1 for a 2-step turn", len(ends))
|
|
}
|
|
if ends[0].UserText != "echo this" {
|
|
t.Fatalf("TurnEnd.UserText = %q, want the turn's user text", ends[0].UserText)
|
|
}
|
|
if ends[0].AssistantText != "Echoed!" || ends[0].Steps != 2 {
|
|
t.Fatalf("TurnEnd = %+v, want the final answer at 2 steps", ends[0])
|
|
}
|
|
if res.Mood != "calm" {
|
|
t.Fatalf("Result.Mood = %q, want calm", res.Mood)
|
|
}
|
|
}
|
|
|
|
// TestRunNoTurnEndOnError 钉住失败的 turn 不上报:什么都没
|
|
// 落定,因此人格状态不得推进。
|
|
func TestRunNoTurnEndOnError(t *testing.T) {
|
|
t.Parallel()
|
|
ad := &mockAdapter{chat: []mgResp{{err: errors.New("model down")}}}
|
|
a := newTestAgent(t, ad, nil)
|
|
|
|
calls := 0
|
|
l := New(Options{
|
|
Agent: a,
|
|
OnTurnEnd: func(TurnEnd) string { calls++; return "happy" },
|
|
})
|
|
if _, err := l.Run(context.Background(), "web", []model.Message{model.TextMessage(model.RoleUser, "我爱你")}); err == nil {
|
|
t.Fatal("Run: want an error")
|
|
}
|
|
if calls != 0 {
|
|
t.Fatalf("OnTurnEnd calls = %d, want 0 on a failed turn", calls)
|
|
}
|
|
}
|
|
|
|
// TestRunWithoutTurnEndHook 钉住 nil 容忍:未接线的钩子让
|
|
// 情绪保持为空,Run 的行为与从前完全一致。
|
|
func TestRunWithoutTurnEndHook(t *testing.T) {
|
|
t.Parallel()
|
|
ad := &mockAdapter{chat: []mgResp{{resp: model.Response{Content: "hi"}}}}
|
|
l := New(Options{Agent: newTestAgent(t, ad, nil)})
|
|
|
|
res, err := l.Run(context.Background(), "web", []model.Message{model.TextMessage(model.RoleUser, "hi")})
|
|
if err != nil {
|
|
t.Fatalf("Run: %v", err)
|
|
}
|
|
if res.Mood != "" {
|
|
t.Fatalf("Result.Mood = %q, want empty without a hook", res.Mood)
|
|
}
|
|
}
|