Files
tamakyi 46932b4c53 fix(chat): 会话消息只取最早 50 条,第 51 条起对双方永久不可见
GetByConversationID 原来是 Order("created_at ASC") + Limit(50),而会话上限是
conf.Limits.ChatMaxMessages(默认 200)。唯一调用方 chat_handler 永远传 nil cursor,
于是取到的永远是「最早 50 条」:第 51 条起写库成功、message_count 原子递增、
页面显示 51/200,但列表里没有它 —— 等多久、换设备、重新登录都不会出现。
本地实测 55 条会话只渲染 #1–#50,双方都以为对方没回,直到 200 条被判满自动关闭。

改为:
- Order("id DESC") 取最新 limit 条,再在内存反转成时间正序(气泡从上到下=旧→新)
- limit 取 conf.Limits.ChatMaxMessages,兜底只处理零值(配置未初始化);
  不写成 limit<50→50,那样会把箱主显式配的小上限抬大
- 排序键用 id 而非 created_at:同毫秒写入的 created_at 相同,排序不稳定
- 游标 query 去掉 NULL 风险:显式要求 cursor.Value 非空

验证:55 条实测渲染 55 条、范围 #1–#55;单测 3 项(全量可见 / 超限保留最新 /
游标翻页),变异验证把 ORDER 改回 ASC 时「超限保留最新」立刻变红。
2026-10-29 15:53:22 +08:00

140 lines
4.5 KiB
Go
Raw Permalink 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.
// 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)
}