Files
AskaEth 61cf80d5ee
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
docs(comments): 注释中文化 · internal/runtime/memory(31 文件,纯替换)
- 只翻注释/文档字符串:820 增 / 820 删 严格 1:1;剥掉整行注释与行尾注释尾巴后,
  逐文件与 HEAD 比对「代码部分差异 = 0」;裁决/design 编号计数逐项与 HEAD 一致
- 保留可执行 SQL 样例、SQLite 报错原文、pgvector/HNSW/ANN/BLOB 等技术词原文
- 复核:go build 绿 + memory/projectindex/vector 三包测试绿;gofmt 仅剩 2 个既有未格式化文件(改动前即如此,未顺手修)
2026-10-04 13:32:54 +08:00

268 lines
7.7 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 memory
import (
"context"
"strings"
"testing"
"time"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/embedding"
)
// addItem 是一个便捷函数:持久化一条条目,出错则让测试失败。
func addItem(t *testing.T, s *Store, item MemoryItem) MemoryItem {
t.Helper()
ctx := context.Background()
stored, err := s.Add(ctx, item)
if err != nil {
t.Fatalf("Add item: %v", err)
}
return stored
}
func TestRetriever_Retrieve_Relevance_正常(t *testing.T) {
t.Parallel()
ctx := context.Background()
s := newTestStore(t)
addItem(t, s, MemoryItem{
PersonaID: "persona.a", Content: "user loves coffee", Tags: []string{"coffee"},
Scope: ScopePersona, Kind: KindWorldEvent,
})
addItem(t, s, MemoryItem{
PersonaID: "persona.a", Content: "user plays guitar", Tags: []string{"music"},
Scope: ScopePersona, Kind: KindWorldEvent,
})
r := NewRetriever(s)
results, err := r.Retrieve(ctx, "persona.a", "coffee", 10)
if err != nil {
t.Fatalf("Retrieve: %v", err)
}
if len(results) == 0 {
t.Fatal("expected at least one result")
}
if got := results[0].Item.Content; got != "user loves coffee" {
t.Fatalf("top result = %q, want the coffee item", got)
}
if results[0].Score <= 0 {
t.Fatalf("top result score = %v, want > 0", results[0].Score)
}
}
func TestRetriever_Retrieve_Recency_正常(t *testing.T) {
t.Parallel()
ctx := context.Background()
s := newTestStore(t)
old := time.Now().UTC().Add(-48 * time.Hour)
newer := time.Now().UTC().Add(-2 * time.Hour)
addItem(t, s, MemoryItem{
PersonaID: "persona.a", Content: "old memory", UpdatedAt: old,
Scope: ScopePersona, Kind: KindWorldEvent,
})
addItem(t, s, MemoryItem{
PersonaID: "persona.a", Content: "new memory", UpdatedAt: newer,
Scope: ScopePersona, Kind: KindWorldEvent,
})
r := NewRetriever(s)
// query 为空时无任何命中,因此排序回退到新近度。
results, err := r.Retrieve(ctx, "persona.a", "", 10)
if err != nil {
t.Fatalf("Retrieve: %v", err)
}
if len(results) != 2 {
t.Fatalf("got %d results, want 2", len(results))
}
if got := results[0].Item.Content; got != "new memory" {
t.Fatalf("top result = %q, want the newer memory", got)
}
// 较旧的记忆必须排在后面。
if got := results[1].Item.Content; got != "old memory" {
t.Fatalf("second result = %q, want the older memory", got)
}
}
func TestRetriever_Retrieve_TopK_正常(t *testing.T) {
t.Parallel()
ctx := context.Background()
s := newTestStore(t)
for i := 0; i < 5; i++ {
addItem(t, s, MemoryItem{
PersonaID: "persona.a", Content: "shared term item",
Tags: []string{"term"}, Scope: ScopePersona, Kind: KindWorldEvent,
})
}
r := NewRetriever(s)
results, err := r.Retrieve(ctx, "persona.a", "term", 3)
if err != nil {
t.Fatalf("Retrieve: %v", err)
}
if len(results) != 3 {
t.Fatalf("got %d results, want topK=3", len(results))
}
}
func TestRetriever_Retrieve_Isolation_正常(t *testing.T) {
t.Parallel()
ctx := context.Background()
s := newTestStore(t)
addItem(t, s, MemoryItem{
PersonaID: "persona.a", Content: "coffee for a", Tags: []string{"coffee"},
Scope: ScopePersona, Kind: KindRelationship,
})
addItem(t, s, MemoryItem{
PersonaID: "persona.b", Content: "coffee for b", Tags: []string{"coffee"},
Scope: ScopePersona, Kind: KindRelationship,
})
r := NewRetriever(s)
results, err := r.Retrieve(ctx, "persona.a", "coffee", 10)
if err != nil {
t.Fatalf("Retrieve: %v", err)
}
for _, res := range results {
if res.Item.PersonaID != "persona.a" {
t.Fatalf("retrieved item belongs to persona %q, want persona.a", res.Item.PersonaID)
}
}
if len(results) != 1 {
t.Fatalf("got %d results for persona.a, want 1", len(results))
}
}
func TestRetriever_Retrieve_WithEmbedder_正常(t *testing.T) {
t.Parallel()
ctx := context.Background()
s := newTestStore(t)
addItem(t, s, MemoryItem{
PersonaID: "persona.a", Content: "she is a coffee lover", Tags: []string{"drink"},
Scope: ScopePersona, Kind: KindRelationship,
})
addItem(t, s, MemoryItem{
PersonaID: "persona.a", Content: "she plays the guitar", Tags: []string{"music"},
Scope: ScopePersona, Kind: KindRelationship,
})
emb := embedding.NewNGram(64)
r := NewRetriever(s, WithEmbedder(emb))
results, err := r.Retrieve(ctx, "persona.a", "coffee", 10)
if err != nil {
t.Fatalf("Retrieve with embedder: %v", err)
}
if len(results) == 0 {
t.Fatal("expected results when embedding is enabled")
}
if got := results[0].Item.Content; got != "she is a coffee lover" {
t.Fatalf("top result = %q, want the coffee item", got)
}
}
func TestRetriever_Retrieve_EmptyPersona_正常(t *testing.T) {
t.Parallel()
ctx := context.Background()
s := newTestStore(t)
r := NewRetriever(s)
results, err := r.Retrieve(ctx, "ghost", "anything", 10)
if err != nil {
t.Fatalf("Retrieve: %v", err)
}
if len(results) != 0 {
t.Fatalf("got %d results for empty persona, want 0", len(results))
}
}
func TestRetriever_Retrieve_SharedScopesUnion_共享域可读(t *testing.T) {
t.Parallel()
ctx := context.Background()
s := newTestStore(t)
// persona.a 自己的条目 + persona.b 写入三个作用域各一条。
addItem(t, s, MemoryItem{
PersonaID: "persona.a", Content: "a 自己的咖啡记忆", Tags: []string{"咖啡"},
Scope: ScopePersona, Kind: KindRelationship,
})
addItem(t, s, MemoryItem{
PersonaID: "persona.b", Content: "用户习惯:喜欢手冲咖啡", Tags: []string{"咖啡"},
Scope: ScopeUser, Kind: KindRelationship,
})
addItem(t, s, MemoryItem{
PersonaID: "persona.b", Content: "项目记忆:咖啡项目用 Go 编写", Tags: []string{"咖啡"},
Scope: ScopeProject, Kind: KindWorldEvent,
})
addItem(t, s, MemoryItem{
PersonaID: "persona.b", Content: "b 的角色日常:周末爬山", Tags: []string{"咖啡"},
Scope: ScopePersona, Kind: KindRelationship,
})
r := NewRetriever(s)
results, err := r.Retrieve(ctx, "persona.a", "咖啡", 10)
if err != nil {
t.Fatalf("Retrieve: %v", err)
}
seen := map[string]bool{}
for _, res := range results {
seen[res.Item.PersonaID+"/"+string(res.Item.Scope)] = true
}
// 可见:自己的条目 + user/project 共享域(对所有角色可读,design/03 §3/§4)。
for _, want := range []string{"persona.a/persona", "persona.b/user", "persona.b/project"} {
if !seen[want] {
t.Fatalf("candidate set missing %s (shared scopes must be unioned in): %+v", want, seen)
}
}
// 不可见:他角色的角色日常 —— 直接检索不越权,跨角色读只走
// 子代理派发(design/03 §4,design/22 §9)。
if seen["persona.b/persona"] {
t.Fatal("candidate set includes persona.b's persona-scope memory: cross-role daily memory must not be directly retrievable")
}
// 并集不得重复计入同一行。
if len(results) != 3 {
t.Fatalf("got %d results, want 3 (own + shared user + shared project, no dupes)", len(results))
}
}
func TestRetriever_Inject_Format_正常(t *testing.T) {
t.Parallel()
ctx := context.Background()
s := newTestStore(t)
addItem(t, s, MemoryItem{
PersonaID: "persona.a", Content: "她喜欢冰美式", Tags: []string{"coffee"},
Scope: ScopePersona, Kind: KindRelationship,
})
r := NewRetriever(s)
block, err := r.Inject(ctx, "persona.a", "冰美式", 10)
if err != nil {
t.Fatalf("Inject: %v", err)
}
if !strings.Contains(block, "相关记忆") {
t.Fatalf("inject block missing header: %q", block)
}
if !strings.Contains(block, "她喜欢冰美式") {
t.Fatalf("inject block missing item content: %q", block)
}
}
func TestRetriever_Inject_NoResults_正常(t *testing.T) {
t.Parallel()
ctx := context.Background()
s := newTestStore(t)
r := NewRetriever(s)
block, err := r.Inject(ctx, "ghost", "anything", 10)
if err != nil {
t.Fatalf("Inject: %v", err)
}
if block != "" {
t.Fatalf("Inject on empty persona = %q, want empty string", block)
}
}