Files
Cyrene_v2/internal/services/gateway/history_lastmessage_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

62 lines
2.3 KiB
Go

package gateway
import (
"context"
"net/http"
"testing"
"git.yeij.top/AskaEth/Cyrene_v2/internal/auth"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history"
)
// TestHistorySessionsLastMessage asserts the GET /api/v1/sessions wire contract for the
// last-message preview (B3): every session carries lastMessage {role,content,createdAt}
// with the newest message's whitespace-collapsed content, and a message-less session
// omits the field.
func TestHistorySessionsLastMessage(t *testing.T) {
t.Parallel()
_, ts, jwt, st := newHistoryEnv(t)
ctx := context.Background()
if err := st.Append(ctx, history.Message{SessionID: history.MainSessionID, Role: history.RoleUser, Content: "你好", CreatedAt: 1000}); err != nil {
t.Fatalf("seed user: %v", err)
}
if err := st.Append(ctx, history.Message{SessionID: history.MainSessionID, Role: history.RoleAssistant, Content: " 在的\n请说 ", CreatedAt: 2000}); err != nil {
t.Fatalf("seed assistant: %v", err)
}
sub, err := st.NewSubSession(ctx, "owner")
if err != nil {
t.Fatalf("new sub: %v", err)
}
tok := signAny(t, jwt, "acct", auth.RoleOwner)
resp, body := doReq(t, http.MethodGet, ts.URL+"/api/v1/sessions", nil, tok)
if resp.StatusCode != http.StatusOK {
t.Fatalf("status = %d, want 200 (body=%s)", resp.StatusCode, body)
}
// Decode into a JSON-tagged shape so the wire contract (key names) is asserted,
// not just the in-package struct.
var sessions []struct {
ID string `json:"id"`
LastMessage *struct {
Role string `json:"role"`
Content string `json:"content"`
CreatedAt int64 `json:"createdAt"`
} `json:"lastMessage"`
}
decodeDataSlice(t, body, &sessions)
if len(sessions) != 2 || sessions[0].ID != history.MainSessionID {
t.Fatalf("sessions = %+v, want main first", sessions)
}
main := sessions[0].LastMessage
if main == nil || main.Role != history.RoleAssistant || main.Content != "在的 请说" || main.CreatedAt != 2000 {
t.Fatalf("main lastMessage = %+v, want assistant/在的 请说/2000", main)
}
// The empty sub session omits lastMessage entirely.
if sessions[1].ID != sub {
t.Fatalf("sessions[1].id = %q, want %q", sessions[1].ID, sub)
}
if sessions[1].LastMessage != nil {
t.Fatalf("empty sub lastMessage = %+v, want omitted", sessions[1].LastMessage)
}
}