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 writes a canonical intent record memory (the closed-loop write-back // format produced by capacity.RecordIntent) recording that "搞一下这个部署" is a task // for 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 proves the L4 feed: with a // retrieved user habit recording that the phrase is a task, an otherwise-undecided // turn wakes in the owner-full env, and the model is advertised the awake tool set. 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 read the learned habit → awake → web-search is advertised (awake set). 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 proves the L4 isolation red // line: in a group/stranger context the loop passes no recall, so the decider falls // back to L1/L2 and the owner's habit memory is never used to wake the intent. 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) // Restricted env → L4 silent → undecided turn stays quiet → only quiet tools, no // web-search, no owner habit read. if !hasAll(set, "terminal", "file") { t.Fatalf("restricted tools = %v, want quiet set terminal+file", toolNames(ad.streamReqs[0].Tools)) } if set["web-search"] { t.Fatalf("restricted tools = %v, must NOT wake (owner habit not read)", toolNames(ad.streamReqs[0].Tools)) } }