package loop import ( "context" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" ) // fixedExtractor(定义见 flow_spans_test.go)返回一组固定条目,用于驱动持久化 // 路径的边界断言。 // TestPersistMemories_ScopedWriteRule_RejectsForeignPersona 钉住审计 2026-10-05 // §1.3「Store 作用域写规则未接线」:提取持久化经 Store 的作用域写规则 // (AddScoped)落库。一条归属**别的角色**的角色作用域条目必须被拒(ErrReadOnly), // 绝不被冒充成本回合角色的日常记忆。 func TestPersistMemories_ScopedWriteRule_RejectsForeignPersona(t *testing.T) { t.Parallel() store := newTestMemoryStore(t) l := New(Options{ Memory: store, Extractor: fixedExtractor{items: []memory.MemoryItem{ {PersonaID: "persona-b", Scope: memory.ScopePersona, Kind: memory.KindRelationship, Content: "b 的角色日常"}, }}, PersonaID: "persona-a", }) ctx := WithSessionEnv(context.Background(), OwnerEnv()) l.extractMemory(ctx, []model.Message{model.TextMessage(model.RoleUser, "记住 b 的日常")}, 1) items, err := store.List(context.Background()) if err != nil { t.Fatalf("List: %v", err) } if len(items) != 0 { t.Fatalf("跨角色的角色作用域写被接受:%d 条(作用域写规则未接线)", len(items)) } } // TestPersistMemories_PersonaAgnosticExtractor_StampedByStore 证明接线后正常路径 // 不变:与角色无关的提取器(PersonaID 为空)产出的条目仍由 Store 用本回合角色 // (actorID)补齐归属,照常落库。 func TestPersistMemories_PersonaAgnosticExtractor_StampedByStore(t *testing.T) { t.Parallel() store := newTestMemoryStore(t) l := New(Options{ Memory: store, Extractor: fixedExtractor{items: []memory.MemoryItem{ {Scope: memory.ScopePersona, Kind: memory.KindRelationship, Content: "主人爱喝冰美式"}, }}, PersonaID: "persona-a", }) ctx := WithSessionEnv(context.Background(), OwnerEnv()) l.extractMemory(ctx, []model.Message{model.TextMessage(model.RoleUser, "记住 我爱喝冰美式")}, 1) items, err := store.ListByPersona(context.Background(), "persona-a") if err != nil { t.Fatalf("ListByPersona: %v", err) } if len(items) != 1 || items[0].PersonaID != "persona-a" { t.Fatalf("角色无关提取器未被 Store 盖章:%+v", items) } }