Files
Cyrene_v2/internal/runtime/loop/turn_persona_test.go
T
AskaEth 18381f65fd
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
fix(gateway,loop,tools): 失败回合必落可见回复 + knowledge 角色可信
根因(agent-191,均给出文件:行):
1) knowledge 的 persona 在 schema 里是 optional,模型省略时工具把空串透给
   memory.Retriever → 硬校验 ErrMissingPersona → 模型只能重复调用同一堵墙。
   修:新增 tool/turncontext.go(loop 播种本回合角色),knowledge 依次取
   「参数 → 本回合角色 → 可消化拒绝」,拒绝文案明确「重试不会成功」。
2) persistAssistantReply 对空文本直接 return,而三条正常收尾出口(空回复 /
   轮次用尽 / 回合内部报错)都会给空文本 ⇒ 历史只剩 user 消息、客户端静默。
   修:新增 session_turn_reply.go 的 persistTurnReply(唯一落库入口 + 按原因
   的中文诚实说明);中断 / 陈旧代次 / 静默内部流程(backthink、子代理调度)
   仍不落库,真实文本行为逐字未改。

测试:knowledge_test 改断言 + turn_persona_test(新)+ session_turn_reply_test(新,7 条)
变异:去掉 loop 角色发布 ⇒ loop×2 + gateway「工具执行次数 = 0, want 2」红
2026-10-04 22:14:42 +08:00

81 lines
3.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package loop
import (
"context"
"testing"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/tool"
)
// 本文件锁定切片 KB1 的 loop 侧一半:**本回合角色必须发布给工具**。
//
// 复现的形状:模型省略 knowledge 工具的可选参数 persona,工具此前把空角色透给
// 记忆存储,后者以 ErrMissingPersona 硬失败 —— 模型既不知道该填哪个角色 id,
// 参数说明还写着 optional,于是连撞同一堵墙。修复是循环在 Run 顶部把 personaFor
// 解析出的角色经 tool.WithTurnPersona 发布到本轮 ctx 上,工具据此查到「以谁的
// 名义」检索。
//
// 断言走真路径:真 Run → 真 tool.Dispatcher.Invoke → 真工具 Execute 读 ctx。
// 工具看到的值必须与提示词/记忆同源(personaFor):会话环境有显式角色时取它,
// 否则取循环配置的默认角色。
// personaProbeTool 记录 Execute 时在 ctx 上读到的本回合角色。
type personaProbeTool struct {
got []string
}
func (p *personaProbeTool) Name() string { return "persona_probe" }
func (p *personaProbeTool) Description() string { return "记录本回合角色" }
func (p *personaProbeTool) Parameters() (map[string]any, error) {
return map[string]any{"type": "object"}, nil
}
func (p *personaProbeTool) Execute(ctx context.Context, _ any) (any, error) {
p.got = append(p.got, tool.TurnPersonaFrom(ctx))
return "ok", nil
}
// runPersonaProbe 跑一个「一次工具调用 + 收尾」的回合,返回工具在 ctx 上读到的角色。
func runPersonaProbe(t *testing.T, ctx context.Context, configured string) string {
t.Helper()
call := `{"tool_calls":[{"name":"persona_probe","args":{}}]}`
ad := &mockAdapter{chat: []mgResp{
{resp: model.Response{Content: call}},
{resp: model.Response{Content: "收尾"}},
}}
probe := &personaProbeTool{}
td := dispatcherWith(t, probe)
a := newTestAgent(t, ad, td)
l := New(Options{Agent: a, MaxRounds: 8, PersonaID: configured})
if _, err := l.Run(ctx, "sess", []model.Message{model.TextMessage(model.RoleUser, "查一下")}); err != nil {
t.Fatalf("Run: %v", err)
}
if len(probe.got) != 1 {
t.Fatalf("工具执行次数 = %d, want 1", len(probe.got))
}
return probe.got[0]
}
// TestRun_PublishesTurnPersonaToTools 断言:会话环境给了显式角色时,工具看到的是
// 那个角色(而不是循环配置的默认角色)。
func TestRun_PublishesTurnPersonaToTools(t *testing.T) {
t.Parallel()
env := OwnerEnv()
env.PersonaID = "persona-x"
ctx := WithSessionEnv(context.Background(), env)
if got := runPersonaProbe(t, ctx, "default-persona"); got != "persona-x" {
t.Fatalf("工具读到的本回合角色 = %q, want persona-x(应与提示词/记忆同源)", got)
}
}
// TestRun_PublishesConfiguredPersonaWhenEnvEmpty 断言:会话环境没给角色
// (channelbridge / backthink 等既有调用方)时,工具看到的是循环配置的默认角色 ——
// 正是「空角色不许透给下游」的兜底。
func TestRun_PublishesConfiguredPersonaWhenEnvEmpty(t *testing.T) {
t.Parallel()
if got := runPersonaProbe(t, context.Background(), "default-persona"); got != "default-persona" {
t.Fatalf("工具读到的本回合角色 = %q, want default-persona", got)
}
}