- history/title.go: RenameSession(改名字段)+ensureTitle(首条用户消息自动命名,折叠空白取前20rune,纯空白回落主会话/次级会话)
- store.go insert() 前调用 ensureTitle(覆盖Web+IM,最小侵入), HistoryStore 新增 RenameSession
- gateway: PATCH /api/v1/sessions/{id} (owner/admin, ≤80rune, 主会话可改名), handleRenameSession
- 新增/扩展测试(title_test, history_handler_test, fake同步)
- Docs/api-client.md v0.21 同步
153 lines
4.9 KiB
Go
153 lines
4.9 KiB
Go
package history
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// titleOf returns the stored title of a session, failing the test when it is absent.
|
|
func titleOf(t *testing.T, st *Store, id string) string {
|
|
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.Title
|
|
}
|
|
}
|
|
t.Fatalf("session %q not found in %+v", id, sessions)
|
|
return ""
|
|
}
|
|
|
|
func TestEnsureTitleFromFirstUserMessage(t *testing.T) {
|
|
t.Parallel()
|
|
st := newStore(t)
|
|
ctx := context.Background()
|
|
|
|
// The main session starts nameless.
|
|
if got := titleOf(t, st, MainSessionID); got != "" {
|
|
t.Fatalf("initial main title = %q, want empty", got)
|
|
}
|
|
|
|
// The first user message names it, with whitespace runs collapsed.
|
|
if _, err := st.AppendFull(ctx, Message{SessionID: MainSessionID, Role: RoleUser, Content: " 你好\n世界 "}); err != nil {
|
|
t.Fatalf("append first user: %v", err)
|
|
}
|
|
if got := titleOf(t, st, MainSessionID); got != "你好 世界" {
|
|
t.Fatalf("main title = %q, want 你好 世界", got)
|
|
}
|
|
|
|
// A later user message does not overwrite the established title.
|
|
if _, err := st.AppendFull(ctx, Message{SessionID: MainSessionID, Role: RoleUser, Content: "second"}); err != nil {
|
|
t.Fatalf("append second user: %v", err)
|
|
}
|
|
if got := titleOf(t, st, MainSessionID); got != "你好 世界" {
|
|
t.Fatalf("main title after second = %q, want unchanged", got)
|
|
}
|
|
}
|
|
|
|
func TestEnsureTitleTruncatesAndFallsBack(t *testing.T) {
|
|
t.Parallel()
|
|
st := newStore(t)
|
|
ctx := context.Background()
|
|
|
|
// A long first user message is truncated to 20 runes plus an ellipsis.
|
|
sub, err := st.NewSubSession(ctx, "owner")
|
|
if err != nil {
|
|
t.Fatalf("new sub: %v", err)
|
|
}
|
|
long := strings.Repeat("字", autoTitleMaxRunes+5)
|
|
if _, err := st.AppendFull(ctx, Message{SessionID: sub, Role: RoleUser, Content: long}); err != nil {
|
|
t.Fatalf("append long: %v", err)
|
|
}
|
|
want := strings.Repeat("字", autoTitleMaxRunes) + "…"
|
|
if got := titleOf(t, st, sub); got != want {
|
|
t.Fatalf("sub title = %q, want %q", got, want)
|
|
}
|
|
|
|
// An all-whitespace first message (e.g. an image-only turn) falls back to the
|
|
// session-type label, for both a sub and the main session.
|
|
blank, err := st.NewSubSession(ctx, "owner")
|
|
if err != nil {
|
|
t.Fatalf("new blank sub: %v", err)
|
|
}
|
|
if _, err := st.AppendFull(ctx, Message{SessionID: blank, Role: RoleUser, Content: " \n\t "}); err != nil {
|
|
t.Fatalf("append blank: %v", err)
|
|
}
|
|
if got := titleOf(t, st, blank); got != titleSubFallback {
|
|
t.Fatalf("blank sub title = %q, want %q", got, titleSubFallback)
|
|
}
|
|
|
|
st2 := newStore(t)
|
|
if _, err := st2.AppendFull(ctx, Message{SessionID: MainSessionID, Role: RoleUser, Content: " "}); err != nil {
|
|
t.Fatalf("append blank main: %v", err)
|
|
}
|
|
if got := titleOf(t, st2, MainSessionID); got != titleMainFallback {
|
|
t.Fatalf("blank main title = %q, want %q", got, titleMainFallback)
|
|
}
|
|
}
|
|
|
|
func TestEnsureTitleOnlyFromFirstUserMessage(t *testing.T) {
|
|
t.Parallel()
|
|
st := newStore(t)
|
|
ctx := context.Background()
|
|
|
|
// An assistant message first does not name the session.
|
|
sub, err := st.NewSubSession(ctx, "owner")
|
|
if err != nil {
|
|
t.Fatalf("new sub: %v", err)
|
|
}
|
|
if _, err := st.AppendFull(ctx, Message{SessionID: sub, Role: RoleAssistant, Content: "hello"}); err != nil {
|
|
t.Fatalf("append assistant: %v", err)
|
|
}
|
|
if got := titleOf(t, st, sub); got != "" {
|
|
t.Fatalf("sub title after assistant = %q, want empty", got)
|
|
}
|
|
|
|
// The first user message afterwards still names it.
|
|
if _, err := st.AppendFull(ctx, Message{SessionID: sub, Role: RoleUser, Content: "real question"}); err != nil {
|
|
t.Fatalf("append user: %v", err)
|
|
}
|
|
if got := titleOf(t, st, sub); got != "real question" {
|
|
t.Fatalf("sub title after user = %q, want real question", got)
|
|
}
|
|
}
|
|
|
|
func TestRenameSession(t *testing.T) {
|
|
t.Parallel()
|
|
st := newStore(t)
|
|
ctx := context.Background()
|
|
|
|
// The fixed main session may be renamed (unlike deletion).
|
|
if err := st.RenameSession(ctx, MainSessionID, "工作台"); err != nil {
|
|
t.Fatalf("rename main: %v", err)
|
|
}
|
|
if got := titleOf(t, st, MainSessionID); got != "工作台" {
|
|
t.Fatalf("main title = %q, want 工作台", got)
|
|
}
|
|
|
|
// A sub session may be renamed, and a rename overwrites an auto title.
|
|
sub, err := st.NewSubSession(ctx, "owner")
|
|
if err != nil {
|
|
t.Fatalf("new sub: %v", err)
|
|
}
|
|
if _, err := st.AppendFull(ctx, Message{SessionID: sub, Role: RoleUser, Content: "auto"}); err != nil {
|
|
t.Fatalf("append: %v", err)
|
|
}
|
|
if err := st.RenameSession(ctx, sub, "手动标题"); err != nil {
|
|
t.Fatalf("rename sub: %v", err)
|
|
}
|
|
if got := titleOf(t, st, sub); got != "手动标题" {
|
|
t.Fatalf("sub title = %q, want 手动标题", got)
|
|
}
|
|
|
|
// An unknown id is reported as not found.
|
|
if err := st.RenameSession(ctx, "web:missing", "x"); !errors.Is(err, ErrSessionNotFound) {
|
|
t.Fatalf("rename unknown err = %v, want ErrSessionNotFound", err)
|
|
}
|
|
}
|