package memory import ( "context" "strings" "testing" ) // 本文件是审计 §9-4 明确要求补的**对抗用例**:群里有人直接问「她住哪」时,主人 // 私事**不许进受限车道的候选池**(审计原话:这类提问会把花溪类记忆直接顶到 topK, // 风险高于随机触发)。 // // 🔴 它同时钉住两个方向,缺一个就不算修好: // - 受限车道(群聊 / 陌生人)的可见集里**没有**私事; // - 主人车道(同一次检索,不带限制)**照常看得到** —— sensitive 是数据层标记, // 不是权限系统(塔裁决 2026-10-30)。 // // ⚠️ 这里的受限可见集用 `MemoryItem.SharedVisible()` 复现 —— 它正是 // `internal/runtime/loop/memory_filter.go` 的 FilterSharedVisible 该用的判据 // (loop 侧把 `sharedScope(r.Item.Scope)` 换成 `r.Item.SharedVisible()` 即完成接线, // 那一行在 loop 包,不在本切片的 scope 内)。本文件因此保证的是**数据侧判据**; // 「loop 真的接了线」由 loop 的用例与端到端回放兜底。 // restrictedVisible 复现受限车道过滤:只保留 SharedVisible 的命中。 // (loop 侧还有一条「剔除 L4 意图记录」,与本片无关,故不在此复现。) func restrictedVisible(results []Result) []Result { visible := make([]Result, 0, len(results)) for _, r := range results { if !r.Item.SharedVisible() { continue } visible = append(visible, r) } return visible } // contents 把命中拼成一段文本,供 Contains 断言。 func contents(results []Result) string { var b strings.Builder for _, r := range results { b.WriteString(r.Item.Content) b.WriteByte('\n') } return b.String() } // TestRestrictedRecall_群里问住址拿不到私事 是对抗主用例:query 刻意写成群里那句 // 「她住哪」的直白形态(上海 / 住在哪 / 花溪),让私事记忆在**排序上**占优 —— // 这样才能证明挡住它的是敏感标记而不是相关度不够。 func TestRestrictedRecall_群里问住址拿不到私事(t *testing.T) { t.Parallel() ctx := context.Background() s := newTestStore(t) addItem(t, s, MemoryItem{ PersonaID: "persona.a", Scope: ScopeUser, Kind: KindWorldEvent, Content: "用户居住在花溪(贵阳花溪区),不在上海工作", }) addItem(t, s, MemoryItem{ PersonaID: "persona.a", Scope: ScopeUser, Kind: KindRelationship, Content: "用户喜欢冰美式", }) addItem(t, s, MemoryItem{ PersonaID: "persona.a", Scope: ScopeProject, Kind: KindWorldEvent, ProjectID: "proj-1", Content: "Cyrene 用 Go 编写,编号规则 CY-日期-序号", }) r := NewRetriever(s) results, err := r.Retrieve(ctx, "persona.a", "她住在哪里?是上海还是花溪?", 10) if err != nil { t.Fatalf("Retrieve: %v", err) } // 前提:这条私事确实在候选池里、且排得进前 K —— 否则下面的断言是空转。 if !strings.Contains(contents(results), "花溪") { t.Fatalf("主人车道读不到这条私事,用例前提不成立:%s", contents(results)) } restricted := contents(restrictedVisible(results)) if strings.Contains(restricted, "花溪") { t.Fatalf("受限车道拿到了住址:\n%s", restricted) } if !strings.Contains(restricted, "冰美式") { t.Fatalf("受限车道把合法的用户习惯也挡掉了(冻结裁决 design/03 §5 被推翻):\n%s", restricted) } if !strings.Contains(restricted, "Cyrene 用 Go 编写") { t.Fatalf("受限车道丢了项目域记忆:\n%s", restricted) } } // TestRestrictedRecall_主人车道照常读到全部 钉住「sensitive 不是权限系统」: // 同一个 Store、同一批条目,不经受限过滤的读路径(owner 车道走的就是这条) // 必须一条不少地读到,含敏感项。判据一旦被写成权限,这条立刻红。 func TestRestrictedRecall_主人车道照常读到全部(t *testing.T) { t.Parallel() ctx := context.Background() s := newTestStore(t) stored := addItem(t, s, MemoryItem{ PersonaID: "persona.a", Scope: ScopeUser, Kind: KindWorldEvent, Content: "主人希望被称呼为「叶酱」", }) if !stored.Sensitive { t.Fatal("私称没有被标成敏感,用例前提不成立") } byID, err := s.Get(ctx, stored.ID) if err != nil { t.Fatalf("Get: %v", err) } if !byID.Sensitive || !strings.Contains(byID.Content, "叶酱") { t.Fatal("主人车道的按 id 读取被敏感标记挡住了 —— sensitive 变成了权限") } personaItems, err := s.ListByPersona(ctx, "persona.a") if err != nil { t.Fatalf("ListByPersona: %v", err) } if !containsSensitive(personaItems, stored.ID) { t.Fatal("主人车道的列表读路径读不到这条(或被读成非敏感)") } // 检索路径同样不因敏感而剔除:owner 车道的 Retrieve 结果里必须有它。 r := NewRetriever(s) results, err := r.Retrieve(ctx, "persona.a", "主人希望被怎么称呼?叶酱吗?", 10) if err != nil { t.Fatalf("Retrieve: %v", err) } if !strings.Contains(contents(results), "叶酱") { t.Fatalf("主人车道的检索丢了敏感项:\n%s", contents(results)) } }