- 只翻注释/文档字符串:820 增 / 820 删 严格 1:1;剥掉整行注释与行尾注释尾巴后, 逐文件与 HEAD 比对「代码部分差异 = 0」;裁决/design 编号计数逐项与 HEAD 一致 - 保留可执行 SQL 样例、SQLite 报错原文、pgvector/HNSW/ANN/BLOB 等技术词原文 - 复核:go build 绿 + memory/projectindex/vector 三包测试绿;gofmt 仅剩 2 个既有未格式化文件(改动前即如此,未顺手修)
268 lines
7.7 KiB
Go
268 lines
7.7 KiB
Go
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)
|
||
}
|
||
}
|