74 lines
3.3 KiB
Go
74 lines
3.3 KiB
Go
package loop
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
"time"
|
||
|
||
"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"
|
||
)
|
||
|
||
// seedDecayProbe 在独立 store 上放一条**会被命中**(content 含 query 词)且
|
||
// updated_at 很旧(60 天前,衰减明显 < 1.0)的记忆,返回 store 与落库条目。
|
||
func seedDecayProbe(t *testing.T, query string) (*memory.Store, memory.MemoryItem) {
|
||
t.Helper()
|
||
ctx := context.Background()
|
||
store := newTestMemoryStore(t)
|
||
stored, err := store.Add(ctx, memory.MemoryItem{
|
||
PersonaID: "persona-a", Content: query + " preference",
|
||
UpdatedAt: time.Now().UTC().Add(-60 * 24 * time.Hour),
|
||
Scope: memory.ScopeUser, Kind: memory.KindRelationship,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("seed memory: %v", err)
|
||
}
|
||
return store, stored
|
||
}
|
||
|
||
// TestMemoryBlock_ReadOnlyLane_NoDecayWrite 钉住 AGENTS §9 / design/03 §5 的红线
|
||
// 「群聊 / 陌生人只读不写记忆」在 **loop 级**成立:真实走 memoryBlock →
|
||
// retrieveMemoryBlock 的受限车道(群聊/陌生人,MemWritable=false)召回时,命中
|
||
// 记忆的 decay_weight 与 updated_at 必须逐字不变(零写入)。
|
||
//
|
||
// 为什么必须落在 loop 级:关掉衰减回写的是**受限车道自己在检索调用点**传的
|
||
// memory.WithoutDecayWriteback(memory.go 的 retrieveMemoryBlock)。只在 Retriever
|
||
// 上单测「显式传入时只读」钉不住「受限车道真的传了它」——把那一行去掉,那些
|
||
// 测试仍全绿(审计实证的测试缺口)。
|
||
//
|
||
// 正向对照:主人车道(OwnerEnv)走同一路径时两者必须被推进,否则本测试可能
|
||
// 因为「根本没命中」而假绿。
|
||
func TestMemoryBlock_ReadOnlyLane_NoDecayWrite(t *testing.T) {
|
||
t.Parallel()
|
||
ctx := context.Background()
|
||
msgs := []model.Message{model.TextMessage(model.RoleUser, "coffee")}
|
||
|
||
// 正向对照:主人车道推进衰减(证明命中条件成立,不是空跑)。
|
||
ownerStore, ownerSeed := seedDecayProbe(t, "coffee")
|
||
owner := New(Options{Retriever: memory.NewRetriever(ownerStore), PersonaID: "persona-a"})
|
||
owner.memoryBlock(WithSessionEnv(ctx, OwnerEnv()), msgs)
|
||
ownerAfter, err := ownerStore.Get(ctx, ownerSeed.ID)
|
||
if err != nil {
|
||
t.Fatalf("owner Get: %v", err)
|
||
}
|
||
if ownerAfter.DecayWeight >= ownerSeed.DecayWeight || ownerAfter.UpdatedAt.Equal(ownerSeed.UpdatedAt) {
|
||
t.Fatalf("主人车道未推进衰减(对照不成立):before=%v/%v after=%v/%v",
|
||
ownerSeed.DecayWeight, ownerSeed.UpdatedAt, ownerAfter.DecayWeight, ownerAfter.UpdatedAt)
|
||
}
|
||
|
||
// 红线:受限车道对命中记忆零写入。
|
||
groupEnv := SessionEnv{ActorEnv: restrict.ActorGroupMember, MemWritable: false}
|
||
groupStore, groupSeed := seedDecayProbe(t, "coffee")
|
||
group := New(Options{Retriever: memory.NewRetriever(groupStore), PersonaID: "persona-a"})
|
||
group.memoryBlock(WithSessionEnv(ctx, groupEnv), msgs)
|
||
groupAfter, err := groupStore.Get(ctx, groupSeed.ID)
|
||
if err != nil {
|
||
t.Fatalf("group Get: %v", err)
|
||
}
|
||
if groupAfter.DecayWeight != groupSeed.DecayWeight || !groupAfter.UpdatedAt.Equal(groupSeed.UpdatedAt) {
|
||
t.Fatalf("受限车道写入了记忆(红线破坏):before=%v/%v after=%v/%v",
|
||
groupSeed.DecayWeight, groupSeed.UpdatedAt, groupAfter.DecayWeight, groupAfter.UpdatedAt)
|
||
}
|
||
}
|