- per-role 注入补齐:Options.PersonaAwakening(personaID) → Loop.personaAwakening → l.awakeningFor(ctx) → capacity.Input.Awakening(toolSchemas 是全仓唯一构造该 Input 处) · 按 turn 解析(不绑 Loop 构造期,避免并发会话数据竞争),与既有 personaFor/knobsFor 同构 · 未知角色/空角色/档案无该块 ⇒ 零值 ⇒ 回落 Config.Awakening(不声明者行为与今天构造性一致) - **数据丢失修复**:persona PUT 是整块替换,此前只保留 thinking ⇒ 保存一次会同时抹掉 awakening / visual(头像·背景) / triggerWords(群聊提醒词) / emotion(情绪机参数) / affection(好感度曲线)。 新增 applyAwakeningEdit + applyPersonaReadOnlyBlocks 逐一保留(cyrene.yaml 五块全写,属必现故障) - 文件守 500 行:主人档案段原样搬移到 admin_owner_profile.go(零逻辑变更) - 复核:build/vet 绿 + 全树 `go test ./... -count=1` 0 FAIL;端到端=真 YAML→loader→buildCapacity→真 loop 流式请求→断言工具面(tasky 声明 force_state 唤醒 → 含 web-search;cyrene 不声明 → 与无解析器对照逐字相同); **8 条变异反证**先红后还原(含逐块删除各自的保留逻辑) - 残留缺口(登记):wire_runtime.go:254 那一行本身无用例覆盖;loader 非 strict(未知 YAML 键静默忽略, 需主人裁决是否开 KnownFields);POST /admin/personas 仍写不出这五个块
86 lines
3.7 KiB
Go
86 lines
3.7 KiB
Go
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)
|
||
}
|
||
}
|