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 口径一致
This commit is contained in:
AskaEth committed 2026-10-04 12:07:44 +08:00
1 parent af8591b78a
commit 0ce498754a
11 files changed
+428 -60

No files matched your search

+56
View File
@@ -0,0 +1,56 @@
package memory
import "testing"
// TestSourceConversation_Roundtrip 证明溯源编码是机器可回溯的:
// Format 后 Parse 得回同一对真实 id(裁决 04.4.3,design/03 §2)。
func TestSourceConversation_Roundtrip(t *testing.T) {
t.Parallel()
raw := FormatSourceConversation("web:abc", "turn-1")
ref, ok := ParseSourceConversation(raw)
if !ok {
t.Fatalf("ParseSourceConversation(%q) ok = false, want structured", raw)
}
if ref.SessionID != "web:abc" || ref.MessageID != "turn-1" {
t.Fatalf("parsed ref = %+v, want session web:abc / message turn-1", ref)
}
}
// TestSourceConversation_BothBlank 证明两个 id 都缺时不落伪溯源
// (空串而不是空 JSON 对象)。
func TestSourceConversation_BothBlank(t *testing.T) {
t.Parallel()
if got := FormatSourceConversation("", " "); got != "" {
t.Fatalf("FormatSourceConversation(blank) = %q, want empty", got)
}
if _, ok := ParseSourceConversation(""); ok {
t.Fatal("ParseSourceConversation(\"\") ok = true, want false")
}
}
// TestSourceConversation_SessionOnly 证明只知会话粒度时溯源降级为
// 仅 session_id(渠道路径未挂 turn id 时的真实形态)。
func TestSourceConversation_SessionOnly(t *testing.T) {
t.Parallel()
raw := FormatSourceConversation("qq-obv11:group:123", "")
ref, ok := ParseSourceConversation(raw)
if !ok {
t.Fatalf("ParseSourceConversation(%q) ok = false, want structured", raw)
}
if ref.SessionID != "qq-obv11:group:123" || ref.MessageID != "" {
t.Fatalf("parsed ref = %+v, want session-only provenance", ref)
}
}
// TestSourceConversation_LegacyFreeTextNotStructured 证明裁决前的
// LLM 自由文本来源解不回结构化形状 —— 调用方靠 ok=false 区分
// 遗留行与真实溯源,而不是把自由文本误当 id。
func TestSourceConversation_LegacyFreeTextNotStructured(t *testing.T) {
t.Parallel()
for _, legacy := range []string{"conv.1", "昨天聊咖啡时提到的", `["session"]`, `{"other":1}`} {
if _, ok := ParseSourceConversation(legacy); ok {
t.Fatalf("ParseSourceConversation(%q) ok = true, want false for legacy free text", legacy)
}
}
}