// Copyright 2026 tamakyi. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package db import ( "context" "fmt" "testing" "github.com/stretchr/testify/require" "github.com/tamakyi/TamaBox/internal/conf" "github.com/tamakyi/TamaBox/internal/dbutil" ) // TestGetByConversationIDReturnsLatest 是本轮修复的回归测试。 // // 修复前的行为:Order("created_at ASC") + Limit(50),取的是「最早的 50 条」。 // 会话上限是 200,所以第 51 条起写库成功、计数在涨,但列表里永远看不到 —— // 实测 55 条会话只能渲染 #1–#50,双方都以为对方没回。 // // 现在要求:默认一次取满会话上限,且返回的是「最新」的那一批。 func TestGetByConversationIDReturnsLatest(t *testing.T) { database, cleanup := newTestDB(t) ctx := context.Background() t.Cleanup(func() { if err := cleanup("messages", "conversations"); err != nil { t.Fatal(err) } }) conv := &Conversation{OwnerID: 1} require.NoError(t, database.Create(conv).Error) const total = 55 for i := 1; i <= total; i++ { require.NoError(t, database.Create(&Message{ ConversationID: conv.ID, SenderID: 0, Content: fmt.Sprintf("msg #%d", i), }).Error) } store := NewMessageStore(database) got, err := store.GetByConversationID(ctx, conv.ID, nil) require.NoError(t, err) // ① 不再被截断在 50 条 require.Len(t, got, total, "整个会话都应可见,而不是只取最早的 50 条") // ② 顺序是时间正序(视图层气泡从上到下=旧→新) require.Equal(t, "msg #1", got[0].Content) require.Equal(t, fmt.Sprintf("msg #%d", total), got[len(got)-1].Content) for i := 1; i < len(got); i++ { require.Less(t, got[i-1].ID, got[i].ID, "返回结果必须按 id 升序,否则气泡顺序会乱") } } // TestGetByConversationIDKeepsNewestWhenOverLimit 覆盖「消息数超过单次加载上限」的场景。 // // 此时必须丢最老的、保最新的 —— 这正是修复前反过来的地方。 func TestGetByConversationIDKeepsNewestWhenOverLimit(t *testing.T) { database, cleanup := newTestDB(t) ctx := context.Background() t.Cleanup(func() { if err := cleanup("messages", "conversations"); err != nil { t.Fatal(err) } }) // 临时把上限压到 10,避免造 200+ 条拖慢测试 original := conf.Limits.ChatMaxMessages conf.Limits.ChatMaxMessages = 10 t.Cleanup(func() { conf.Limits.ChatMaxMessages = original }) conv := &Conversation{OwnerID: 1} require.NoError(t, database.Create(conv).Error) const total = 15 for i := 1; i <= total; i++ { require.NoError(t, database.Create(&Message{ ConversationID: conv.ID, SenderID: 0, Content: fmt.Sprintf("msg #%d", i), }).Error) } store := NewMessageStore(database) got, err := store.GetByConversationID(ctx, conv.ID, nil) require.NoError(t, err) require.Len(t, got, 10, "超过上限时应只取上限条数") // 保最新:应拿到 #6–#15,#1–#5 被丢弃 require.Equal(t, "msg #6", got[0].Content) require.Equal(t, "msg #15", got[len(got)-1].Content) } // TestGetByConversationIDCursorLoadsOlder 覆盖「加载更早消息」的游标语义。 // // 游标必须与 ORDER BY id DESC 配套;修复前 cursor 走 id < 但排序是 ASC, // 接上「加载更多」后会反复返回同一批最老的消息。 func TestGetByConversationIDCursorLoadsOlder(t *testing.T) { database, cleanup := newTestDB(t) ctx := context.Background() t.Cleanup(func() { if err := cleanup("messages", "conversations"); err != nil { t.Fatal(err) } }) conv := &Conversation{OwnerID: 1} require.NoError(t, database.Create(conv).Error) const total = 12 ids := make([]uint, 0, total) for i := 1; i <= total; i++ { m := &Message{ConversationID: conv.ID, SenderID: 0, Content: fmt.Sprintf("msg #%d", i)} require.NoError(t, database.Create(m).Error) ids = append(ids, m.ID) } store := NewMessageStore(database) // 以第 7 条的 id 为界,往前取 3 条 → 应得到 #4–#6 got, err := store.GetByConversationID(ctx, conv.ID, &dbutil.Cursor{Value: ids[6], PageSize: 3}) require.NoError(t, err) require.Len(t, got, 3) require.Equal(t, "msg #4", got[0].Content) require.Equal(t, "msg #6", got[2].Content) // 再往前翻一页:以第 4 条的 id 为界取 3 条 → #1–#3 got, err = store.GetByConversationID(ctx, conv.ID, &dbutil.Cursor{Value: ids[3], PageSize: 3}) require.NoError(t, err) require.Len(t, got, 3) require.Equal(t, "msg #1", got[0].Content) require.Equal(t, "msg #3", got[2].Content) }