- 只翻注释/文档字符串:820 增 / 820 删 严格 1:1;剥掉整行注释与行尾注释尾巴后, 逐文件与 HEAD 比对「代码部分差异 = 0」;裁决/design 编号计数逐项与 HEAD 一致 - 保留可执行 SQL 样例、SQLite 报错原文、pgvector/HNSW/ANN/BLOB 等技术词原文 - 复核:go build 绿 + memory/projectindex/vector 三包测试绿;gofmt 仅剩 2 个既有未格式化文件(改动前即如此,未顺手修)
370 lines
10 KiB
Go
370 lines
10 KiB
Go
package memory
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestNewStore_NilDB_异常(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
if _, err := NewStore(ctx, nil, testLogger()); !errors.Is(err, ErrNilDB) {
|
|
t.Fatalf("NewStore(nil db) err = %v, want ErrNilDB", err)
|
|
}
|
|
}
|
|
|
|
func TestStore_AddGet_正常(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
|
|
item := baseItem()
|
|
// Add 返回已落库条目,id 与时间戳均已填充。
|
|
stored, err := s.Add(ctx, item)
|
|
if err != nil {
|
|
t.Fatalf("Add: %v", err)
|
|
}
|
|
if stored.ID == "" {
|
|
t.Fatal("Add returned empty id")
|
|
}
|
|
if stored.CreatedAt.IsZero() || stored.UpdatedAt.IsZero() {
|
|
t.Fatal("Add did not assign timestamps")
|
|
}
|
|
|
|
got, err := s.Get(ctx, stored.ID)
|
|
if err != nil {
|
|
t.Fatalf("Get: %v", err)
|
|
}
|
|
if got.ID != stored.ID {
|
|
t.Errorf("ID = %q, want %q", got.ID, stored.ID)
|
|
}
|
|
if got.PersonaID != item.PersonaID {
|
|
t.Errorf("PersonaID = %q, want %q", got.PersonaID, item.PersonaID)
|
|
}
|
|
if got.RoleID != item.RoleID {
|
|
t.Errorf("RoleID = %q, want %q", got.RoleID, item.RoleID)
|
|
}
|
|
if got.Scope != item.Scope {
|
|
t.Errorf("Scope = %q, want %q", got.Scope, item.Scope)
|
|
}
|
|
if got.Kind != item.Kind {
|
|
t.Errorf("Kind = %q, want %q", got.Kind, item.Kind)
|
|
}
|
|
if got.Content != item.Content {
|
|
t.Errorf("Content = %q, want %q", got.Content, item.Content)
|
|
}
|
|
if got.Confidence != item.Confidence {
|
|
t.Errorf("Confidence = %v, want %v", got.Confidence, item.Confidence)
|
|
}
|
|
if len(got.Tags) != 2 || got.Tags[0] != "habit" || got.Tags[1] != "coffee" {
|
|
t.Errorf("Tags = %v, want [habit coffee]", got.Tags)
|
|
}
|
|
}
|
|
|
|
func TestStore_Add_Defaults_正常(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
|
|
// 空的 Scope/Kind/Confidence/DecayWeight 规范化到其默认值。
|
|
stored, err := s.Add(ctx, MemoryItem{
|
|
PersonaID: "persona.a",
|
|
Content: "我们约定每周日晚上看电影",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Add: %v", err)
|
|
}
|
|
if stored.Scope != ScopePersona {
|
|
t.Errorf("Scope = %q, want persona default", stored.Scope)
|
|
}
|
|
if stored.Kind != KindRelationship {
|
|
t.Errorf("Kind = %q, want relationship default", stored.Kind)
|
|
}
|
|
if stored.Confidence != 1.0 {
|
|
t.Errorf("Confidence = %v, want 1.0 default", stored.Confidence)
|
|
}
|
|
if stored.DecayWeight != 1.0 {
|
|
t.Errorf("DecayWeight = %v, want 1.0 default", stored.DecayWeight)
|
|
}
|
|
}
|
|
|
|
func TestStore_Add_Invalid_异常(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
|
|
tests := []struct {
|
|
name string
|
|
item MemoryItem
|
|
wantErr error
|
|
}{
|
|
{name: "missing persona", item: MemoryItem{Content: "x"}, wantErr: ErrMissingPersona},
|
|
{name: "missing content", item: MemoryItem{PersonaID: "p"}, wantErr: ErrMissingContent},
|
|
{name: "invalid scope", item: MemoryItem{PersonaID: "p", Content: "x", Scope: "bogus"}, wantErr: ErrInvalidScope},
|
|
{name: "invalid kind", item: MemoryItem{PersonaID: "p", Content: "x", Kind: "bogus"}, wantErr: ErrInvalidKind},
|
|
}
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
if _, err := s.Add(ctx, tt.item); !errors.Is(err, tt.wantErr) {
|
|
t.Fatalf("Add err = %v, want wrap of %v", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStore_Get_NotFound_异常(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
|
|
for _, id := range []string{"", "does-not-exist"} {
|
|
if _, err := s.Get(ctx, id); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("Get(%q) err = %v, want ErrNotFound", id, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStore_ListByPersona_Isolation(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
|
|
for _, p := range []string{"persona.a", "persona.b"} {
|
|
stored, err := s.Add(ctx, MemoryItem{
|
|
PersonaID: p,
|
|
Content: "内容 " + p,
|
|
Scope: ScopePersona,
|
|
Kind: KindWorldEvent,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Add for %s: %v", p, err)
|
|
}
|
|
if stored.PersonaID != p {
|
|
t.Fatalf("stored persona = %q, want %q", stored.PersonaID, p)
|
|
}
|
|
}
|
|
|
|
items, err := s.ListByPersona(ctx, "persona.a")
|
|
if err != nil {
|
|
t.Fatalf("ListByPersona: %v", err)
|
|
}
|
|
if len(items) != 1 {
|
|
t.Fatalf("ListByPersona(a) = %d items, want 1", len(items))
|
|
}
|
|
if items[0].PersonaID != "persona.a" {
|
|
t.Errorf("item persona = %q, want persona.a", items[0].PersonaID)
|
|
}
|
|
}
|
|
|
|
func TestStore_ListByPersona_MissingPersona_异常(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
|
|
if _, err := s.ListByPersona(ctx, ""); !errors.Is(err, ErrMissingPersona) {
|
|
t.Fatalf("ListByPersona(\"\") err = %v, want ErrMissingPersona", err)
|
|
}
|
|
}
|
|
|
|
func TestStore_List_正常(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
|
|
// 空存储列不出任何东西,但仍是非 nil。
|
|
if got, err := s.List(ctx); err != nil || got == nil || len(got) != 0 {
|
|
t.Fatalf("List() on empty store = %#v, err=%v; want empty non-nil", got, err)
|
|
}
|
|
|
|
for _, item := range []MemoryItem{
|
|
{PersonaID: "persona.a", Content: "A 的记忆", Scope: ScopePersona, Kind: KindRelationship},
|
|
{PersonaID: "persona.b", Content: "B 的记忆", Scope: ScopePersona, Kind: KindWorldEvent},
|
|
{PersonaID: "persona.a", Content: "项目记忆", Scope: ScopeProject, Kind: KindWorldEvent, ProjectID: "proj.1"},
|
|
} {
|
|
if _, err := s.Add(ctx, item); err != nil {
|
|
t.Fatalf("Add(%s): %v", item.Content, err)
|
|
}
|
|
}
|
|
|
|
got, err := s.List(ctx)
|
|
if err != nil {
|
|
t.Fatalf("List: %v", err)
|
|
}
|
|
if len(got) != 3 {
|
|
t.Fatalf("List = %d items, want 3", len(got))
|
|
}
|
|
// 最新的在前(created_at DESC)。
|
|
if got[0].Content != "项目记忆" {
|
|
t.Errorf("got[0].Content = %q, want the latest added (项目记忆)", got[0].Content)
|
|
}
|
|
if got[2].Content != "A 的记忆" {
|
|
t.Errorf("got[2].Content = %q, want the oldest (A 的记忆)", got[2].Content)
|
|
}
|
|
if got[0].Scope != ScopeProject || got[0].PersonaID != "persona.a" {
|
|
t.Errorf("project item fields = %+v", got[0])
|
|
}
|
|
}
|
|
|
|
// TestStore_List_变宽时间戳_排序 是变宽时间戳缺陷的回归:
|
|
// time.RFC3339Nano 会丢弃末尾小数零,因此较旧的条目
|
|
// 曾被存成 ":05.83"、较新的存成 ":05.8309789",而 SQLite 对这些列
|
|
// 施加的文本比较把较旧的判成更新的。
|
|
func TestStore_List_变宽时间戳_排序(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
|
|
sec := time.Date(2026, 9, 7, 3, 20, 5, 0, time.UTC)
|
|
older := sec.Add(830 * time.Millisecond) // RFC3339Nano 下的 ":05.83"
|
|
newer := older.Add(978900 * time.Nanosecond) // RFC3339Nano 下的 ":05.8309789"
|
|
|
|
// 较新的条目先插入,因此只按 rowid 打破并列会把较旧的
|
|
// 抬到前面:只有正确排序的 created_at 文本才会让较晚的在前。
|
|
for _, in := range []struct {
|
|
content string
|
|
at time.Time
|
|
}{{"较晚", newer}, {"较早", older}} {
|
|
item := MemoryItem{PersonaID: "persona.a", Content: in.content, CreatedAt: in.at, UpdatedAt: in.at}
|
|
if _, err := s.Add(ctx, item); err != nil {
|
|
t.Fatalf("Add(%s): %v", in.content, err)
|
|
}
|
|
}
|
|
|
|
var raw string
|
|
if err := s.db.QueryRowContext(ctx,
|
|
`SELECT created_at FROM memory_items WHERE content = ?`, "较早").Scan(&raw); err != nil {
|
|
t.Fatalf("read stored created_at: %v", err)
|
|
}
|
|
if len(raw) != storedTimeLen {
|
|
t.Errorf("stored created_at = %q (%d bytes), want fixed width %d", raw, len(raw), storedTimeLen)
|
|
}
|
|
|
|
got, err := s.List(ctx)
|
|
if err != nil {
|
|
t.Fatalf("List: %v", err)
|
|
}
|
|
if len(got) != 2 {
|
|
t.Fatalf("List = %d items, want 2", len(got))
|
|
}
|
|
if got[0].Content != "较晚" {
|
|
t.Errorf("got[0].Content = %q, want the later timestamp (较晚)", got[0].Content)
|
|
}
|
|
if got[1].Content != "较早" {
|
|
t.Errorf("got[1].Content = %q, want the earlier timestamp (较早)", got[1].Content)
|
|
}
|
|
}
|
|
|
|
// TestStore_List_同一时间戳_插入序 覆盖并列情形:宿主时钟
|
|
// 精度足够粗,三次连续的 Add 可能共享同一时间戳,因此排序必须
|
|
// 回退到 rowid,而不是任意行序。
|
|
func TestStore_List_同一时间戳_插入序(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
|
|
at := time.Date(2026, 9, 7, 3, 20, 5, 830978900, time.UTC)
|
|
for _, content := range []string{"第一条", "第二条", "第三条"} {
|
|
item := MemoryItem{PersonaID: "persona.a", Content: content, CreatedAt: at, UpdatedAt: at}
|
|
if _, err := s.Add(ctx, item); err != nil {
|
|
t.Fatalf("Add(%s): %v", content, err)
|
|
}
|
|
}
|
|
|
|
want := []string{"第三条", "第二条", "第一条"}
|
|
for _, tc := range []struct {
|
|
name string
|
|
list func(context.Context) ([]MemoryItem, error)
|
|
}{
|
|
{"List", s.List},
|
|
{"ListByPersona", func(ctx context.Context) ([]MemoryItem, error) {
|
|
return s.ListByPersona(ctx, "persona.a")
|
|
}},
|
|
} {
|
|
got, err := tc.list(ctx)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", tc.name, err)
|
|
}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("%s = %d items, want %d", tc.name, len(got), len(want))
|
|
}
|
|
for i, w := range want {
|
|
if got[i].Content != w {
|
|
t.Errorf("%s got[%d].Content = %q, want %q (rowid DESC)", tc.name, i, got[i].Content, w)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStore_Delete_正常(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
|
|
stored, err := s.Add(ctx, baseItem())
|
|
if err != nil {
|
|
t.Fatalf("Add: %v", err)
|
|
}
|
|
if err := s.Delete(ctx, stored.ID); err != nil {
|
|
t.Fatalf("Delete: %v", err)
|
|
}
|
|
if _, err := s.Get(ctx, stored.ID); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("Get after delete err = %v, want ErrNotFound", err)
|
|
}
|
|
}
|
|
|
|
func TestStore_Delete_NotFound_异常(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
|
|
for _, id := range []string{"", "missing"} {
|
|
if err := s.Delete(ctx, id); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("Delete(%q) err = %v, want ErrNotFound", id, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStore_Concurrent_并发(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
|
|
const n = 40
|
|
var wg sync.WaitGroup
|
|
errs := make(chan error, n)
|
|
for i := 0; i < n; i++ {
|
|
wg.Add(1)
|
|
go func(i int) {
|
|
defer wg.Done()
|
|
_, err := s.Add(ctx, MemoryItem{
|
|
PersonaID: "persona.a",
|
|
Content: "并发内容",
|
|
Scope: ScopePersona,
|
|
Kind: KindRelationship,
|
|
})
|
|
errs <- err
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
close(errs)
|
|
|
|
for err := range errs {
|
|
if err != nil {
|
|
t.Fatalf("concurrent Add error: %v", err)
|
|
}
|
|
}
|
|
items, err := s.ListByPersona(ctx, "persona.a")
|
|
if err != nil {
|
|
t.Fatalf("ListByPersona: %v", err)
|
|
}
|
|
if len(items) != n {
|
|
t.Fatalf("ListByPersona(a) = %d items, want %d", len(items), n)
|
|
}
|
|
}
|