Files
Cyrene_v2/internal/runtime/loop/memory_readonly_decay_test.go
AskaEth 509dc412eb
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(memory): 记忆 P2(项目索引物化 / decay 回写 / 作用域写规则 / 静默吞点 + loop 级只读红线测试)
2026-10-05 11:47:39 +08:00

74 lines
3.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}