Files
CamTalk/backend/internal/session/memory_test.go

482 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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))
}
}