实现会话生命周期管理,让 WS handler 能追踪会话、存储对话历史。 #35

Merged
huanghaosheng merged 5 commits from feature/phase2 into develop 2026-06-13 15:34:04 +08:00
Showing only changes of commit 26f87ce35a - Show all commits

View File

@@ -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())
}
}