package session import ( "context" "testing" "time" "github.com/hhs/camtalk/internal/logger" "github.com/hhs/camtalk/internal/models" ) func init() { logger.Init("debug", "console") } func TestCreateAndGet(t *testing.T) { m := NewMemoryManager(30*time.Minute, 20) defer m.Stop() ctx := context.Background() config := models.DefaultConfig() id, err := m.Create(ctx, "", config) if err != nil { t.Fatalf("Create: %v", err) } if id == "" { t.Fatal("Create returned empty ID") } sess, err := m.Get(ctx, id) if err != nil { t.Fatalf("Get: %v", err) } if sess.ID != id { t.Errorf("ID = %q, want %q", sess.ID, id) } if sess.Config.Language != "zh-CN" { t.Errorf("Language = %q, want %q", sess.Config.Language, "zh-CN") } } func TestGetNotFound(t *testing.T) { m := NewMemoryManager(30*time.Minute, 20) defer m.Stop() ctx := context.Background() _, err := m.Get(ctx, "nonexistent") if err != ErrSessionNotFound { t.Errorf("Get nonexistent: err = %v, want ErrSessionNotFound", err) } } func TestExpire(t *testing.T) { // 使用极短 TTL 测试过期 m := NewMemoryManager(50*time.Millisecond, 20) defer m.Stop() ctx := context.Background() id, _ := m.Create(ctx, "", models.DefaultConfig()) // 未过期时应能获取 _, err := m.Get(ctx, id) if err != nil { t.Fatalf("Get before expire: %v", err) } // 等待过期 time.Sleep(80 * time.Millisecond) _, err = m.Get(ctx, id) if err != ErrSessionNotFound { t.Errorf("Get after expire: err = %v, want ErrSessionNotFound", err) } } func TestDestroy(t *testing.T) { m := NewMemoryManager(30*time.Minute, 20) defer m.Stop() ctx := context.Background() id, _ := m.Create(ctx, "", models.DefaultConfig()) if err := m.Destroy(ctx, id); err != nil { t.Fatalf("Destroy: %v", err) } _, err := m.Get(ctx, id) if err != ErrSessionNotFound { t.Errorf("Get after Destroy: err = %v, want ErrSessionNotFound", err) } } func TestDestroyNotFound(t *testing.T) { m := NewMemoryManager(30*time.Minute, 20) defer m.Stop() ctx := context.Background() err := m.Destroy(ctx, "nonexistent") if err != ErrSessionNotFound { t.Errorf("Destroy nonexistent: err = %v, want ErrSessionNotFound", err) } } func TestAppendMessageAndGetHistory(t *testing.T) { m := NewMemoryManager(30*time.Minute, 20) defer m.Stop() ctx := context.Background() id, _ := m.Create(ctx, "", models.DefaultConfig()) msgs := []models.Message{ {Role: "user", Content: "你好"}, {Role: "assistant", Content: "你好!有什么可以帮你的吗?"}, {Role: "user", Content: "这是什么?"}, {Role: "assistant", Content: "这是一朵花。"}, } for _, msg := range msgs { if err := m.AppendMessage(ctx, id, msg); err != nil { t.Fatalf("AppendMessage: %v", err) } } history, err := m.GetHistory(ctx, id, 0) if err != nil { t.Fatalf("GetHistory: %v", err) } if len(history) != 4 { t.Fatalf("GetHistory len = %d, want 4", len(history)) } if history[0].Content != "你好" { t.Errorf("history[0] = %q, want %q", history[0].Content, "你好") } } func TestGetHistoryLimit(t *testing.T) { m := NewMemoryManager(30*time.Minute, 20) defer m.Stop() ctx := context.Background() id, _ := m.Create(ctx, "", models.DefaultConfig()) for i := 0; i < 10; i++ { m.AppendMessage(ctx, id, models.Message{Role: "user", Content: "msg"}) } history, err := m.GetHistory(ctx, id, 3) if err != nil { t.Fatalf("GetHistory: %v", err) } if len(history) != 3 { t.Fatalf("GetHistory limit=3: len = %d, want 3", len(history)) } } func TestHistoryLimit(t *testing.T) { const maxHistory = 5 m := NewMemoryManager(30*time.Minute, maxHistory) defer m.Stop() ctx := context.Background() id, _ := m.Create(ctx, "", models.DefaultConfig()) // 插入超过上限的消息 for i := 0; i < 10; i++ { m.AppendMessage(ctx, id, models.Message{Role: "user", Content: "msg"}) } history, err := m.GetHistory(ctx, id, 0) if err != nil { t.Fatalf("GetHistory: %v", err) } if len(history) != maxHistory { t.Fatalf("GetHistory after overflow: len = %d, want %d", len(history), maxHistory) } } func TestUpdateConfig(t *testing.T) { m := NewMemoryManager(30*time.Minute, 20) defer m.Stop() ctx := context.Background() id, _ := m.Create(ctx, "", models.DefaultConfig()) ttsEnabled := false detailLevel := "high" patch := models.SessionConfigPatch{ TTSEnabled: &ttsEnabled, DetailLevel: &detailLevel, } if err := m.UpdateConfig(ctx, id, patch); err != nil { t.Fatalf("UpdateConfig: %v", err) } sess, _ := m.Get(ctx, id) if sess.Config.TTSEnabled != false { t.Errorf("TTSEnabled = %v, want false", sess.Config.TTSEnabled) } if sess.Config.DetailLevel != "high" { t.Errorf("DetailLevel = %q, want %q", sess.Config.DetailLevel, "high") } // Language 未传,应保持原值 if sess.Config.Language != "zh-CN" { t.Errorf("Language = %q, want %q", sess.Config.Language, "zh-CN") } } func TestActiveRequest(t *testing.T) { m := NewMemoryManager(30*time.Minute, 20) defer m.Stop() ctx := context.Background() id, _ := m.Create(ctx, "", models.DefaultConfig()) // 初始应为空 reqID, err := m.GetActiveRequestID(ctx, id) if err != nil { t.Fatalf("GetActiveRequestID: %v", err) } if reqID != "" { t.Errorf("initial active request = %q, want empty", reqID) } // 设置 if err := m.SetActiveRequest(ctx, id, "req-123"); err != nil { t.Fatalf("SetActiveRequest: %v", err) } reqID, _ = m.GetActiveRequestID(ctx, id) if reqID != "req-123" { t.Errorf("active request = %q, want %q", reqID, "req-123") } // 清除 if err := m.ClearActiveRequest(ctx, id); err != nil { t.Fatalf("ClearActiveRequest: %v", err) } reqID, _ = m.GetActiveRequestID(ctx, id) if reqID != "" { t.Errorf("active request after clear = %q, want empty", reqID) } } func TestTouchRefreshesTTL(t *testing.T) { m := NewMemoryManager(100*time.Millisecond, 20) defer m.Stop() ctx := context.Background() id, _ := m.Create(ctx, "", models.DefaultConfig()) // 50ms 后 Touch,应重置 TTL time.Sleep(50 * time.Millisecond) if err := m.Touch(ctx, id); err != nil { t.Fatalf("Touch: %v", err) } // 再等 70ms(距创建 120ms,但距 Touch 只有 70ms),不应过期 time.Sleep(70 * time.Millisecond) _, err := m.Get(ctx, id) if err != nil { t.Errorf("Get after Touch: %v, want nil (should not expire yet)", err) } // 再等 50ms(距 Touch 120ms),应过期 time.Sleep(50 * time.Millisecond) _, err = m.Get(ctx, id) if err != ErrSessionNotFound { t.Errorf("Get after TTL: err = %v, want ErrSessionNotFound", err) } } func TestActiveCount(t *testing.T) { m := NewMemoryManager(30*time.Minute, 20) defer m.Stop() ctx := context.Background() if m.ActiveCount() != 0 { t.Errorf("initial ActiveCount = %d, want 0", m.ActiveCount()) } m.Create(ctx, "", models.DefaultConfig()) m.Create(ctx, "", models.DefaultConfig()) if m.ActiveCount() != 2 { t.Errorf("ActiveCount = %d, want 2", m.ActiveCount()) } } func TestCreateWithUserID(t *testing.T) { m := NewMemoryManager(30*time.Minute, 20) defer m.Stop() ctx := context.Background() id, err := m.Create(ctx, "user-123", models.DefaultConfig()) if err != nil { t.Fatalf("Create: %v", err) } sess, err := m.Get(ctx, id) if err != nil { t.Fatalf("Get: %v", err) } if sess.UserID != "user-123" { t.Errorf("UserID = %q, want %q", sess.UserID, "user-123") } if sess.Title != models.DefaultSessionTitle { t.Errorf("Title = %q, want %q", sess.Title, models.DefaultSessionTitle) } if sess.UpdatedAt.IsZero() { t.Error("UpdatedAt should not be zero") } } func TestUpdateTitle(t *testing.T) { m := NewMemoryManager(30*time.Minute, 20) defer m.Stop() ctx := context.Background() id, _ := m.Create(ctx, "user-1", models.DefaultConfig()) if err := m.UpdateTitle(ctx, id, "自定义标题"); err != nil { t.Fatalf("UpdateTitle: %v", err) } sess, _ := m.Get(ctx, id) if sess.Title != "自定义标题" { t.Errorf("Title = %q, want %q", sess.Title, "自定义标题") } } func TestUpdateTitleNotFound(t *testing.T) { m := NewMemoryManager(30*time.Minute, 20) defer m.Stop() ctx := context.Background() err := m.UpdateTitle(ctx, "nonexistent", "标题") if err != ErrSessionNotFound { t.Errorf("UpdateTitle nonexistent: err = %v, want ErrSessionNotFound", err) } } func TestAutoTitleOnFirstMessage(t *testing.T) { m := NewMemoryManager(30*time.Minute, 20) defer m.Stop() ctx := context.Background() id, _ := m.Create(ctx, "user-1", models.DefaultConfig()) // 首条 user 消息应自动更新标题 m.AppendMessage(ctx, id, models.Message{Role: "user", Content: "你好世界"}) sess, _ := m.Get(ctx, id) if sess.Title != "你好世界" { t.Errorf("Title = %q, want %q", sess.Title, "你好世界") } } func TestAutoTitleLongMessage(t *testing.T) { m := NewMemoryManager(30*time.Minute, 20) defer m.Stop() ctx := context.Background() id, _ := m.Create(ctx, "user-1", models.DefaultConfig()) // 超过 20 字符的消息应截断 longMsg := "这是一条很长很长很长很长很长很长很长很长的消息" m.AppendMessage(ctx, id, models.Message{Role: "user", Content: longMsg}) sess, _ := m.Get(ctx, id) expected := string([]rune(longMsg)[:20]) + "…" if sess.Title != expected { t.Errorf("Title = %q, want %q", sess.Title, expected) } } func TestAutoTitleNotOverwritten(t *testing.T) { m := NewMemoryManager(30*time.Minute, 20) defer m.Stop() ctx := context.Background() id, _ := m.Create(ctx, "user-1", models.DefaultConfig()) // 首条消息设置标题 m.AppendMessage(ctx, id, models.Message{Role: "user", Content: "第一条消息"}) // 第二条消息不应覆盖已有的标题 m.AppendMessage(ctx, id, models.Message{Role: "user", Content: "第二条消息"}) sess, _ := m.Get(ctx, id) if sess.Title != "第一条消息" { t.Errorf("Title = %q, want %q", sess.Title, "第一条消息") } } func TestListByUser(t *testing.T) { m := NewMemoryManager(30*time.Minute, 20) defer m.Stop() ctx := context.Background() // 创建两个用户的不同会话 id1, _ := m.Create(ctx, "user-1", models.DefaultConfig()) m.AppendMessage(ctx, id1, models.Message{Role: "user", Content: "会话1"}) id2, _ := m.Create(ctx, "user-1", models.DefaultConfig()) m.AppendMessage(ctx, id2, models.Message{Role: "user", Content: "会话2"}) m.Create(ctx, "user-2", models.DefaultConfig()) // 其他用户的会话 list, total, err := m.ListByUser(ctx, "user-1", 1, 10) if err != nil { t.Fatalf("ListByUser: %v", err) } if total != 2 { t.Errorf("total = %d, want 2", total) } if len(list) != 2 { t.Fatalf("len = %d, want 2", len(list)) } // 按 UpdatedAt 降序,id2 应在前 if list[0].ID != id2 { t.Errorf("list[0].ID = %q, want %q", list[0].ID, id2) } if list[0].Title != "会话2" { t.Errorf("list[0].Title = %q, want %q", list[0].Title, "会话2") } if list[0].LastMessage != "会话2" { t.Errorf("list[0].LastMessage = %q, want %q", list[0].LastMessage, "会话2") } } func TestListByUserPagination(t *testing.T) { m := NewMemoryManager(30*time.Minute, 20) defer m.Stop() ctx := context.Background() // 创建 5 个会话 for i := 0; i < 5; i++ { id, _ := m.Create(ctx, "user-1", models.DefaultConfig()) m.AppendMessage(ctx, id, models.Message{Role: "user", Content: "msg"}) } // 第 1 页,每页 2 条 list, total, _ := m.ListByUser(ctx, "user-1", 1, 2) if total != 5 { t.Errorf("total = %d, want 5", total) } if len(list) != 2 { t.Errorf("page 1 len = %d, want 2", len(list)) } // 第 2 页 list, _, _ = m.ListByUser(ctx, "user-1", 2, 2) if len(list) != 2 { t.Errorf("page 2 len = %d, want 2", len(list)) } // 第 3 页(最后一页) list, _, _ = m.ListByUser(ctx, "user-1", 3, 2) if len(list) != 1 { t.Errorf("page 3 len = %d, want 1", len(list)) } // 超出范围的页 list, _, _ = m.ListByUser(ctx, "user-1", 10, 2) if len(list) != 0 { t.Errorf("out of range page len = %d, want 0", len(list)) } } func TestListByUserEmpty(t *testing.T) { m := NewMemoryManager(30*time.Minute, 20) defer m.Stop() ctx := context.Background() list, total, err := m.ListByUser(ctx, "no-such-user", 1, 10) if err != nil { t.Fatalf("ListByUser: %v", err) } if total != 0 { t.Errorf("total = %d, want 0", total) } if len(list) != 0 { t.Errorf("len = %d, want 0", len(list)) } }