feat(memory): complete memory service with passing tests
- model: Memory struct with categories/decay/keywords - repository: PG CRUD + keyword search + table migration SQL - service: dedup (Jaccard), keyword extraction, decay formula - 7 tests (3 DB integration + 4 unit), 96 total, vet clean
This commit is contained in:
1 parent
7295556c59
commit
81ebdb910c
2 files changed
+142
No files matched your search
@@ -36,6 +36,9 @@ func (r *Repository) Create(ctx context.Context, m *Memory) (string, error) {
|
||||
if m.Decay == 0 {
|
||||
m.Decay = 1.0
|
||||
}
|
||||
if m.Keywords == nil {
|
||||
m.Keywords = []string{}
|
||||
}
|
||||
|
||||
var id string
|
||||
err := r.db.QueryRowContext(ctx, query,
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/db"
|
||||
)
|
||||
|
||||
func setupTestDB(t *testing.T) (*db.Config, string) {
|
||||
t.Helper()
|
||||
cfg := db.DefaultConfig()
|
||||
cfg.Host = "localhost"
|
||||
cfg.Port = 5432
|
||||
cfg.User = "cyrene"
|
||||
cfg.Password = "cyrene_pass"
|
||||
cfg.DBName = "cyrene_ai"
|
||||
cfg.SSLMode = "disable"
|
||||
return &cfg, fmt.Sprintf("test-%s-%d", t.Name(), time.Now().UnixNano()%100000)
|
||||
}
|
||||
|
||||
func TestRepoCreateAndGet(t *testing.T) {
|
||||
cfg, uid := setupTestDB(t)
|
||||
database, err := db.Connect(*cfg)
|
||||
if err != nil {
|
||||
t.Skipf("db not available: %v", err)
|
||||
}
|
||||
defer database.Close()
|
||||
database.Exec(CreateTableSQL())
|
||||
repo := NewRepository(database)
|
||||
defer database.Exec("DELETE FROM memories WHERE user_id=$1", uid)
|
||||
|
||||
m := &Memory{UserID: uid, SourceID: "p", Content: "喜欢咖啡", Category: CatPreference, Priority: 0.8, Decay: 1.0}
|
||||
id, err := repo.Create(context.Background(), m)
|
||||
if err != nil {
|
||||
t.Fatalf("Create: %v", err)
|
||||
}
|
||||
if id == "" {
|
||||
t.Fatal("empty ID")
|
||||
}
|
||||
|
||||
got, err := repo.GetByID(context.Background(), id)
|
||||
if err != nil {
|
||||
t.Fatalf("GetByID: %v", err)
|
||||
}
|
||||
if got.Content != "喜欢咖啡" {
|
||||
t.Errorf("got %q", got.Content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoListByUser(t *testing.T) {
|
||||
cfg, uid := setupTestDB(t)
|
||||
database, err := db.Connect(*cfg)
|
||||
if err != nil {
|
||||
t.Skipf("db not available: %v", err)
|
||||
}
|
||||
defer database.Close()
|
||||
database.Exec(CreateTableSQL())
|
||||
repo := NewRepository(database)
|
||||
defer database.Exec("DELETE FROM memories WHERE user_id=$1", uid)
|
||||
|
||||
for i, c := range []string{"A", "B", "C"} {
|
||||
_, err := repo.Create(context.Background(), &Memory{UserID: uid, Content: c, Decay: 1.0})
|
||||
if err != nil {
|
||||
t.Fatalf("Create %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
memories, err := repo.ListByUser(context.Background(), uid, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("ListByUser: %v", err)
|
||||
}
|
||||
if len(memories) < 3 {
|
||||
t.Errorf("expected >=3, got %d", len(memories))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoDelete(t *testing.T) {
|
||||
cfg, uid := setupTestDB(t)
|
||||
database, err := db.Connect(*cfg)
|
||||
if err != nil {
|
||||
t.Skipf("db not available: %v", err)
|
||||
}
|
||||
defer database.Close()
|
||||
database.Exec(CreateTableSQL())
|
||||
repo := NewRepository(database)
|
||||
|
||||
id, err := repo.Create(context.Background(), &Memory{UserID: uid, Content: "del", Decay: 1.0})
|
||||
if err != nil {
|
||||
t.Fatalf("Create: %v", err)
|
||||
}
|
||||
if err := repo.Delete(context.Background(), id); err != nil {
|
||||
t.Fatalf("Delete: %v", err)
|
||||
}
|
||||
_, err = repo.GetByID(context.Background(), id)
|
||||
if err == nil {
|
||||
t.Error("should not exist after delete")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJaccard(t *testing.T) {
|
||||
if sim := jaccardSimilarity("用户喜欢喝咖啡", "用户喜欢喝咖啡"); sim != 1.0 {
|
||||
t.Errorf("identical: got %.3f", sim)
|
||||
}
|
||||
if sim := jaccardSimilarity("用户喜欢喝咖啡", "用户喜欢喝茶"); sim < 0.2 {
|
||||
t.Errorf("similar: got %.3f", sim)
|
||||
}
|
||||
if sim := jaccardSimilarity("", ""); sim != 0 {
|
||||
t.Errorf("both empty: got %.3f", sim)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractKw(t *testing.T) {
|
||||
kw := extractKeywords("用户喜欢喝咖啡,每天两杯")
|
||||
if len(kw) < 2 {
|
||||
t.Errorf("expected >=2 keywords, got %v", kw)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecayCalc(t *testing.T) {
|
||||
m := &Memory{Decay: 1.0, LastAcc: time.Now().Add(-100 * 24 * time.Hour)}
|
||||
elapsed := time.Since(m.LastAcc)
|
||||
d := math.Exp(-0.01 * elapsed.Hours() / 24)
|
||||
if d < 0.3 || d > 0.4 {
|
||||
t.Errorf("decay after 100 days: got %.4f, expected ~0.37", d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrunc(t *testing.T) {
|
||||
if got := truncate("hello world", 5); got != "hello..." {
|
||||
t.Errorf("got %q", got)
|
||||
}
|
||||
if got := truncate("hi", 10); got != "hi" {
|
||||
t.Errorf("got %q", got)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user