package agent import ( "context" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" ) // 本文件钉住非流式路径(agent.RunStep)的 persona 钩子(裁决 03.4.1):它必须与 // 流式主路径(loop.buildRequest 的 RouteRequestForPersona)同源。角色由适配器视图 // 携带(model.PersonaScoped),因为它不经 ctx 也能到达这里,且一角色一 agent 的装配 // 方式(design/22 §4.2 方案 A)本来就把它定在装配处。 // personaScopedAdapter 是带角色的 mockAdapter 视图,模拟 model.PersonaStack: // RunStep 应据此把角色写进请求,并按角色路由模型名。 type personaScopedAdapter struct { mockAdapter persona string } // ScopedPersona 实现 model.PersonaScoped。 func (p *personaScopedAdapter) ScopedPersona() string { return p.persona } // TestRunStep_PersonaScopedAdapterRoutesByPersona 断言:带角色的适配器视图让非流式 // 路径按 persona 路由(而不是按消息 role),并把角色带到请求上,供模型层选 // persona 的协议链(models.json personaProviders)。 func TestRunStep_PersonaScopedAdapterRoutesByPersona(t *testing.T) { t.Parallel() router := model.NewRouter("default", model.WithRoleModel(model.RoleUser, "user-model"), model.WithPersonaModel("cyrene", "persona-model"), ) ad := &personaScopedAdapter{ mockAdapter: mockAdapter{resp: model.Response{Model: "persona-model", Content: "ok"}}, persona: "cyrene", } a := newTestAgent(t, ad, nil, WithModel(ad, router)) if _, err := a.RunStep(context.Background(), "s", []model.Message{model.TextMessage(model.RoleUser, "hi")}); err != nil { t.Fatalf("RunStep: %v", err) } if ad.gotReq.PersonaID != "cyrene" { t.Fatalf("req.PersonaID = %q, want cyrene", ad.gotReq.PersonaID) } if ad.gotReq.Model != "persona-model" { t.Fatalf("req.Model = %q, want persona-model (persona binding wins over the message role)", ad.gotReq.Model) } } // TestRunStep_PersonaStampedWithoutRouter asserts the persona travels with the // adapter view even when no router is injected — the carrier must not depend on // the routing decision, or an adapter-only wiring would silently lose it. func TestRunStep_PersonaStampedWithoutRouter(t *testing.T) { t.Parallel() ad := &personaScopedAdapter{ mockAdapter: mockAdapter{resp: model.Response{Content: "ok"}}, persona: "cyrene", } a := newTestAgent(t, ad, nil, WithModel(ad, nil)) if _, err := a.RunStep(context.Background(), "s", []model.Message{model.TextMessage(model.RoleUser, "hi")}); err != nil { t.Fatalf("RunStep: %v", err) } if ad.gotReq.PersonaID != "cyrene" { t.Fatalf("req.PersonaID = %q, want cyrene", ad.gotReq.PersonaID) } } // TestRunStep_UnknownPersonaKeepsLegacyRouting 断言未绑定角色的 agent 行为不变: // 没有角色就继续走原来的 RouteRequest,其中包含「有图像 → 换视觉模型」这条老规则 // (RouteRequestForPersona 故意不含它,图像路由已归 vision.Router)。这条断言防止 // 把两代路由语义混在一起。 func TestRunStep_UnknownPersonaKeepsLegacyRouting(t *testing.T) { t.Parallel() router := model.NewRouter("default", model.WithRoleModel(model.RoleUser, "user-model"), model.WithVisionModel("vision-model"), ) ad := &mockAdapter{resp: model.Response{Model: "vision-model", Content: "ok"}} a := newTestAgent(t, ad, nil, WithModel(ad, router)) msg := model.ImageMessage(model.RoleUser, "what is this", "data:image/png;base64,abc") if _, err := a.RunStep(context.Background(), "s", []model.Message{msg}); err != nil { t.Fatalf("RunStep: %v", err) } if ad.gotReq.PersonaID != "" { t.Fatalf("req.PersonaID = %q, want empty for an unbound agent", ad.gotReq.PersonaID) } if ad.gotReq.Model != "vision-model" { t.Fatalf("req.Model = %q, want vision-model (the legacy image routing must survive)", ad.gotReq.Model) } }