Files
sundynix-agentix/sundynix-mcp-go/internal/memory/store_test.go
T
Blizzard 79f9912615 refactor(mcp-go): Profile 套雪花id+软删规约,补集成测试(还清 DB 规约欠债)
项目级 DB 规约(雪花字符串 id + created_at/updated_at + 软删索引)此前只在网关落地,
mcp-go 的 sundynix_user_profile(Profile) 仍是旧复合主键 (user_id,key)、无 id/时间戳——
本次对齐,全库统一。

- 新增 BaseModel + NewID(snowflake node=2,与网关 node=1 区分,避免共享 PG 中 id 冲突)。
- Profile 嵌 BaseModel:雪花 id 主键 + (user_id,key) 唯一索引。
- Upsert 改 OnConflict((user_id,key)),冲突覆盖 value 且保留原 id/created_at。
- 一次性迁移 migrateLegacyProfile:检测旧表(无 id 列)→ 备份偏好 → drop → 按新规约重建 → 回灌。
- 加 mcp-go 首个集成测试(store_test.go,gated on MEMORY_TEST_DSN):
  · TestBaseModelID 纯单测(id 非空/不重/BeforeCreate 补id不覆盖);
  · Integration:迁移后字段齐全 + Upsert 同键覆盖不新增 + id 稳定 + Get 渲染;
  · LegacyMigration:旧复合主键表 + 1 条偏好 → 迁移后 id 列存在、数据保留补新 id。

实测(docker postgres):三测全过,迁移日志确认旧偏好回灌保留 ✓。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 14:56:16 +08:00

125 lines
4.0 KiB
Go

package memory
import (
"context"
"os"
"testing"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// TestBaseModelID 验证雪花 ID 规约:NewID 非空且不重复;BeforeCreate 补 ID、已有则保留。
func TestBaseModelID(t *testing.T) {
a, b := NewID(), NewID()
if a == "" || b == "" {
t.Fatal("NewID 不应为空")
}
if a == b {
t.Errorf("两次 NewID 应不同: %s", a)
}
var m BaseModel
if err := m.BeforeCreate(nil); err != nil {
t.Fatal(err)
}
if m.ID == "" {
t.Error("BeforeCreate 应补 ID")
}
m.ID = "fixed"
_ = m.BeforeCreate(nil)
if m.ID != "fixed" {
t.Error("BeforeCreate 不应覆盖已有 ID")
}
}
// TestProfileStore_Integration 跑真实 Postgres(设 MEMORY_TEST_DSN 才执行):
// 验证迁移后表含雪花规约字段、Upsert (user_id,key) 冲突覆盖而非新增、Get 渲染。
func TestProfileStore_Integration(t *testing.T) {
dsn := os.Getenv("MEMORY_TEST_DSN")
if dsn == "" {
t.Skip("设 MEMORY_TEST_DSN 启用 Postgres 集成测试")
}
raw, err := gorm.Open(postgres.New(postgres.Config{DSN: dsn}), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
if err != nil {
t.Fatalf("连接 Postgres: %v", err)
}
_ = raw.Migrator().DropTable("sundynix_user_profile") // 干净起点
s := Open(dsn) // 触发迁移 + AutoMigrate
if s.db == nil {
t.Fatal("Store 不应降级")
}
ctx := context.Background()
// 新规约字段齐全。
for _, col := range []string{"id", "created_at", "updated_at", "deleted_at"} {
if !raw.Migrator().HasColumn(&Profile{}, col) {
t.Errorf("表应含规约字段 %s", col)
}
}
// Upsert 两次同 (user_id,key) → 覆盖,不新增;id 稳定。
if err := s.Upsert(ctx, "u1", "城市", "北京"); err != nil {
t.Fatal(err)
}
var first Profile
raw.Where("user_id = ? AND key = ?", "u1", "城市").First(&first)
if first.ID == "" || first.CreatedAt.IsZero() {
t.Error("行应有雪花 id 与创建时间")
}
if err := s.Upsert(ctx, "u1", "城市", "上海"); err != nil {
t.Fatal(err)
}
var cnt int64
raw.Model(&Profile{}).Where("user_id = ?", "u1").Count(&cnt)
if cnt != 1 {
t.Errorf("同键 upsert 应覆盖,期望 1 行,得 %d", cnt)
}
var after Profile
raw.Where("user_id = ? AND key = ?", "u1", "城市").First(&after)
if after.Value != "上海" || after.ID != first.ID {
t.Errorf("应覆盖 value 且保留 id: value=%s id=%s/%s", after.Value, after.ID, first.ID)
}
// Get 渲染多行(按 key 排序)。
_ = s.Upsert(ctx, "u1", "爱好", "围棋")
got, _ := s.Get(ctx, "u1")
if got == "" || got != "- 城市:上海\n- 爱好:围棋" {
t.Errorf("Get 渲染不符: %q", got)
}
}
// TestProfileStore_LegacyMigration 验证旧复合主键表(无 id)→ 雪花规约表的迁移保留数据。
func TestProfileStore_LegacyMigration(t *testing.T) {
dsn := os.Getenv("MEMORY_TEST_DSN")
if dsn == "" {
t.Skip("设 MEMORY_TEST_DSN 启用 Postgres 集成测试")
}
raw, err := gorm.Open(postgres.New(postgres.Config{DSN: dsn}), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
if err != nil {
t.Fatalf("连接 Postgres: %v", err)
}
// 造一个旧 schema 表(复合主键、无 id/时间戳)并塞一条偏好。
raw.Migrator().DropTable("sundynix_user_profile")
if err := raw.Exec(`CREATE TABLE sundynix_user_profile (user_id varchar(64), "key" varchar(64), value text, PRIMARY KEY(user_id, "key"))`).Error; err != nil {
t.Fatalf("建旧表: %v", err)
}
raw.Exec(`INSERT INTO sundynix_user_profile (user_id, "key", value) VALUES ('legacy', '语言', 'Go')`)
s := Open(dsn) // 应触发 migrateLegacyProfile:备份→重建→回灌
if s.db == nil {
t.Fatal("Store 不应降级")
}
if !raw.Migrator().HasColumn(&Profile{}, "id") {
t.Error("迁移后应有 id 列")
}
var p Profile
if err := raw.Where("user_id = ? AND key = ?", "legacy", "语言").First(&p).Error; err != nil {
t.Fatalf("迁移后旧数据应保留: %v", err)
}
if p.Value != "Go" || p.ID == "" {
t.Errorf("旧偏好应保留且补新 id: value=%s id=%s", p.Value, p.ID)
}
}