Files
Cyrene_v2/internal/runtime/loop/memory_source_test.go
T
AskaEth 0ce498754a fix(memory,loop): 记忆作用域读写成对 + source_conversation 结构化(裁决 1.9/04.4.2/04.4.3)
- 04.4.2/1.9 读侧:Retrieve 候选集并集 = 本人 persona 域 ∪ user 共享域 ∪ project 共享域(他角色 persona 域仍排除),群聊/陌生人取受限收窄
- 写侧:群聊/陌生人整段跳过记忆提取(可查不可写),persona 域仅属主可写
- 04.4.3:source_conversation 不再由 LLM 自由生成(prompt 不索要、候选结构不含该键),落库时无条件盖真实 {session_id,message_id},编解码集中在 memory/source.go
- 新增跨角色种子用例(原用例即使关掉并集也会通过)+ 成对用例「同群聊上下文:读得到共享域、写不进 store」
- 同步 store.go/retrieve.go 文档注释,与 design/03 §3/§4 口径一致
2026-10-04 12:07:44 +08:00

68 lines
2.6 KiB
Go
Raw 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"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
)
// TestPersistMemories_StampsRealSource 证明裁决 04.4.3 的落库值:
// 提取调用点(persistMemories)从运行上下文盖真实 session/message id,
// extractor 产出的自由文本来源被无条件覆盖。
func TestPersistMemories_StampsRealSource(t *testing.T) {
t.Parallel()
store := newTestMemoryStore(t)
l := New(Options{Memory: store, Extractor: memory.NewManualExtractor("persona-a", "owner"), PersonaID: "persona-a"})
// extractor 刻意夹带一条伪造来源,证明持久化层会盖掉它。
bogus := []memory.MemoryItem{{
PersonaID: "persona-a", Scope: memory.ScopeUser, Kind: memory.KindRelationship,
Content: "用户习惯:爱喝冰美式", SourceConversation: "模型瞎编的来源",
}}
ctx := withLoopSession(WithTurnID(WithSessionEnv(context.Background(), OwnerEnv()), "turn-1"), "web:abc")
l.persistMemories(ctx, bogus, 1)
items, err := store.ListByPersona(context.Background(), "persona-a")
if err != nil {
t.Fatalf("ListByPersona: %v", err)
}
if len(items) != 1 {
t.Fatalf("stored %d items, want 1", len(items))
}
ref, ok := memory.ParseSourceConversation(items[0].SourceConversation)
if !ok {
t.Fatalf("SourceConversation %q is not structured, want real ids", items[0].SourceConversation)
}
if ref.SessionID != "web:abc" || ref.MessageID != "turn-1" {
t.Fatalf("source ref = %+v, want session web:abc / message turn-1", ref)
}
}
// TestPersistMemories_SourceSessionOnly 证明未挂 turn id 的调用方
// (渠道路径现状)溯源降级为仅会话粒度 —— 真实、可解析,不伪造。
func TestPersistMemories_SourceSessionOnly(t *testing.T) {
t.Parallel()
store := newTestMemoryStore(t)
l := New(Options{Memory: store, Extractor: memory.NewManualExtractor("persona-a", "owner"), PersonaID: "persona-a"})
ctx := withLoopSession(WithSessionEnv(context.Background(), OwnerEnv()), "qq-obv11:group:42")
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 {
t.Fatalf("stored %d items, want 1", len(items))
}
ref, ok := memory.ParseSourceConversation(items[0].SourceConversation)
if !ok {
t.Fatalf("SourceConversation %q is not structured", items[0].SourceConversation)
}
if ref.SessionID != "qq-obv11:group:42" || ref.MessageID != "" {
t.Fatalf("source ref = %+v, want session-only provenance", ref)
}
}