package loop import ( "context" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/restrict" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/capacity" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" ) // seedL4Habit 写入一条规范的 intent record 记忆(由 // capacity.RecordIntent 产出的闭环回写格式),记录 "搞一下这个部署" 是 // persona-a 的任务。 func seedL4Habit(t *testing.T, store *memory.Store) { t.Helper() ctx := context.Background() if _, err := store.Add(ctx, memory.MemoryItem{ PersonaID: "persona-a", Scope: memory.ScopeUser, Kind: memory.KindRelationship, Content: "intent:isTask=true;taskKind=ops;confidence=0.85;text=搞一下这个部署", }); err != nil { t.Fatalf("seed L4 habit memory: %v", err) } } // TestLoop_Capacity_L4_WakesOnLearnedHabit_OwnerEnv 证明 L4 输入:在 // 检索到记录该短语是任务的用户习惯后,一个原本未决的 // turn 在 owner 全权限环境中唤醒,且模型被广告 awake 工具集。 func TestLoop_Capacity_L4_WakesOnLearnedHabit_OwnerEnv(t *testing.T) { t.Parallel() store := newTestMemoryStore(t) seedL4Habit(t, store) 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}) l := New(Options{ Agent: a, MaxRounds: 8, OnStream: func(model.StreamEvent) {}, Capacity: dec, Retriever: memory.NewRetriever(store), PersonaID: "persona-a", }) ctx := WithSessionEnv(context.Background(), OwnerEnv()) if _, err := l.Run(ctx, "sess", []model.Message{model.TextMessage(model.RoleUser, "搞一下那个部署")}); err != nil { t.Fatalf("Run: %v", err) } if len(ad.streamReqs) == 0 { t.Fatal("no streaming request captured") } set := toolSet(ad.streamReqs[0].Tools) // L4 读到了学到的习惯 → 唤醒 → 广告出 web-search(awake 集)。 if !hasAll(set, "terminal", "file", "web-search") { t.Fatalf("owner L4 tools = %v, want awake set incl web-search", toolNames(ad.streamReqs[0].Tools)) } } // TestLoop_Capacity_L4_RestrictedEnv_NoOwnerHabitRead 证明 L4 隔离红线: // 在群聊/陌生人上下文中循环不传任何召回,因此判定器回退到 // L1/L2,主人的习惯记忆绝不被用来唤醒意图。 func TestLoop_Capacity_L4_RestrictedEnv_NoOwnerHabitRead(t *testing.T) { t.Parallel() store := newTestMemoryStore(t) seedL4Habit(t, store) 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}) l := New(Options{ Agent: a, MaxRounds: 8, OnStream: func(model.StreamEvent) {}, Capacity: dec, Retriever: memory.NewRetriever(store), PersonaID: "persona-a", }) ctx := WithSessionEnv(context.Background(), SessionEnv{ActorEnv: restrict.ActorGroupMember, MemWritable: false}) if _, err := l.Run(ctx, "sess", []model.Message{model.TextMessage(model.RoleUser, "搞一下那个部署")}); err != nil { t.Fatalf("Run: %v", err) } if len(ad.streamReqs) == 0 { t.Fatal("no streaming request captured") } set := toolSet(ad.streamReqs[0].Tools) // 受限环境 → L4 静默 → 该 turn 保持未决,且 design/11 §3 // 安全门隐藏所有未标记 AllowInGroup 的工具(terminal/file // 未标记)。因此模型什么也看不到:web-search 缺席(主人的习惯 // 未被读取),连 quiet 集工具也对群聊会话关闭。 if len(set) != 0 { t.Fatalf("restricted tools = %v, want none (security gate closes unmarked tools)", toolNames(ad.streamReqs[0].Tools)) } }