diff --git a/backend/internal/session/memory_test.go b/backend/internal/session/memory_test.go new file mode 100644 index 0000000..acfa433 --- /dev/null +++ b/backend/internal/session/memory_test.go @@ -0,0 +1,286 @@ +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()) + } +}