Files
Cyrene_v2/internal/runtime/memory/store_test.go
T
Aska 04bfa2c7a7
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(gateway): admin 列表端点接真实数据——persona/session/memory 补 List() 并注入 gateway.Deps
根因:gateway.Server 定义 SessionStore/PersonaStore/MemoryStore 接口但生产从未满足:
- 底层 manager 无 List() 方法;
- wireFrontend 的 gateway.Deps{} 未传 Session/Personas/Memories。
→ /api/v1/sessions|personas 与 /api/v1/admin/{sessions,personas,memories} 恒返回 []。

改动:
- persona/registry:Registry.List() []Character(并发安全、值拷贝)。
- channel/session:SessionManager.List(ctx)(查 channel_sessions 表+按 actor_role 重建)。
- runtime/memory:Store.List(ctx)(全表,复用 scanItems)。
- gateway 三 Store 接口改为返回域类型(Character/MemoryItem),映射统一放 collect* 侧。
- cmd/cyrene/wire.go:Deps 补传 Session/Personas/Memories。
- .gitignore:cyrene→/cyrene(锚定根,不再误伤 cmd/cyrene 目录)。

实机验证:/api/v1/admin/personas 返回 {id:cyrene,name:昔涟,isDefault:true}。
2026-09-06 14:05:34 +08:00

279 lines
7.3 KiB
Go

package memory
import (
"context"
"errors"
"sync"
"testing"
)
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 returns the stored item with id and timestamps filled in.
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)
// Empty Scope/Kind/Confidence/DecayWeight normalize to their defaults.
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)
// An empty store lists nothing but stays non-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))
}
// Newest first (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])
}
}
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)
}
}