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 断言 GET /api/v1/sessions 中最后消息预览的 // 线上契约(B3):每个会话都携带 lastMessage {role,content,createdAt}, // 其 content 是折叠空白后的最新消息内容,而没有消息的会话 // 省略该字段。 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) } // 解码进带 JSON 标签的形状,因此断言的是线上契约(键名), // 而不只是包内结构体。 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) } // 空的子会话完全省略 lastMessage。 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) } }