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:
Aska committed 2026-08-09 20:51:01 +08:00
1 parent 7295556c59
commit 81ebdb910c
2 files changed
+142

No files matched your search

+3
View File
@@ -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,
+139
View File
@@ -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)
}
}