package loop import ( "context" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/capacity" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" ) // 本文件锁定裁决 14.4.3 后半在**循环**这一层的契约:唤醒档位按该 turn 的角色 // 解析(Options.PersonaAwakening),而不是绑定在 Loop 构造期;未注入解析器或 // 角色未知时一律零覆盖、回落到判定器的全局兜底 —— 即接线前的行为。 // lastStreamTools 返回最新一次流式请求广告给模型的工具名集合。 func lastStreamTools(t *testing.T, ad *mockAdapter) map[string]bool { t.Helper() if len(ad.streamReqs) == 0 { t.Fatal("adapter recorded no streaming request") } return toolSet(ad.streamReqs[len(ad.streamReqs)-1].Tools) } // TestLoop_CapacityAwakeningFollowsTheTurnRole 断言同一句「意图各层都拿不准」的 // 输入下("这个"),声明了档位的角色唤醒、没声明的角色静息,而**未知角色**与 // **未注入解析器**两种退化都保持判定器的既有行为(静息,且不广告 web-search)。 func TestLoop_CapacityAwakeningFollowsTheTurnRole(t *testing.T) { t.Parallel() td := capacityToolSet(t) ad := &mockAdapter{stream: []mgResp{{resp: model.Response{Content: "陪你"}}}} a := newTestAgent(t, ad, td) dec := capacity.New(capacity.Config{Enabled: true, MinConfidence: 0.6}) awakened := map[string]capacity.Awakening{"Y": {ForceState: capacity.StateAwake}} newLoop := func(resolve func(string) capacity.Awakening) *Loop { return New(Options{ Agent: a, PersonaID: "X", Capacity: dec, OnStream: func(model.StreamEvent) {}, PersonaAwakening: resolve, }) } const ambiguous = "这个" resolved := newLoop(func(id string) capacity.Awakening { return awakened[id] }) runPersonaTurn(t, resolved, roleEnv("X"), "web:x1", ambiguous) if got := lastStreamTools(t, ad); got["web-search"] { t.Fatalf("无档位的角色 X 的工具面 = %v, want 静息(不得广告 web-search)", got) } runPersonaTurn(t, resolved, roleEnv("Y"), "web:y1", ambiguous) if got := lastStreamTools(t, ad); !got["web-search"] { t.Fatalf("声明 force_state=awake 的角色 Y 的工具面 = %v, want 唤醒", got) } // 未知角色(解析器不认识它)不得让该 turn 失败,也不得泄漏别人的档位。 runPersonaTurn(t, resolved, roleEnv("nobody"), "web:z1", ambiguous) if got := lastStreamTools(t, ad); got["web-search"] { t.Fatalf("未知角色的工具面 = %v, want 静息(按无覆盖处理)", got) } // 未注入解析器 = 接线前的形状:所有角色都走全局兜底。 before := newLoop(nil) runPersonaTurn(t, before, roleEnv("Y"), "web:y2", ambiguous) if got := lastStreamTools(t, ad); got["web-search"] { t.Fatalf("未注入解析器时角色 Y 的工具面 = %v, want 与接线前一致(静息)", got) } } // TestLoop_AwakeningForDegradesQuietly 覆盖解析器的两个降级口:未注入解析器、 // 以及该 turn 没有角色(SessionEnv.PersonaID 与 Options.PersonaID 都为空)时, // awakeningFor 一律返回零覆盖而不是 panic 或猜一个角色。 func TestLoop_AwakeningForDegradesQuietly(t *testing.T) { t.Parallel() plain := WithSessionEnv(context.Background(), OwnerEnv()) if got := New(Options{}).awakeningFor(plain); !got.IsZero() { t.Fatalf("未注入解析器的循环返回了 %+v, want 零覆盖", got) } l := New(Options{PersonaAwakening: func(string) capacity.Awakening { return capacity.Awakening{ForceState: capacity.StateAwake} }}) if got := l.awakeningFor(plain); !got.IsZero() { t.Fatalf("没有角色的 turn 返回了 %+v, want 零覆盖(不猜角色)", got) } }