Files
Cyrene_v2/internal/runtime/loop/memory_gating_test.go
T
AskaEth 91e8d7a029 docs(runtime): internal/runtime/loop 44 个文件注释改中文(逐行 1:1,代码零字节改动)
主人 2026-10-03 拍板「注释一律中文」;本模块按长期分批协议(一个模块一次派工)执行。
注释行数 1329 → 1329 逐行 1:1(含中文 33 → 1304);行尾注释 5 条一并译。
校验:机械校验(非注释改动行)= 空;独立 Go AST 程序(剥注释复渲)证明 44/44 文件代码全等;
全树 go test ./... 58 包 ok / 0 FAIL;交叉编译绿。
gofmt:capacity_loop_test.go / flow.go 改动前即未格式化,按铁律保留原样未动。
2026-10-03 22:28:21 +08:00

154 lines
6.2 KiB
Go

package loop
import (
"context"
"strings"
"testing"
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/restrict"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
)
// seedMemories 为每个作用域各填一条记忆到 store,使测试能断言
// 记忆门的作用域过滤。
func seedMemories(t *testing.T, store *memory.Store) {
t.Helper()
ctx := context.Background()
seeds := []memory.MemoryItem{
{PersonaID: "persona-a", Content: "用户习惯:喜欢冰美式", Scope: memory.ScopeUser, Kind: memory.KindRelationship},
{PersonaID: "persona-a", Content: "角色日常:周末喜欢爬山", Scope: memory.ScopePersona, Kind: memory.KindRelationship},
{PersonaID: "persona-a", Content: "项目记忆:Cyrene 用 Go 编写", Scope: memory.ScopeProject, Kind: memory.KindWorldEvent},
}
for _, it := range seeds {
if _, err := store.Add(ctx, it); err != nil {
t.Fatalf("seed memory: %v", err)
}
}
}
// TestInjectMemory_OwnerEnv_InjectsAllScopes 证明 owner 全权限环境保持
// 既有的完整召回:用户、角色与项目记忆都被注入。
func TestInjectMemory_OwnerEnv_InjectsAllScopes(t *testing.T) {
t.Parallel()
store := newTestMemoryStore(t)
seedMemories(t, store)
l := New(Options{Retriever: memory.NewRetriever(store), PersonaID: "persona-a"})
msgs := []model.Message{model.TextMessage(model.RoleUser, "go")}
ctx := WithSessionEnv(context.Background(), OwnerEnv())
view := l.assembleContext(ctx, msgs, 1)
if len(view) != 2 {
t.Fatalf("owner view = %d messages, want 2 (memory injected)", len(view))
}
block := messageText(view[0])
for _, want := range []string{"喜欢冰美式", "喜欢爬山", "Cyrene 用 Go 编写", "相关记忆"} {
if !strings.Contains(block, want) {
t.Fatalf("owner block missing %q:\n%s", want, block)
}
}
}
// TestInjectMemory_RestrictedEnv_OnlyProject 证明群聊/陌生人环境是
// 只读隔离:只注入非主人私有的项目记忆,
// 主人的用户/角色记忆被排除,且该块被标记为 群聊隔离。
func TestInjectMemory_RestrictedEnv_OnlyProject(t *testing.T) {
t.Parallel()
store := newTestMemoryStore(t)
seedMemories(t, store)
l := New(Options{Retriever: memory.NewRetriever(store), PersonaID: "persona-a"})
msgs := []model.Message{model.TextMessage(model.RoleUser, "go")}
ctx := WithSessionEnv(context.Background(), SessionEnv{ActorEnv: restrict.ActorGroupMember, MemWritable: false})
view := l.assembleContext(ctx, msgs, 1)
if len(view) != 2 {
t.Fatalf("restricted view = %d messages, want 2 (project memory injected)", len(view))
}
block := messageText(view[0])
if !strings.Contains(block, "群聊隔离") || !strings.Contains(block, "Cyrene 用 Go 编写") {
t.Fatalf("restricted block missing project memory / marker:\n%s", block)
}
for _, banned := range []string{"喜欢冰美式", "喜欢爬山"} {
if strings.Contains(block, banned) {
t.Fatalf("restricted block leaked owner memory %q:\n%s", banned, block)
}
}
}
// TestInjectMemory_Restricted_NoProjectMemory_NoInject 证明当群聊
// 或陌生人上下文没有非主人私有(项目)记忆时,什么也不
// 注入 —— 该对话不会用主人的上下文来播种。
func TestInjectMemory_Restricted_NoProjectMemory_NoInject(t *testing.T) {
t.Parallel()
store := newTestMemoryStore(t)
// 只存在主人私有(用户/角色)记忆。
for _, it := range []memory.MemoryItem{
{PersonaID: "persona-a", Content: "用户习惯:喜欢冰美式", Scope: memory.ScopeUser, Kind: memory.KindRelationship},
} {
if _, err := store.Add(context.Background(), it); err != nil {
t.Fatalf("seed memory: %v", err)
}
}
l := New(Options{Retriever: memory.NewRetriever(store), PersonaID: "persona-a"})
msgs := []model.Message{model.TextMessage(model.RoleUser, "go")}
ctx := WithSessionEnv(context.Background(), SessionEnv{ActorEnv: restrict.ActorStranger, MemWritable: false})
view := l.assembleContext(ctx, msgs, 1)
if len(view) != 1 {
t.Fatalf("restricted (no project) view = %d messages, want 1 (no injected memory)", len(view))
}
}
// TestExtractMemory_OwnerEnv_Writes 证明 owner 全权限环境像从前
// 一样持久化提取出的记忆。
func TestExtractMemory_OwnerEnv_Writes(t *testing.T) {
t.Parallel()
store := newTestMemoryStore(t)
l := New(Options{Memory: store, Extractor: memory.NewManualExtractor("persona-a", "owner"), 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("list: %v", err)
}
if len(items) != 1 {
t.Fatalf("owner extract persisted %d items, want 1", len(items))
}
}
// TestExtractMemory_RestrictedEnv_SkipsWrite 证明群聊/陌生人环境
// 绝不写共享记忆:提取被跳过,store 保持为空。
func TestExtractMemory_RestrictedEnv_SkipsWrite(t *testing.T) {
t.Parallel()
store := newTestMemoryStore(t)
l := New(Options{Memory: store, Extractor: memory.NewManualExtractor("persona-a", "owner"), PersonaID: "persona-a"})
ctx := WithSessionEnv(context.Background(), SessionEnv{ActorEnv: restrict.ActorGroupMember, MemWritable: false})
l.extractMemory(ctx, []model.Message{model.TextMessage(model.RoleUser, "记住 我爱喝冰美式")}, 1)
items, err := store.ListByPersona(context.Background(), "persona-a")
if err != nil {
t.Fatalf("list: %v", err)
}
if len(items) != 0 {
t.Fatalf("restricted extract wrote %d items, want 0 (read-only isolation)", len(items))
}
}
// TestSessionEnvFrom_DefaultOwnerFull 钉住向后兼容的默认值:未挂载
// env 的 context 解析为 owner 全权限环境,因此未设置 env 的
// 调用方(Web 主会话、内部自动化任务)保持完整的
// 记忆读写。
func TestSessionEnvFrom_DefaultOwnerFull(t *testing.T) {
t.Parallel()
env := sessionEnvFrom(context.Background())
if env.MemWritable != true || env.Restricted() {
t.Fatalf("default env = %+v, want owner-full (MemWritable=true, not restricted)", env)
}
if env.ActorEnv != restrict.ActorOwner {
t.Fatalf("default env actor = %q, want %q", env.ActorEnv, restrict.ActorOwner)
}
}