Files
Cyrene_v2/internal/runtime/history/session_preview_test.go
T
AskaEth d102f75469
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(history): 会话列表lastMessage预览(B3) — 单查询无N+1
- history/session_preview.go: SessionMeta.LastMessage + 单条SQL(LEFT JOIN取每会话ORDER BY created_at DESC,rowid DESC LIMIT 1,走覆盖索引)
- ListSessions 迁出重写(签名不变), content折叠空白截前60rune
- gateway: historySessionSummary 增 lastMessage{role,content,createdAt}(omitempty), /sessions与/admin/sessions共用
- 新增测试(session_preview_test, history_lastmessage_test)
- Docs/api-client.md v0.22 同步
2026-09-08 21:17:26 +08:00

135 lines
4.6 KiB
Go

package history
import (
"context"
"strings"
"testing"
)
// sessionOf returns the SessionMeta of one session from ListSessions, failing the test
// when the session is absent.
func sessionOf(t *testing.T, st *Store, id string) SessionMeta {
t.Helper()
sessions, err := st.ListSessions(context.Background())
if err != nil {
t.Fatalf("list sessions: %v", err)
}
for _, m := range sessions {
if m.ID == id {
return m
}
}
t.Fatalf("session %q not found in %+v", id, sessions)
return SessionMeta{}
}
func TestListSessionsLastMessagePreview(t *testing.T) {
t.Parallel()
st := newStore(t)
ctx := context.Background()
// The empty main session has no preview.
if got := sessionOf(t, st, MainSessionID); got.LastMessage != nil {
t.Fatalf("empty main LastMessage = %+v, want nil", got.LastMessage)
}
sub, err := st.NewSubSession(ctx, "owner")
if err != nil {
t.Fatalf("new sub: %v", err)
}
if err := st.Append(ctx, Message{SessionID: MainSessionID, Role: RoleUser, Content: "first", CreatedAt: 1000}); err != nil {
t.Fatalf("append main user: %v", err)
}
if err := st.Append(ctx, Message{SessionID: MainSessionID, Role: RoleAssistant, Content: "latest reply", CreatedAt: 2000}); err != nil {
t.Fatalf("append main assistant: %v", err)
}
if err := st.Append(ctx, Message{SessionID: sub, Role: RoleUser, Content: "sub question", CreatedAt: 1500}); err != nil {
t.Fatalf("append sub: %v", err)
}
// Main stays first and carries its newest message, not the first one.
sessions, err := st.ListSessions(ctx)
if err != nil {
t.Fatalf("list sessions: %v", err)
}
if len(sessions) != 2 || sessions[0].ID != MainSessionID {
t.Fatalf("sessions = %+v, want main first", sessions)
}
main := sessions[0].LastMessage
if main == nil || main.Role != RoleAssistant || main.Content != "latest reply" || main.CreatedAt != 2000 {
t.Fatalf("main LastMessage = %+v, want assistant/latest reply/2000", main)
}
subPreview := sessions[1].LastMessage
if subPreview == nil || subPreview.Role != RoleUser || subPreview.Content != "sub question" || subPreview.CreatedAt != 1500 {
t.Fatalf("sub LastMessage = %+v, want user/sub question/1500", subPreview)
}
// A sub session created after the main one but never written has no preview.
empty, err := st.NewSubSession(ctx, "owner")
if err != nil {
t.Fatalf("new empty sub: %v", err)
}
if got := sessionOf(t, st, empty); got.LastMessage != nil {
t.Fatalf("empty sub LastMessage = %+v, want nil", got.LastMessage)
}
}
func TestListSessionsLastMessageTruncatesAndCollapses(t *testing.T) {
t.Parallel()
st := newStore(t)
ctx := context.Background()
long := strings.Repeat("字", lastMessagePreviewRunes+10) + "\n\n尾部"
if err := st.Append(ctx, Message{SessionID: MainSessionID, Role: RoleUser, Content: long, CreatedAt: 1000}); err != nil {
t.Fatalf("append long: %v", err)
}
want := strings.Repeat("字", lastMessagePreviewRunes) + "…"
if got := sessionOf(t, st, MainSessionID).LastMessage; got == nil || got.Content != want {
t.Fatalf("truncated preview = %+v, want %q", got, want)
}
// Whitespace runs (including newlines) collapse to single spaces, and a short
// message is returned whole.
sub, err := st.NewSubSession(ctx, "owner")
if err != nil {
t.Fatalf("new sub: %v", err)
}
if err := st.Append(ctx, Message{SessionID: sub, Role: RoleUser, Content: " 你好\n\t世界 ", CreatedAt: 1000}); err != nil {
t.Fatalf("append spaced: %v", err)
}
if got := sessionOf(t, st, sub).LastMessage; got == nil || got.Content != "你好 世界" {
t.Fatalf("collapsed preview = %+v, want %q", got, "你好 世界")
}
}
func TestListSessionsLastMessageTieBreaksByInsertion(t *testing.T) {
t.Parallel()
st := newStore(t)
ctx := context.Background()
// Two messages sharing a millisecond timestamp: the later insert is the newest.
for _, content := range []string{"older", "newer"} {
if err := st.Append(ctx, Message{SessionID: MainSessionID, Role: RoleAssistant, Content: content, CreatedAt: 5000}); err != nil {
t.Fatalf("append %s: %v", content, err)
}
}
if got := sessionOf(t, st, MainSessionID).LastMessage; got == nil || got.Content != "newer" {
t.Fatalf("tie-break preview = %+v, want newer", got)
}
}
func TestPreviewContentBounds(t *testing.T) {
t.Parallel()
if got := previewContent(""); got != "" {
t.Fatalf("empty preview = %q, want empty", got)
}
exact := strings.Repeat("a", lastMessagePreviewRunes)
if got := previewContent(exact); got != exact {
t.Fatalf("exact-length preview changed: %q", got)
}
over := strings.Repeat("a", lastMessagePreviewRunes+1)
if got := previewContent(over); got != exact+"…" {
t.Fatalf("over-length preview = %q, want ellipsis", got)
}
}