Files
CamTalk/backend/internal/session/memory_test.go
hhs 6487a8ecab feat: 扩展 Session Manager 接口,新增 ListByUser、UpdateTitle 方法
- Manager.Create 签名新增 userID 参数
- 新增 ConversationSummary 类型和 ListByUser 分页查询
- 新增 UpdateTitle 方法
- MemoryManager 实现:ListByUser 遍历+过滤+排序,UpdateTitle,自动标题生成
- RedisManager 实现:user:{id}:sessions 索引,ListByUser 通过 SMEMBERS 查询
- AppendMessage 自动更新标题(首条 user 消息时,取前 20 字符)
- 更新 ws handler、api/session.go、orchestrator mock 的 Create 调用
2026-06-14 17:35:20 +08:00

287 lines
6.8 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())
}
}